1. 程式人生 > IOS開發 >iOS 逆向 - Mach-O檔案

iOS 逆向 - Mach-O檔案

前言

前幾篇文章中 :

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 命令可以檢視檔案型別

也就是說 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 可執行檔案的支援架構的 .

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

通用二進位制檔案

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

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

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

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

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

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

Fat binary 的組合與拆分

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

2 - 編輯執行模式

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

3 - Build Settings

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

4 - 選擇真機 run

run 起來後找到 Mach-O 檔案

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

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

拆分 Fat binary

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

例:

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

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

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

合併 Fat binary

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

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

一模一樣的 .

Tips:

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

補充

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

Mach-O 檔案結構

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

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

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

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

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

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

    • 包含 Segement 的具體資料

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

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

Mach Header

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 為例.

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

_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 檔案結構我們已經講述完了 . 後續在逆向的過程中涉及到具體儲存內容我們會繼續介紹 .