前言
本篇文章開始給大家分享下Hook(鉤子)
的原理吏廉,包括iOS系統(tǒng)原生的Method Swizzle
,還有很有名的Hook第三方框架垮兑,例如fishHook
抚芦、Cydia Substrate
以及inlineHook
等,然后會重點(diǎn)介紹下fishHook
的底層處理流程洋丐,希望大家能夠跟著實(shí)操一遍先朦。
一霸妹、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. 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)系??
方法交換的實(shí)現(xiàn)方式
主要有3種??
-
method_exchangeImplementations
?? 在分類
中直接交換就可以了雳刺,如果不在分類
劫灶,需要配合class_addMethod
實(shí)現(xiàn)跳回到原方法
。 -
class_replaceMethod
?? 直接替換原方法掖桦。 -
method_setImplementation
?? 重新賦值原方法浑此,通過getImp
和setImp
配合。
具體使用案例可以參考我之前寫的文章 ?? 11-代碼注入(??注意:拉到最后面
??)
2. fishhook
是Facebook
提供的一個(gè)動態(tài)
修改鏈接MachO文件
的工具滞详。利用MachO文件加載原理凛俱,通過修改懶加載
和非懶加載
兩個(gè)表的指針,達(dá)到C函數(shù)(系統(tǒng)C函數(shù))
HOOK的目的料饥。
大概流程 ?? dyld 更新 Mach-O 二進(jìn)制的 __DATA segment
的__la_symbol_str
中的指針蒲犬,使用 rebind_symbol
方法更新兩個(gè)符號位置來進(jìn)行符號的重新綁定
。后面我會詳細(xì)的分析底層的流程岸啡。
3. Cydia Substrate
Cydia Substrate
原名為 Mobile Substrate
原叮,主要作用是針對OC方法
、C函數(shù)
以及函數(shù)地址
進(jìn)行HOOK
操作。并不僅僅針對iOS
而設(shè)計(jì)奋隶,安卓一樣可以用擂送。
Cydia Substrate結(jié)構(gòu)
Cydia Substrate
主要分為3部分:Mobile Hooker
、MobileLoader
唯欣、safe mode
嘹吨。
- Mobile Hooker
它定義了一系列的宏和函數(shù),底層調(diào)用objc
的runtime
和fishhook
來替換系統(tǒng)或者目標(biāo)應(yīng)用的函數(shù)境氢。其中有兩個(gè)函數(shù):
-
MSHookMessageEx
:主要作用于OC
方法 MSHookMessageExvoid MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP result)
-
MSHookFunction
:(inline hook
)主要作用于C
和C++
函數(shù) MSHookFunction蟀拷。Logos
語法的%hook
就是對這個(gè)函數(shù)做了一層封裝。void MSHookFunction(voidfunction,void* replacement,void** p_original)
MobileLoader
MobileLoader
用于加載第三方dylib
在運(yùn)行的應(yīng)用程序萍聊。啟動時(shí)MobileLoader
會根據(jù)規(guī)則把指定目錄的第三方的動態(tài)庫加載進(jìn)去问芬,第三方的動態(tài)庫也就是我們寫的破解程序。safe mode
破解程序本質(zhì)是dylib
寄生在別人進(jìn)程里寿桨。 系統(tǒng)進(jìn)程一旦出錯(cuò)此衅,可能導(dǎo)致整個(gè)進(jìn)程崩潰,崩潰后就會造成iOS
癱瘓亭螟。所以CydiaSubstrate
引入了安全模式炕柔,在安全模式下所有基于CydiaSubstratede
的三方dylib
都會被禁用,便于查錯(cuò)與修復(fù)媒佣。
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)用時(shí)也糊,最終會調(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
。
5. inlinehook
Inline Hook
就是在運(yùn)行的流程中插入跳轉(zhuǎn)指令來搶奪運(yùn)行流程的一個(gè)方法虑省。大體分為三步??
- 將
原函數(shù)
的前 N 個(gè)字節(jié)搬運(yùn)到Hook 函數(shù)
的前 N 個(gè)字節(jié); - 然后將
原函數(shù)
的前 N 個(gè)字節(jié)填充跳轉(zhuǎn)
到Hook 函數(shù)
的跳轉(zhuǎn)指令僧凰; - 在
Hook 函數(shù)末尾
幾個(gè)字節(jié)填充跳轉(zhuǎn)回原函數(shù)
+N 的跳轉(zhuǎn)指令探颈;
大致流程如下圖??
其中, Cydia Substrate
框架中的MSHookFunction
就是使用的inlinehook
原理训措。
Dobby
Dobby(原名:HOOKZz)
是一個(gè)全平臺
的inlineHook框架
伪节,它用起來就和fishhook
一樣光羞。
Dobby
通過 mmap
把整個(gè) Mach-O 文件映射到用戶的內(nèi)存空間,寫入完成保存本地怀大。所以 Dobby
并不是在原 Mach-O 上進(jìn)行操作纱兑,而是重新生成并替換
。
Dobby
是通過插入 __zDATA
段和 __zTEXT
段到 Mach-O 中化借。
-
__zDATA
?? 記錄 Hook 信息(Hook 數(shù)量潜慎、每個(gè) Hook 方法的地址)、每個(gè) Hook 方法的信息(函數(shù)地址屏鳍、跳轉(zhuǎn)指令地址、寫 Hook 函數(shù)的接口地址)局服、每個(gè) Hook 的接口(指針)钓瞭。
*__zText
?? 記錄每個(gè) Hook 函數(shù)的跳轉(zhuǎn)指令。
二淫奔、fishHook
2.1 fishhook的使用
首先我們看看fishhook
是如何使用的 ?? 當(dāng)然看.h頭文件??
/*
* A structure representing a particular intended rebinding from a symbol
* name to its replacement
*/
struct rebinding {
const char *name;//需要HOOK的函數(shù)名稱山涡,C字符串
void *replacement;//新函數(shù)的地址
void **replaced;//原始函數(shù)地址的指針!
};
/*
* For each rebinding in rebindings, rebinds references to external, indirect
* symbols with the specified name to instead point at replacement for each
* image in the calling process as well as for all future images that are loaded
* by the process. If rebind_functions is called more than once, the symbols to
* rebind are added to the existing list of rebindings, and if a given symbol
* is rebound more than once, the later rebinding will take precedence.
*/
FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
/*
* Rebinds as above, but only in the specified image. The header should point
* to the mach-o header, the slide should be the slide offset. Others as above.
*/
FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel);
很簡單唆迁,只提供了一個(gè)結(jié)構(gòu)體rebinding
和兩個(gè)函數(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個(gè)函數(shù)
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);
-
header
?? image的Header -
slide
?? ASLR -
rebindings[]
?? 存放rebinding結(jié)構(gòu)體的數(shù)組(可以同時(shí)交換多個(gè)函數(shù)) -
rebindings_nel
?? rebindings數(shù)組的長度
示例演示
示例一:HOOK NSLog
現(xiàn)在我們使用fishHook
hook一下系統(tǒng)的NSLog函數(shù),代碼??
- (void)hook_NSLog {
struct rebinding rebindNSLog;
rebindNSLog.name = "NSLog";
rebindNSLog.replacement = LG_NSLog;
rebindNSLog.replaced = (void *)&sys_NSLog;
struct rebinding rebinds[] = {rebindNSLog};
rebind_symbols(rebinds, 1);
}
//原函數(shù)于颖,函數(shù)指針
static void (*sys_NSLog)(NSString *format, ...);
//新函數(shù)
void LG_NSLog(NSString *format, ...) {
format = [format stringByAppendingFormat:@"被 Hook了4艋摺!森渐!"];
//調(diào)用系統(tǒng)NSLog
sys_NSLog(format);
}
調(diào)用代碼??
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"hello");
}
run??
此時(shí)就已經(jīng)Hook住NSLog做入,走到了LG_NSLog中。
Hook代碼調(diào)用完畢同衣,sys_NSLog
保存系統(tǒng)NSLog原地址
母蛛,NSLog
就指向LG_NSLog
。
示例二: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 = LG_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 LG_func(const char * str) {
NSLog(@"Hook func");
original_func(str);
}
調(diào)用代碼??
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self hook_func];
func("hello");
}
運(yùn)行??
我們發(fā)現(xiàn)毯焕,此時(shí)是沒有Hook
到func函數(shù)的。由此得出??
自定義的函數(shù)
fishhook不能hook
修肠,系統(tǒng)的函數(shù)
fishhook可以hook
秫逝。
2.2 fishhook原理
fishhook
可以HOOK C函數(shù)
恕出,但是我們知道函數(shù)是靜態(tài)
的,也就是說在編譯
的時(shí)候就確定了實(shí)現(xiàn)地址
违帆,這也是C函數(shù)只寫函數(shù)聲明浙巫,在調(diào)用時(shí)會報(bào)錯(cuò)的原因。那么為什么fishhook
還能夠改變C函數(shù)的調(diào)用
呢刷后?是否像Method Swizzle
一樣改變了函數(shù)實(shí)現(xiàn)的地址的畴?帶著這些問題,我們繼續(xù)往下看尝胆。
首先我們得弄清楚 ?? 系統(tǒng)函數(shù)
和本地函數(shù)
有什么區(qū)別丧裁?
2.2.1 符號 & 符號綁定 & 符號表 & 重綁定符號
NSLog
函數(shù)的地址在編譯的那一刻,我們的App程序并不知道NSLog函數(shù)實(shí)現(xiàn)的真實(shí)地址
?? 因?yàn)?code>NSLog在Foundation
框架中含衔,在運(yùn)行時(shí)NSLog
函數(shù)的實(shí)現(xiàn)地址在 共享緩存
中煎娇。只有系統(tǒng)的dyld
知道這個(gè)真實(shí)地址。
在LLVM
編譯器生成MachO
文件時(shí)贪染,我們知道MachO中分為Text(只讀)
和Data(可讀可寫)
缓呛,如果先空著
系統(tǒng)函數(shù)的地址,等運(yùn)行起來
再替換
系統(tǒng)函數(shù)的地址杭隙,顯然這種方式行不通哟绊,因?yàn)槟悴恢酪斩嗌倏臻g,而且也比較浪費(fèi)空間痰憎。
可行的方案 ?? 在Data段
放一個(gè) 占位符(8字節(jié))
匿情,讓代碼編譯的時(shí)候直接bl 占位符
。在運(yùn)行的時(shí)候(即dyld
加載應(yīng)用的時(shí)候)信殊,將Data段的地址修改
為NSLog真實(shí)
地址炬称,代碼bl 占位符
此時(shí)不變 ,這樣就能保證運(yùn)行NSLog
時(shí)執(zhí)行的是真實(shí)的實(shí)現(xiàn)代碼涡拘。這個(gè)技術(shù)就叫做 PIC(position independent code)
位置無關(guān)代碼玲躯。(當(dāng)然實(shí)際的實(shí)現(xiàn)并不是這么簡單)
-
占位符
就叫做符號
-
dyld
將data段
符號進(jìn)行修改
的這個(gè)過程叫做符號綁定
- 一個(gè)又一個(gè)的符號放在一起形成了一個(gè)列表,叫做
符號表
所以鳄乏,外部的C函數(shù)是通過符號
找 地址
跷车, 那么,我們就有機(jī)會動態(tài)的Hook外部C函數(shù)橱野。OC的Method Swizzle
是修改SEL與IMP
對應(yīng)的關(guān)系朽缴,對于符號
, 當(dāng)然也能修改符號所對應(yīng)的地址
水援。這個(gè)動作叫做 重新綁定符號表
密强。這也就是fishhook
hook的原理茅郎。
2.2.2 示例驗(yàn)證
首先在Hook NSLog前后分別調(diào)用NSLog??
NSLog(@"Hook 前");
[self hook_NSLog];
NSLog(@"Hook 后");
接著編譯,查看Mach-O的懶加載和非懶加載符號表??
我們在懶加載表中找到NSLog或渤,說明NSLog是懶加載符號
?? 只有調(diào)用的時(shí)候才去綁定系冗。
在MachO中可以看到_NSLog的Data(值)
是10000064EC
,offset值
為0x8010
薪鹦。
綁定前的地址
然后我們在 NSLog(@"Hook 前");
打上斷點(diǎn)掌敬,lldb調(diào)試如下??
我們通過image list
指令,查看程序的起始地址是0x0000000100624000
池磁,其中ASLR的值是0x624000
奔害。接著我們打開匯編調(diào)試??
然后進(jìn)入NSLog
??
最終,我們得到NSLog在內(nèi)存中的地址是0x00000001043464ec
地熄。
回到Mach-O中华临,NSLog的Data值是0x10000064EC
+ ASLR值0x4340000
= 0x00000001043464ec
。那么我們由此可以推斷出??
Mach-O
中記錄的NSLog
的Data值是沒有ASLR(虛擬地址偏移)
的离斩。
綁定后的地址
繼續(xù)運(yùn)行斷點(diǎn)到綁定后的NSLog银舱,同理瘪匿,查看地址??
程序起始地址0x0000000104340000
+ NSLog的偏移地址0x8010
得到了NSLog的真實(shí)地址跛梗,然后通過lldb的x
指令查看起始的8字節(jié)中存儲的值是地址0x0104345650
,再通過dis -s
查看改地址對應(yīng)的匯編代碼棋弥,發(fā)現(xiàn)就是LG_NSLog
方法核偿。由此可見??
懶加載符號表
里面綁定的地址已經(jīng)改變了。
2.3 符號綁定過程
接下來顽染,我們來分析一下漾岳,上面懶加載符號表中,綁定的地址發(fā)生變化的過程粉寞,也就是符號綁定的過程
尼荆。
- iOS中函數(shù)名、變量名唧垦、方法名捅儒、編譯完成后會生成一張
符號表
- 符號有2種類型 ??
內(nèi)部符號
&外部符號
2.3.1 內(nèi)部符號:內(nèi)部函數(shù),方法名稱
如ViewDidLoad
振亮。內(nèi)部符號又分為??
-
本地符號
?? 自己內(nèi)部使用的 -
全局符號
?? 外部也可以使用
示例演示
新建工程symbolTest
巧还,定義一個(gè)全局函數(shù)代碼??
//全局符號,可以暴露給外界
void test(){
}
本地函數(shù)??
//本地符號 作用域相當(dāng)于本文件
static void test1(){
NSLog(@"test1");
}
??注意:App在上架時(shí)會
去符號
坊秸,去的是本地符號
麸祷。
我們可以通過dump指令查看Mach-O中的所有符號??
objdump --macho -t
xxx(你的MachO文件名稱)
再使用MachOView查看??
符號表
Symbols
包含所有的符號
?? 本地符號,全局符號褒搔,間接符號阶牍。
2.3.2 外部符號(間接符號表
)
MachO
文件中調(diào)用外部方法名稱
喷面,如NSLog
,LLVM編譯時(shí)期并不知道
外部(MachO文件以外)方法的地址荸恕。
間接符號
有個(gè)專門的符號表Indirect Symbols
乖酬,用到的外部符號例如NSStringFromClass
,編譯時(shí)會生成一個(gè)符號??
2.3.3 符號綁定過程
接著回到正題融求,看看符號綁定過程
咬像。首先,有以下代碼??
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"外部函數(shù)第一次調(diào)用");
NSLog(@"外部函數(shù)第二次調(diào)用");
}
斷點(diǎn)斷到第一個(gè)NSLog生宛,查看匯編??
可以看到兩次調(diào)用NSLog是同一個(gè)地址0x102c06524
县昂,并且通過image list
得到程序的起始地址是0x0000000102c00000
,那么0x1049ee524
- 0x00000001049e8000
= 0x6524
陷舅,而0x6524
是??
0x6524
在MachO的Symbol Stubs
中倒彰。這個(gè)就是NSLog的樁(外部符號的樁)
,值為1F2003D510D7005800021FD6
(是代碼
)莱睁,這個(gè)代碼是??
查看第一句匯編的地址0x1049ee524
存儲的值??
就是NSLog樁
的值4洹!仰剿!
繼續(xù)執(zhí)行完NSLog代碼??
上圖可知创淡,執(zhí)行完NSLog匯編后,通過讀取x16寄存器的值(即返回值
)可知??
執(zhí)行
Symbol Stubs
樁中的代碼來找到Symbol Stubs
符號中的代碼南吮。
至此琳彩,我們定位到了地址00000001000065CC
,而65CC
地址在__stub_helper
中??
其中部凑,綠框中執(zhí)行的b 0x1000065b4
就是符號綁定
的過程露乏,我們繼續(xù)執(zhí)行匯編代碼??
上圖的第一句匯編在MachO中其實(shí)對應(yīng)的就是adr x17,12204
,因?yàn)?code>0x1049ee5b4 - 0x00000001049e8000
(程序起始地址) = 0x65B4
涂邀。
繼續(xù)執(zhí)行瘟仿,進(jìn)去??
而MachO中的dyld_stub_binder
是??
和上面的匯編就一一對應(yīng)上了!1让恪劳较!????????????
實(shí)際上執(zhí)行的是dyld_stub_binder
,綜上可以得出結(jié)論??
懶加載符號表
里面的初始值
都是執(zhí)行符號綁定的函數(shù)
敷搪。
但是dyld_stub_binder
也是外部符號
兴想,那接下來的問題就是 ?? 怎么找到dyld_stub_binder
這個(gè)符號呢?
繼續(xù)執(zhí)行匯編代碼赡勘,走到0x1049ee5c8: br x16
這句??
上圖我們通過lldb指令讀取x16寄存器
的地址是0x0000000181041474
嫂便,該地址正是dyld_stub_binder
的實(shí)現(xiàn)地址,那么接下來就是該值是如何計(jì)算出
的呢闸与?依舊看MachO??
這個(gè)符號在非懶加載表中(一運(yùn)行就綁定)??
綜上所述毙替,第一次
符號綁定的過程??
- 程序一運(yùn)行岸售,先綁定
No-Lazy Symbol Pointers
表中dyld_stub_binder
函數(shù)的值。 - 調(diào)用
NSLog
時(shí)厂画,先找Symbol Stubs樁
凸丸,執(zhí)行樁中的代碼,樁中的代碼是對應(yīng)找到懶加載符號表
中的代碼去執(zhí)行袱院。 - 懶加載符號表中的初始值是
本地的源代碼
屎慢,這個(gè)代碼去NoLazy表
中找綁定函數(shù)地址
。 - 最后就執(zhí)行
dyld_stub_binder
函數(shù)進(jìn)行符號綁定
忽洛。
繼續(xù)執(zhí)行NSLog
第2次執(zhí)行NSLog的時(shí)候腻惠,通過樁直接跳到了真實(shí)地址,因?yàn)榉柋碇幸呀?jīng)保存了地址執(zhí)行代碼欲虚。
小結(jié)
符號綁定的整個(gè)流程圖如下??
-
外部函數(shù)
調(diào)用時(shí)執(zhí)行樁(__TEXT,__stubs)
中的代碼 - 樁中的代碼去
懶加載符號表(__DATA,__la_symbo_ptrl)
中找地址執(zhí)行- 綁定過 ?? 要么直接調(diào)用綁定的函數(shù)地址
- 未綁定 ??去
__TEXT,__stubhelper
中找綁定函數(shù)dyld_stub_binder
進(jìn)行綁定集灌。 -
懶加載符號表
中默認(rèn)保存的是尋找binder的代碼
- 懶加載中的代碼去
__TEXT,__stubhelper
中執(zhí)行綁定代碼(binder函數(shù))。 -
dyld_stub_binder
在非懶加載符號表中(__DATA._got)
复哆,程序運(yùn)行就綁定好了欣喧。
2.4 通過符號找字符串
我們使用fishhook
的時(shí)候我們是通過rebindNSLog.name = "NSLog"
;來hook NSLog
。那么fishhook
通過NSLog
字符串怎么找到的NSLog函數(shù)符號
的呢梯找?
根據(jù)上面分析的符號綁定過程
唆阿,我們知道,在綁定的時(shí)候是去Lazy Symbo
l中去找的NSLog對應(yīng)的綁定代碼
??
0x00008008
這個(gè)地址初肉,在Lazy Symbol
中NSLog排在第一個(gè)
酷鸦。在Indirect Symbols
間接符號表中可以看到順序
和Lazy Symbols
中相同
??
所以反過來饰躲,要找Lazy Symbols
中的符號牙咏,只要找到Indirect Symbols
中對應(yīng)的索引值
就可以了,那么接下來就是確定索引值
了嘹裂。
我們注意到妄壶,在上圖的間接符號表中,NSLog
對應(yīng)的Data值是000000BD(十六進(jìn)制)
寄狼,轉(zhuǎn)換成十進(jìn)制是189
丁寄,這個(gè)189
就是代表著NSLog
在總符號表(Symbols)
中的角標(biāo)
??
注意到Data中保存的是000000D4
(十六機(jī)制),這是NSLog
在String Table
中偏移值??
通過偏移值計(jì)算得到0xD334
泊愧,就找到了_NSLog(長度+首地址)
伊磺。
??注意:
.
表示分隔符,函數(shù)名前面有_
至此删咱,我們就從Lazy Symbols -> Indirect Symbols -> Symbols - > String Table
通過符號找到了字符串屑埋。fishhook
找符號的過程就是這么處理的,通過遍歷所有符號和要hook的數(shù)組中的字符串做對比痰滋。
在fishhook
gitHub網(wǎng)址中有一張圖說明這個(gè)關(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字符串
反過來摘能,那么通過字符串找符號
過程??
- 在
String Table
中找到字符串续崖,計(jì)算偏移值
- 通過
偏移值
在Symbols
中找到角標(biāo)
- 通過
角標(biāo)
在Indirect Symbols
中找到對應(yīng)的符號
,也能取到這個(gè)符號的index
- 通過找到的
index
在Lazy Symbols
中找到對應(yīng)index
的符號
团搞。
2.5 去掉符號 & 恢復(fù)符號
符號本身在MachO
文件中严望,占用包體積大小 ,在我們分析別人的App
時(shí)符號是去掉的
逻恐。
2.5.1 去掉符號
- 對于
App
來說像吻,會去掉
所有符號(間接符號
除外) - 對于
動態(tài)庫
來說要保留全局符號
(外部要調(diào)用)
脫符號的設(shè)置
去掉符號在Build setting
中設(shè)置??
Strip Style
說明??
- All Symbols去掉
所有符號
(間接除外
) - Non-Global Symbols去掉
除全局符號外
的符號 - Debugging Symbols去掉
調(diào)試符號
??注意:
Deployment Postprocessing
?? 設(shè)置為YES則在編譯階段
去符號,否則在打包階段
去符號复隆。
All Symbols
設(shè)置Deployment Postprocessing
為YES
萧豆,Strip Style
為All Symbols
,然后編譯昏名,打開包所在的位置??
查看多了一個(gè).bcsymbolmap
文件涮雷,這個(gè)文件就是bitcode
。接著我們查看MachO
文件中Symbols
總符號表??
上圖中轻局,我們看到NSLog的Value段存儲的地址是0000000000000000
洪鸭,value
為函數(shù)的實(shí)現(xiàn)地址(imp)
,所以代碼中打斷點(diǎn)就斷不住了
??
直接跑完了仑扑。要斷住NSLog就要打符號斷點(diǎn)
??
再運(yùn)行??
bt
指令查看調(diào)用棧览爵,發(fā)現(xiàn)??
frame #0: 0x0000000182762ba8 Foundation`NSLog
frame #1: 0x0000000104e51fc4 symbolTest`___lldb_unnamed_symbol2$$symbolTest + 72
說明自定義的方法test1
是unnamed
,這個(gè)很明顯就是去掉符號
的镇饮。這種情況下就不好分析代碼了蜓竹。
之前學(xué)習(xí)匯編的時(shí)候,可知道储藐,oc方法
調(diào)用則直接讀取x0俱济,x1
就能獲取self和cmd
,例如??
接著钙勃,我們可以下地址斷點(diǎn)
蛛碌,再通過image list
指令,結(jié)合ASLR值
辖源,計(jì)算出偏移值
??
后面蔚携,就能ASLR+偏移值
直接下斷點(diǎn),找到方法的imp地址
克饶,這就是動態(tài)調(diào)試
酝蜒。
2.5.2 恢復(fù)符號
動態(tài)調(diào)試
下斷點(diǎn),使用起來還是比較麻煩矾湃,需要計(jì)算亡脑,如果能恢復(fù)符號
的話就方便很多了。
我明知道,在上面的例子中去掉所有符號
后Symbol Table
中只有間接符號
了远豺,雖然符號表
中沒有了奈偏,但是類列表
和方法列表
中依然存在。
這也就為我們提供了恢復(fù)Symbol Table
的機(jī)會躯护。
恢復(fù)指令
可以通過restore-symbol工具
恢復(fù)符號(只能恢復(fù)oc
的惊来,runtime機(jī)制導(dǎo)致)??
./restore-symbol
原始Macho文件
-o恢復(fù)后文件
查看恢復(fù)后的machO??
這個(gè)時(shí)候就可以重簽名后進(jìn)行動態(tài)調(diào)試了。
restore-symbol工具鏈接
2.6 fishhook源碼解析
最后棺滞,也是本篇文章的重點(diǎn)裁蚁,就是fishhook源碼解析
,廢話不多說继准,直接上源碼枉证。
2.6.1 rebind_symbols
//第一次是拿dyld的回調(diào),之后是手動拿到所有image去調(diào)用移必。這里因?yàn)闆]有指定image所以需要拿到所有的室谚。
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
//prepend_rebindings的函數(shù)會將整個(gè) rebindings 數(shù)組添加到 _rebindings_head 這個(gè)鏈表的頭部
//Fishhook采用鏈表的方式來存儲每一次調(diào)用rebind_symbols傳入的參數(shù),每次調(diào)用崔泵,就會在鏈表的頭部插入一個(gè)節(jié)點(diǎn)秒赤,鏈表的頭部是:_rebindings_head
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
//根據(jù)上面的prepend_rebinding來做判斷,如果小于0的話憎瘸,直接返回一個(gè)錯(cuò)誤碼回去
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裝載的時(shí)候觸發(fā)回調(diào)潮售。這里相當(dāng)于注冊了一個(gè)回調(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();//這個(gè)相當(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ù)。
rebindings_entry鏈表
其中游沿,_rebindings_head
是指向鏈表rebindings_entry
結(jié)構(gòu)體的指針??
struct rebindings_entry {
struct rebinding *rebindings; // HOOK的相關(guān)信息
size_t rebindings_nel; // 所占空間大小
struct rebindings_entry *next; // 鏈表的next指針
};
static struct rebindings_entry *_rebindings_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);
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_image
流程比rebind_symbols
簡單很多饰抒,直接調(diào)用rebind_symbols_for_image
,因?yàn)橹付?code>void *header诀黍,不需要遍歷
所有的image
袋坑。
2.6.2 _rebind_symbols_for_image
//兩個(gè)參數(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鏈表地址
枣宫。
2.6.4 rebind_symbols_for_image
//回調(diào)的最終就是這個(gè)函數(shù)婆誓! 三個(gè)參數(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)址空間的其中一個(gè)加載模塊(可執(zhí)行庫或共享庫)內(nèi)洋幻,如果某個(gè)地址位于在其上面映射加載模塊的基址和為該加載模塊映射的最高虛擬地址之間(包括兩端),則認(rèn)為該地址在加載模塊的范圍內(nèi)翅娶。如果某個(gè)加載模塊符合這個(gè)條件文留,則會搜索其動態(tài)符號表,以查找與指定的address 最接近的符號竭沫。最接近的符號是指其值等于燥翅,或最為接近但小于指定的address 的符號。
*/
/*
如果指定的address 不在其中一個(gè)加載模塊的范圍內(nèi)蜕提,則返回0 森书;且不修改Dl_info 結(jié)構(gòu)的內(nèi)容。否則谎势,將返回一個(gè)非零值拄氯,同時(shí)設(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;
//這個(gè)dladdr函數(shù)就是在程序里面找header
if (dladdr(header, &info) == 0) {
return;
}
//下面就是定義好幾個(gè)變量镊折,準(zhǔn)備從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;
}
}
//如果剛才獲取的恨胚,有一項(xiàng)為空就直接返回
if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
!dysymtab_cmd->nindirectsyms) {
return;
}
// Find base symbol/string table addresses
//鏈接時(shí)程序的基址 = __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) {
//尋找到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);
}
}
}
}
}
核心的步驟有??
- 根據(jù)
linkedit
和偏移值
分別找到符號表的地址
和字符串表的地址
以及間接符號表地址
骂因。 - 遍歷
load commands
和data段
找到懶加載符號表
和非懶加載符號表
。 - 找到表的同時(shí)就直接調(diào)用
perform_rebinding_with_section
進(jìn)行hook
替換函數(shù)符號赃泡。
2.6.5 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。也就是第幾個(gè)這里是和間接符號表中相對應(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里面的每一個(gè)符號(懶加載/非懶加載)
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ù)的名稱是否有兩個(gè)字符辰企,因?yàn)楹瘮?shù)前面有個(gè)_风纠,所以方法的名稱最少要1個(gè)
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]兩個(gè)函數(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表的時(shí)候直接就走這個(gè)替換的地址了帮辟。
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
//替換完成跳轉(zhuǎn)外層循環(huán)速址,到(懶加載/非懶加載)數(shù)組的下一個(gè)數(shù)據(jù)。
goto symbol_loop;
}
}
//沒有找到就找自己要替換的函數(shù)數(shù)組的下一個(gè)函數(shù)由驹。
cur = cur->next;
}
symbol_loop:;
}
}
核心步驟??
- 首先通過
懶加載/非懶加載符號表
和間接符號表
找到所有的index
角標(biāo)芍锚,reserved1
確認(rèn)了懶加載和非懶加載符號
在間接符號表中的index
值。
- 將
懶加載/非懶加載符號表
的Data值
放入indirect_symbol_bindings數(shù)組
中蔓榄。
- 遍歷
懶加載/非懶加載符號表
??
- 讀取
indirect_symbol_indices
找到符號在Indrect Symbol Table
表中的值放入symtab_index
并炮。 - 以
symtab_index
作為下標(biāo),訪問symbol table
甥郑,拿到string table
的strtab_offset
偏移值逃魄。 - 根據(jù)
strtab_offset
偏移值獲取字符地址symbol_name
字符名。 - 循環(huán)遍歷
rebindings
鏈表(即自定義的Hook數(shù)據(jù)) - 判斷
&symbol_name[1]
和rebindings[j].name
兩個(gè)函數(shù)的名字是否都是一致
的澜搅,以及判斷字符長度是否大于1心步驟??
伍俘。 -
相同
?? 先保存(replaced時(shí),沒有replaced則不保存)原地址到自定義函數(shù)指針勉躺。并且用要Hook的目標(biāo)函數(shù)replacement
替換indirect_symbol_bindings
癌瘾,這里就完成了Hook
。