iOS 逆向 - Mach-O檔案

李斌同學發表於2019-10-30

前言

前幾篇文章中 :

shell 指令碼自動重簽名與程式碼注入

應用簽名原理及重簽名 (重籤微信應用實戰)

重籤應用除錯與程式碼修改 (Hook)

我們對重簽名和程式碼注入有了一定的瞭解 . 那麼這個過程中我們反覆提到一個最重要的檔案 -- Mach-O .

那麼說來說去 , 這個Mach-O 到底是個什麼 . 既然它這麼重要 , 那麼我們有必要去好好的瞭解一下它 .

( 對概念不太感興趣的同學可以直接跳到第二章節 Mach-O 的檔案結構 )

MachO 檔案

Mach-O 其實是 Mach Object 檔案格式的縮寫,是 mac 以及 iOS 上可執行檔案的格式, 類似於 windows 上的 PE 格式 ( Portable Executable ) , linux 上的 elf 格式 ( Executable and Linking Format ) .

它是一種用於可執行檔案、目的碼、動態庫的檔案格式。作為 a.out 格式的替代,Mach-O 提供了更強的擴充套件性。

但是除了可執行檔案外 , 其實還有一些檔案也是使用的 Mach-O 的檔案格式 .

屬於 Mach-O 格式的常見檔案

  • 目標檔案 .o
  • 庫檔案
    • .a
    • .dylib
    • Framework
  • 可執行檔案
  • dyld ( 動態連結器 )
  • .dsym ( 符號表 )

Tips : 使用 file 命令可以檢視檔案型別

iOS 逆向 - Mach-O檔案

也就是說 Mach-O 並非一定是可執行檔案 , 它是一種檔案格式 , 分為 Mach-O Object 目標檔案 、 Mach-O ececutable 可執行檔案、 Mach-O dynamically 動態庫檔案、 Mach-O dynamic linker 動態連結器檔案、 Mach-O dSYM companion 符號表檔案 , 等等 .

大家可以自己通過 vim 幾個 .c , 然後 clang 生成 .o 目標檔案和可執行檔案來玩一下 , 以便更好地理解這幾種檔案以及其編譯的模式 .

那麼上圖中我們還看到一個 arm64 , 這個是什麼意思呢 ?

  • 在 release 模式下
  • 支援 iOS 11.0 系統版本以下

當滿足這兩個條件時 , 我們的應用打包出來的 Mach-O ececutable 可執行檔案是包含 arm64 以及 arm_v7 的架構的 , iPhone 5C 以上機型都是 64 位系統了 .

那麼包含了支援多架構的 Mach-O ececutable 可執行檔案被稱為 : 通用二進位制檔案 , 即多種架構都可讀取執行 .

另外 Xcode 中通過編譯設定 Architectures 是可以更改所生成的 Mach-O ececutable 可執行檔案的支援架構的 .

iOS 逆向 - Mach-O檔案

編譯器在生成 Mach-O 檔案會選擇 Architectures 以及 Valid Architectures 的交集 , 因此想要支援多架構的話 , 在Valid Architectures 中繼續新增就可以了 , 編譯生成 Mach-O 之後 , 使用 file 命令可以檢查下結果 .

通用二進位制檔案

  • 蘋果公司提出的一種程式程式碼。能同時適用多種架構的二進位制檔案

  • 同一個程式包中同時為多種架構提供最理想的效能。

  • 因為需要儲存多種程式碼,通用二進位制應用程式通常比單一平臺二進位制的程式要大。

  • 但是由於兩種架構有共通的非執行資源,所以並不會達到單一版本的兩倍之多。

  • 而且由於執行中只呼叫一部分程式碼,執行起來也不需要額外的記憶體。

通用二進位制檔案通常被稱為 Universal binary , 在 MachOView 等 中叫做 Fat binary , 這種二進位制檔案是可以完全拆分開來 , 或者重新組合的 , 那麼接下來我們來玩一下 .

Fat binary 的組合與拆分

1 - 新建工程 , 選擇支援系統版本 10.3 .

iOS 逆向 - Mach-O檔案

2 - 編輯執行模式

iOS 逆向 - Mach-O檔案

iOS 逆向 - Mach-O檔案

選擇 Release ( 測試完畢改回來 . 否則 run 太慢 )

3 - Build Settings

iOS 逆向 - Mach-O檔案
第一行是自帶的環境變數 , 你自己也可以刪掉自己寫 , iOS 10.3 以上 + release 環境下會預設包含 arm64 + armv7 的架構 , 因此我們自己加上 armv7sarm64e .

4 - 選擇真機 run

run 起來後找到 Mach-O 檔案

iOS 逆向 - Mach-O檔案

iOS 逆向 - Mach-O檔案

可以看到 , 我們的 Fat binary 就已經生成好了 .

使用 lipo - info 命令也是可以檢視支援架構的

iOS 逆向 - Mach-O檔案

拆分 Fat binary

lipo macho檔名稱 -thin 要拆分哪個架構 -output 拆分出來檔名
複製程式碼

例:

lipo 通用二進位制MachO_Test -thin armv7s -output macho_armv7s
複製程式碼

然後我們就看到資料夾多了一個 macho_armv7s , 檢視一下 :

iOS 逆向 - Mach-O檔案

另外拆分後原始檔並不會改變.

合併 Fat binary

