在 iOS 上,Objective-C runtime 提供了一系列函數(shù)熊杨,可以很容易地 hook Objective-C 的方法上炎。因?yàn)?Objective-C 的動態(tài)性很高荡澎,每個 Objective-C 的方法(SEL
)都是對應(yīng)一個匿名 C 函數(shù)的實(shí)現(xiàn)(IMP
),只要去修改這個 Objective-C 方法 與 C 實(shí)現(xiàn)的映射關(guān)系局装,就可以很容易地做到 hook 的功能坛吁。但是對于 C 函數(shù)本身,就不是那么簡單的事情了贼邓。
Mach-O 的映像結(jié)構(gòu)
要想了解如何 hook C 函數(shù)阶冈,需要先了解下 iOS 下 Mach-O 可執(zhí)行文件載入的過程。一個 iOS app 進(jìn)程可以包含多個映像(image)塑径,可執(zhí)行文件自己的代碼是一個 image女坑,它所鏈接的每個動態(tài)庫也各分配了一個 image。每個映像分為三個區(qū)域统舀,mach header, load commands 和 data匆骗,圖示如下(以下說明都以 64 位架構(gòu)為準(zhǔn),32 位也是差不多的):
(圖片來自seriot.ch - Hello Mach-O)
Mach header 用來記錄映像的元信息誉简,比如 CPU 架構(gòu)等碉就,具體細(xì)節(jié)我們不關(guān)心。load commands 區(qū)域是由若干個長度不等的 load command 排在一起闷串,每個 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 */
};
一個 segment 可以有0或多個 section凉蜂,從 nsects
里可以獲得琼梆,這些 section 就是緊接著 segment 后面指定性誉。load commands 后面就是實(shí)際的數(shù)據(jù)了。
遍歷方法
由于這三個部分是緊密地排在一起的茎杂,因此只要知道映像的首址和每個部分的大小错览,就可以通過指針?biāo)銛?shù)獲取每個區(qū)塊的內(nèi)容。比如我們通過 _dyld_get_image_header
可以獲得映像的 header 的地址蛉顽,然后加上一個偏移量sizeof(struct mach_header_64)
蝗砾,就是 load commands 區(qū)域的首址,header 中會有一個名為ncmds
的字段記錄該區(qū)域有幾條 load command携冤,每個 load command 最前面必定是有兩個字段:
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ù)和計算出來的 load commands 區(qū)域的首址曾棕,就可以遍歷該區(qū)域的所有 load command 了。
ASLR
ASLR 是 Address Space Layout Randomization 的縮寫菜循,這個概念在業(yè)界由來已久翘地,并非蘋果原創(chuàng)。由于 vmaddr 是鏈接器鏈接的時候?qū)懭?Mach-O 文件的癌幕,對于一個進(jìn)程來說是靜態(tài)不變的衙耕,因此給黑客攻擊帶來了便利,iOS 4.3 以后引入了 ASLR勺远,給每個 image 在 vmaddr 的基礎(chǔ)上再加一個隨機(jī)的偏移量 slide橙喘,因此每段數(shù)據(jù)的真實(shí)的虛擬地址是 vmaddr + slide。
開始 hook
兩個函數(shù)表
在__DATA
段有兩個特殊的節(jié):__nl_symbol_ptr
和__la_symbol_ptr
胶逢,這兩個節(jié)都是一個函數(shù)數(shù)組厅瞎,前者存儲非懶加載解析的 C 函數(shù)地址,后者存儲懶加載存儲的函數(shù)地址初坠。
對于以非懶加載的動態(tài)庫和簸,加載動態(tài)庫映像的時候,將所有的符號全部解析出來填入該表碟刺,而對于懶加載的動態(tài)庫锁保,則默認(rèn)用一個特殊的函數(shù) dyld_stub_helper
填充之,懶加載的函數(shù)第一次調(diào)用的時候半沽,從映像中解析出地址爽柒,然后填充調(diào)用之。因此只要我們修改這兩個表的內(nèi)容抄囚,就可以替換原先函數(shù)的實(shí)現(xiàn)了霉赡。但問題是,這兩個節(jié)存儲的都是函數(shù)地址幔托,沒有函數(shù)名穴亏,那么我們怎樣通過函數(shù)名找到對應(yīng)的函數(shù)地址呢蜂挪?
__LINKEDIT 段
Mach-O 文件里另有一個特殊的段,這個段存儲了很多符號信息嗓化,與我們 hook C 函數(shù)有關(guān)的有三個數(shù)組:
- 間接符號表
- 符號表
- 字符串表
間接符號表記錄了前面函數(shù)表里的函數(shù)所對應(yīng)的符號表下標(biāo)棠涮,比如說某個函數(shù)表里分別表示的是 A, B, C, D 四個函數(shù)的地址,而對應(yīng)的符號表里四個函數(shù)的順序?yàn)?B, D, C, A刺覆,那么這個函數(shù)表所對應(yīng)的間接符號表的元素就是 3, 0, 2, 1严肪。我們通過間接符號表就從函數(shù)地址查到函數(shù)在符號表的索引,然后通過這個索引再查符號表谦屑,符號表的每個表項(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) */
};
這個結(jié)構(gòu)體的 n_un.n_strx
就是該函數(shù)的名字在字符串表中的索引驳糯,通過這個索引查字符串表就能得到函數(shù)名字。流程總結(jié)一下:
- 從間接地址表得到符號表索引
- 通過符號表和符號表索引得到函數(shù)對應(yīng)的符號表表項(xiàng)
- 通過符號表表項(xiàng)得到函數(shù)名在字符串表的索引
- 通過字符串表和字符串表索引找到函數(shù)名
然后比較函數(shù)名是否是要 hook 的函數(shù)氢橙,是的話酝枢,就用新的函數(shù)替換原先的表項(xiàng),當(dāng)然在替換之前最好把原先的地址拿出來悍手,供新函數(shù)使用帘睦。
Facebook 的開源庫 fishhook
以上的流程實(shí)際上編碼起來是很繁瑣的,好在 Facebook 已經(jīng)幫我們做好了一個庫:fishhook坦康,這個庫進(jìn)行 hook 的原理就是上面所說的這些竣付,F(xiàn)acebook 自己的循環(huán)引用檢測庫 FBRetainCycleDetector 就基于 fishhook 實(shí)現(xiàn)的。