一、Fishhook 是什么命满?
簡單來說Fishhook就是hook函數(shù)的一種工具童芹,當(dāng)然它hook的原理和我們熟知的Method Swizzle 方式是不一樣的涮瞻,它是Facebook提供的一個動態(tài)修改鏈接mach-O文件的工具。
- Method Swizzle 是利用OC的Runtime特性假褪,動態(tài)改變SEL(方法編號)和IMP(方法實現(xiàn))的對應(yīng)關(guān)系署咽,達(dá)到OC方法調(diào)用流程改變的目的。主要用于OC方法生音。
- Fishhook 是利用MachO文件加載原理宁否,通過修改懶加載和非懶加載兩個表的指針達(dá)到C函數(shù)HOOK的目的。
二缀遍、Fishhook的簡單使用
首先在github上下載fishhook庫:
解壓zip包后慕匠,將其中的.c和.h文件拖入你想hook的項目當(dāng)中,我們所用到的就是這兩個文件瑟由,當(dāng)然網(wǎng)上有大把的關(guān)于fishhook的源碼剖析資料絮重,大家可以自行查閱,這里我就直接上代碼了歹苦,拖入步驟不作詳細(xì)介紹。
#import "ViewController.h"
#import "fishhook.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//定義rebinding 結(jié)構(gòu)體
struct rebinding rebind = {};
rebind.name = "NSLog";
rebind.replacement = hookNSLog;
rebind.replaced = (void *)&nslogMethod;
//將上面的結(jié)構(gòu)體 放入 reb結(jié)構(gòu)體數(shù)組中
struct rebinding red[] = {rebind};
/*
* arg1 : 結(jié)構(gòu)體數(shù)據(jù)組
* arg2 : 數(shù)組的長度
*/
rebind_symbols(red, 1);
}
//定義一個函數(shù)指針 用于指向原來的NSLog函數(shù)
static void (*nslogMethod)(NSString *format, ...);
void hookNSLog(NSString *format, ...){
format = [format stringByAppendingString:@"被勾住了"];
nslogMethod(format);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"原有NSLog函數(shù)");
}
將文件導(dǎo)入項目后督怜,在ViewController.m 文件中寫入代碼如上殴瘦,代碼目的是:將點擊屏幕時執(zhí)行的NSLog函數(shù),替換成執(zhí)行hookNSLog函數(shù)号杠。
點擊屏幕時執(zhí)行結(jié)果:
我們先不管原理是怎樣蚪腋,大家是不是覺得丰歌,這執(zhí)行結(jié)果和以上代碼干的事,是不是很像runtime的方法交換屉凯。
但是從表面上看立帖,runtime和fishhook的區(qū)別,大家都知道使用runtime交換的是OC的方法悠砚,而fishhook卻是交換C函數(shù)晓勇,這里就會有個疑惑,C語言不是靜態(tài)語言嗎灌旧,為什么fishhook可以直接交換函數(shù)呢绑咱?
ok ! 在疑問之前,我們先將以上代碼稍作修改枢泰,在看一下結(jié)果描融。
#import "ViewController.h"
#import "fishhook.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//定義rebinding 結(jié)構(gòu)體
struct rebinding rebind = {};
rebind.name = "funcDlog";
rebind.replacement = hookNSLog;
rebind.replaced = (void *)&nslogMethod;
//將上面的結(jié)構(gòu)體 放入 reb結(jié)構(gòu)體數(shù)組中
struct rebinding red[] = {rebind};
/*
* arg1 : 結(jié)構(gòu)體數(shù)據(jù)組
* arg2 : 數(shù)組的長度
*/
rebind_symbols(red, 1);
}
//定義一個函數(shù)指針 用于指向原來的NSLog函數(shù)
static void (*nslogMethod)(NSString *format, ...);
void hookNSLog(NSString *format, ...){
format = [format stringByAppendingString:@"被勾住了"];
nslogMethod(format);
}
void funcDlog(NSString *format, ...){
NSLog(@"%@", format);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
funcDlog(@"原有NSLog函數(shù)");
}
@end
來我們看下代碼的改動:
我們將原來hook的目標(biāo)轉(zhuǎn)換了,自己定義了一個函數(shù)void funcDlog(NSString *format, ...)
,在函數(shù)調(diào)用NSLog函數(shù)衡蚂,而我們這次Hook的目標(biāo)變成了 funcDlog函數(shù)窿克,按照之前的運行結(jié)果,猜測這次的運行結(jié)果應(yīng)該還是“ 原有NSLog函數(shù)被勾住了”毛甲,但是實際運行結(jié)果卻是:
結(jié)果顯示我們的代碼并沒有hook成功年叮,綜合以上兩次運行結(jié)果,我們發(fā)現(xiàn)了丽啡,fishhook可以實現(xiàn)runtime做不到的將C函數(shù)進行交換谋右,但是我們自己定義的C函數(shù)卻不能交換,只能hook系統(tǒng)的函數(shù)补箍,這是為什么呢改执?我們來看下一個章節(jié)。
三坑雅、Fishhook的原理探究
首先在探究fishhook的原理之前辈挂,我要清楚幾個問題:
- MachO是怎么加載的
- 首先我們要知道,MachO文件是被dyld加載的裹粤,dyld是動態(tài)加載终蒂,也負(fù)責(zé)動態(tài)庫的加載。我們都知道動態(tài)庫加載時并不在我們自己的MachO文件內(nèi)部遥诉,是存在系統(tǒng)動態(tài)緩存區(qū)拇泣,當(dāng)我們MachO文件加載過后會動態(tài)加載我們所依賴的那些動態(tài)庫。
- ASLR技術(shù)
- 我們的MachO文件在內(nèi)存當(dāng)中每次的運行地址是不一樣的矮锈,MachO文件加載的時地址是隨機的霉翔,這就是ASLR技術(shù)。蘋果每次加載MachO文件時會隨機分配一個地址苞笨,來降低緩沖區(qū)溢出债朵。
那問題來了子眶,每次MachO文件加載的地址不一樣,我們大家都知道序芦,系統(tǒng)的動態(tài)庫緩存區(qū)的地址也是變化的臭杰,那每次dyld加載動態(tài)庫的時候是怎樣找到依賴動態(tài)庫進行加載的呢?
-
PIC 位置代碼獨立
- 當(dāng)MachO內(nèi)部需要調(diào)用系統(tǒng)的庫函數(shù)時谚中,會現(xiàn)在MachO文件_DATA段中建立一個指針渴杆,指向外部函數(shù),dyld就會將指針與函數(shù)進行動態(tài)綁定藏杖,將MachO中的DATA段中的指針指向外部函數(shù)将塑,從而實現(xiàn)加載。
我們來看下第一份代碼的MachO文件蝌麸,如圖:
當(dāng)MachO內(nèi)部有兩個指針用來加載動態(tài)庫:
- Non-Lazy Symbol Pointers :不懶加載指針
- Lazy Symbol Pointers :懶加載指針
這兩個指針就是dyld用來在MachO內(nèi)部調(diào)用系統(tǒng)庫函數(shù)時來指向外部函數(shù)進行動態(tài)綁定加載点寥。從上圖我們可以看出在Lazy Symbol Pointers
指針表中NSLog的位置,以及偏移地址是0x8018来吩。
那我們根據(jù)這個偏移地址在第一份代碼中進行LLDB調(diào)試看看結(jié)果如何:
-
首先我們先在 fishhook的代碼執(zhí)行前設(shè)置斷點:
-
然后我們先拿到MachO文件在內(nèi)存中的首地址:0x0000000100154000
-
根據(jù)偏移地址拿到指針指向的地址值:
-
根據(jù)地址值中的地址進行反匯編敢辩,我看下結(jié)果:
反匯編后結(jié)果清楚顯示地址指向的是Foundation下的 NSLog
函數(shù)
ok! 這是還沒有進行fishhook前的NSLog ,那我們將斷點斷到fishhook代碼執(zhí)行后在看結(jié)果:
同樣的偏移地址弟疆,地址值卻發(fā)生了變化戚长,反匯編后卻發(fā)現(xiàn),地址指向的是fishhookDemo1下的hookNSLog
函數(shù)怠苔。
這就是Fishhook的原理:在dyld加載MachO文件所需要的系統(tǒng)函數(shù)時同廉,通過改變MachO文件中的DATA段中指向外部函數(shù)的指針?biāo)赶虻牡刂罚瑏韺崿F(xiàn)動態(tài)交換柑司。
這里就可以解釋我們在兩次運行結(jié)果對比留下的兩個疑惑:
- 為什么fishhook可以交換C函數(shù)
- fishhook 是通過改變dyld加載動態(tài)庫所用的指針指向的函數(shù)地址迫肖,來實現(xiàn)的函數(shù)交換 ,所以C函數(shù)通過dyld動態(tài)加載動態(tài)庫也展現(xiàn)出C函數(shù)動態(tài)的一面攒驰。
- 為什么我們自己寫的C函數(shù)fishhook交換不了
- 我們自己寫的C函數(shù)不在系統(tǒng)動態(tài)庫緩存區(qū)蟆湖,而是存在我們自己的MachO文件當(dāng)中,不經(jīng)過dyld加載玻粪,直接編譯成匯編語言隅津,在MachO中的TEXT段中。所以fishhook的原理不能交換我們自己寫的C函數(shù)劲室。
四伦仍、通過符號找到字符串
在上面我們知道了系統(tǒng)的動態(tài)庫與我們的MachO文件是通過dyld在加載時在MachO文件中的一個指針指向了外部函數(shù)來進行加載,但是這只是個地址很洋,我們在代碼中調(diào)用函數(shù)時通過函數(shù)名稱來調(diào)用的呢铆,那這個地址和我們調(diào)用的函數(shù)名稱是如何建立起聯(lián)系的呢?我們先來看下剛才的MachO文件蹲缠,如圖:
圖1:
圖2:
從圖1棺克、圖2中我們可以看出Lazy Symbol Pointers 表 和 Dyamic Symbol Table 表中的關(guān)系是一一對應(yīng)的,遍歷前一張表的index就可以對應(yīng)到后一張表线定。注意圖2中的對應(yīng)index行中的 Data 值為0x7F娜谊。
圖3:
在Symbol Table 表中先拿到第一條的偏移地址0xC500。
圖4:
在圖2中index中獲取的Data值為0x7F斤讥,這值是圖4中index的標(biāo)號纱皆,由于圖4中index的偏移為0x10,所以偏移地址+Data對應(yīng)的偏移地址為0xCCF0,找到當(dāng)前index的Data值是0xA7芭商。
圖5:
最后在String Table 表中 偏移首地址+0xA7 的值為0x0000D02B派草,在表中差得字符串以“_”開頭,以“.”結(jié)尾铛楣。所得的字符串就是外部函數(shù)地址所對應(yīng)的函數(shù)名近迁。