一、Hook概述
HOOK中文譯為掛鉤或鉤子。在iOS逆向中是指改變程序運行流程的一種技術(shù)。通過hook可以讓別人的程序執(zhí)行自己所寫的代碼。在逆向中經(jīng)常使用這種技術(shù)。只有了解其原理才能夠?qū)阂獯a進(jìn)行有效的防護(hù)。
比如很久之前的微信自動搶紅包插件:

1.1Hook的幾種方式
iOS中HOOK技術(shù)的大致上分為5種:Method Swizzle、fishhook、Cydia Substrate、libffi、inlinehook。
1.1.1 Method Swizzle (OC)
利用OC的Runtime特性,動態(tài)改變SEL(方法編號)和IMP(方法實現(xiàn))的對應(yīng)關(guān)系,達(dá)到OC方法調(diào)用流程改變的目的。主要用于OC方法。
可以將SEL 和 IMP 之間的關(guān)系理解為一本書的目錄。SEL 就像標(biāo)題,IMP就像頁碼。他們是一一對應(yīng)的關(guān)系。(書的目錄不一定一一對應(yīng),可能頁碼相同,理解就行。)。

Runtime提供了交換兩個SEL和IMP對應(yīng)關(guān)系的函數(shù):
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
通過這個函數(shù)交換兩個SEL和IMP對應(yīng)關(guān)系的技術(shù),稱之為Method Swizzle(方法欺騙)

runtime中有3種方式實現(xiàn)方法交換:
-
method_exchangeImplementations:在分類中直接交換就可以了,如果不在分類需要配合class_addMethod實現(xiàn)原方法的回調(diào)。 -
class_replaceMethod:直接替換原方法。 -
method_setImplementation:重新賦值原方法,通過getImp和setImp配合。
runtime都比較熟悉,就不多介紹了,不是很了解使用的可以參考:第4部分代碼注入
1.1.2 fishhook (外部函數(shù))
是Facebook提供的一個動態(tài)修改鏈接mach-O文件的工具。利用MachO文件加載原理,通過修改懶加載和非懶加載兩個表的指針達(dá)到C(系統(tǒng)C函數(shù))函數(shù)HOOK的目的。fishhook
總結(jié)下來是:dyld 更新 Mach-O 二進(jìn)制的 __DATA segment 的 __la_symbol_str 中的指針,使用 rebind_symbol方法更新兩個符號位置來進(jìn)行符號的重新綁定。
1.1.3 Cydia Substrate
Cydia Substrate 原名為 Mobile Substrate ,主要作用是針對OC方法、C函數(shù)以及函數(shù)地址進(jìn)行HOOK操作。并不僅僅針對iOS而設(shè)計,安卓一樣可以用。Cydia Substrate官方
Cydia Substrate主要分為3部分:Mobile Hooker、MobileLoader、safe mode。
Mobile Hooker
它定義了一系列的宏和函數(shù),底層調(diào)用objc的runtime和fishhook來替換系統(tǒng)或者目標(biāo)應(yīng)用的函數(shù)。其中有兩個函數(shù):
-
MSHookMessageEx:主要作用于OC方法 MSHookMessageExvoid MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP result) -
MSHookFunction:(inline hook)主要作用于C和C++函數(shù) MSHookFunction。Logos語法的%hook就是對這個函數(shù)做了一層封裝。void MSHookFunction(voidfunction,void* replacement,void** p_original)
MobileLoader
MobileLoader用于加載第三方dylib在運行的應(yīng)用程序。啟動時MobileLoader會根據(jù)規(guī)則把指定目錄的第三方的動態(tài)庫加載進(jìn)去,第三方的動態(tài)庫也就是我們寫的破解程序。
safe mode
破解程序本質(zhì)是dylib寄生在別人進(jìn)程里。 系統(tǒng)進(jìn)程一旦出錯,可能導(dǎo)致整個進(jìn)程崩潰,崩潰后就會造成iOS癱瘓。所以CydiaSubstrate引入了安全模式,在安全模式下所有基于CydiaSubstratede的三方dylib都會被禁用,便于查錯與修復(fù)。
1.1.4 libffi
基于libbfi動態(tài)調(diào)用C函數(shù)。使用libffi中的ffi_closure_alloc構(gòu)造與原方法參數(shù)一致的"函數(shù)" (stingerIMP),以替換原方法函數(shù)指針;此外,生成了原方法和Block的調(diào)用的參數(shù)模板cif和blockCif。方法調(diào)用時,最終會調(diào)用到void _st_ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata), 在該函數(shù)內(nèi),可獲取到方法調(diào)用的所有參數(shù)、返回值位置,主要通過ffi_call根據(jù)cif調(diào)用原方法實現(xiàn)和切面block。AOP庫 Stinger和BlockHook就是使用libbfi做的。
1.1.5 inlinehook 內(nèi)聯(lián)鉤子 (靜態(tài))
Inline Hook 就是在運行的流程中插入跳轉(zhuǎn)指令來搶奪運行流程的一個方法。大體分為三步:
- 將原函數(shù)的前
N個字節(jié)搬運到Hook函數(shù)的前N個字節(jié); - 然后將原函數(shù)的前
N個字節(jié)填充跳轉(zhuǎn)到Hook函數(shù)的跳轉(zhuǎn)指令; - 在
Hook函數(shù)末尾幾個字節(jié)填充跳轉(zhuǎn)回原函數(shù)+N的跳轉(zhuǎn)指令;


