轉:https://blog.csdn.net/MinggeQingchun/article/details/80070534
前言
符號表歷來是逆向工程中的“必爭之地”,而iOS應用在上線前都會裁去符號表,以避免被逆向分析。
本文會介紹一個自己寫的工具,用於恢復iOS應用的符號表。
直接看效果,支付寶恢復符號表後的樣子:
文章有點長,請耐心看到最後,亮點在最後。
爲什麼要恢復符號表
逆向工程中,調試器的動態分析是必不可少的,而 Xcode + lldb 確實是非常好的調試利器, 比如我們在Xcode裏可以很方便的查看調用堆棧,如上面那張圖可以很清晰的看到支付寶登錄的RPC調用過程。
實際上,如果我們不恢復符號表的話,你看到的調試頁面應該是下面這個樣子:
同一個函數調用過程,Xcode的顯示簡直天差地別。
原因是,Xcode顯示調用堆棧中符號時,只會顯示符號表中有的符號。爲了我們調試過程的順利,我們有必要把可執行文件中的符號表恢復回來。
符號表是什麼
我們要恢復符號表,首先要知道符號表是什麼,他是怎麼存在於 Mach-O 文件中的。
符號表儲存在 Mach-O 文件的 __LINKEDIT 段中,涉及其中的符號表(Symbol Table)和字符串表(String Table)。
這裏我們用 MachOView 打開支付寶的可執行文件,找到其中的 Symbol Table 項。
符號表的結構是一個連續的列表,其中的每一項都是一個 struct nlist
。
1 2 3 4 5 6 7 8 9 10 11 12 |
// 位於系統庫 <macho-o/nlist.h> 頭文件中 struct nlist { union { //符號名在字符串表中的偏移量 uint32_t n_strx; } n_un; uint8_t n_type; uint8_t n_sect; int16_t n_desc; //符號在內存中的地址,類似於函數指針 uint32_t n_value; }; |
這裏重點關注第一項和最後一項,第一項是符號名在字符串表中的偏移量,用於表示函數名,最後一項是符號在內存中的地址,類似於函數指針(這裏只說明大概的結構,詳細的信息請參考官方Mach O文件格式的文檔)。
也就是說如果我們知道了符號名和內存地址的對應關係,我們是可以根據這個結構來逆向構造出符號表數據的。
知道了如何構造符號表,下一步就是收集符號名和內存地址的對應關係了。
獲取OC方法的符號表
因爲OC語言的特性,編譯器會將類名、函數名等編譯進最後的可執行文件中,所以我們可以根據Mach-O文件的結構逆向還原出工程裏的所有類,這也就是大名鼎鼎的逆向工具 class-dump 了。class-dump 出來的頭文件裏是有函數地址的:
所以我們只要對class-dump的源碼稍作修改,即可獲取我們要的信息。
符號表恢復工具
整理完數據格式,又理清了數據來源,我們就可以寫工具了。
實現過程就不詳細說明了,工具開源在我的Github上了,鏈接:
https://github.com/tobefuturer/restore-symbol
我們來看看怎麼用這個工具:
1.下載源碼編譯
1 2 3 |
git clone --recursive https://github.com/tobefuturer/restore-symbol.git cd restore-symbol && make ./restore-symbol |
2.恢復OC的符號表,非常簡單
1 |
./restore-symbol ./origin_AlipayWallet -o ./AlipayWallet_with_symbol |
origin_AlipayWallet 爲Clutch砸殼後,沒有符號表的 Mach-O 文件
-o 後面跟輸出文件位置
3.把 Mach-O 文件重簽名打包,看效果
文件恢復符號表後,多出了20M的符號表信息
Xcode裏查看調用棧
可以看到,OC函數這部分的符號已經恢復了,函數調用棧裏已經能看出大致的調用過程了,但是支付寶裏,採用了block的回調形式,所以還有很大一部分的符號沒能正確顯示。
下面我們就來看看怎麼樣恢復這部分block的符號。
獲取block的符號信息
還是同樣的思路,要恢復block的符號信息,我們必須知道block在文件中的儲存形式。
block在內存中的結構
首先,我們先分析下運行時,block在內存中的存在形式。block在內存中是以一個結構體的形式存在的,大致的結構如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct __block_impl { /** block在內存中也是類NSObject的結構體, 結構體開始位置是一個isa指針 */ Class isa; /** 這兩個變量暫時不關心 */ int flags; int reserved; /** 真正的函數指針!! */ void (*invoke)(...); ... } |
說明下block中的isa指針,根據實際情況會有三種不同的取值,來表示不同類型的block:
-
_NSConcreteStackBlock
棧上的block,一般block創建時是在棧上分配了一個block結構體的空間,然後對其中的isa等變量賦值。
-
_NSConcreteMallocBlock
堆上的block,當block被加入到GCD或者被對象持有時,將棧上的block複製到堆上,此時複製得到的block類型變爲了_NSConcreteMallocBlock。
-
_NSConcreteGlobalBlock
全局靜態的block,當block不依賴於上下文環境,比如不持有block外的變量、只使用block內部的變量的時候,block的內存分配可以在編譯期就完成,分配在全局的靜態常量區。
第2種block在運行時纔會出現,我們只關注1、3兩種,下面就分析這兩種isa指針和block符號地址之間的關聯。
block isa指針和符號地址之間的關聯
分析這部分需要用到IDA這個反彙編軟件, 這裏結合兩個實際的小例子來說明:
1._NSConcreteStackBlock
假設我們的源代碼是這樣很簡單的一個block:
1 2 3 4 5 6 7 8 9 10 11 |
@implementation ViewController - (void)viewDidLoad { int t = 2; void (^ foo)() = ^(){ NSLog(@"%d", t); //block 引用了外部的變量t }; foo(); } @end |
編譯完後,實際的彙編長這個樣子:
實際運行時,block的構造過程是這樣:
- 爲block開闢棧空間
- 爲block的isa指針賦值(一定會引用全局變量:
_NSConcreteStackBlock
) - 獲取函數地址,賦值給函數指針
所以我們可以整理出這樣一個特徵:
重點來了!!!
凡是代碼裏用到了棧上的block,一定會獲取__NSConcreteStackBlock
作爲isa指針,同時會緊接着獲取一個函數地址,那個函數地址就是block的函數地址。
結合下面這個圖,仔細理解上面這句話
(這張圖和上面那張圖是同一個文件,不過裁掉了符號表)
利用這個特徵,逆向分析時我們可以做如下推斷:
在一個OC方法裏發現引用了__NSConcreteStackBlock
這個變量,那麼在這附近,一定會出現一個函數地址,這個函數地址就是這個OC方法裏的一個block。
比如上面圖中,我們發現 viewDidLoad 裏,引用了__NSConcreteStackBlock
,同時緊接着加載了 sub_100049D4 的函數地址,那我們就可以認定sub_100049D4是viewDidLoad裏的一個block, sub_100049D4函數的符號名應該是 viewDidLoad_block.
2. _NSConcreteGlobalBlock
全局的靜態block,是那種不引用block外變量的block,他因爲不引用外部變量,所以他可以在編譯期就進行內存分配操作,也不用擔心block的複製等等操作,他存在於可執行文件的常量區裏。
不太理解的話,看個例子:
我們把源代碼改成這樣:
1 2 3 4 5 6 7 8 9 10 11 12 |
@implementation ViewController - (void)viewDidLoad { void (^ foo)() = ^(){ //block 不引用外部的變量 NSLog(@"%d", 123); }; foo(); } @end |
那麼在編譯後會變成這樣:
那麼借鑑上面的思路,在逆向分析的時候,我們可以這麼推斷
- 在靜態常量區發現一個_NSConcreteGlobalBlock的引用
- 這個地方必然存在一個block的結構體數據
- 在這個結構體第16個字節的地方會出現一個值,這個值是一個block的函數地址
3. block 的嵌套結構
實際在使用中,可能會出現block內嵌block的情況:
1 2 3 4 5 6 7 8 |
- (void)viewDidLoad { dispatch_async(background_queue ,^{ ... dispatch_async(main_queue, ^{ ... }); }); } |
所以這裏block就出現了父子關係,如果我們將這些父子關係收集起來,就可以發現,這些關係會構成圖論裏的森林結構,這裏可以簡單用遞歸的深度優先搜索來處理,詳細過程不再描述。
block符號表提取腳本(IDA+python)
整理上面的思路,我們發現搜索過程依賴於IDA提供各種引用信息,而IDA是提供了編程接口的,可以利用這些接口來提取引用信息。
IDA提供的是Python的SDK,最後完成的腳本也放在倉庫裏search_oc_block/ida_search_block.py。
提取block符號表
這裏簡單介紹下怎麼使用上面這個腳本
- 用IDA打開支付寶的 Mach-O 文件
- 等待分析完成! 可能要一個小時
- Alt + F7 或者 菜單欄
File -> Script file...
- 等待腳本運行完成,預計30s至60s,運行過程中會有這樣的彈窗
- 彈窗消失即block符號表提取完成
- 在IDA打開文件的目錄下,會輸出一份名爲
block_symbol.json
的json格式block符號表
恢復符號表&實際分析
用之前的符號表恢復工具,將block的符號表導入Mach-O文件
1 |
./restore-symbol ./origin_AlipayWallet -o ./AlipayWallet_with_symbol -j block_symbol.json |
-j 後面跟上之前得到的json符號表
最後得到一份同時具有OC函數符號表和block符號表的可執行文件
這裏簡單介紹一個分析案例, 你就能體會到這個工具的強大之處了。
- 在Xcode裏對
-[UIAlertView show]
設置斷點 - 運行程序,並在支付寶的登錄頁面輸入手機號和錯誤的密碼,點擊登錄
- Xcode會在‘密碼錯誤’的警告框彈出時停下,左側會顯示出這樣的調用棧
一張圖看完支付寶的登錄過程
項目開源地址: