如何在 iOS 上 hook 一個(gè) C 函數(shù)

在 iOS 上,Objective-C runtime 提供了一系列函數(shù),可以很容易地 hook Objective-C 的方法。因?yàn)?Objective-C 的動(dòng)態(tài)性很高,每個(gè) Objective-C 的方法(SEL)都是對應(yīng)一個(gè)匿名 C 函數(shù)的實(shí)現(xiàn)(IMP),只要去修改這個(gè) Objective-C 方法 與 C 實(shí)現(xiàn)的映射關(guān)系,就可以很容易地做到 hook 的功能。但是對于 C 函數(shù)本身,就不是那么簡單的事情了。

Mach-O 的映像結(jié)構(gòu)

要想了解如何 hook C 函數(shù),需要先了解下 iOS 下 Mach-O 可執(zhí)行文件載入的過程。一個(gè) iOS app 進(jìn)程可以包含多個(gè)映像(image),可執(zhí)行文件自己的代碼是一個(gè) image,它所鏈接的每個(gè)動(dòng)態(tài)庫也各分配了一個(gè) image。每個(gè)映像分為三個(gè)區(qū)域,mach header, load commands 和 data,圖示如下(以下說明都以 64 位架構(gòu)為準(zhǔn),32 位也是差不多的):

Mach-O 的映像結(jié)構(gòu)

(圖片來自seriot.ch - Hello Mach-O)

Mach header 用來記錄映像的元信息,比如 CPU 架構(gòu)等,具體細(xì)節(jié)我們不關(guān)心。load commands 區(qū)域是由若干個(gè)長度不等的 load command 排在一起,每個(gè) load command 用來告訴加載器進(jìn)行一些加載工作,其中最主要的 load command 類型是 segment command,目前我們只關(guān)心這一種命令,該命令讓加載器在把指定的數(shù)據(jù)(由文件偏移量fileoff和大小filesize決定)加載到指定的地址里,地址為 vmaddr+slide,slide 后文再說。知道了地址,就能對指定段和節(jié)的數(shù)據(jù)進(jìn)行操作了。

struct segment_command_64 { /* for 64-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT_64 */
    uint32_t    cmdsize;    /* includes sizeof section_64 structs */
    char        segname[16];    /* segment name */
    uint64_t    vmaddr;     /* memory address of this segment */
    uint64_t    vmsize;     /* memory size of this segment */
    uint64_t    fileoff;    /* file offset of this segment */
    uint64_t    filesize;   /* amount to map from the file */
    vm_prot_t   maxprot;    /* maximum VM protection */
    vm_prot_t   initprot;   /* initial VM protection */
    uint32_t    nsects;     /* number of sections in segment */
    uint32_t    flags;      /* flags */
};

一個(gè) segment 可以有0或多個(gè) section,從 nsects里可以獲得,這些 section 就是緊接著 segment 后面指定。load commands 后面就是實(shí)際的數(shù)據(jù)了。

遍歷方法

由于這三個(gè)部分是緊密地排在一起的,因此只要知道映像的首址和每個(gè)部分的大小,就可以通過指針?biāo)銛?shù)獲取每個(gè)區(qū)塊的內(nèi)容。比如我們通過 _dyld_get_image_header 可以獲得映像的 header 的地址,然后加上一個(gè)偏移量sizeof(struct mach_header_64),就是 load commands 區(qū)域的首址,header 中會(huì)有一個(gè)名為ncmds的字段記錄該區(qū)域有幾條 load command,每個(gè) load command 最前面必定是有兩個(gè)字段:

struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};

cmd 用來表示 load command 的類型,cmdsize 表示該命令所占的空間大小,這樣結(jié)合前面 header 提供的命令數(shù)和計(jì)算出來的 load commands 區(qū)域的首址,就可以遍歷該區(qū)域的所有 load command 了。

ASLR

