在上上篇文章iOS應用安全4 -- 代碼注入,竊取微信登陸密碼中咱們知道了如何hook App中的OC方法,即便用OC的運行時機制,在運行期間替換相應方法的實現。
那麼有沒有想過咱們如何才能hook C語言寫的函數呢?
這就要使用到咱們今天所要講的fishhook了,下載地址。git
fishhook是facebook提供的一個動態修改連接mach-O文件的工具。利用MachO文件加載原理,經過修改懶加載和非懶加載兩個表的指針達到C函數hook的目的。github
話很少說,先把它下載下來添加到工程。 算法
下載完fishhook以後能夠發現裏面有這5個文件,咱們只須要使用這倆.h和.c就能夠了,新建工程,將這倆文件拖如工程目錄下。// rebinding--->從新綁定
struct rebinding {
const char *name; // 要hook的函數名稱,char* 是C語言的字符串
void *replacement; // 用來 替換原始函數 的函數的地址
void **replaced; // 被替換掉的原始函數的地址,注意是void**
};
複製代碼
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);
複製代碼
很明顯,第二個函數是對第一個函數的擴展,那麼就先來分析一下第一個函數如何使用吧。數組
爲了簡單起見,就再也不重簽名一個App再測試了,直接在新建的項目的ViewController
類中hook
系統的C函數NSLog
。代碼不多,就下面這些xcode
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 就以hook系統的NSLog函數爲例
// 建立一個結構體變量
struct rebinding rebind;
rebind.name = "NSLog"; // 注意是C語言的字符串不用加@
rebind.replacement = cus_log; // C語言函數名 即 函數指針
// 這裏傳的是函數地址的地址,爲的是在rebind_symbols函數內部附上NSLog的地址
rebind.replaced = (void *)&sys_log;
// 定義rebinding數組,裏面只有一個元素。固然,也能夠存多個
struct rebinding rbs[] = {rebind};
rebind_symbols(rbs, 1);
}
// 定義函數指針用來`接收`系統的NSLog函數的地址
static void (*sys_log)(NSString *format, ...);
// 自定義打印函數,用來替換NSLog
void cus_log(NSString *format, ...) {
// 給打印的字符串後面添加一個標記
format = [format stringByAppendingString:@"-------->cus_log的標記"];
// 調用系統NSLog函數
sys_log(format);
}
// 測試
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"點擊屏幕了😂😂😂");
}
複製代碼
我認爲上面的代碼註釋的很清晰了,這裏再也不重複解釋。但有一點仍是須要說明一下,rebinding
結構體的replaced
變量是void**
類型,若是對此不太瞭解的能夠本身搜索一下C語言傳值和傳地址的區別,也能夠看我前幾天寫的文章數據結構與算法2 -- 鏈表的最下面總結部分,有對這一塊的詳細解釋。緩存
運行效果以下: 安全
上面咱們實現了使用fishhook替換系統C函數NSLog的實現,那麼在App中由開發者本身寫的C函數能不能使用fishhook替換呢?
試試就知道了,測試代碼以下:微信
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
struct rebinding rb;
rb.name = "printHelloWorld";
rb.replacement = hook_printHelloWorld;
rb.replaced = (void *)&app_printHelloWorld;
struct rebinding rbs[] = {rb};
rebind_symbols(rbs, 1);
}
// 一個簡單的C語言函數, 假設這個函數是別人App中實現的C語言函數
void printHelloWorld() {
NSLog(@"Hello World!");
}
// 接收printHelloWorld函數的實現地址
static void (*app_printHelloWorld)(void);
// 替換printHelloWorld函數的函數
void hook_printHelloWorld() {
NSLog(@"在Hello World!以前打印一句話,表明了hook成功");
// 調用原始的printHelloWorld函數
app_printHelloWorld();
}
// 測試
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
printHelloWorld();
}
複製代碼
運行效果以下,能夠看到hook失敗了。
由於在Hello World!以前打印一句話,表明了hook成功
這句話並無在Hello World!
打印前打印。 數據結構
P一、爲何會失敗呢?爲何不能hook這個自定義的C函數呢?app
仔細想一想咱們會發現如下兩件事!
Method Swizzling
等方法來實現hook OC的方法。P二、爲何fishhook能夠hook NSLog函數呢?NSLog難道不是C函數嗎?
首先說明NSLog
確實是C語言函數,這一點沒問題。
之因此fishhook可以hook NSLog函數,這個我默認你已經看過上一篇文章iOS應用安全5 -- main函數調用以前作了些什麼?。
上一篇文章主要介紹了MachO文件
和dyld是如何加載應用程序的?
這兩部分,那麼固然不會無緣無故的來說這兩塊內容,確定是有用的。
在dyld加載應用程序的過程當中,有一個步驟叫加載共享緩存,相信你們應該都還記得這一步是作什麼的?
咱們都知道NSLog是系統庫Foundation框架中的一個C函數,而這個Foundation框架自己就是一個真正意義上的動態庫,即它具備在多個進程中共享的特性。而咱們平時說的共享緩存就是指這一類系統庫。
系統定義的C函數,因爲具體的函數實現是在系統共享庫中,所以在程序編譯期間是沒法獲取到這個C函數的實現地址,只能經過一種叫符號綁定的方法動態連接到函數名。
這樣直接說有點抽象,下面我列舉個場景:
一、新建一個工程,在
ViewController.m
的viewDidLoad
方法中寫一句NSLog(@"哈哈哈");
二、而後command+B
編譯程序,那麼在編譯到NSLog(@"哈哈哈");
這一句代碼時,xcode會判斷NSLog
函數是否是系統庫裏面的函數。
三、發現NSLog
是Foundation
框架裏的函數,那麼就會將NSLog(@"哈哈哈");
這句代碼和符號表中的一個char*
類型的C字符串"NSLog"
進行綁定。
四、當command+R
運行應用程序時,就會在dyld
加載應用程序時將iOS
系統共享緩存區
的Foundation
庫加載到內存中。
五、程序運行過程當中,當代碼第一次執行到NSLog
函數調用時,就會到Foundation
庫的MachO文件中查詢NSLog
函數的實現地址。而後將函數的實現地址和符號表中的"NSLog"
字符串進行綁定,這樣後面再調用到NSLog
函數時就可以直接找到函數的實現地址。(有點懶加載的意思,因此這種方法也被稱爲懶綁定)
到這,想必不用我說,也都知道了自定義的C函數是怎麼回事了吧?
自定義的C函數,因爲函數的實現和函數的調用是在同一個MachO文件(App自己的MachO文件)中,所以在編譯連接期間,xcode就直接將函數調用語句和函數的實現地址進行了連接,也就不會有系統C函數的那些步驟了。
理解了上面系統C函數的工做原理,咱們應該也能猜到一些fishhook hook系統C函數的原理了。下面仍然以NSLog函數爲例:
從上面咱們知道,當第一次調用
NSLog
函數的時候,系統就會到Foundation
框架中找NSLog
的函數實現地址,與App的MachO文件中的符號表中的"NSLog"
字符串進行綁定。
fishhook就是利用這一點,回過頭看fishhook
中的那個函數名rebind_symbols
,翻譯爲從新綁定符號,再看看那個rebinding
結構體裏面有哪些東西?
// rebinding--->從新綁定
struct rebinding {
const char *name; // 要hook的函數名稱,char* 是C語言的字符串
void *replacement; // 用來 替換原始函數 的函數的地址
void **replaced; // 被替換掉的原始函數的地址,注意是void**
};
複製代碼
是否是一切都是那麼的恰到好處?
咱們給rebind_symbols
函數傳進去一個rebinding
結構體,包括了符號表中對應NSLog
函數的符號"NSLog"
,而後rebind_symbols
函數經過符號"NSLog"
找到Foundation
框架中NSLog
函數的實現地址,將這個函數地址賦值給replaced
變量返回給咱們,再將符號"NSLog"
綁定的函數地址替換爲咱們傳進去的函數地址replacement
。(注意:fishhook爲了保證返回給咱們的replaced
是正確有值的,必要時它會在rebind_symbols
函數內部先默認調用一次對應的函數)
說了那麼大一堆東西,誰知道是真的仍是框個人?
rebind_symbols
函數調用以前手動先調用一次NSLog
函數。如圖,添加一句NSLog(@"123456");
,並在調用前打上斷點。
lldb
的image list
命令查看主程序MachO在內存中的偏移值(即ASLR)。
符號NSLog
當前綁定的地址在內存中的真實地址。lldb
命令memory read
查看咱們計算出來的那塊內存。
符號NSLog
綁定的內存地址保存的值,這個值是一個指針,由於iOS默認是小端模式,高位字節保存在低位地址中,因此這個指針指向的真實地址應該是0x00000001005b6904。lldb
的dis -s
指令反彙編這個地址。能夠看到這個地址如今就是一塊無心義的地址,也就是符號NSLog
默認綁定的地址。
NSLog(@"123456");
斷點停在rebind_symbols
函數以前。
lldb
命令memory read
查看內存,能夠發現,同一塊內存地址,裏面保存的地址已經發生了改變。
lldb
的dis -s
指令反彙編這個地址(注意小端)。
符號NSLog
綁定的那個指針保存的地址已經由最開始的那個無心義的地址變成了Foundation
框架裏面的NSLog函數的地址
了。rebind_symbols
函數執行。再次使用lldb
命令memory read
查看那塊內存。
符號NSLog
綁定指針保存的Foundation裏面的NSLog函數
地址已經被fishhook修改爲了fishhook-test裏面的cus_log函數
。整個流程就是這樣,沒有框你吧?
從上面咱們知道了如何經過MachO文件中懶加載符號表中的符號找到函數在內存中的地址,進而hook這個函數。而這整個過程都有一個前提,那就是要找到符號表中的NSLog,那麼如何找呢?
別急,仔細看fishhook的rebinding結構體,咱們出來要傳入hook函數的地址以外還須要提供一個和系統C函數同名的C字符串。那麼確定就是經過這個C字符串來找對應函數的符號了。
String Table
,聽名字就知道這是一個字符串表,既然要經過字符串"NSLog"
找函數符號,那麼固然就要先從字符串表開始了。String Table
以下:
.
進行分割,使用_
表示後面的是一個函數,仔細看上圖中的"._NSLog"
對應的二進制值,能夠發現二進制值其實就是每一個字符的ascii
碼。"NSLog"
中的字符取出來,轉換成對應的ascii
碼,再到MachO
的String Table
表中查詢,就能夠找到"NSLog"
字符串在String Table
表中的偏移量。
"_NSLog"
的偏移量而不是"NSLog"
的偏移量(因此上圖計算的結果應該是0x00010F5B,而非0x00010F5C)。"_NSLog"
相對於MachO文件的偏移量 減去 String Table
相對於MachO文件的偏移量便可獲得"_NSLog"
相對於String Table
的偏移量。Symbol Table
能夠發現,每個符號都對應的有一個String Table Index
的東西,這個東西就是上一步計算出來的。
String Table Index
等於0x9B的,發現它對應的字符串值Value
就是_NSLog
,說明沒有找錯。"_NSLog"
在符號中對應的位置是105,換算成16進制是0x69。再換到Dynamic Symbol Table
目錄下,查找值是0x69的行。
0x69
對應的Symbol
符號也正是_NSLog
。再次印證了沒錯。而且關鍵點就在於下面的那個Indirect Address
行,0x10000C000。Load Commands
裏面的_PAGEZERO
的大小,這個大小實際上是一個虛擬大小
Indirect Address
真正的偏移量是0xC000,這個0xC000有沒有以爲很熟悉?好,再看懶加載符號表--->Lazy Symbol Pointers
_NSLog
了,至此終於和上一個標題接上了。經過這篇文章,咱們學到了如下幾點:
以前就講過,hook OC的方法,能夠經過3種方式交換方法的實現地址,使得App調用方法1的時候執行交換後的方法2,咱們再調用方法2的時候執行咱們添加的代碼和方法1本來的實現。具體能夠回頭再看看iOS應用安全4 -- 代碼注入,竊取微信登陸密碼這篇文章。
那麼面對這種破解,咱們做爲開發者要如何防禦呢?
很簡單,能夠利用本篇文章講述的fishhook,把runtime
裏面的那幾個方法交換的C函數所有替換成沒法使用的C函數,這樣,別人再使用方法交換方法的時候就會調用到咱們替換以後的沒法使用的C函數。哈哈😄。
可是這樣就徹底能夠保證安全了嗎?
還差得遠呢,別人仍然可使用其餘方法來破壞或者繞過你的保護。
攻防永遠是對立的,沒有絕對的安全,只有相對的安全。黑客若真的鐵了心的要破解你的App,你再怎麼防禦也是沒用的。咱們要作的就是要讓黑客花費很長很長的時間才能破解出來,讓他知難而退便可。