iOS程序員的自我修養-fishhook原理(五)

目錄

fishhook原理

MachO文件動態連接裏面講到,模塊間的數據訪問和函數調用,都是用間接尋址。主模塊將要訪問動態庫裏的數據符號地址放在got(也稱Non-Lazy Symbol Pointers)數據段,調用動態庫的函數的地址放在la_symbol_ptr數據段。而數據段是可讀寫的,因此程序運行期間咱們能夠經過修改got(nl_symbol_ptr)和la_symbol_ptr數據段,來替換函數跟全局變量的地址。這個就是fishhook的原理。模塊內部的數據跟函數地址,靜態連接時候已經肯定好了,並且在代碼段(可讀、可執行、不可寫),因此fishhook是不能rebinding模塊內部的symbols。git

facebook是這樣介紹fishhook的:程序員

A library that enables dynamically rebinding symbols in Mach-O binaries running on iOS.github

這裏的symbols,就是指動態庫裏暴露出來的變量跟函數。因此fishhook是能夠替換變量跟函數的。bash

舉個🌰 (動態替換變量跟函數)

// b.m文件
char *global_var = "world";

=========================

//main.m文件
#import <Foundation/Foundation.h>
#import "fishhook.h"

static void (*orgi_NSLog)(NSString *format, ...);
char *orgi_var = "wukaikai";
extern char *global_var;

void my_NSLog(NSString *format, ...)
{
    printf("hello %s\n", global_var);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        printf("hello %s\n", global_var);
        struct rebinding rebind[2] = {{"NSLog", my_NSLog, (void *)&orgi_NSLog}, {"global_var", &orgi_var, NULL} };
        rebind_symbols(rebind, 2);
        NSLog(@"%s",global_var);
    }
    return 0;
}

=========================

//依次執行這兩個命令,生成可執行文件main (不懂爲啥是這兩個命令,回顧前面博客)
clang -fpic -shared b.m -o libStr.dylib
clang -framework Foundation  main.m fishhook.c -o main -L . -l str

=========================

//輸出
hello world
hello wukaikai
//能夠看到,global_var和NSLog都被替換了

複製代碼

fishhook實現分析

fishhook用到LINKEDIT去計算基址,這裏我先講這個加載命令LC_SEGMENT_64(_LINKEDIT)iphone

LINKEDIT

LINKEDIT segment是link editor在連接時候建立生成的segment,這個段包含了符號表(symtab)、間接符號表(dysymtab)、字符串表(string table)等。ide

這個我在MachO文件結構分析最後講到:從連接的角度來看,Mach-O文件是按照section來存儲文件的,segment只不過是把多個section打包放在一塊兒而已;可是從Mach-O文件裝載到內存的角度來看,Mach-O文件是按照segment(編譯時候,編譯器把相同權限的數據放在一塊兒,成爲segment)來存儲的,即便一個segment裏的內容小於1頁空間的內存,可是仍是會佔用一頁空間的內存,因此segment裏不只有filesize,也有vmsize,而section不須要有vmsize。不信你看符號表和間接符號表這兩個加載命令裏都沒有vmsize,因此我是否是也能夠把符號表和間接符號表理解成兩個section。函數

我我的以爲segment、section、加載命令這些概念都是從不一樣角度去看待的,不用嚴格區分。oop

替換函數/變量地址過程

從上面原理中,咱們知道替換過程很是簡單,以下:
  1. 傳入須要替換的函數/變量。(這個函數跟變量是其它模塊(dylib)中的)
  2. 找到nl_symbol_ptr(got)/la_symbol_ptr數據段,依次遍歷這個數據段,找到符號名跟第一步傳入的符號名匹配時候,進行替換便可。

第二步又有兩個問題須要解決,nl_symbol_ptr(got)/la_symbol_ptr這兩個數據段存放的是符號地址(指針),1. 如何知道這個指針對應的符號名?2. 如何找到nl_symbol_ptr(got)/la_symbol_ptr數據段?源碼分析

指針對應的符號名

MachO文件動態連接裏面講到post

value = IndirectSymbolTable[got.section_64.reserved1];
symbolTable[value] 就是got數據段的第一個符號。
symbolTable[value+1] 就是got數據段的第二個符號。
...依次類推

//1. 從got的section_64能夠找到got數據段裏面元素對應的符號
//2. 符號(nlist_64)裏的n_strx,去字符串表獲取符號名
//la_symbol_ptr也是一樣的方法找到符號名
==============
若是看不懂經過reserved1,一步一步獲取到符號名。那說明這系列課程前面部分,你須要再回顧一遍。
複製代碼

因此咱們找到符號表、字符串表、間接符號表,就能夠獲得指針對應的符號名了。經過加載命令,很容易獲得這些。

找到nl_symbol_ptr(got)/la_symbol_ptr

因爲這兩個section都是在DATA segment裏,咱們先根據加載命令獲得DATA;而後根據section_64的flag,能夠找到nl_symbol_ptr(got)/la_symbol_ptr