lipo -create macho_arm64 macho_arm64e macho_armv7 macho_armv7s -output newMachO
複製程式碼

合併後我們來看下新生成的 和以前的檔案的雜湊值 .

iOS 逆向 - Mach-O檔案

一模一樣的 .

Tips:

這種方式在我們合併靜態庫的時候會經常用到 , 因為靜態庫本身就是 Mach-O 檔案嘛 , 另外我們在逆向的時候 , 有時也經常會用這種方法拆分二進位制檔案 , 因為我們只需要分析單一架構即可 , 無須肥大的二進位制檔案.

補充

另外稍微補充一點 , 多架構二進位制檔案組合成通用二進位制檔案時 , 程式碼部分是不共用的 ( 因為程式碼的二進位制檔案不同的組合在不同的 cpu 上可能會是不同的意義 ) . 而公共資原始檔是會共用的 .

Mach-O 檔案結構

iOS 逆向 - Mach-O檔案

Mach-O 的組成結構如圖所示包括了

  • Header 包含該二進位制檔案的一般資訊

    • 位元組順序、架構型別、載入指令的數量等。

    • 使得可以快速確認一些資訊,比如當前檔案用於 32 位還是 64 位,對應的處理器是什麼、檔案型別是什麼

  • Load commands 一張包含很多內容的表

    • 內容包括區域的位置、符號表、動態符號表等。
  • Data 通常是物件檔案中最大的部分

    • 包含 Segement 的具體資料

我們來找一個 Mach-O 檔案 使用 MachOView 或者 otool 命令去檢視一下檔案結構 .

iOS 逆向 - Mach-O檔案

iOS 逆向 - Mach-O檔案

那麼這個 Mach-O 到底這些部分存放的是什麼內容 , 加下來我們就來一一探索一下 .

Mach Header

iOS 逆向 - Mach-O檔案

Header 中儲存的內容大致如上圖所示 , 那麼每一條到底對應著什麼呢 ? , 我們開啟原始碼看一下, cmd + shift + o , 搜尋 load.h , 找 mach_header_64 結構體.

struct mach_header_64 {
    uint32_t	magic;		/* 魔數,快速定位64位/32位 */
    cpu_type_t	cputype;	/* cpu 型別 比如 ARM */
    cpu_subtype_t	cpusubtype;	/* cpu 具體型別 比如arm64 , armv7 */
    uint32_t	filetype;	/* 檔案型別 例如可執行檔案 .. */
    uint32_t	ncmds;		/* load commands 載入命令條數 */
    uint32_t	sizeofcmds;	/* load commands 載入命令大小*/
    uint32_t	flags;		/* 標誌位標識二進位制檔案支援的功能 , 主要是和系統載入、連結有關*/
    uint32_t	reserved;	/* reserved , 保留欄位 */
};
複製程式碼

mach_header_64 相較於 mach_header , 也就是 32 位標頭檔案 , 只是多了一個保留欄位 . mach_header 是連結器載入時最先讀取的內容 , 它決定了一些基礎架構 , 系統型別 , 指令條數等資訊.

Load Commands

Load Commands 詳細儲存著載入指令的內容 , 告訴連結器如何去載入這個 Mach-O 檔案.

通過檢視記憶體地址我們發現 , 在記憶體中 , Load Commands 是緊跟在 Mach_header 之後的 .

那麼這些 Load Commands 對應了什麼呢 ? 我們以 arm64 為例.

iOS 逆向 - Mach-O檔案

其中 _TEXT 段和 _DATA 段 , 是我們經常需要研究的 , MachOView 下面也有詳細列出.

iOS 逆向 - Mach-O檔案

_TEXT 段

我們來看看 _TEXT 段裡都存放了什麼 , 其實真正開始讀取就是從 _TEXT 段開始讀取的 .

名稱 內容
_text 主程式程式碼
_stubs , _stub_helper 動態連結
_objc_methodname 方法名稱
_objc_classname 類名稱
_objc_methtype 方法型別 ( v@: )
_cstring 靜態字串常量

_DATA 段

_DATA 在記憶體中是緊跟在 _TEXT 段之後的.

名稱 內容
_got : Non-Lazy Symbol Pointers 非懶載入符號表
_la_symbol_ptr : Lazy Symbol Pointers 懶載入符號表
_objc_classlist 類列表

...

以及以一些資料來源 就不一一列舉了 .

補充

另外有一點值得提一下的就是系統庫的方法 , 由於是公用的 , 存放在共享快取中 , 那麼我們的 Mach-O 中呼叫系統方法 ,

例如 : 呼叫 NSLog("%@,@"haha");

這個方法的實現肯定不在我們的 Mach-O 裡 , 那麼它如何找到方法實現呢 ?

其實就是 dyld 在進行連結的時候 , 會將 Mach-O 裡呼叫存放在共享快取中的方法進行符號繫結 , 而這個符號在 release 的時候是會被自動去掉的. 這也是我們經常使用收集 bug 工具時需要恢復符號表的原因. 而因此 fishhoohhook 系統函式的時候名字叫 reBind 的原因 .

關於符號繫結這一點我們在講 fishhook 的時候會詳細講述一下 .

至此 , 整個 Mach-O 檔案結構我們已經講述完了 . 後續在逆向的過程中涉及到具體儲存內容我們會繼續介紹 .

相關文章