11、HOOK原理(上)--- fishHook

HOOK

建議大家先對MachO有一定的了解,因為下面的內容會涉及到MachO里面的內容3、iOS強化 --- Mach-O 文件
HOOK,中文譯為“掛鉤”或“鉤子”。在iOS逆向中時指改變程序運行流程的一種技術。通過HOOK可以讓別人的程序執行自己所寫的代碼。在逆向中經常使用這種技術。
下面我們來簡單看一下HOOK的工作流程是怎樣的。

HOOK示意圖


iOS中HOOK的幾種方式

  • 1、Method Swizzle
    利用OC的Runtime特性,動態改變SEL(方法編號)IMP(方法實現)的對應關系,達到OC方法調用流程改變的目的。主要用于OC方法。這個我們在10、代碼的注入里面有詳細講過。
  • 2、fishHook
    它是Facebook提供的一個動態修改鏈接Mach-O文件的工具。利用Mach-O文件加載原理,通過修改懶加載非懶加載兩個表的指針達到C函數HOOK的目的。
  • 3、Cydia Substrate
    Cydia Substrate原名Mobile Substrate,它的主要作用是針對OC方法、C函數以及函數地址進行HOOK。當然它并不是針對iOS而設計的,Android一樣可以使用。官網地址

fishHook

這里我們簡單的使用一下fishHookfishHook地址
fishHook只給我們提供了兩個函數和一個結構體

FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
// 參數一 : 存放rebinding結構體的數組(可以同時交換多個函數)
// 參數二 : rebinding數組長度
// 跟上面的函數比起來,這個函數可以指定image(鏡像文件)
FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
                         intptr_t slide,
                         struct rebinding rebindings[],
                         size_t rebindings_nel);
struct rebinding {
  const char *name;//需要HOOK的函數名稱,C字符串
  void *replacement;//新函數的地址
  void **replaced;//原始函數地址的指針!
};

下面我們就來使用一下fishHook

  • 首先將fishHook文件拖進我們的工程,并且在要使用fishHook的類里面,引用fishHook:
    引入fishHook
  • HOOK 系統函數 NSLog
    HOOK NSLog

    運行結果:
    HOOK結果

    可以看到,已經HOOK成功了。這里有一點要跟大家講清楚,就是fishHook在使用的過程中sys_NSLog怎么就跟NSLog聯系上了呢?
    其實是這個樣子的,我們在HOOK過程中:
    ifishHookNSLog(系統的)的真實地址,賦給了sys_NSLog,注意sys_NSLog只是一個函數指針。
    ii:緊接著將NSLog指向我們自定義的my_NSLog
    iii:這樣在執行完my_NSLog的代碼之后,我們可以利用sys_NSLog回到原本的業務邏輯中,不影響原始業務的執行。

  • fishHook源碼探究
    上面我們簡單的講了一下fishHook的運行機制,那么我們接下來就來動態調試一下。
    首先我們進入rebind_symbols這個方法去看一下,它里面的實現:
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
    //prepend_rebindings的函數會將整個 rebindings 數組添加到 _rebindings_head 這個鏈表的頭部
    //Fishhook采用鏈表的方式來存儲每一次調用rebind_symbols傳入的參數,每次調用,就會在鏈表的頭部插入一個節點,鏈表的頭部是:_rebindings_head
    int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
    //根據上面的prepend_rebinding來做判斷,如果小于0的話,直接返回一個錯誤碼回去
    if (retval < 0) {
    return retval;
  }
    //根據_rebindings_head->next是否為空判斷是不是第一次調用。
  if (!_rebindings_head->next) {
      //第一次調用的話,調用_dyld_register_func_for_add_image注冊監聽方法.
      //已經被dyld加載的image會立刻進入回調。
      //之后的image會在dyld裝載的時候觸發回調。
    _dyld_register_func_for_add_image(_rebind_symbols_for_image);
  } else {
      //遍歷已經加載的image,進行的hook
    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));
    }
  }
  return retval;
}

根據代碼,我們可以看到,首先會有一個retval的判斷,那么我們來看看prepend_rebindings這個方法

