本文介紹了恢復符號表的技巧,而且利用該技巧實現了在 Xcode 中對目標程序下符號斷點調試,該技巧能夠顯著地減小逆向分析時間。在文章的最後,做者以支付寶爲例,展現出經過在 UIAlertView 的 show 方法處下斷點,從而得到支付寶的調用棧的過程。python
本文涉及的代碼也開源在:https://github.com/tobefuturer/restore-symbol,歡迎 Star 和提 Issue。感謝做者受權發表。git
做者介紹:楊君,中山大學計算機系研究生,iOS 開發者,擅長領域 iOS 安全和逆向工程,我的博客:http://blog.imjun.net 。github
符號表從來是逆向工程中的 「必爭之地」,而 iOS 應用在上線前都會裁去符號表,以免被逆向分析。編程
本文會介紹一個本身寫的工具,用於恢復 iOS 應用的符號表。json
直接看效果 , 支付寶恢復符號表後的樣子:安全
逆向工程中,調試器的動態分析是必不可少的,而 Xcode + lldb 確實是很是好的調試利器 , 好比咱們在 Xcode 裏能夠很方便的查看調用堆棧,如上面那張圖能夠很清晰的看到支付寶登陸的 RPC 調用過程。async
實際上,若是咱們不恢復符號表的話,你看到的調試頁面應該是下面這個樣子:函數
同一個函數調用過程,Xcode 的顯示簡直天差地別。工具
緣由是,Xcode 顯示調用堆棧中符號時,只會顯示符號表中有的符號。爲了咱們調試過程的順利,咱們有必要把可執行文件中的符號表恢復回來。ui
咱們要恢復符號表,首先要知道符號表是什麼,他是怎麼存在於 Mach-O 文件中的。
符號表儲存在 Mach-O 文件的 __LINKEDIT 段中,涉及其中的符號表(Symbol Table)和字符串表(String Table)。
這裏咱們用 MachOView 打開支付寶的可執行文件,找到其中的 Symbol Table 項。
符號表的結構是一個連續的列表,其中的每一項都是一個 struct nlist
。
// 位於系統庫 <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 語言的特性,編譯器會將類名、函數名等編譯進最後的可執行文件中,因此咱們能夠根據 Mach-O 文件的結構逆向還原出工程裏的全部類,這也就是大名鼎鼎的逆向工具 class-dump 了。class-dump 出來的頭文件裏是有函數地址的:
因此咱們只要對 class-dump 的源碼稍做修改,便可獲取咱們要的信息。
整理完數據格式,又理清了數據來源,咱們就能夠寫工具了。
實現過程就不詳細說明了,工具開源在個人 Github 上了,連接:
https://github.com/tobefuturer/restore-symbol
咱們來看看怎麼用這個工具:
1. 下載源碼編譯
git clone --recursive https://github.com/tobefuturer/restore-symbol.git cd restore-symbol && make ./restore-symbol
2. 恢復 OC 的符號表,很是簡單
./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 在內存中是以一個結構體的形式存在的,大體的結構以下:
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 在運行時纔會出現,咱們只關注 一、3 兩種,下面就分析這兩種 isa 指針和 block 符號地址之間的關聯。
分析這部分須要用到 IDA 這個反彙編軟件 , 這裏結合兩個實際的小例子來講明:
假設咱們的源代碼是這樣很簡單的一個 block:
@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.
全局的靜態 block,是那種不引用 block 外變量的 block,他由於不引用外部變量,因此他能夠在編譯期就進行內存分配操做,也不用擔憂 block 的複製等等操做,他存在於可執行文件的常量區裏。
不太理解的話,看個例子:
咱們把源代碼改爲這樣:
@implementation ViewController - (void)viewDidLoad { void (^ foo)() = ^(){ //block 不引用外部的變量 NSLog(@"%d", 123); }; foo(); } @end
那麼在編譯後會變成這樣:
那麼借鑑上面的思路,在逆向分析的時候,咱們能夠這麼推斷
在靜態常量區發現一個 _NSConcreteGlobalBlock 的引用
這個地方必然存在一個 block 的結構體數據
在這個結構體第 16 個字節的地方會出現一個值,這個值是一個 block 的函數地址
實際在使用中,可能會出現 block 內嵌 block 的狀況:
- (void)viewDidLoad { dispatch_async(background_queue ,^{ ... dispatch_async(main_queue, ^{ ... }); }); }
因此這裏 block 就出現了父子關係,若是咱們將這些父子關係收集起來,就能夠發現,這些關係會構成圖論裏的森林結構,這裏能夠簡單用遞歸的深度優先搜索來處理,詳細過程再也不描述。
整理上面的思路,咱們發現搜索過程依賴於 IDA 提供各類引用信息,而 IDA 是提供了編程接口的,能夠利用這些接口來提取引用信息。
IDA 提供的是 Python 的 SDK,最後完成的腳本也放在倉庫裏 search_oc_block/ida_search_block.py (https://github.com/tobefuturer/restore-symbol/blob/master/search_oc_block/ida_search_block.py。
這裏簡單介紹下怎麼使用上面這個腳本:
用 IDA 打開支付寶的 Mach-O 文件
等待分析完成! 可能要一個小時
Alt + F7 或者 菜單欄 File -> Script file...
等待腳本運行完成,預計 30s 至 60s,運行過程當中會有這樣的彈窗
彈窗消失即 block 符號表提取完成
在 IDA 打開文件的目錄下 , 會輸出一份名爲block_symbol.json
的 json 格式 block 符號表
用以前的符號表恢復工具,將 block 的符號表導入 Mach-O 文件
./restore-symbol ./origin_AlipayWallet -o ./AlipayWallet_with_symbol -j block_symbol.json
-j 後面跟上以前獲得的 json 符號表
最後獲得一份同時具備 OC 函數符號表和 block 符號表的可執行文件
這裏簡單介紹一個分析案例 , 你就能體會到這個工具的強大之處了。
在 Xcode 裏對 -[UIAlertView show]
設置斷點
運行程序,並在支付寶的登陸頁面輸入手機號和 錯誤的密碼 ,點擊登陸
Xcode 會在 ‘密碼錯誤’ 的警告框彈出時停下,左側會顯示出這樣的調用棧
項目開源地址: