一、Fishhook 是什么?
簡單來說Fishhook就是hook函數(shù)的一種工具,當然它hook的原理和我們熟知的Method Swizzle 方式是不一樣的,它是Facebook提供的一個動態(tài)修改鏈接mach-O文件的工具。
- Method Swizzle 是利用OC的Runtime特性,動態(tài)改變SEL(方法編號)和IMP(方法實現(xiàn))的對應關系,達到OC方法調(diào)用流程改變的目的。主要用于OC方法。
- Fishhook 是利用MachO文件加載原理,通過修改懶加載和非懶加載兩個表的指針達到C函數(shù)HOOK的目的。
二、Fishhook的簡單使用
首先在github上下載fishhook庫:

解壓zip包后,將其中的.c和.h文件拖入你想hook的項目當中,我們所用到的就是這兩個文件,當然網(wǎng)上有大把的關于fishhook的源碼剖析資料,大家可以自行查閱,這里我就直接上代碼了,拖入步驟不作詳細介紹。
#import "ViewController.h"
#import "fishhook.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//定義rebinding 結構體
struct rebinding rebind = {};
rebind.name = "NSLog";
rebind.replacement = hookNSLog;
rebind.replaced = (void *)&nslogMethod;
//將上面的結構體 放入 reb結構體數(shù)組中
struct rebinding red[] = {rebind};
/*
* arg1 : 結構體數(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ù)");
}
將文件導入項目后,在ViewController.m 文件中寫入代碼如上,代碼目的是:將點擊屏幕時執(zhí)行的NSLog函數(shù),替換成執(zhí)行hookNSLog函數(shù)。
點擊屏幕時執(zhí)行結果:

我們先不管原理是怎樣,大家是不是覺得,這執(zhí)行結果和以上代碼干的事,是不是很像runtime的方法交換。
但是從表面上看,runtime和fishhook的區(qū)別,大家都知道使用runtime交換的是OC的方法,而fishhook卻是交換C函數(shù),這里就會有個疑惑,C語言不是靜態(tài)語言嗎,為什么fishhook可以直接交換函數(shù)呢?
ok ! 在疑問之前,我們先將以上代碼稍作修改,在看一下結果。
#import "ViewController.h"
#import "fishhook.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//定義rebinding 結構體
struct rebinding rebind = {};
rebind.name = "funcDlog";
rebind.replacement = hookNSLog;
rebind.replaced = (void *)&nslogMethod;
//將上面的結構體 放入 reb結構體數(shù)組中
struct rebinding red[] = {rebind};
/*
* arg1 : 結構體數(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的目標轉換了,自己定義了一個函數(shù)void funcDlog(NSString *format, ...),在函數(shù)調(diào)用NSLog函數(shù),而我們這次Hook的目標變成了 funcDlog函數(shù),按照之前的運行結果,猜測這次的運行結果應該還是“ 原有NSLog函數(shù)被勾住了”,但是實際運行結果卻是:

結果顯示我們的代碼并沒有hook成功,綜合以上兩次運行結果,我們發(fā)現(xiàn)了,fishhook可以實現(xiàn)runtime做不到的將C函數(shù)進行交換,但是我們自己定義的C函數(shù)卻不能交換,只能hook系統(tǒng)的函數(shù),這是為什么呢?我們來看下一個章節(jié)。
三、Fishhook的原理探究
首先在探究fishhook的原理之前,我要清楚幾個問題:
- MachO是怎么加載的
- 首先我們要知道,MachO文件是被dyld加載的,dyld是動態(tài)加載,也負責動態(tài)庫的加載。我們都知道動態(tài)庫加載時并不在我們自己的MachO文件內(nèi)部,是存在系統(tǒng)動態(tài)緩存區(qū),當我們MachO文件加載過后會動態(tài)加載我們所依賴的那些動態(tài)庫。
- ASLR技術
- 我們的MachO文件在內(nèi)存當中每次的運行地址是不一樣的,MachO文件加載的時地址是隨機的,這就是ASLR技術。蘋果每次加載MachO文件時會隨機分配一個地址,來降低緩沖區(qū)溢出。
那問題來了,每次MachO文件加載的地址不一樣,我們大家都知道,系統(tǒng)的動態(tài)庫緩存區(qū)的地址也是變化的,那每次dyld加載動態(tài)庫的時候是怎樣找到依賴動態(tài)庫進行加載的呢?
-
PIC 位置代碼獨立
- 當MachO內(nèi)部需要調(diào)用系統(tǒng)的庫函數(shù)時,會現(xiàn)在MachO文件_DATA段中建立一個指針,指向外部函數(shù),dyld就會將指針與函數(shù)進行動態(tài)綁定,將MachO中的DATA段中的指針指向外部函數(shù),從而實現(xiàn)加載。
我們來看下第一份代碼的MachO文件,如圖:

當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)試看看結果如何:
-
首先我們先在 fishhook的代碼執(zhí)行前設置斷點:
image.png
-
然后我們先拿到MachO文件在內(nèi)存中的首地址:0x0000000100154000
image.png
-
根據(jù)偏移地址拿到指針指向的地址值:
image.png
-
根據(jù)地址值中的地址進行反匯編,我看下結果:
image.png
反匯編后結果清楚顯示地址指向的是Foundation下的 NSLog函數(shù)
ok! 這是還沒有進行fishhook前的NSLog ,那我們將斷點斷到fishhook代碼執(zhí)行后在看結果:

同樣的偏移地址,地址值卻發(fā)生了變化,反匯編后卻發(fā)現(xiàn),地址指向的是fishhookDemo1下的hookNSLog函數(shù)。
這就是Fishhook的原理:在dyld加載MachO文件所需要的系統(tǒng)函數(shù)時,通過改變MachO文件中的DATA段中指向外部函數(shù)的指針所指向的地址,來實現(xiàn)動態(tài)交換。
這里就可以解釋我們在兩次運行結果對比留下的兩個疑惑:
- 為什么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文件當中,不經(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 表中的關系是一一對應的,遍歷前一張表的index就可以對應到后一張表。注意圖2中的對應index行中的 Data 值為0x7F。
圖3:

在Symbol Table 表中先拿到第一條的偏移地址0xC500。
圖4:

在圖2中index中獲取的Data值為0x7F,這值是圖4中index的標號,由于圖4中index的偏移為0x10,所以偏移地址+Data對應的偏移地址為0xCCF0,找到當前index的Data值是0xA7。
圖5:

最后在String Table 表中 偏移首地址+0xA7 的值為0x0000D02B,在表中差得字符串以“_”開頭,以“.”結尾。所得的字符串就是外部函數(shù)地址所對應的函數(shù)名。