ASLR 是 Address Space Layout Randomization 的縮寫,這個(gè)概念在業(yè)界由來已久,并非蘋果原創(chuàng)。由于 vmaddr 是鏈接器鏈接的時(shí)候?qū)懭?Mach-O 文件的,對于一個(gè)進(jìn)程來說是靜態(tài)不變的,因此給黑客攻擊帶來了便利,iOS 4.3 以后引入了 ASLR,給每個(gè) image 在 vmaddr 的基礎(chǔ)上再加一個(gè)隨機(jī)的偏移量 slide,因此每段數(shù)據(jù)的真實(shí)的虛擬地址是 vmaddr + slide。

開始 hook

兩個(gè)函數(shù)表

__DATA 段有兩個(gè)特殊的節(jié):__nl_symbol_ptr__la_symbol_ptr,這兩個(gè)節(jié)都是一個(gè)函數(shù)數(shù)組,前者存儲非懶加載解析的 C 函數(shù)地址,后者存儲懶加載存儲的函數(shù)地址。

對于以非懶加載的動(dòng)態(tài)庫,加載動(dòng)態(tài)庫映像的時(shí)候,將所有的符號全部解析出來填入該表,而對于懶加載的動(dòng)態(tài)庫,則默認(rèn)用一個(gè)特殊的函數(shù) dyld_stub_helper填充之,懶加載的函數(shù)第一次調(diào)用的時(shí)候,從映像中解析出地址,然后填充調(diào)用之。因此只要我們修改這兩個(gè)表的內(nèi)容,就可以替換原先函數(shù)的實(shí)現(xiàn)了。但問題是,這兩個(gè)節(jié)存儲的都是函數(shù)地址,沒有函數(shù)名,那么我們怎樣通過函數(shù)名找到對應(yīng)的函數(shù)地址呢?

__LINKEDIT 段

Mach-O 文件里另有一個(gè)特殊的段,這個(gè)段存儲了很多符號信息,與我們 hook C 函數(shù)有關(guān)的有三個(gè)數(shù)組:

  1. 間接符號表
  2. 符號表
  3. 字符串表
    間接符號表記錄了前面函數(shù)表里的函數(shù)所對應(yīng)的符號表下標(biāo),比如說某個(gè)函數(shù)表里分別表示的是 A, B, C, D 四個(gè)函數(shù)的地址,而對應(yīng)的符號表里四個(gè)函數(shù)的順序?yàn)?B, D, C, A,那么這個(gè)函數(shù)表所對應(yīng)的間接符號表的元素就是 3, 0, 2, 1。我們通過間接符號表就從函數(shù)地址查到函數(shù)在符號表的索引,然后通過這個(gè)索引再查符號表,符號表的每個(gè)表項(xiàng)是 struct nlist_64
struct nlist_64 {
    union {
        uint32_t  n_strx; /* index into the string table */
    } n_un;
    uint8_t n_type;        /* type flag, see below */
    uint8_t n_sect;        /* section number or NO_SECT */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};

這個(gè)結(jié)構(gòu)體的 n_un.n_strx 就是該函數(shù)的名字在字符串表中的索引,通過這個(gè)索引查字符串表就能得到函數(shù)名字。流程總結(jié)一下:

  1. 從間接地址表得到符號表索引
  2. 通過符號表和符號表索引得到函數(shù)對應(yīng)的符號表表項(xiàng)
  3. 通過符號表表項(xiàng)得到函數(shù)名在字符串表的索引
  4. 通過字符串表和字符串表索引找到函數(shù)名
    然后比較函數(shù)名是否是要 hook 的函數(shù),是的話,就用新的函數(shù)替換原先的表項(xiàng),當(dāng)然在替換之前最好把原先的地址拿出來,供新函數(shù)使用。

Facebook 的開源庫 fishhook

以上的流程實(shí)際上編碼起來是很繁瑣的,好在 Facebook 已經(jīng)幫我們做好了一個(gè)庫:fishhook,這個(gè)庫進(jìn)行 hook 的原理就是上面所說的這些,F(xiàn)acebook 自己的循環(huán)引用檢測庫 FBRetainCycleDetector 就基于 fishhook 實(shí)現(xiàn)的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容