iOS 符號表恢復 & 逆向支付寶

推薦序

本文介紹了恢復符號表的技巧,而且利用該技巧實現了在 Xcode 中對目標程序下符號斷點調試,該技巧能夠顯著地減小逆向分析時間。在文章的最後,做者以支付寶爲例,展現出經過在 UIAlertView 的 show 方法處下斷點,從而得到支付寶的調用棧的過程。python

本文涉及的代碼也開源在:https://github.com/tobefuturer/restore-symbol,歡迎 Star 和提 Issue。感謝做者受權發表。git

做者介紹:楊君,中山大學計算機系研究生,iOS 開發者,擅長領域 iOS 安全和逆向工程,我的博客:http://blog.imjun.netgithub

前言

符號表從來是逆向工程中的 「必爭之地」,而 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 方法的符號表

由於 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 在內存中的結構

首先,咱們先分析下運行時,block 在內存中的存在形式。block 在內存中是以一個結構體的形式存在的,大體的結構以下:

struct __block_impl {  
 /**  block 在內存中也是類 NSObject 的結構體,  結構體開始位置是一個 isa 指針  */  Class isa;  
 /** 這兩個變量暫時不關心 */  int flags;  int reserved;  
 /**  真正的函數指針!!  */  void (*invoke)(...);  ... }

說明下 block 中的 isa 指針,根據實際狀況會有三種不一樣的取值,來表示不一樣類型的 block:

  1. _NSConcreteStackBlock

    棧上的 block,通常 block 建立時是在棧上分配了一個 block 結構體的空間,而後對其中的 isa 等變量賦值。

  2. _NSConcreteMallocBlock

    堆上的 block,當 block 被加入到 GCD 或者被對象持有時,將棧上的 block 複製到堆上,此時複製獲得的 block 類型變爲了 _NSConcreteMallocBlock。

  3. _NSConcreteGlobalBlock

    全局靜態的 block,當 block 不依賴於上下文環境,好比不持有 block 外的變量、只使用 block 內部的變量的時候,block 的內存分配能夠在編譯期就完成,分配在全局的靜態常量區。

第 2 種 block 在運行時纔會出現,咱們只關注 一、3 兩種,下面就分析這兩種 isa 指針和 block 符號地址之間的關聯。

block isa 指針和符號地址之間的關聯

分析這部分須要用到 IDA 這個反彙編軟件 , 這裏結合兩個實際的小例子來講明:

1._NSConcreteStackBlock

假設咱們的源代碼是這樣很簡單的一個 block:

@implementation ViewController

- (void)viewDidLoad {
    int t = 2;
    void (^ foo)() = ^(){
        NSLog(@"%d", t); //block 引用了外部的變量 t
    };
    foo();
}

@end

編譯完後,實際的彙編長這個樣子:

實際運行時,block 的構造過程是這樣:

  1. 爲 block 開闢棧空間

  2. 爲 block 的 isa 指針賦值(必定會引用全局變量:_NSConcreteStackBlock

  3. 獲取函數地址,賦值給函數指針

因此咱們能夠整理出這樣一個特徵:

重點來了 !!!

凡是代碼裏用到了棧上的 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 的複製等等操做,他存在於可執行文件的常量區裏。

不太理解的話,看個例子:

咱們把源代碼改爲這樣:

@implementation ViewController

- (void)viewDidLoad {

    void (^ foo)() = ^(){
        //block 不引用外部的變量
        NSLog(@"%d", 123);
    };
    foo();
}

@end

那麼在編譯後會變成這樣:

那麼借鑑上面的思路,在逆向分析的時候,咱們能夠這麼推斷

  1. 在靜態常量區發現一個 _NSConcreteGlobalBlock 的引用

  2. 這個地方必然存在一個 block 的結構體數據

  3. 在這個結構體第 16 個字節的地方會出現一個值,這個值是一個 block 的函數地址

3. block 的嵌套結構

實際在使用中,可能會出現 block 內嵌 block 的狀況:

- (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 (https://github.com/tobefuturer/restore-symbol/blob/master/search_oc_block/ida_search_block.py

提取 block 符號表

這裏簡單介紹下怎麼使用上面這個腳本:

  1. 用 IDA 打開支付寶的 Mach-O 文件

  2. 等待分析完成! 可能要一個小時

  3. Alt + F7 或者 菜單欄 File -> Script file...

等待腳本運行完成,預計 30s 至 60s,運行過程當中會有這樣的彈窗

  1. 彈窗消失即 block 符號表提取完成

  2. 在 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 符號表的可執行文件

這裏簡單介紹一個分析案例 , 你就能體會到這個工具的強大之處了。

  1. 在 Xcode 裏對 -[UIAlertView show] 設置斷點

 

    • 運行程序,並在支付寶的登陸頁面輸入手機號和 錯誤的密碼 ,點擊登陸

    • Xcode 會在 ‘密碼錯誤’ 的警告框彈出時停下,左側會顯示出這樣的調用棧

一張圖看完支付寶的登陸過程

項目開源地址:

https://github.com/tobefuturer/restore-symbol

相關文章
相關標籤/搜索