來自:https://www.cnblogs.com/dengzhuli/p/9952202.html
一、前言
?? ?在正題開始之前,我們先來聊聊iOS中的hook技術。一談到hook,很多人首先想到的是runtime,runtime確實強大,但是它存在很多局限性:
1)、侵入性:一旦hook了某個類的方法,那么只能這個類的所有對象的方法都會被hook。
2)、語言上的局限性:runtime 的hook 只能作用于OC方法。
?? ?開源框架Aspects很巧妙的解決了第一個問題,Aspects通過動態創建子類的方式將對當前類的hook轉換為對當前類動態生成的子類的hook,以此避免對當前類其他對象的代碼侵入,這與KVO的實現思路是一致的。而fishhook能從一定程度上輔助runtime解決hook對語言局限性的問題。
二、淺談fishhook
?? ?fishhook是Facebook開源的一個C語言的hook工具,我們可以使用fishhook來hook動態鏈接的C函數。為什么在這里要強調動態鏈接呢?因為fishhook只能hook iOS系統的C函數,你自己編寫的C函數是無法hook的。
?? ?fishhook使用起來很簡單,在這里就不談了,先來簡單介紹下fishhook的實現原理。由于動態庫并不參與前期的靜態編譯鏈接,所以在程序的可執行文件中,代碼段并不包含動態庫相關函數的匯編后的指令。那么系統是如何根據函數的調用符號找到真實的函數地址呢?在Mach-O文件中存在符號表和動態符號表以及字符串表,字符串表中存儲了所有的字符信息,比如代碼int a = 100;這個變量a的名字即存在字符串表中。符號表則存儲了所有符號位于字符串表中的位置信息,動態符號表存儲了動態庫符號位于符號表中的偏移信息。動態庫的section中的reserved1存儲了該section的偏移量X,動態符號表偏移X后即是該section的符號表索引數組Indices的首地址,以Indices數組中的值為索引,可以在符號表中獲取到當前的符號在字符串表中的偏移,從而獲取到符號字符串。通過該section的addr字段可以獲取到該section的符號綁定表,表中記錄著動態符號如:printf所對應的函數地址,修改符號綁定表的內容為指定函數地址即實現了hook。fishhook看起來非常的繞,這是由于動態鏈接存在復雜的索引關系,在這里就不過多介紹了,有興趣的可以搜索下有關fishhook的博文,優秀博文非常多。
? ? fishhook很厲害,但是在剛接觸時我有兩個疑問:1、fishhook能hook C++函數嗎?在我的前篇文章中也提出了hook C++函數的問題,但是在留言中貌似沒有得到有效的答案。2、fishhook 為什么不能hook自己寫的C函數呢?下面我們來一一解答這兩個問題。
三、C++的符號修飾
在程序員還是使用紙帶寫代碼的時候,人們約定在指定的某幾位代表指令,不同的0、1組合代表不同的指令。如:01000000中,0100代表跳轉指令,后面的0000代表目標地址。由于匯編的出現,0100被用jump來代替,這就是最早的符號,符號表能映根據符號映射到一個指令。C語言也是與此類似,實際上我們也是通過一個符號來代表一個函數的地址,但是隨著程序的不斷變大,符號沖突的概率逐漸增加。一個程序員在一個.c文件實現了hello函數,可能另一個程序員在另一個.c文件中也實現了一個同名的hello函數,在這兩個文件進行編譯和匯編后,會在各自的目標文件中形成同名的強符號,導致最終鏈接時報錯。這是由于在C語言中,函數和初始化后的全局變量默認都是強符號,如果你想改為弱符號,那么可以使用__attribute__((weak))修飾。在這里提一下最近58客戶端發現的一個有意思的事情。在iOS 8.11.1版本以后,我們發現buggly上崩潰日志都會攜帶一個來自RN的函數調用棧RCTFBQuickPerformanceLoggerConfigureHooks,在RN中它的聲明如下,
但是在源碼中,這個函數沒有任何實現,完全是一個空函數。看名字這個函數是hook使用的,那么它是怎么實現hook的呢?將RCT__EXTERN 展開后為__attribute__((visibility("default"))),其作用為將RCTFBQuickPerformanceLoggerConfigureHooks向外界暴露,如果外界存在同名函數,那么RCTFBQuickPerformanceLoggerConfigureHooks會報符號沖突的錯誤。那么如何做到即能暴露符號,又不造成符號沖突呢?這就利用了__attribute__((weak)),將RCTFBQuickPerformanceLoggerConfigureHooks生命為弱符號,當外界有同名函數時,SDK內部調用外屆的函數,否則調用內部空函數。
?? ?為了防止出現函數名沖突,在UNIX的C環境下,所有的函數會被加上”_”前綴,也就是說void hello ( ),符號實際上為”_hello”,這種機制能夠避免與系統函數的沖突。C++為了解決符號沖突的問題,表現的更為徹底。與C相比,C++有命名空間的限制,可以極大地避免函數的沖突,除了命名空間外,C++還存在構造和析構函數,函數重載等特征。這就導致C++的函數符號要比C函數更復雜。同樣的一個函數,在C和C++中,函數符號是完全不一樣的。假設有函數
在C環境下,它的符號為”_cleanup”,而在C++環境下它的符號為“__Z7cleanupPv”,這就表明,同樣一個函數在C和C++中,修飾機制是不一樣的。為了避免由于符號不同導致的問題,很多開源代碼會加上extern?"C”?{}來限定函數在C環境。但是在C環境中并不識別extern?"C”標識,因此你會看到很多的開源代碼中存在以下代碼
其意圖在于如果在C++環境中則限定為C環境。那么究竟C++的修飾機制是怎樣的呢?我們看到一個C++函數,如何推斷出它的符號呢?很遺憾,我沒有找到明確的關于C++函數符號修飾的介紹,不同的編譯器不同的平臺簽名有所不同。不過沒有關系,辦法還是有的,假設我想知道JavaScriptCore中某個C++函數的符號,那么我們可以創建一個cpp文件,將C++函數名復制過去,
聲明命名空間和枚舉
創建函數
然后通過gcc -c將文件編譯成目標文件WBIMC++.o,然后調用命令nm?WBIMC++.o即可查看相應的符號。
能獲取到C++的符號,是不是也就意味著hook C++函數是可行的。我們在fishhook的符號中隨便傳入一個JavaScriptCore的C++函數符號”_ZNK3JSC11SlotVisitor18containsOpaqueRootEPv“,通過代碼斷點調試發現,fishhook能夠正確獲取和替換函數指針
因此hook C++是可行的。
四、Mach-O文件簡介
? ? 在接觸Mach-O之前,我有兩個疑問,第一個是之前提出的問題,fishhook為什么不能hook自己寫的C函數。第二個問題是跟58正在做的技術項目相關,如何動態調用static 函數。弄清楚這兩個問題必須要對Mach-O有較為透徹的了解。
? ? 什么是Mach-O,按我的理解就是遵循特定結構的文件。一般比較常見的文件有:應用程序、目標文件、動態庫、鏈接器等,其中應用程序、目標文件.o是尤為重要的。Mach-O可以分為三個部分:
1)、Header
Header是文件的頭部信息,包括CPU信息、文件類型、Command條數及Size信息。總體來說,作為開發者Header使用的較少,比較常用的是(uintptr_t)&_mh_execute_header獲取header地址進行計算用。
Header
2)、Commands
Commands描述的是文件的加載信息,加載信息有很多,加載的段、符號表、動態庫信息等都在Commands中取到。這個部分信息還是比較有用的,我們可以從這里獲取到符號表和字符串表的偏移量,下文中會有詳細的解釋。
Commands
首先來說下段(Segment),上圖中可以看出共加載了4個段,__PAGEZERO是一個空段,它位于文件起始段的位置。__TEXT和__DATA分別是文本段和數據段,分別存儲了代碼信息和數據信息。__LINKEDIT是鏈接信息段,可以通過__LINKEDIT進行地址計算。段又可以細分為section,每個Segment可以包含多個section。
段展開
3)、數據區
?? ?除了Header和Commands外所有的原始數據。Commands是對數據的匯總提示,而數據區則是真實的數據。Commands與數據區的關系就像size和char*的關系。
數據展示
接下來先介紹幾個比較重要的模塊:
1)、(__TEXT,__text)
這里存放的是匯編后的代碼,當我們進行編譯時,每個.m文件會經過預編譯->編譯->匯編形成.o文件,稱之為目標文件。匯編后,所有的代碼會形成匯編指令存儲在.o文件的(__TEXT,__text)區((__DATA,__data)也是類似)。鏈接后,所有的.o文件會合并成一個文件,所有.o文件的(__TEXT,__text)數據都會按鏈接順序存放到應用文件的(__TEXT,__text)中。
(__TEXT,__text)
2)、(__DATA,__data)
存儲數據的section,static在進行非零賦值后會存儲在這里,如果static 變量沒有賦值或者賦值為0,那么它會存儲在(__DATA,__bss)中。
(__DATA,__data)
3)、Symbol Table
符號表,這個是重點中的重點,符號表是將地址和符號聯系起來的橋梁。符號表并不能直接存儲符號,而是存儲符號位于字符串表的位置。
Symbol Table
4)、String Table
字符串表所有的變量名、函數名等,都以字符串的形式存儲在字符串表中。
String Table
5)、動態符號表
動態符號表存儲的是動態庫函數位于符號表的偏移信息。(__DATA,__la_symbol_ptr) section 可以從動態符號表中獲取到該section位于符號表的索引數組。
動態符號表
動態符號表并不存儲符號信息,而是存儲其位于符號表的偏移信息。Fishhook源碼看起來比較復雜主要是因為hook的是動態鏈接的函數,索引和鏈接關系比較繞。但是我們自己編寫的C函數不是動態鏈接的,而是在編譯鏈接后代碼指令就存儲在文件內部的函數,因此不會用到動態符號表。接下來我們以static 函數為例,看看如何動態的查找自己編寫的函數地址。
五、動態調用static函數
在58iOS客戶端中大量存在static函數,這些static函數該如何動態調用呢?能否通過腳本來調用static函數呢?在調研Mach-O之前,我們是一愁莫展,嘗試使用dlsym函數獲取靜態函數,但是實踐發現dlsym并不能獲取到函數地址。在了解Mach-O后,我們發現Mach-O文件中存放了所有編譯過的的函數指令,static 函數也一定在文件中。假設在文件中下面函數
示例函數
在Mach-O文件中,搜索代碼段,可以發現靜態函數存放在代碼段中,其地址為0x1000010C0
查看函數地址
那么我們如何通過函數的名稱獲取到函數地址呢?所謂的函數名實際上就是函數符號,因此函數地址與函數名強關聯。
符號表與字符串表、函數地址、section的關系
符號表實際上是個結構體數組,結構體nlist_64中包括該符號位于字符串表的偏移,section索引,以及對應的地址信息。在符號表中,實際上不能直接獲取到其對應的符號,在圖中我們能看到符號為”_s_cleanup”,這實際上是工具幫我們獲取好后展示出來的,實際上我們在代碼中只能拿到其位于字符串表中的索引,在_s_cleanup的符號表中其索引為0x594,也就是說字符串表+0x594即為_s_cleanup字符串符號。
字符串表的起始地址
從上圖中可以看出字符串表的起始地址為0x6004,0x6004+0x594 = 0x6598。
計算符號位置獲取符號
獲取到字符串符號后,我們可以知道這個符號是不是我們想要的符號,如果是我們想要的符號,那么獲取其函數地址。到這里,應該說通過Mach-O文件獲取靜態鏈接函數地址已經完美解決了。需要注意的是,這個函數地址并不是真實的地址,需要計算出其相對于真實地址的偏移,再加上真實文件地址即為真實函數地址。
函數地址計算
那么如何獲取函數符號表、字符串表呢?實際上segment和符號表在Commands中是順序存放的,_mh_execute_header.ncmds可以根據索引遍歷所有的Command。
找到LC_SYMTAB后,LC_SYMTAB會告訴我們符號表位于可執行文件的偏移以及字符串表位于可執行文件的偏移。地址計算后即可得到符號表和字符串表
獲取符號表和字符串表
最終效果如下:
動態調用static函數演示
細心的同學可能會發現一個bug,static 函數在不同的文件中是可以同名的,參數只有一個函數符號的話如何確定是哪個文件中函數?實際上在符號表中,是可以存在相同符號的,即如果兩個文件中都存在s_cleanup函數,那么符號表中會存在兩個_s_cleanup,只不過他們的函數地址不同。那么如何區分同名靜態函數呢?實際上在鏈接時,各個段可以理解為按文件的順序存放的,也就是說符號表實際上也是存在文件順序的。
鏈接過程簡圖
符號表的type可以區分出這個符號是否是文件相關信息,type == 0x64則是文件相關信息,因此在遍歷符號表時可以判斷出當前正在遍歷哪個文件的符號。能判斷出正在遍歷哪個文件,那么bug就迎刃而解。
獲取文件名
? ? 另外,如果static 函數只是在代碼中實現了,但是并沒有任何調用的地方,那么在編譯時,編譯器會將static函數優化掉,不會生成相關指令。因此符號表中不會存儲static函數相關信息,也就無法實現動態調用。如果想要做到static 數據存取,那么方式與此類似,只不過獲取到的地址不是函數地址,而是數據存儲地址,如果static 是函數內的局部變量,那么其符號需要加上函數符號,比如
那么它的靜態變量s_iData符號為"_application:didFinishLaunchingWithOptions:.s_iData”。通過memset即可修改變量的值。
? ? 關于Mach-O還有個比較有意思的是,我們可以自定義section,將數據和函數指令放入我們指定的section中,
修改函數存放section
編譯鏈接后,其文件中多了個(__TEXT,__mysection),并且函數還能正常運行。這為我們進行代碼混淆又提供了一個手段。
在了解Mach-O之前,我們無法動態調用內聯函數,動態調用任意C函數首先需要嘗試是否能夠通過dlsym函數獲取到指針,如果獲取不到函數指針則可能說明是內聯函數,因此需要根據if-else來判斷是哪個內聯函數。但是現在我們可以通過Mach-O發現,所謂的內聯函數在iOS代碼中都是以static inline 修飾的,那么在編譯時內聯函數的函數符號會被寫入當前目標文件的符號表,函數實現會被當做指令寫入代碼段,如同普通函數一樣。在AppDelegate.m中調用CGSizeMake后,查看AppDelegate的目標文件符號表,可以看出符號表中包含內聯函數的符號,如同普通函數一樣。
_CGSizeMake
六、總結
? ? 在iOS領域hook的方式有很多種,在不是必須的情況下還是少用為妙,hook之后出現問題非常難排查。本文主要介紹了如何根據Mach-O文件,獲取靜態鏈接的函數地址,動態鏈接的函數可以參考fishhook。