static int prepend_rebindings(struct rebindings_entry **rebindings_head,
                              struct rebinding rebindings[],
                              size_t nel) {
  struct rebindings_entry *new_entry = (struct rebindings_entry *) malloc(sizeof(struct rebindings_entry));
  if (!new_entry) {
    return -1;
  }
  new_entry->rebindings = (struct rebinding *) malloc(sizeof(struct rebinding) * nel);
  if (!new_entry->rebindings) {
    free(new_entry);
    return -1;
  }
  memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);
  new_entry->rebindings_nel = nel;
  new_entry->next = *rebindings_head;
  *rebindings_head = new_entry;
  return 0;
}

可以看到*rebindings_head = new_entry;,也就是我們之前傳入的&_rebindings_head。我們再來看一下_rebindings_head是什么:

struct rebindings_entry {
  struct rebinding *rebindings;
  size_t rebindings_nel;
  struct rebindings_entry *next;
};

static struct rebindings_entry *_rebindings_head;

到這里不難看出_rebindings_head其實就是一個鏈表的表頭。
下面我們繼續在rebind_symbols方法里面往下走,這里跟大家說一下,我們在動態調試的時候,無論是第一次調用還是第N次調用,都會進入_rebind_symbols_for_image,這里大家只要打斷點調試一下就可以看到。那么我們直接去_rebind_symbols_for_image方法。

/*****************************************/
// _rebind_symbols_for_image
/*****************************************/
static void _rebind_symbols_for_image(const struct mach_header *header,
                                      intptr_t slide) {
    rebind_symbols_for_image(_rebindings_head, header, slide);
}
??
/*****************************************/
// rebind_symbols_for_image
/*****************************************/
//回調的最終就是這個函數! 三個參數:要交換的數組  、 image的頭 、 ASLR的偏移
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                     const struct mach_header *header,
                                     intptr_t slide){}

rebind_symbols_for_image方法中有一個函數,要先跟大家講清楚,那就是dladdr()

  • dladdr() :用來確認指定的address是否位于構成進程的地址空間中的其中一個加載模塊內(可執行庫或共享庫)。
    ○:如果某個地址位于在其上面映射加載模塊的基址,和為該加載模塊映射的最高虛擬地址之間(包括兩端),則認為該地址在加載模塊的范圍內。
    ○:如果某個加載模塊符合這個條件,則會搜索其動態符號表,以查找與指定的adress最接近的符號。最接近的符號是指其值等于,或最為接近但小于指定的adress的符號。
    那么dladdr()執行會有什么效果呢?拿下面代碼中的來說:
    i:如果指定的adress不再其中一個加載模塊的范圍內,則返回0;且不修改Dl_info結構的內容。否則,返回一個非零值,同時設置Dl_info結構的字段。
    ii:如果指定的adress在加載模塊的范圍內,找不到其值小于或等于adress的符號,則dli_snamedli_saddrdli_size字段將設置為0dli_bind字段設置為STB_LOCALdli_type字段設置為STT_NOTYPE
//回調的最終就是這個函數! 三個參數:要交換的數組  、 image的頭 、 ASLR的偏移
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
                                     const struct mach_header *header,
                                     intptr_t slide) {
  //這個dladdr函數就是在程序里面找header
  Dl_info info;
  if (dladdr(header, &info) == 0) {
    return;
  }
  //下面就是定義好幾個變量,準備從MachO里面去找!
  segment_command_t *cur_seg_cmd;
  segment_command_t *linkedit_segment = NULL;
  struct symtab_command* symtab_cmd = NULL;
  struct dysymtab_command* dysymtab_cmd = NULL;
  //跳過header的大小,找loadCommand
  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;
    }
  }
   //如果剛才獲取的,有一項為空就直接返回
  if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
      !dysymtab_cmd->nindirectsyms) {
    return;
  }

  // Find base symbol/string table addresses
  //鏈接時程序的基址 = __LINKEDIT.VM_Address -__LINKEDIT.File_Offset + silde的改變值
  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);

  // Get indirect symbol table (array of uint32_t indices into symbol table)
  //動態符號表地址 = 基址 + 動態符號表偏移量
  uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

  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) {
        //尋找到data段
      if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
          strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
        continue;
      }
        
      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);
        }
      }
    }
  }
}

