在掌握上文所述工具後,就可以著手進行插件的開發。首先應該思考插件要實現的需求,找到相關的視圖和控制器。第一步可以先使用class-dump 分析已砸殼的APP,class-dump 可以根據Mach-O 檔案中的符號表( symbol table )分析出所有的類別名稱和方法宣告。
~ » ./class-dump -H -o /header/path WeChat
在匯出頭檔之後,將這些頭檔拉到一個XCode 工程中,方便後續的查找。
有了頭檔後,要鎖定到相關介面的控制器類別。透過OpenSSH 用Mac 連接越獄iPhone,利用Cycript 注入進程,呼叫方法[[UIApp keyWindow] recursiveDescription]
得到目前視圖的層次結構,拿到某視圖的位址後重複呼叫[#0x2b2b2b00 nextResponder]
方法,會最終得到該介面的控制器類別。
至此,一般有兩個切入點繼續進行逆向分析。
如果我們想Hook 的操作是介面上某個視圖的touch 事件觸發的,定位到該動作一般有兩種手段:
[button allTargets]
可以得到Targets 位址, [button actionsForTarget: Targets forControlEvent: [button allControlEvents]]
可以得到對應的動作方法。插件編寫過程中可能會用到該控制器的資料來源。可以從該控制器類別的成員變數清單中分析得到資料來源,也可以根據某些視圖(如TableView )的資料來源代理方法分析得到。有了資料來源,才能得到有關該控制器的更豐富的資訊。
簡單的插件可能只用Cycript 就能鎖定目標進行Hook,但是很多情況下目標函數帶有多個參數,邏輯較為複雜,如果想知道它的內部實現的細節,必須藉助靜態分析神器IDA ----它可以將Objective-C 編寫的程式碼反編譯成彙編程式碼,功能的實作細節將一覽無遺。在分析反組譯結果時,由於Objective-C 的訊息傳遞特性,一般訊息的發送其實是呼叫了objc_msgSend
這個函數。只需要牢記objc_msgSend
各個參數的意義及ARM 架構的呼叫慣例,即可順利從組譯程式碼完成對函數呼叫的復原。呼叫慣例是函數的前四個參數使用R0-R3 通用暫存器進行傳遞,更多的參數會被壓人堆疊中,返回值儲存在R0暫存器中。那麼[aObject aMessage: arg1];
對應objc_msgSend(aObject, aMessage, arg1);
,R0 存放訊息的接收者位址,R1 存放selector,R2 存放第一個參數位址。更多參數的訊息格式如下:
objc_msgSend(R0, R1, R2, R3, *SP, *(SP + sizeOfLastArg), …)
透過上述格式,能把某函數中的邏輯一步步解析出來。這裡如果再輔以LLDB 的動態單步驟調試,在某句組合語句的位址下斷點追蹤調試,有助於理解功能實現的細節。
Hook 目標函數有很多種方案,但原理上都是基於Objective-C 的動態特性進行Method Swizzling 來取代原有的實作。本次將詳細介紹CaptainHook 函式庫的使用方法。這個函式庫是基於Cydia Substrate 中的MSHookMessageEx()
來實現的,該函數的宣告為:
void MSHookMessageEx(Class _class, SEL message, IMP hook, IMP *old);
在iOSOpenDev 安裝完成後,即可使用CaptainHook Tweak 範本建立一個工程。 CaptainHook 函式庫引進了一系列編寫Hook 函式的新語法。首先要在CHConstructor()
中載入要Hook 的函數所在的類,如CHLoadLateClass(UIView)
。然後再註冊要Hook 的函數CHHook(argNumber, className, arg1, arg2)
。 CHConstructor
的宏定義如下:
#define CHConcat(a, b) CHConcat_(a, b)
#define CHConstructor static __attribute__((constructor)) void CHConcat(CHConstructor, __LINE__)()
在__attribute__((constructor))
後的內容能保證在dylib 載入時運行,一般是在程式啟動的時刻。類似地,其他符號的引入也是透過巨集定義的方法。
再介紹如何用CaptainHook 宣告Hook 函數並實現,直接上程式碼。
CHDeclareClass ( BXViewController );
CHOptimizedMethod ( 0 , self , void , BXViewController , viewDidLoad ) {
CHSuper ( 0 , BXViewController , viewDidLoad );
/* HERE TO WRITE YOUR CODE */
}
CHDeclareMethod0 ( void , BXViewController , addFriends ) {
/* HERE TO WRITE YOUR CODE */
}
編寫完成後,連接手邊的iPhone 進行編譯,確保產生對應架構的動態函式庫。
使用工具yololib 將編譯好的dylib 檔案注入到Mach-O 執行檔的Load Commands 清單中。
~ » ./yololib [binary] [dylib file]
Mach-O 檔案的結構主要包括三大部分。最前端的部分是Header 結構體,保存了Mach-O 的平台類型、檔案類型、 LoadCommands 數目等資訊;緊跟著Header 的是Load Commands 部分,透過解析這一部分可以確定檔案的邏輯結構和它在虛擬記憶體中的佈局。 yololib 工具正是改變Load Commands 部分的資訊來對dylib 進行載入。具體的實現過程逐步如下:
因為Load Commands 資訊的改變,對應的Header 結構體中ncmds 和sizeofcmds 都會改變,所以要先對Header 進行修改:
// 取出 Header
fseek(newFile, top, SEEK_SET);
struct mach_header mach;
fread(&mach, sizeof(struct mach_header), 1, newFile);
NSData* data = [DYLIB_PATH dataUsingEncoding:NSUTF8StringEncoding];
// 计算 dylib 的大小
uint32_t dylib_size = (uint32_t)[data length] + sizeof(struct dylib_command);
dylib_size += sizeof(long) - (dylib_size % sizeof(long));
// 修改 cmds 和 sizeofcmds
mach.ncmds += 1;
uint32_t sizeofcmds = mach.sizeofcmds;
mach.sizeofcmds += dylib_size;
// 写回修改后的 Header
fseek(newFile, -sizeof(struct mach_header), SEEK_CUR);
fwrite(&mach, sizeof(struct mach_header), 1, newFile);
接著改變Load Commands 部分,加入dylib 的載入資訊:
fseek(newFile, sizeofcmds, SEEK_CUR);
// 创建一个 dylib 类型的 command
struct dylib_command dyld;
fread(&dyld, sizeof(struct dylib_command), 1, newFile);
// 修改 dyld 结构体数据
dyld.cmd = LC_LOAD_DYLIB;
dyld.cmdsize = dylib_size;
dyld.dylib.compatibility_version = DYLIB_COMPATIBILITY_VERSION;
dyld.dylib.current_version = DYLIB_CURRENT_VER;
dyld.dylib.timestamp = 2;
dyld.dylib.name.offset = sizeof(struct dylib_command);
// 写回修改
fseek(newFile, -sizeof(struct dylib_command), SEEK_CUR);
fwrite(&dyld, sizeof(struct dylib_command), 1, newFile);
最後寫入dylib 的資料。
fwrite([data bytes], [data length], 1, newFile);
用codesign
指令重簽名產生的動態函式庫和APP 中所有的執行檔(包括Plugin 資料夾中的APP Extension),用xcrun -sdk iphoneos PackageApplication -v
指令將動態函式庫和所有檔案一起打包,整個過程你懂的。如果有企業證書,進行簽章打包後的應用程式可以安裝在信任該證書的非越獄iPhone 上。一顆賽艇!