MSHookFunction就是inline hook。
基于 Dobby 的 Inline Hook。Dobby 是通過插入 __zDATA 段和__zTEXT 段到 Mach-O 中。
-
__zDATA用來記錄Hook信息(Hook數(shù)量、每個Hook方法的地址)、每個Hook方法的信息(函數(shù)地址、跳轉(zhuǎn)指令地址、寫Hook函數(shù)的接口地址)、每個Hook的接口(指針)。 -
__zText用來記錄每個Hook函數(shù)的跳轉(zhuǎn)指令。
dobby
Dobby(原名:HOOKZz)是一個全平臺的inlineHook框架,它用起來就和fishhook一樣。
Dobby 通過 mmap 把整個 Mach-O 文件映射到用戶的內(nèi)存空間,寫入完成保存本地。所以 Dobby 并不是在原 Mach-O 上進(jìn)行操作,而是重新生成并替換。
Doddy
二 fishHook
2.1 fishhook的使用
在fishhook源碼.h文件中只提供了兩個函數(shù)和一個結(jié)構(gòu)體rebinding。
rebind_symbols、rebind_symbols_image
FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel);
-
rebindings[]:存放rebinding結(jié)構(gòu)體的數(shù)組(可以同時交換多個函數(shù))。 -
rebindings_nel:rebindings數(shù)組的長度。 -
slide:ASLR。 -
header:image的Header。
只有兩個函數(shù)重新綁定符號,兩個函數(shù)的區(qū)別是一個指定image一個不指定。按照我們一般的理解放在前面的接口更常用,參數(shù)少的更簡單。
rebinding
struct rebinding {
const char *name;//需要HOOK的函數(shù)名稱,C字符串
void *replacement;//新函數(shù)的地址
void **replaced;//原始函數(shù)地址的指針!
};
-
name:要HOOK的函數(shù)名稱,C字符串。 -
replacement:新函數(shù)的地址。(函數(shù)指針,也就是函數(shù)名稱)。 -
replaced:原始函數(shù)地址的指針。(二級指針)。
2.1.1 Hook NSLog
現(xiàn)在有個需求,Hook系統(tǒng)的NSLog函數(shù)。
Hook代碼:
- (void)hook_NSLog {
struct rebinding rebindNSLog;
rebindNSLog.name = "NSLog";
rebindNSLog.replacement = HP_NSLog;
rebindNSLog.replaced = (void *)&sys_NSLog;
struct rebinding rebinds[] = {rebindNSLog};
rebind_symbols(rebinds, 1);
}
//原函數(shù),函數(shù)指針
static void (*sys_NSLog)(NSString *format, ...);
//新函數(shù)
void HP_NSLog(NSString *format, ...) {
format = [format stringByAppendingFormat:@"\n Hook"];
//調(diào)用系統(tǒng)NSLog
sys_NSLog(format);
}
調(diào)用:
[self hook_NSLog];
NSLog(@"hook_NSLog");
輸出:
hook_NSLog
Hook
這個時候就已經(jīng)Hook住NSLog,走到了HP_NSLog中。
Hook代碼調(diào)用完畢,sys_NSLog保存系統(tǒng)NSLog原地址,NSLog指向HP_NSLog。
2.1.2 Hook 自定義 C 函數(shù)
Hook一下自己的C函數(shù):
void func(const char * str) {
NSLog(@"%s",str);
}
- (void)hook_func {
struct rebinding rebindFunc;
rebindFunc.name = "func";
rebindFunc.replacement = HP_func;
rebindFunc.replaced = (void *)&original_func;
struct rebinding rebinds[] = {rebindFunc};
rebind_symbols(rebinds, 1);
}
//原函數(shù),函數(shù)指針
static void (*original_func)(const char * str);
//新函數(shù)
void HP_func(const char * str) {
NSLog(@"Hook func");
original_func(str);
}
調(diào)用:
[self hook_func];
func("HotPotCat");
輸出:
HotPotCat
這個時候可以看到?jīng)]有Hook的func。
結(jié)論:自定義的函數(shù)fishhook hook 不了,系統(tǒng)的可以hook。
2.2 fishhook原理
fishHOOK可以HOOK C函數(shù),但是我們知道函數(shù)是靜態(tài)的,也就是說在編譯的時候,編譯器就知道了它的實現(xiàn)地址,這也是為什么C函數(shù)只寫函數(shù)聲明調(diào)用時會報錯。那么為什么fishhook還能夠改變C函數(shù)的調(diào)用呢?難道函數(shù)也有動態(tài)的特性存在?
是否意味著C Hook就必須修改調(diào)用地址?那意味著要修改二進(jìn)制。(原理上使用匯編可以實現(xiàn)。fishhook不是這么處理的)
那么系統(tǒng)函數(shù)和本地函數(shù)區(qū)別到底在哪里?
2.2.1 符號 & 符號綁定 & 符號表 & 重綁定符號
NSLog函數(shù)的地址在編譯的那一刻并不知道NSLog的真實地址。NSLog在Foundation框架中。在運行時NSLog的地址在 共享緩存 中。在整個手機(jī)中只有dyld知道NSLog的真實地址。
在LLVM編譯器生成MachO文件時,如果讓我們做就先空著系統(tǒng)函數(shù)的地址,等運行起來再替換。我們知道MachO中分為Text(只讀)和Data(可讀可寫),那么顯然這種方式行不通。
那么可行的方案是在Data段放一個 占位符(8字節(jié))讓代碼編譯的時候直接bl 占位符。在運行的時候dyld加載應(yīng)用的時候?qū)?code>Data段的地址修改為NSLog真實地址,代碼bl 占位符沒有變 。這個技術(shù)就叫做 PIC(position independent code`)位置無關(guān)代碼。(實際不是這么簡單)
- 占位符 就叫做 符號。
-
dyld將data段符號進(jìn)行修改的這個過程叫做 符號綁定。 - 一個又一個的符號放在一起形成了一個列表,叫做 符號表。
對于外部的C函數(shù)通過 符號 找 地址 也就給了我們機(jī)會動態(tài)的Hook外部C函數(shù)。OC是修改SEL與IMP對應(yīng)的關(guān)系,符號 也是修改符號所對應(yīng)的地址。這個動作叫做 重新綁定符號表。這也就是fishhook``hook的原理。
2.2.2驗證
在Hook NSLog前后分別調(diào)用NSLog:
NSLog(@"before");
[self hook_NSLog];
NSLog(@"after");

在
MachO中我們能看到懶加載和非懶加載符號表,dyld綁定過程中綁定的是非懶加載符號和弱符號的。NSLog是懶加載符號,只有調(diào)用的時候才去綁定。
在MachO中可以看到_NSLog的Data(值)是0000000100006960。offset為:0x8010
在第一個NSLog處打個斷點 運行查看:
主程序開始0x0000000100b24000,ASLR是0xb24000:
0x0000000100b24000 + 0x8010中存儲的內(nèi)容為0x0100b2a960。
0000000100006960 + 0xb24000 (ASLR) = 0x100B2A960。
所以這里就對應(yīng)上了。0x0100b2a960這個地址就是(符號表中的值其實是一個代碼的地址,指向本地代碼。)。

執(zhí)行完第一個NSLog后(hook前):

符號表指向了
NSLog。執(zhí)行完
hook后:
符號表指向了
HP_NSLog。
這也就是fishhook能夠Hook的真正原因(修改懶加載符號表)。
2.3 符號綁定過程(間接)
剛才在上面NSLog第一次執(zhí)行之前我們拿到的地址0x0100b2a960實際上指向一段本地代碼,加上ASLR后執(zhí)行對應(yīng)地址的代碼然后就修改了懶加載符號表。
那么這個過程究竟是怎么做的呢?
先說明一些符號的情況:
- 本地符號:只能本
MachO用。 - 全局符號:暴露給外面用。
- 間接符號:當(dāng)我們要調(diào)用外部函數(shù)/方法時,在編譯時期地址是不知道的。比如系統(tǒng)的
NSLog。
間接符號專門有個符號表Indirect Symbols:

Symbols包含了所有的符號。
有以下代碼:
NSLog(@"外部函數(shù)第一次調(diào)用");
NSLog(@"外部函數(shù)第二次調(diào)用");
斷點斷到第一個NSLog,可以看到兩次調(diào)用NSLog是同一個地址0x100e12998:

比首地址大
0x0000000100e0c000,所以這個地址在本MachO中。0x100e12998 - 0x0000000100e0c000 = 0x6998。
6998在MachO的Symbol Stubs中:

這個就是
NSLog的樁(外部符號的樁),值為1F2003D570B3005800021FD6(代碼),這個代碼是:
這個時候就對應(yīng)上了:

執(zhí)行樁中的代碼:

這段代碼的意思是執(zhí)行樁中的代碼找到符號表中代碼跳轉(zhuǎn)執(zhí)行(0000000100006A28)。
6A28這段代碼在__stub_helper中:

這里執(zhí)行的是符號綁定。
繼續(xù)動態(tài)調(diào)試:

這塊是剛好對應(yīng)上。
繼續(xù)進(jìn)去:

繼續(xù)進(jìn)去:

對應(yīng)上了。實際上執(zhí)行的是
dyld_stub_binder。也就是說懶加載符號表里面的初始值都是執(zhí)行符號綁定的函數(shù)。
dyld_stub_binder是外部函數(shù),那么怎么得到的dyld_stub_binder函數(shù)呢?

在MachO中x16是0x100008000:

這個符號在非懶加載表中(一運行就綁定):

所以
dyld_stub_binder是通過去非懶加載表中查找。驗證 :

驗證確認(rèn),
No-Lazy Symbol Pointers表中默認(rèn)值是0。
符號綁定過程:
- 程序一運行,先綁定
No-Lazy Symbol Pointers表中dyld_stub_binder的值。 - 調(diào)用
NSLog先找樁,執(zhí)行樁中的代碼。樁中的代碼是找懶加載符號表中的代碼去執(zhí)行。 - 懶加載符號表中的初始值是本地的源代碼,這個代碼去
NoLazy表中找綁定函數(shù)地址。 - 進(jìn)入
dyld的binder函數(shù)進(jìn)行綁定。
binder函數(shù)執(zhí)行完畢后就調(diào)用第一次的NSLog了。這個時候再看一下懶加載符號表中的符號:

符號已經(jīng)變了。這個時候符號就已經(jīng)綁定成功了。
接著執(zhí)行第二次NSLog,這個時候依然是去找樁中的代碼執(zhí)行:

這個繼續(xù)執(zhí)行就執(zhí)行到Foundation框架的NSLog了(已經(jīng)綁定過了,不需要繼續(xù)綁定):

這個時候通過樁直接跳到了真實地址(還是虛擬的)。這個做的原因是符號表中保存地址執(zhí)行代碼,代碼是保存在代碼段的(樁)。

- 外部函數(shù)調(diào)用時執(zhí)行樁中的代碼(
__TEXT,__stubs)。 - 樁中的代碼去懶加載符號表中找地址執(zhí)行(
__DATA,__la_symbo_ptrl)。- 通過懶加載符號表中的地址去執(zhí)行。要么直接調(diào)用函數(shù)地址(綁定過了),要么去
__TEXT,__stubhelper中找綁定函數(shù)進(jìn)行綁定。懶加載符號表中默認(rèn)保存的是尋找binder的代碼。
- 通過懶加載符號表中的地址去執(zhí)行。要么直接調(diào)用函數(shù)地址(綁定過了),要么去
- 懶加載中的代碼去
__TEXT,__stubhelper中執(zhí)行綁定代碼(binder函數(shù))。 - 綁定函數(shù)在非懶加載符號表中(
__DATA._got),程序運行就綁定好了dyld。
2.4 通過符號找字符串
上面使用fishhook的時候我們是通過rebindNSLog.name = "NSLog";告訴fishhook要hook NSLog。那么fishhook通過NSLog怎么找到的符號的呢?
首先,我們清楚在綁定的時候是去Lazy Symbol中去找的NSLog對應(yīng)的綁定代碼:

找的是
0x00008008這個地址,在Lazy Symbol中NSLog排在第一個。
在Indirect Symbols中可以看到順序和Lazy Symbols中相同,也就是要找Lazy Symbols中的符號,只要找到Indirect Symbols中對應(yīng)的第幾個就可以了。

那么怎么確認(rèn)Indirect Symbols中的第幾個呢?
在Indirect Symbols中data對應(yīng)值(十六進(jìn)制)這里NSLog是101,這個代表著NSLog在總的符號表(Symbols)中的角標(biāo):

在這里我們可以看到NSLog在String Table中偏移為0x98(十六進(jìn)制)。

通過偏移值計算得到
0xCC38就確認(rèn)到了_NSLog(長度+首地址)。
這里通過
.隔開,函數(shù)名前面有_。
這樣我們就從Lazy Symbols -> Indirect Symbols -> Symbols - > String Table 通過符號找到了字符串。那么fishhook的過程就是這么處理的,通過遍歷所有符號和要hook的數(shù)組中的字符串做對比。
在fishhook中有一張圖說明這個關(guān)系:

這里是通過符號查找
close字符串。
-
Lazy Symbol Pointer Table中closeindex為1061。 - 在
Indirect Symbol Table1061對應(yīng)的角標(biāo)為0X00003fd7(十進(jìn)制16343)。 - 在
Symbol Table找角標(biāo)16343對應(yīng)的字符串表中的偏移值70026。 - 在
String Table中找首地址+偏移值(70026)就找到了close
字符串。
實際的原理還是通過傳遞的字符串找到符號進(jìn)行替換:通過字符串找符號過程:
- 在
String Table中找到字符串計算偏移值。 - 通過偏移值在
Symbols中找到角標(biāo)。 - 通過角標(biāo)在
Indirect Symbols中找到對應(yīng)的符號。這個時候就能拿到這個符號的index了。 - 通過找到的
index在Lazy Symbols中找到對應(yīng)index的符號。
2.5 去掉符號&恢復(fù)符號
符號本身在MachO文件中,占用包體積大小 ,在我們分析別人的App時符號是去掉的。
2.5.1 去除符號
符號基本分為:全局符號、間接符號(導(dǎo)出&導(dǎo)入)、本地符號。
對于App來說會去掉所有符號(間接符號除外)。對于動態(tài)庫來說要保留全局符號(外部要調(diào)用)。
去掉符號在Build setting中設(shè)置:

-
Deployment Postprocessing:設(shè)置為YES則在編譯階段去符號,否則在打包階段去符號。 -
Strip Style:All Symbols去掉所有符號(間接除外),Non-Global Symbols去掉除全局符號外的符號。Debugging Symbols去掉調(diào)試符號。
設(shè)置Deployment Postprocessing為YES,Strip Style為All Symbols。編譯查看多了一個.bcsymbolmap文件,這個文件就是bitcode。

這個時候的MachO文件中Symbols就只剩下間接符號表中的符號了:

其中
value為函數(shù)的實現(xiàn)地址(imp)。間接符號不會找到符號表中地址執(zhí)行,是找Lazy Symbol Table中的地址。
代碼中打斷點就斷不住了:

要斷住NSLog就要打符號斷點了:

bt看下調(diào)用棧:

發(fā)現(xiàn)自定義方法全是
unnamed,這個很明顯就是去掉符號的。這種情況下就不好分析代碼了。如果是
oc方法調(diào)用則直接讀取x0,x1就能獲取self和cmd:
在這里我們就要下斷點在方法調(diào)用之前,可以通過下地址斷點。

先計算出偏移值,下次直接
ASLR+偏移值直接斷點。這個也就是動態(tài)調(diào)試常用的方法。
2.5.2 恢復(fù)符號
前面動態(tài)調(diào)試下斷點比較麻煩,如果能恢復(fù)符號的話就方便很多了。
在上面的例子中去掉所有符號后Symbol Table中只有間接符號了。雖然符號表中沒有了,但是類列表和方法列表中依然存在。

這也就為我們提供了創(chuàng)建
Symbol Table的機(jī)會。可以通過
restore-symbol工具恢復(fù)符號(只能恢復(fù)oc的,runtime機(jī)制導(dǎo)致):./restore-symbol 原始Macho文件 -o 恢復(fù)后文件
./restore-symbol FishHookDemo -o recoverDemo

這個時候就恢復(fù)了,查看
MachO(恢復(fù)的符號在Symbol Table后面):
這個時候就可以重簽名后進(jìn)行動態(tài)調(diào)試了。
restore-symbol地址
2.6 fishhook源碼解析
rebind_symbols
rebind_symbols的實現(xiàn):
//第一次是拿dyld的回調(diào),之后是手動拿到所有image去調(diào)用。這里因為沒有指定image所以需要拿到所有的。
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
//prepend_rebindings的函數(shù)會將整個 rebindings 數(shù)組添加到 _rebindings_head 這個鏈表的頭部
//Fishhook采用鏈表的方式來存儲每一次調(diào)用rebind_symbols傳入的參數(shù),每次調(diào)用,就會在鏈表的頭部插入一個節(jié)點,鏈表的頭部是:_rebindings_head
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
//根據(jù)上面的prepend_rebinding來做判斷,如果小于0的話,直接返回一個錯誤碼回去
if (retval < 0) {
return retval;
}
//根據(jù)_rebindings_head->next是否為空判斷是不是第一次調(diào)用。
if (!_rebindings_head->next) {
//第一次調(diào)用的話,調(diào)用_dyld_register_func_for_add_image注冊監(jiān)聽方法.
//已經(jīng)被dyld加載的image會立刻進(jìn)入回調(diào)。之后的image會在dyld裝載的時候觸發(fā)回調(diào)。這里相當(dāng)于注冊了一個回調(diào)到 _rebind_symbols_for_image 函數(shù)。
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
//不是第一次調(diào)用,遍歷已經(jīng)加載的image,進(jìn)行的hook
uint32_t c = _dyld_image_count();//這個相當(dāng)于 image list count
for (uint32_t i = 0; i < c; i++) {
//遍歷重新綁定image header aslr
_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
}
}
return retval;
}
- 首先通過
prepend_rebindings函數(shù)生成鏈表,存放所有要Hook的函數(shù)。 - 根據(jù)
_rebindings_head->next是否為空判斷是不是第一次調(diào)用,第一次調(diào)用走系統(tǒng)的回調(diào),第二次則自己獲取所有的image list進(jìn)行遍歷。 - 最后都會走
_rebind_symbols_for_image函數(shù)。
image list驗證:
image.png
_rebind_symbols_for_image
//兩個參數(shù) header 和 ASLR
static void _rebind_symbols_for_image(const struct mach_header *header,
intptr_t slide) {
//_rebindings_head 參數(shù)是要交換的數(shù)據(jù),head的頭
rebind_symbols_for_image(_rebindings_head, header, slide);
}
這里直接調(diào)用了rebind_symbols_for_image,傳遞了head鏈表地址。
rebind_symbols_image
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel) {
struct rebindings_entry *rebindings_head = NULL;
int retval = prepend_rebindings(&rebindings_head, rebindings, rebindings_nel);
//如果指定image就直接調(diào)用了 rebind_symbols_for_image,沒有遍歷了。
rebind_symbols_for_image(rebindings_head, (const struct mach_header *) header, slide);
if (rebindings_head) {
free(rebindings_head->rebindings);
}
free(rebindings_head);
return retval;
}
底層和rebind_symbols都調(diào)用到了rebind_symbols_for_image,由于給定了image所以不需要循環(huán)遍歷。
rebind_symbols_for_image
//回調(diào)的最終就是這個函數(shù)! 三個參數(shù):要交換的數(shù)組 、 image的頭 、 ASLR的偏移
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {
/*dladdr() 可確定指定的address 是否位于構(gòu)成進(jìn)程的進(jìn)址空間的其中一個加載模塊(可執(zhí)行庫或共享庫)內(nèi),如果某個地址位于在其上面映射加載模塊的基址和為該加載模塊映射的最高虛擬地址之間(包括兩端),則認(rèn)為該地址在加載模塊的范圍內(nèi)。如果某個加載模塊符合這個條件,則會搜索其動態(tài)符號表,以查找與指定的address 最接近的符號。最接近的符號是指其值等于,或最為接近但小于指定的address 的符號。
*/
/*
如果指定的address 不在其中一個加載模塊的范圍內(nèi),則返回0 ;且不修改Dl_info 結(jié)構(gòu)的內(nèi)容。否則,將返回一個非零值,同時設(shè)置Dl_info 結(jié)構(gòu)的字段。
如果在包含address 的加載模塊內(nèi),找不到其值小于或等于address 的符號,則dli_sname 、dli_saddr 和dli_size字段將設(shè)置為0 ; dli_bind 字段設(shè)置為STB_LOCAL , dli_type 字段設(shè)置為STT_NOTYPE 。
*/
// typedef struct dl_info {
// const char *dli_fname; //image 鏡像路徑
// void *dli_fbase; //鏡像基地址
// const char *dli_sname; //函數(shù)名字
// void *dli_saddr; //函數(shù)地址
// } Dl_info;
Dl_info info;//拿到image的信息
//dladdr函數(shù)就是在程序里面找header
if (dladdr(header, &info) == 0) {
return;
}
//準(zhǔn)備從MachO里面去找!
segment_command_t *cur_seg_cmd;//臨時變量
//這里與MachOView中看到的對應(yīng)
segment_command_t *linkedit_segment = NULL;//SEG_LINKEDIT
struct symtab_command* symtab_cmd = NULL;//LC_SYMTAB 符號表地址
struct dysymtab_command* dysymtab_cmd = NULL;//LC_DYSYMTAB 動態(tài)符號表地址
//cur為了跳過header的大小,找loadCommands cur = 首地址 + mach_header大小
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
//循環(huán)load commands找對應(yīng)的 SEG_LINKEDIT LC_SYMTAB LC_DYSYMTAB
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
//這里`SEG_LINKEDIT`獲取和`LC_SYMTAB`與`LC_DYSYMTAB`不同是因為在`MachO`中分別對應(yīng)`LC_SEGMENT_64(__LINKEDIT)`、`LC_SYMTAB`、`LC_DYSYMTAB`
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;
}
}
//有任何一項為空就直接返回,nindirectsyms表示間接符號表中符號數(shù)量,沒有則直接返回
if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
!dysymtab_cmd->nindirectsyms) {
return;
}
// Find base symbol/string table addresses
//符號表和字符串表都屬于data段中的linkedit,所以以linkedit基址+偏移量去獲取地址(這里的偏移量不是整個macho的偏移量,是相對基址的偏移量)
//鏈接時程序的基址 = __LINKEDIT.VM_Address -__LINKEDIT.File_Offset + silde的改變值
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
//printf("地址:%p\n",linkedit_base);
//符號表的地址 = 基址 + 符號表偏移量
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)
//動態(tài)(間接)符號表地址 = 基址 + 動態(tài)符號表偏移量
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) {
//尋找到load command 中的data【LC_SEGEMNT_64(__DATA)】,相當(dāng)于拿到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;
//找懶加載表(lazy symbol table)
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
//找到直接調(diào)用函數(shù) perform_rebinding_with_section,這里4張表就都已經(jīng)找到了。傳入要hook的數(shù)組、ASLR、以及4張表
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
//非懶加載表(Non-Lazy symbol table)
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
}
}
}
}
- 找到
SEG_LINKEDIT、LC_SYMTAB、LC_DYSYMTAB的load commans。
SEG_LINKEDIT獲取和LC_SYMTAB與LC_DYSYMTAB不同是因為在Load Commands中本來就不同,我們解析其它字段也要做類似操作。具體如下:
image.png
- 根據(jù)
linkedit和偏移值分別找到符號表的地址和字符串表的地址以及間接符號表地址。 - 遍歷
load commands和data段找到懶加載符號表和非懶加載符號表。 - 找到表的同時就直接調(diào)用
perform_rebinding_with_section進(jìn)行hook替換函數(shù)符號。
perform_rebinding_with_section
//rebindings:要hook的函數(shù)鏈表,可以理解為數(shù)組
//section:懶加載/非懶加載符號表地址
//slide:ASLR
//symtab:符號表地址
//strtab:字符串標(biāo)地址
//indirect_symtab:動態(tài)(間接)符號表地址
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字段指明對應(yīng)的indirect symbol table起始的index。也就是第幾個這里是和間接符號表中相對應(yīng)的
//這里就拿到了index
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
//slide+section->addr 就是符號對應(yīng)的存放函數(shù)實現(xiàn)的數(shù)組也就是我相應(yīng)的__nl_symbol_ptr和__la_symbol_ptr相應(yīng)的函數(shù)指針都在這里面了,所以可以去尋找到函數(shù)的地址。
//indirect_symbol_bindings中是數(shù)組,數(shù)組中是函數(shù)指針。相當(dāng)于lazy和non-lazy中的data
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中的數(shù)據(jù)
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作為下標(biāo),訪問symbol table,拿到string table 的偏移值
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
//獲取到symbol_name 首地址 + 偏移值。(char* 字符的地址)
char *symbol_name = strtab + strtab_offset;
//判斷是否函數(shù)的名稱是否有兩個字符,因為函數(shù)前面有個_,所以方法的名稱最少要1個
bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
//遍歷最初的鏈表,來判斷名字進(jìn)行hook
struct rebindings_entry *cur = rebindings;
while (cur) {
for (uint j = 0; j < cur->rebindings_nel; j++) {
//這里if的條件就是判斷從symbol_name[1]兩個函數(shù)的名字是否都是一致的,以及判斷字符長度是否大于1
if (symbol_name_longer_than_1 &&
strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
//判斷replaced的地址不為NULL 要替換的自己實現(xiàn)的方法和rebindings[j].replacement的方法不一致。
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
//讓rebindings[j].replaced保存indirect_symbol_bindings[i]的函數(shù)地址,相當(dāng)于將原函數(shù)地址給到你定義的指針的指針。
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
//替換內(nèi)容為自己自定義函數(shù)地址,這里就相當(dāng)于替換了內(nèi)存中的地址,下次樁直接找到lazy/non-lazy表的時候直接就走這個替換的地址了。
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
//替換完成跳轉(zhuǎn)外層循環(huán),到(懶加載/非懶加載)數(shù)組的下一個數(shù)據(jù)。
goto symbol_loop;
}
}
//沒有找到就找自己要替換的函數(shù)數(shù)組的下一個函數(shù)。
cur = cur->next;
}
symbol_loop:;
}
}
- 首先通過懶加載/非懶加載符號表和間接符號表找到所有的
index。 - 將懶加載/非懶加載符號表的
data放入indirect_symbol_bindings數(shù)組中。
indirect_symbol_bindings就是存放lazy和non-lazy表中的data數(shù)組:
image.png
- 遍歷懶加載/非懶加載符號表。
- 讀取
indirect_symbol_indices找到符號在Indrect Symbol Table表中的值放入symtab_index。 - 以
symtab_index作為下標(biāo),訪問symbol table,拿到string table的偏移值。 - 根據(jù)
strtab_offset偏移值獲取字符地址symbol_name,也就相當(dāng)于字符名稱。 - 循環(huán)遍歷
rebindings也就是鏈表(自定義的Hook數(shù)據(jù)) - 判斷
&symbol_name[1]和rebindings[j].name兩個函數(shù)的名字是否都是一致的,以及判斷字符長度是否大于1。 - 相同則先保存原地址到自定義函數(shù)指針(如果
replaced傳值的話,沒有傳則不保存)。并且用要Hook的目標(biāo)函數(shù)replacement替換indirect_symbol_bindings,這里就完成了Hook。
- 讀取
-
reserved1確認(rèn)了懶加載和非懶加載符號在間接符號表中的index值。
疑問點:懶加載和非懶加載怎么和間接符號表index對應(yīng)的呢?
直接Hook dyld_stub_binder以及NSLog看下index對應(yīng)的值:

在間接符號表中非懶加載符號從
20開始供兩個,懶加載從22開始,這也就對應(yīng)上了。這也就驗證了懶加載和非懶加載符號都在間接符號表中能對應(yīng)上。
總結(jié)

libffi
inlinehook
http://www.itdecent.cn/p/7954e6cde245
越獄和防護(hù)相關(guān)內(nèi)容
https://www.desgard.com/2020/08/05/why-hook-msg_objc-can-use-asm-2.html
https://www.die.lu/core/index.php/2020/05/30/299/
dobby


