fishhook是Facebook提供的一個動態修改鏈接mach-O文件的工具。利用MachO文件加載原理,通過修改懶加載和非懶加載兩個表的指針達到C函數HOOK的目的。
前提
在分析fishhook原理前,我們先來想兩個問題:
1. Mach-O文件是被誰加載的?
?我們知道,在程序啟動的時候 Mach-O 文件會被 DYLD (動態加載器)加載進內存。加載完 Mach-O 后,DYLD接著會去加載 Mach-O 所依賴的動態庫。
2. 何為ASLR技術?
地址空間布局隨機化。它會讓 Mach-O 文件加載的時候是隨機地址。有了這個技術,Mach-O 文件每次加載進內存的時候地址都是不一樣的。主要是為了防止逆向技術。
Mach-O 文件里只有我們自己寫的函數,系統的動態庫的函數是不在 Mach-O 文件里的。也就是說每次啟動從 Mach-O 文件到系統動態庫函數的偏移地址都是變化的。
問題
一、那么我們如何在 Mach-O 文件里找到系統的函數地址呢?或者說 Mach-O 文件是如何鏈接外部函數的呢?
我們程序的底層都是匯編,匯編代碼都是寫死的內存地址。我們該怎么找呢?而且系統的動態庫在內存里面的地址是不固定的,每次啟動程序的時候地址都是隨機的。
蘋果為了能在 Mach-O 文件中訪問外部函數,采用了一個技術,叫做PIC(位置代碼獨立)技術。
當你的應用程序想要調用 Mach-O 文件外部的函數的時候,或者說如果 Mach-O 內部需要調用系統的庫函數時,Mach-O 文件會:
- 先在 Mach-O 文件的 _DATA 段中建立一個指針(8字節的數據,放的全是0),這個指針變量指向外部函數。
- DYLD 會動態的進行綁定!將 Mach-O 中的 _DATA 段中的指針,指向外部函數。
所以說,C的底層也有動態的表現。C在內部函數的時候是靜態的,在編譯后,函數的內存地址就確定了。但是,外部的函數是不能確定的,也就是說C的底層也有動態的。fishhook 之所以能 hook C函數,是利用了 Mach-O 文件的 PIC 技術特點。也就造就了靜態語言C也有動態的部分,通過 DYLD 進行動態綁定的時候做了手腳。
我們經常說符號,其實 _DATA 段中建立的指針就是符號。fishhook的原理其實就是,將指向系統方法(外部函數)的符號重新進行綁定指向內部的函數。這樣就把系統方法與自己定義的方法進行了交換。這也就是為什么C的內部函數修改不了,自定義的函數修改不了,只能修改 Mach-O 外部的函數。
接下來我們以 NSLog 為例,看 fishhook 是如何通過修改懶加載和非懶加載兩個表的指針達到C函數HOOK的目的。(NSLog 是在懶加載表里)
注:對于非懶加載符號表,DYLD會立刻馬上去鏈接動態庫
?? 對于懶加載符號表,DYLD會在執行代碼的時候去動態的鏈接動態庫
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//這里必須要先加載一次NSLog,如果不寫NSLog,符號表里面根本就不會出現NSLog的地址
NSLog(@"123");
//定義rebinding結構體
struct rebinding nslogBind;
//函數的名稱
nslogBind.name = "NSLog";
//新的函數地址
nslogBind.replacement = myMethod;
//保存原始函數地址變量的指針
nslogBind.replaced = (void *)&old_nslog;
//定義數組
struct rebinding rebs[] = {nslogBind};
/**
arg1: 存放rebinding結構體的數組
arg2: 數組的長度
*/
rebind_symbols(rebs, 1);
}
//函數指針,用來保存原始的函數地址
static void (*old_nslog)(NSString *format, ...);
//新的NSLog
void myMethod(NSString *format, ...) {
//再調用原來的
old_nslog(@"勾上了!");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"點擊屏幕");
}
首先,系統的 NSLog 是在 rebind_symbols(rebs, 1);
方法里替換的,我們可以在這個方法上打個斷點。我們可以先看一下,這個函數執行之前,NSLog 在懶加載符號表中的地址是多少,然后在執行之后,它有沒有變成我們自己的符號表的地址。
那么,我們如何找到 NSLog 的符號表呢?公式如下:
NSLog 懶加載符號表在內存中的地址 = Mach-O 的偏移地址 + NSLog 懶加載符號表在 Mach-O 的偏移地址
查看符號表在 Mach-O 的偏移地址
查看Mach-O 的偏移地址
查看符號表綁定的地址,這個地址其實就是指向外部函數的指針的地址,也就是動態緩存區里面 NSLog 的真實函數地址。這一步是找到了 NSLog 的符號表(Symbols)。
這個真實的函數地址是什么時候保存進去的呢?并不是 Mach-O 文件加載進內存的時候保存的。由于 NSLog 在懶加載符號表里面,所有它是在整個 Mach-O 文件啟動之后,代碼第一次運行 NSLog 時,由 DYLD 綁定該 NSLog 符號指向真實的 NSLog 的地址。
這個時候,我們需要通過反匯編看一下地址的值
可以看到,這個時候 Mach-O 文件的 _DATA 段中建立的指針已經指向了外部函數。
緊接著單步執行,執行完 rebind_symbols(rebs, 1);
函數
這個時候我們再看一下符號表綁定的地址,我們發現地址已經發生了變化
再次通過反匯編看一下地址的值
我們發現 Mach-O 文件的 _DATA 段中建立的指針已經指向了我們自己定義的內部函數。
二、fishhook 是如何通過字符串來找到我們的函數的呢?
//定義rebinding結構體
struct rebinding nslogBind;
//函數的名稱
nslogBind.name = "NSLog"; //如何通過字符串來找到函數的?
//新的函數地址
nslogBind.replacement = myMethod;
//保存原始函數地址變量的指針
nslogBind.replaced = (void *)&old_nslog;
我們可以想到的是,Mach-O 文件里面肯定有一個與字符串相關的東西。
首先,我們從懶加載符號表(Lazy Symbol Pointers)開始入手。懶加載符號表里面第一個符號是 NSLog 的指針。這個懶加載符號表有一個與之一一對應的符號表(Indirect Symbols)。
上圖的 Data 值,是一個真正的符號表的下標。這個符號表是對應著字條串的。比如:NSLog 的 Data 值為0x7A,換成十進制就是122。也就是說 NSLog 這個符號在我們的字符符號表里面的 index 值為122。接著就需要到符號表(Symbols)里面找第122個。這個時候還沒到字符串。
這個時候,NSLog 在真正的字符串里面是在哪個地方呢?注意,上圖有一個偏移0x9C,就是在字符串表(String Table)里面的一個index。也就是說這個 NSLog 在 String Table 里面的偏移地址是0x9C。
如上圖,String Table 是從0x0000CF04開始的,所以開始地址0xCF04 + 偏移地址0x9C = 0xCFA0,就是字符串 NSLog 的位置。
_ 是函數的開始,. 是分隔符 。5F是從 _開始,往后依次 _NSLog
接下來,附上 fishhook 官方文檔的在懶加載和非懶加載符號表里查找一個給定入口的名字的過程。