最后我們看到,無論是懶加載符號表還是非懶加載符號表,都回去調用perform_rebinding_with_section。那我們就繼續跟進perform_rebinding_with_section,這里面就有意思了,有我們想要的HOOK;我們上面提到的函數指針的替換,就在這里面:

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) {
    //nl_symbol_ptr和la_symbol_ptrsection中的reserved1字段指明對應的indirect symbol table起始的index
  uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
    //slide+section->addr 就是符號對應的存放函數實現的數組也就是我相應的__nl_symbol_ptr和__la_symbol_ptr相應的函數指針都在這里面了,所以可以去尋找到函數的地址
  void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
    //遍歷section里面的每一個符號
  for (uint i = 0; i < section->size / sizeof(void *); i++) {
      //找到符號在Indrect Symbol Table表中的值
      //讀取indirect table中的數據
    uint32_t symtab_index = indirect_symbol_indices[i];
    if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
        symtab_index == (INDIRECT_SYMBOL_LOCAL   | INDIRECT_SYMBOL_ABS)) {
      continue;
    }
      //以symtab_index作為下標,訪問symbol table
      uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
      //獲取到symbol_name
      char *symbol_name = strtab + strtab_offset;
      //判斷是否函數的名稱是否有兩個字符,為啥是兩個,因為函數前面有個_,所以方法的名稱最少要1個
      bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
      //遍歷最初的鏈表,來進行hook
      struct rebindings_entry *cur = rebindings;
      while (cur) {
          for (uint j = 0; j < cur->rebindings_nel; j++) {
              //這里if的條件就是判斷從symbol_name[1]兩個函數的名字是否都是一致的,以及判斷兩個
              if (symbol_name_longer_than_1 &&
                  strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
                  //判斷replaced的地址不為NULL以及我方法的實現和rebindings[j].replacement的方法不一致
                  if (cur->rebindings[j].replaced != NULL &&
                      indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
                      //讓rebindings[j].replaced保存indirect_symbol_bindings[i]的函數地址
                      *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
                  }
                  //將替換后的方法給原先的方法,也就是替換內容為自定義函數地址
                  indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
                  goto symbol_loop;
        }
      }
      cur = cur->next;
    }
  symbol_loop:;
  }
}

最關鍵的兩句代碼:

//讓rebindings[j].replaced保存indirect_symbol_bindings[i]的函數地址
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
//將替換后的方法給原先的方法,也就是替換內容為自定義函數地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;

這就與我們之前分析的fishHookHOOK過程對應起來了。


總結:

  • 我們利用fishHook對系統方法NSLog進行了HOOK,這是利用了Runtime的特性;這是因為間接符號邊在編譯的時候,并沒有確定函數的真實地址。只有在運行時在確定,所以我們可以HOOK
  • 如果大家有興趣,可以自己定義一個C函數,看看能不能HOOK。這里先告訴大家,答案是否定的。雖然NSLog也是一個C函數,但是它是外部符號,我們自己在工程中定義的C函數屬于內部符號,在編譯鏈接的時候,會變成地址調用,也就是說會變成類似于這樣的:b 0x00000119890。那么我們再利用fishHook就無法HOOK到了。這里大家有興趣可以試一下(這個之后我們會去探索inlinHook,會去探究靜態函數的HOOK)。
  • 在上面的代碼注釋里面,有提到slide,這就是涉及到PIC計數,也就是ASLR。這個知識點大家要去了解一下,這個以后會經常提到。簡單講,我們的APP包里面的函數地址,或者說我們通過MachOView查看我們的可執行程序時,看到的地址都是虛擬地址,在APP運行在手機上的時候都是會變的。將虛擬地址和ASLR地址結合得到我們要使用的真實地址。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容

  • 1.Fishhook hook原理 ??在一節筆記中我們已經掌握了fishhook的基本使用,也詳細探討了dyld...
    _從今以后_閱讀 702評論 0 0
  • [TOC] 回顧 注入的相關要素: 注入的形式:利用動態庫的特性進行注入,包括Framework、Dylib。可以...
    _順_1896閱讀 876評論 0 0
  • 一、Hook概述 HOOK中文譯為掛鉤或鉤子。在iOS逆向中是指改變程序運行流程的一種技術。通過hook可以讓別人...
    HotPotCat閱讀 4,756評論 1 12
  • HOOK概述 HOOK,中文譯為“掛鉤”或“鉤子”。在iOS逆向中是指改變程序運行流程的一種技術。通過HOOK可以...
    帥駝駝閱讀 1,161評論 0 3
  • 注入小結 通過之前的學習,我們知道了利用動態庫注入的兩種方式: 注入 App 后,使得 項目和動態庫產生關聯關系。...
    Superman168閱讀 14,221評論 4 5