如何在逆向工程中 Hook 得更準 - 微信屏蔽好友&群消息實戰
在逆向工程中往往需要針對想要做的功能 Hook 到相應的方法和屬性,小白面對 class-dump 后的大量頭文件表示只能靠『猜』。這里我分享下逆向微信實現屏蔽群消息和好友消息的實戰經驗,適用于 非越獄機 。
從 UI 猜
先用 Cycript 或 Reveal 獲取視圖層級信息,然后從 View 和 ViewController 的頭文件中尋找信息。然后就憑編程經驗去猜了,比如一些方法屬性的命名,一些常用的代碼設計等等套路。
比如現在我想在群信息頁面和個人聊天詳情頁面增加個屏蔽消息的開關,先找到對應的 ViewController 類,然后到頭文件中去找信息。因為這都是兩個列表 UI,這種信息頁面的列表 Cell 一般不需要重用的,直接數據配置即可。然后很容易發現這兩個 ViewController 都有 m_tableViewInfo 這個屬性,類型為 MMTableViewInfo 。接著順藤摸瓜,發現其與 MMTableViewSectionInfo 和 MMTableViewCellInfo 這兩個類構成了整個列表的數據。Table-Section-Cell 這三個層級的的數據對應著 UI,熟悉 iOS 的很容易看懂。進而使用這三個類的方法來修改 m_tableViewInfo 中的數據,實現修改 UI 的目的。因為是列表數據,憑經驗應該是 reloadData 的時候去做修改。恰好這兩個 ViewController 中都有 reloadTableData 方法,Hook 后果然生效,效果如圖:

