動態庫注入
免越獄除錯的方案中,常用的有 ofollow,noindex">IPAPatch 與 MonkeyDev ,都會將自定義的程式碼打成 framework,注入到 App 中,以實現除錯的功能。
環境資訊
optool: commit(2898b51)
dyld: commit(3f928f3)
關於動態庫注入,IPAPatch 採用的是開源的 optool ,MonkeyDev 採用的是 monkeyparser。相比之下 monkeyparser 比 optool 多一些功能,但在動態庫注入上,原理應該類似。除了免越獄除錯這種玩法外,還可以用於 App 自動化測試、效能分析等。
在不使用 Xcode 除錯的情況下,如果想要獲取 premain 的耗時,可以看 Joy 的 如何精確度量 iOS App 的啟動時間 。其中要記錄所有動態庫載入時間,所以打點的庫需要第一個被載入。對於內部應用,可以用 Cocoapods,對於 ipa 產物來說,可以直接將動態庫注入到第一個 LoadCommand(第一個 LC_LOAD_DYLIB
)。
流程總覽
注入做的事情,其實就是給 MachO 的每個架構中,新增型別為 LC_LOAD_DYLIB
的 LoadCommand。
之後補個圖
optool 原始碼淺析
optool 實現了動態庫注入與移除、移除簽名,移除 ASLR 等功能,主要看動態庫的部分。
基礎配置
拉取原始碼:
# 拉取程式碼 git clone --depth 1 -b master [email protected]:alexzielenski/optool.git # 拉取 optool 用來做引數解析的 FSArgumentParser cd optool git submodule init git submodule update --remote
開啟工程,Xcode -> Product -> Scheme -> Edit Scheme… -> Run -> Arguments 新增入參。相當於控制檯直接呼叫,方便斷點除錯。
# install: 動態庫注入 Action # -t: 指定目標二進位制 # -p: 指定動態庫 install -t /Users/saitjr/Desktop/TTTest.app/TTTest -p /Users/saitjr/Desktop/MTHawkeye.framework/MTHawkeye
關於 -p
引數,也就是動態庫路徑的指定,後文再談,這裡只是為了除錯 optool。目前寫本地絕對路徑,注入後的 App 會啟動閃退。
在 optool 的 main
函式中,如果斷點能正確拿到入參,就說明配置沒問題了。
int main(int argc, const char * argv[]) { @autoreleasepool { BOOL showHelp = NO; NSLog(@"%s", argv[1]); // 輸出 install ... ... }
讀取 MachO Header
引數校驗的部分直接跳過,從讀取二進位制,解析 header 開始:
// 讀取可執行檔案 NSData *originalData = [NSData dataWithContentsOfFile:executablePath]; // mutableCopy 一下,方便編輯 NSMutableData *binary = originalData.mutableCopy; // 初始化 thin header 陣列,陣列長度為 4,也就是最多能包含四個架構 struct thin_header headers[4]; // 初始化 thin header 個數,即可執行檔案中的架構數 uint32_t numHeaders = 0; // 讀取 header headersFromBinary(headers, binary, &numHeaders);
thin_header
結構體是自定義的,裡面直接用的系統的 mach_header
,雖然還有一個是 mach_header_64
,但是 64 裡面多的 reserved
只是保留欄位,沒有具體含義,所以不影響解析。
除此之外,還將 fat header 中,每個架構的 offset
逐個賦值給了 thin header,只是為了方便讀取。
在 headersFromBinary
中,對 header 進行讀取。
之後補個圖
查詢 LoadCommand
header 讀取完後,注入需要用到的資訊有:
- 架構數
- 每個架構的偏移量
- 每個架構的 Load Command 數量
- 每個架構的 Load Command 大小
然後遍歷全部架構,執行 install
,也就是 insertLoadEntryIntoBinary
方法。在真正執行插入之前,會先檢查當前 LoadCommand 是否已經存在,所以有個查詢的操作,方法是 binaryHasLoadCommandForDylib
。
之後補個圖