前言
前幾篇文章中 :
我們對重簽名和程式碼注入有了一定的瞭解 . 那麼這個過程中我們反覆提到一個最重要的檔案 -- 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
的架構 , 因此我們自己加上 armv7s
和 arm64e
.
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
工具時需要恢復符號表的原因. 而因此fishhooh
在hook
系統函式的時候名字叫reBind
的原因 .
關於符號繫結這一點我們在講 fishhook
的時候會詳細講述一下 .
至此 , 整個 Mach-O
檔案結構我們已經講述完了 . 後續在逆向的過程中涉及到具體儲存內容我們會繼續介紹 .