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

轉: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:

  1. _NSConcreteStackBlock

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

  2. _NSConcreteMallocBlock

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

  3. _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的構造過程是這樣:

  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的複製等等操作,他存在於可執行文件的常量區裏。

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

我們把源代碼改成這樣:


 

1

2

3

4

5

6

7

8

9

10

11

12


 

@implementation ViewController

- (void)viewDidLoad {

void (^ foo)() = ^(){

//block 不引用外部的變量

NSLog(@"%d", 123);

};

foo();

}

@end

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

那麼借鑑上面的思路,在逆向分析的時候,我們可以這麼推斷

  1. 在靜態常量區發現一個_NSConcreteGlobalBlock的引用
  2. 這個地方必然存在一個block的結構體數據
  3. 在這個結構體第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符號表

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

  1. 用IDA打開支付寶的 Mach-O 文件
  2. 等待分析完成! 可能要一個小時
  3. Alt + F7 或者 菜單欄 File -> Script file...
  4. 等待腳本運行完成,預計30s至60s,運行過程中會有這樣的彈窗
  5. 彈窗消失即block符號表提取完成
  6. 在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符號表的可執行文件

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

  1. 在Xcode裏對 -[UIAlertView show] 設置斷點
  2. 運行程序,並在支付寶的登錄頁面輸入手機號和錯誤的密碼,點擊登錄
  3. Xcode會在‘密碼錯誤’的警告框彈出時停下,左側會顯示出這樣的調用棧

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

項目開源地址:

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

原文地址http://blog.imjun.net/posts/restore-symbol-of-iOS-app/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章