一后众、Hook概述
HOOK
中文譯為掛鉤
或鉤子
。在iOS
逆向中是指改變程序運(yùn)行流程的一種技術(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
(方法實(shí)現(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
種方式實(shí)現(xiàn)方法交換:
-
method_exchangeImplementations
:在分類中直接交換就可以了歧沪,如果不在分類需要配合class_addMethod
實(shí)現(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è)計(jì)影钉,安卓一樣可以用画髓。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ùn)行的應(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)用原方法實(shí)現(xiàn)和切面block
。AOP
庫 Stinger和BlockHook就是使用libbfi
做的。
1.1.5 inlinehook 內(nèi)聯(lián)鉤子 (靜態(tài))
Inline Hook
就是在運(yùn)行的流程中插入跳轉(zhuǎn)指令來搶奪運(yùn)行流程的一個方法互躬。大體分為三步:
- 將原函數(shù)的前
N
個字節(jié)搬運(yùn)到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)的喂柒,也就是說在編譯的時候,編譯器就知道了它的實(shí)現(xiàn)地址禾嫉,這也是為什么C
函數(shù)只寫函數(shù)聲明調(diào)用時會報(bào)錯灾杰。那么為什么fishhook
還能夠改變C
函數(shù)的調(diào)用呢?難道函數(shù)也有動態(tài)的特性存在熙参?
是否意味著C
Hook
就必須修改調(diào)用地址艳吠?那意味著要修改二進(jìn)制。(原理上使用匯編可以實(shí)現(xiàn)孽椰。fishhook
不是這么處理的)
那么系統(tǒng)函數(shù)和本地函數(shù)區(qū)別到底在哪里昭娩?
2.2.1 符號 & 符號綁定 & 符號表 & 重綁定符號
NSLog
函數(shù)的地址在編譯的那一刻并不知道NSLog
的真實(shí)地址。NSLog
在Foundation
框架中黍匾。在運(yùn)行時NSLog
的地址在 共享緩存 中题禀。在整個手機(jī)中只有dyld
知道NSLog
的真實(shí)地址。
在LLVM
編譯器生成MachO
文件時膀捷,如果讓我們做就先空著系統(tǒng)函數(shù)的地址迈嘹,等運(yùn)行起來再替換。我們知道MachO
中分為Text
(只讀)和Data
(可讀可寫)全庸,那么顯然這種方式行不通秀仲。
那么可行的方案是在Data
段放一個 占位符(8字節(jié))讓代碼編譯的時候直接bl 占位符
。在運(yùn)行的時候dyld
加載應(yīng)用的時候?qū)?code>Data段的地址修改為NSLog
真實(shí)地址壶笼,代碼bl 占位符
沒有變 神僵。這個技術(shù)就叫做 PIC(
position independent code`)位置無關(guān)代碼。(實(shí)際不是這么簡單)
- 占位符 就叫做 符號覆劈。
-
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驗(yàn)證
在Hook NSLog
前后分別調(diào)用NSLog
:
NSLog(@"before");
[self hook_NSLog];
NSLog(@"after");
在
MachO
中我們能看到懶加載和非懶加載符號表,dyld
綁定過程中綁定的是非懶加載符號和弱符號的智末。NSLog
是懶加載符號谅摄,只有調(diào)用的時候才去綁定。
在MachO
中可以看到_NSLog
的Data
(值)是0000000100006960
系馆。offset
為:0x8010
在第一個NSLog
處打個斷點(diǎn) 運(yùn)行查看:
主程序開始0x0000000100b24000
送漠,ASLR
是0xb24000
:
0x0000000100b24000 + 0x8010
中存儲的內(nèi)容為0x0100b2a960
。
0000000100006960 + 0xb24000 (ASLR) = 0x100B2A960
由蘑。
所以這里就對應(yīng)上了闽寡。0x0100b2a960
這個地址就是(符號表中的值其實(shí)是一個代碼的地址,指向本地代碼纵穿。)下隧。
執(zhí)行完第一個NSLog
后(hook前):
符號表指向了
NSLog
奢人。執(zhí)行完
hook
后:符號表指向了
HP_NSLog
谓媒。
這也就是fishhook
能夠Hook
的真正原因(修改懶加載符號表)。
2.3 符號綁定過程(間接)
剛才在上面NSLog
第一次執(zhí)行之前我們拿到的地址0x0100b2a960
實(shí)際上指向一段本地代碼何乎,加上ASLR
后執(zhí)行對應(yīng)地址的代碼然后就修改了懶加載符號表句惯。
那么這個過程究竟是怎么做的呢?
先說明一些符號的情況:
- 本地符號:只能本
MachO
用支救。 - 全局符號:暴露給外面用抢野。
- 間接符號:當(dāng)我們要調(diào)用外部函數(shù)/方法時,在編譯時期地址是不知道的各墨。比如系統(tǒng)的
NSLog
指孤。
間接符號專門有個符號表Indirect Symbols
:
Symbols
包含了所有的符號。
有以下代碼:
NSLog(@"外部函數(shù)第一次調(diào)用");
NSLog(@"外部函數(shù)第二次調(diào)用");
斷點(diǎn)斷到第一個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)上了。實(shí)際上執(zhí)行的是
dyld_stub_binder
酥艳。也就是說懶加載符號表里面的初始值都是執(zhí)行符號綁定的函數(shù)摊溶。
dyld_stub_binder
是外部函數(shù),那么怎么得到的dyld_stub_binder
函數(shù)呢玖雁?
在MachO
中x16
是0x100008000
:
這個符號在非懶加載表中(一運(yùn)行就綁定):
所以
dyld_stub_binder
是通過去非懶加載表中查找更扁。驗(yàn)證 :
驗(yàn)證確認(rèn),
No-Lazy Symbol Pointers
表中默認(rèn)值是0
赫冬。
符號綁定過程:
- 程序一運(yùn)行浓镜,先綁定
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ù)綁定):
這個時候通過樁直接跳到了真實(shí)地址(還是虛擬的)。這個做的原因是符號表中保存地址執(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
)瓤湘,程序運(yùn)行就綁定好了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)制)。
通過偏移值計(jì)算得到
0xCC38
就確認(rèn)到了_NSLog
(長度+首地址)圃伶。
這里通過
.
隔開堤如,函數(shù)名前面有_
。
這樣我們就從Lazy Symbols -> Indirect Symbols -> Symbols - > String Table
通過符號找到了字符串窒朋。那么fishhook
的過程就是這么處理的搀罢,通過遍歷所有符號和要hook
的數(shù)組中的字符串做對比。
在fishhook
中有一張圖說明這個關(guān)系:
這里是通過符號查找
close
字符串侥猩。
-
Lazy Symbol Pointer Table
中close
index
為1061
榔至。 - 在
Indirect Symbol Table
1061
對應(yīng)的角標(biāo)為0X00003fd7
(十進(jìn)制16343
)。 - 在
Symbol Table
找角標(biāo)16343
對應(yīng)的字符串表中的偏移值70026
欺劳。 - 在
String Table
中找首地址+偏移值(70026)
就找到了close
字符串唧取。
實(shí)際的原理還是通過傳遞的字符串找到符號進(jìn)行替換:通過字符串找符號過程:
- 在
String Table
中找到字符串計(jì)算偏移值。 - 通過偏移值在
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ù)的實(shí)現(xiàn)地址(imp
)超全。間接符號不會找到符號表中地址執(zhí)行咆霜,是找Lazy Symbol Table
中的地址。
代碼中打斷點(diǎn)就斷不住了:
要斷住NSLog
就要打符號斷點(diǎn)了:
bt
看下調(diào)用棧:
發(fā)現(xiàn)自定義方法全是
unnamed
嘶朱,這個很明顯就是去掉符號的蛾坯。這種情況下就不好分析代碼了。如果是
oc
方法調(diào)用則直接讀取x0疏遏,x1
就能獲取self
和cmd
:在這里我們就要下斷點(diǎn)在方法調(diào)用之前脉课,可以通過下地址斷點(diǎn)。
先計(jì)算出偏移值财异,下次直接
ASLR+偏移值
直接斷點(diǎn)倘零。這個也就是動態(tài)調(diào)試常用的方法。
2.5.2 恢復(fù)符號
前面動態(tài)調(diào)試下斷點(diǎn)比較麻煩戳寸,如果能恢復(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
的實(shí)現(xiàn):
//第一次是拿dyld的回調(diào)锈拨,之后是手動拿到所有image去調(diào)用砌庄。這里因?yàn)闆]有指定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é)點(diǎn)佛纫,鏈表的頭部是:_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
驗(yàn)證:
_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`不同是因?yàn)樵赻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;
}
}
//有任何一項(xiàng)為空就直接返回稀拐,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
不同是因?yàn)樵?code>Load Commands中本來就不同恐仑,我們解析其它字段也要做類似操作泉坐。具體如下:
- 根據(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ù)實(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ù)的名稱是否有兩個字符,因?yàn)楹瘮?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 要替換的自己實(shí)現(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ù)組:
- 遍歷懶加載/非懶加載符號表。
- 讀取
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
值。
疑問點(diǎn):懶加載和非懶加載怎么和間接符號表index
對應(yīng)的呢往踢?
直接Hook
dyld_stub_binder
以及NSLog
看下index
對應(yīng)的值:
在間接符號表中非懶加載符號從
20
開始供兩個腾誉,懶加載從22
開始,這也就對應(yīng)上了峻呕。這也就驗(yàn)證了懶加載和非懶加載符號都在間接符號表中能對應(yīng)上利职。
總結(jié)
libffi
inlinehook
http://www.reibang.com/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