#define S_NON_LAZY_SYMBOL_POINTERS 0x6 /* section with only non-lazy symbol pointers */
#define S_LAZY_SYMBOL_POINTERS 0x7 /* section with only lazy symbol pointers */
複製代碼

源碼分析

注意,爲了讓讀者注意力都放在主要邏輯線上,下面的源碼,我會省略許多非核心的邏輯,好比邊界判斷等。完整源碼請見fishhook

  1. 第一步:傳入須要替換的函數
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
//將每次傳入的rebindings當作一個結點,構建成鏈表
  int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
  // 第一次調用,進入if裏面;_dyld_register_func_for_add_image作了2件事,第一件事是跟else裏面同樣,爲每一個image(鏡像)調用_rebind_symbols_for_image,第二件事是當dyld後面加載鏡像時候,也爲這個新鏡像調用_rebind_symbols_for_image。
  if (!_rebindings_head->next) {
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
    uint32_t c = _dyld_image_count();
    for (uint32_t i = 0; i < c; i++) {
      _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
    }
  }
}
複製代碼

  1. 第二步:作了三件事
    1. 計算基址(爲第2步服務)
    2. 找到符號表、字符串表、間接符號表
    3. 找到nl_symbol_ptr(got)/la_symbol_ptr

步驟二、3上面已經講了。那爲啥要計算基址呢,由於ASLR技術,簡單理解就是Windows全部程序虛擬內存起始地址是同樣的,可是iOS中,爲了預防黑客攻擊,起始地址有一個隨機偏移值。(不理解ASLR,對理解fishhook沒有影響,可先無論)

//rebindings上面鏈表的表頭;slide ASLR隨機偏移值
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                     const struct mach_header *header,
                                     intptr_t slide) {
  segment_command_t *cur_seg_cmd;
  segment_command_t *linkedit_segment = NULL; //LINKEDIT
  struct symtab_command* symtab_cmd = NULL; //符號表
  struct dysymtab_command* dysymtab_cmd = NULL; //間接符號表
//1. 遍歷加載命令,得到MachO中符號表、間接符號表、LINKEDIT三個加載命令
  uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
  for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
      if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
        linkedit_segment = cur_seg_cmd;
      }
    } else if (cur_seg_cmd->cmd == LC_SYMTAB) {
      symtab_cmd = (struct symtab_command*)cur_seg_cmd;
    } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
      dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
    }
  }
  // 原本是:基址=linkedit內存地址 - linkedit的fileoff
  //因爲ASLR:真實基址 = 基址 + slide
  uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
  //根據真實基址,獲得符號表、間接符號表、字符串表的虛擬內存地址
  nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
  char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
  uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
===========================
//2. 遍歷加載命令,獲得DATA,而後遍歷DATA裏面的section,
//找到nl_symbol_ptr(got)/la_symbol_ptr
  cur = (uintptr_t)header + sizeof(mach_header_t);
  for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
      if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
          strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
        continue;
      }
      //遍歷DATA裏面的section,找到nl_symbol_ptr(got)/la_symbol_ptr
      for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
        section_t *sect =
          (section_t *)(cur + sizeof(segment_command_t)) + j;
        if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
          perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
        }
        if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
          perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
        }
      }
    }
  }
}
複製代碼

你們想過沒有,爲啥計算基址,要用LINKEDIT。其實用TEXT、DATA哪一個加載命令,均可以獲得基址(很容易獲得結論)。我以爲是由於咱們尋找的符號表、間接符號表、字符串表都在LINKEDIT裏面,假如這三個表沒有了,後面操做就不用進行了。因此要是沒有LINKEDIT,確定沒有這三個表,可是其它TEXT/DATA等就沒有這個保證了(好比有這三個表,可是沒有TEXT/DATA),facebook是爲了嚴謹性吧。(這個也是個人推測,有不一樣意見的,歡迎評論區說下你的想法)

  1. 第三步:根據nl_symbol_ptr(got)/la_symbol_ptr數據段,依次遍歷這個數據段的符號名(指針對應的符號名),跟傳入的符號名進行匹配時候,進行替換便可。
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
                                           section_t *section,
                                           intptr_t slide,
                                           nlist_t *symtab,
                                           char *strtab,
                                           uint32_t *indirect_symtab) {
  uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
  void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
  for (uint i = 0; i < section->size / sizeof(void *); i++) {
    uint32_t symtab_index = indirect_symbol_indices[i];
    uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
    char *symbol_name = strtab + strtab_offset;
    struct rebindings_entry *cur = rebindings;
    while (cur) {
      for (uint j = 0; j < cur->rebindings_nel; j++) {
        if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
        //第一次,保存原函數
          if (cur->rebindings[j].replaced != NULL &&
              indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
            *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
          }
          indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
          goto symbol_loop;
        }
      }
      cur = cur->next;
    }
  symbol_loop:;
  }
}
複製代碼

最後

fishhook是一個很好的例子,能夠用來檢驗本身是否理解了MachO文件。若是你看fishhook源代碼沒有障礙,那恭喜你已經對MachO有不錯的理解了;反之你以爲代碼還有不理解地方,那就要看下前幾篇相應的地方了。

相關文章
相關標籤/搜索