逆向的時候需要處處為對方著想,換位思考。如果僅想著 Hook 系統的 API 來修改 UI,在這個例子里顯然要多走些彎路。
剩下的就是獲取好友 ID 和群 ID,用一個字典存儲是否屏蔽的標記,在操作開關的時候對字典賦值。這些功能也能夠通過分析 UI 逆向實現。
關聯相關類一起猜
在我之前寫的 Make WeChat Great Again 里有提到 CMessageMgr 這個類,它是個管理消息的單例,而消息被包裝成 CMessageWrap 對象來傳遞。在 CMessageMgr 中搜索 getmsg 會發現有好幾個方法。因為 CMessageWrap 中包含 m_uiMesLocalID 和 m_n64MesSvrID 屬性,所以鎖定目標為 - (id)GetMsg:n64SvrID: 和 - (id)GetMsg:LocalID: ,經過驗證后發現獲取消息時調用的是后者。PS:進入聊天窗口時其實還調用了 - (id)GetMsgByCreateTime:FromID:FromCreateTime:Limit:LeftCount:FromSequence: 方法。
不過,Hook - (id)GetMsg:LocalID: 之后發現即便不調用原始方法的實現,直接返回 nil ,也依然不能屏蔽消息。這時需要找到調用它的上層方法,然后繼續尋找真正處理消息的邏輯。
逆向工程絕不僅僅靠猜
初步思路是獲取到方法的調用棧,然后查找上一層的方法,并將方法調用的地址換算成 Hopper 反匯編后的地址,這樣就能獲取到方法名了,然后進行 Hook。
獲取方法的 IMP
如果是越獄手機,直接 ssh 到手機執行 debugserver ,然后就可以像平時 debug 那樣用 lldb 盡情調戲程序了。而我這里因為是非越獄機,只能打 Log 了。
WeChat 可執行文件和我注入的 FishChat.dylib 文件加載的地址是隨機的,而且我打的 Log 都是在 FishChat.dylib 中已經 Hook 過的方法中。熟悉 image 加載過程和 Hook 概念很重要,后面會用到。可以參考這篇文章:優化 App 的啟動時間。
如果是自己用 Method Swizzling 寫的 Hook 邏輯,很容易拿到原始方法的 IMP 。但這里是使用 CaptainHook ,是對 Method Swizzling 的宏定義封裝而已,創建了很多內聯函數。所謂的 Method Swizzling 其實也就是Objective-C Runtime 的一種應用而已。
那么該如何找到 CaptainHook 為我們保存的原始的方法 IMP 呢?
首先先新建一個 hook.m 文件,內容如下:
#import "CaptainHook.h"
CHDeclareMethod2(id, CMessageMgr, GetMsg, id, arg1, LocalID, unsigned int, arg2)
用 clang -E hook.m -o hook.c 命令將宏展開,因為文件內容很多,只截取其中比較有意義的部分:
static id (*$CMessageMgr_GetMsg$LocalID$_super)(CMessageMgr * self, SEL _cmd, id arg1, unsigned int arg2);
static id $CMessageMgr_GetMsg$LocalID$_closure(CMessageMgr * self, SEL _cmd, id arg1, unsigned int arg2) {
typedef id (*supType)(CMessageMgr *, SEL, id arg1, unsigned int arg2);
supType supFn = (supType)class_getMethodImplementation(CMessageMgr$.superClass_, _cmd);
return supFn (self, _cmd, arg1, arg2);
}
static id $CMessageMgr_GetMsg$LocalID$_method(CMessageMgr * self, SEL _cmd, id arg1, unsigned int arg2);
__attribute__((always_inline)) static inline void $CMessageMgr_GetMsg$LocalID$_register() {
Method method = class_getInstanceMethod(CMessageMgr$.class_, @selector(GetMsg:LocalID:));
if (method) {
$CMessageMgr_GetMsg$LocalID$_super = (__typeof__($CMessageMgr_GetMsg$LocalID$_super))method_getImplementation(method);
if (class_addMethod(CMessageMgr$.class_, @selector(GetMsg:LocalID:), (IMP)&$CMessageMgr_GetMsg$LocalID$_method, method_getTypeEncoding(method))) {
$CMessageMgr_GetMsg$LocalID$_super = &$CMessageMgr_GetMsg$LocalID$_closure;
} else {
method_setImplementation(method, (IMP)&$CMessageMgr_GetMsg$LocalID$_method);
}
}
else {
// 省略
}
}
結合 CaptainHook.h 中的宏定義,不難找出 $CMessageMgr_GetMsg$LocalID$_super 就是原始方法對應實現的函數指針,也就是 IMP 。 $CMessageMgr_GetMsg$LocalID$_method 是 Hook 過后方法的函數指針。
這里真的不是靠猜,純粹Objective-C Runtime 玩的熟。理解Objective-C Runtime 的一些概念和常用函數后很容易判斷 Hook 的原始方法和新的方法。
所以最后的代碼如下。這里打印了方法傳入的參數、原始方法的 IMP 和方法調用棧。
CHDeclareMethod2(id, CMessageMgr, GetMsg, id, arg1, LocalID, unsigned int, arg2)
{
NSLog(@"GetMsg:%@ LocalID:%d",arg1,arg2);
NSLog(@"originalIMP:%p",$CMessageMgr_GetMsg$LocalID$_super);
NSLog(@"%@",[NSThread callStackSymbols]);
return CHSuper2(CMessageMgr, GetMsg, arg1, LocalID, arg2);
}
打印結果如下:
Mar 2 00:37:36 yangxiaoyude-iPhone WeChat(FishChat.dylib)[22880] <Notice>: GetMsg:weixin LocalID:2
Mar 2 00:37:36 yangxiaoyude-iPhone WeChat(FishChat.dylib)[22880] <Notice>: originalIMP:0x1028821d4
Mar 2 00:37:36 yangxiaoyude-iPhone WeChat(FishChat.dylib)[22880] <Notice>: (
0 FishChat.dylib 0x000000010437dd08 _ZL35$CMessageMgr_GetMsg$LocalID$_methodP11CMessageMgrP13objc_selectorP11objc_objectj + 224
1 WeChat 0x0000000102afb960 _ZN16ClearSessionItem7compareERKNSt3__110shared_ptrIS_EES4_ + 1218052
2 WeChat 0x0000000102afe4b8 _ZN16ClearSessionItem7compareERKNSt3__110shared_ptrIS_EES4_ + 1229148
3 WeChat 0x00000001029f1554 _ZN16ClearSessionItem7compareERKNSt3__110shared_ptrIS_EES4_ + 127480
4 WeChat 0x0000000102a47390 _ZN16ClearSessionItem7compareERKNSt3__110shared_ptrIS_EES4_ + 479284
5 WeChat 0x0000000102ad4d64 _ZN16ClearSessionItem7compareERKNSt3__110shared_ptrIS_EES4_ + 1059336
6 WeChat 0x0000000102a52050 _ZN16ClearSessionItem7compareERKNSt3__110shared_ptrIS_EES4_ + 523508
7 Foundation 0x0000000194125048 <redacted> + 340
8 CoreFounda
將地址翻譯成 Selector
這里分詳細和快速兩種方法來講述如何通過內存地址找到對應的 Selector 。分步驟計算適合對操作系統原理不太熟悉的新手,老司機可以直接進入『快速計算方法』。
分步詳細剖析計算方法
- 反匯編得出方法相對地址
記得在 選擇 FAT 架構時選擇跟手機 CPU 相匹配的架構 ,有 armv7 和 aarch64 兩種可選。我這里以 aarch64 為例。
-[CMessageMgr GetMsg:LocalID:] 在 Hopper 中的地址 0x000000010280e1d4 :

WeChat Mach-O 在 Hopper 中的基地址 0x0000000100000000 :

得出 -[CMessageMgr GetMsg:LocalID:] 在 WeChat 中的相對地址為 0x280E1D4 :
0x000000010280e1d4 - 0x0000000100000000 = 0x280E1D4
- 計算 Mach-O 文件加載的隨機地址
之前打印 -[CMessageMgr GetMsg:LocalID:] 原始實現的 IMP 為 0x1028821d4
WeChat 文件在手機中加載的隨機地址為 『原始 IMP 的地址 - 方法相對地址』 ,結果為 0x100074000 :
0x1028821d4 - 0x280E1D4 = 0x100074000
- 還原調用棧為相對地址
將之前打印出調用棧上的地址轉換成相對地址,再加上 Hopper 上的基地址 0x0000000100000000 ,公式為 『調用棧上的地址 - WeChat 隨機地址 + 0x0000000100000000 』 ,結果如下:
1 WeChat 0x102A87960 _ZN16ClearSessionItem7compareERKNSt3__110shared_ptrIS_EES4_ + 1218052
2 WeChat 0x102A8A4B8 _ZN16ClearSessionItem7compareERKNSt3__110shared_ptrIS_EES4_ + 1229148
3 WeChat 0x10297D554 _ZN16ClearSessionItem7compareERKNSt3__110shared_ptrIS_EES4_ + 127480
4 WeChat 0x1029D3390 _ZN16ClearSessionItem7compareERKNSt3__110shared_ptrIS_EES4_ + 479284
5 WeChat 0x102A60D64 _ZN16ClearSessionItem7compareERKNSt3__110shared_ptrIS_EES4_ + 1059336
6 WeChat 0x1029DE050 _ZN16ClearSessionItem7compareERKNSt3__110shared_ptrIS_EES4_ + 523508
快速計算方法
已知條件:
- -[CMessageMgr GetMsg:LocalID:] 在 Hopper 反匯編后的地址 0x10280e1d4
- -[CMessageMgr GetMsg:LocalID:] 方法內存地址為 0x1028821d4
- -[CMessageMgr GetMsg:LocalID:] 在內存中 0x102afb960 處被調用
求 0x102afb960 對應 Hopper 反匯編后的地址?
因為方法間的相對地址是不變的,所以:
A 方法反匯編地址 - B 方法反匯編地址 = A 方法真實地址 - B 方法真實地址
所以結果為 0x102A87960 :
0x102afb960 - 0x1028821d4 + 0x10280e1d4 = 0x102A87960
跟之前的分步驟計算結果一樣。
還原 Selector
根據反匯編地址在 Hopper 中定位方法名,快捷鍵 G 。
最終得到的方法調用棧如下,調用次序是自底向上:
-[CSyncBaseEvent BatchAddMsg:ShowPush:]
-[CSyncBaseEvent HandleBatch:ShowPush:]
-[NewSyncHandler HandleSyncResp:Push:ShowPush:ContinueFlag:Scene:syncKeyMd5:]
-[NewSyncService HandleSyncResp:Push:ShowPush:Scene:]
-[CNewSyncPrtl HandleResp:]
-[EventService HandleRespThread:]
從匯編代碼繼續猜
雖然可以鎖定添加消息的實現邏輯在 -[CSyncBaseEvent BatchAddMsg:ShowPush:] 方法里,但是查找頭文件發現它的兩個參數和一個返回值竟然都是 BOOL 類型。直接 Hook 掉并返回 NO 雖然可以屏蔽消息,但是卻屏蔽了所有的消息,沒有對消息來源進行篩選。可以肯定的是在其內部通過單例或者類方法獲取和處理了消息對象,然后才調用的 -[CMessageMgr GetMsg:LocalID:] 方法。而真正添加消息的邏輯可能在 -[CMessageMgr GetMsg:LocalID:] 調用之前,也可能在它調用之后。而且好友消息和群消息的處理很可能還是兩個不同的分支。
在不能反編譯的情況下,只能瀏覽下方法的匯編代碼中調用到什么其他方法。消息被封裝成 CMessageWrap 類,所以要格外注意這個類的一些屬性名,或者 MsgWrap 這個詞。進而找到 BatchAddMsgInfo 這個類有一些匯編中出現的消息處理的標志位( isInsertNew , isNeedChangeDisplay , isNotify , isCanAddDB ) 和 CMessageWrap 。又在匯編里找到 MsgHelper 的類,其類方法中帶 BatchAddMsgInfo 中那些標志位的有兩個,雖然按理說應該 Hook 參數更多的方法,但這里我為了偷懶,選的較短的 + AddMessageToDB:MsgWrap:Des:DB:Lock:GetChangeDisplay:InsertNew: 。PS:這也不短啊!
拿到 CMessageWrap 就很好判斷是否需要屏蔽消息了:
CHDeclareClassMethod7(BOOL, MsgHelper, AddMessageToDB, id, arg1, MsgWrap, id, arg2, Des, unsigned int, arg3, DB, id, arg4, Lock, id, arg5, GetChangeDisplay, BOOL *, arg6, InsertNew, BOOL *, arg7)
{
Ivar nsFromUsrIvar = class_getInstanceVariable(objc_getClass("CMessageWrap"), "m_nsFromUsr");
NSString *m_nsFromUsr = object_getIvar(arg2, nsFromUsrIvar);
BOOL result = !([FishConfigurationCenter sharedInstance].chatIgnoreInfo[m_nsFromUsr].boolValue);
if (result) {
CHSuper7(MsgHelper, AddMessageToDB, arg1, MsgWrap, arg2, Des, arg3, DB, arg4, Lock, arg5, GetChangeDisplay, arg6, InsertNew, arg7);
}
*arg6 = result;
*arg7 = result;
return result;
}
最終屏蔽消息功能大功告成。
總結
這里只是做個示范,并不代表我 Hook 得最準。因為條條大路通羅馬,只要達到目的就好。本來逆向工程就是在沒有源碼的情況下揣測和分析,所以不同的人會給出不同的逆向過程,這就像從南坡和北坡一起爬山一樣。
來自:http://yulingtianxia.com/blog/2017/03/06/How-to-hook-the-correct-method-in-reverse-engineering/