拆解:從撤回機(jī)制到Hook+內(nèi)存補(bǔ)丁)
我一直覺(jué)得微信和QQ的“撤回”功能是個(gè)挺玄學(xué)的存在。你說(shuō)它有用吧確實(shí)能在手滑發(fā)錯(cuò)消息時(shí)救你一命你說(shuō)它沒(méi)用吧對(duì)于需要留存證據(jù)或者單純想看看對(duì)方撤回了啥的人來(lái)說(shuō)這功能又顯得有點(diǎn)“此地?zé)o銀三百兩”。尤其是在PC端工作群里經(jīng)常有人撤回了重要通知或者聊天記錄里別人撤回了關(guān)鍵信息等你回頭想找的時(shí)候只剩下一句冰冷的“XXX撤回了一條消息”那種抓心撓肝的感覺(jué)用過(guò)的人都懂。所以今天這篇東西就是專門來(lái)拆解PC端微信和QQ消息防撤回到底是咋回事的。這不僅僅是給你一個(gè)現(xiàn)成的工具更是把這個(gè)技術(shù)背后的原理、實(shí)現(xiàn)路徑、實(shí)操步驟以及那些不為人知的坑一次給你講透。不管你是想自己動(dòng)手研究一下Windows客戶端消息機(jī)制的開發(fā)者還是單純想用工具解決“消息被撤回”痛點(diǎn)、避免錯(cuò)過(guò)關(guān)鍵信息的普通用戶這篇長(zhǎng)文應(yīng)該都能給你想要的答案。老規(guī)矩先放結(jié)論P(yáng)C端的防撤回核心邏輯無(wú)非是兩種思路一種是“在消息被撤回之前就把內(nèi)容本地化保存”另一種是“直接攔截客戶端的撤回指令”。但要把這條路走通里面的門道比你想象的多得多。1. 技術(shù)與需求拆解防回撤到底在防什么先別急著找工具咱們得先搞清楚這個(gè)需求的本質(zhì)。你每天在電腦上打開的微信和QQ本質(zhì)上是一個(gè)運(yùn)行在Windows系統(tǒng)上的GUI進(jìn)程。你在聊天窗口里發(fā)出的每一條消息以及接收到的每一條消息都會(huì)在客戶端本地經(jīng)歷一套完整的“接收-校驗(yàn)-落盤-渲染”流程。而“撤回”這個(gè)動(dòng)作從協(xié)議層面看其實(shí)就是客戶端發(fā)出了一條帶有特定消息ID和控制指令的數(shù)據(jù)包。所以所謂的“防撤回”在技術(shù)圈子里大家玩的無(wú)非是這兩條路本地留存派既然消息來(lái)了都要先經(jīng)過(guò)本地的消息數(shù)據(jù)庫(kù)比如微信的MSG.db或者某些版本的SQLite庫(kù)那我不讓客戶端把這條消息刪了不就行了或者在撤回指令生效前把這條消息的明文內(nèi)容先復(fù)制一份出來(lái)存著。這是最早期、最樸素的思路。指令攔截派消息撤回時(shí)客戶端會(huì)收到服務(wù)器的撤回通知然后再去本地?cái)?shù)據(jù)庫(kù)里把對(duì)應(yīng)記錄標(biāo)記刪除。既然這樣那我直接Hook住客戶端處理撤回通知的那個(gè)函數(shù)讓這個(gè)函數(shù)直接返回“執(zhí)行成功”但不做任何實(shí)際刪除操作或者干脆不處理這個(gè)通知那消息不就一直留在本地了嗎但在動(dòng)手之前我必須先潑一盆冷水這兩條路都建立在“消息已經(jīng)完整到達(dá)本地并被解密”的前提下。如果你在手機(jī)上撤回了一條消息而PC端還沒(méi)來(lái)得及同步這條消息那無(wú)論你怎么防PC端本地是沒(méi)有數(shù)據(jù)的巧婦難為無(wú)米之炊。所以別指望這技術(shù)能幫你變出任何服務(wù)器上沒(méi)有下發(fā)的東西。除了技術(shù)原理你還要想清楚你搞這個(gè)到底是為了啥。是為了守著群里的紅包記錄是為了防止工作對(duì)接時(shí)對(duì)方賴賬還是純粹為了滿足好奇心不同的需求場(chǎng)景對(duì)應(yīng)的技術(shù)實(shí)現(xiàn)方案?jìng)?cè)重點(diǎn)是完全不一樣的。如果只是為防止群消息被撤回那你不需要太復(fù)雜的Hook一個(gè)簡(jiǎn)單的DLL注入內(nèi)存補(bǔ)丁就能搞定但如果你是為了做聊天記錄的合規(guī)性存檔那你需要的是更高的穩(wěn)定性甚至要考慮對(duì)聊天內(nèi)容進(jìn)行額外的加密備份這就不是簡(jiǎn)單的“防撤回”三個(gè)字能覆蓋的了。2. 核心實(shí)現(xiàn)路徑深度解析從Hook到補(bǔ)丁的進(jìn)階之路既然聊到了實(shí)現(xiàn)那咱們就得往深處走走。目前PC端主流的防撤回實(shí)現(xiàn)技術(shù)棧其實(shí)非常集中但每一條鏈路都有不少細(xì)節(jié)坑。下面我把最核心的三條技術(shù)路線逐一拆開盡量讓你讀完能有個(gè)清晰的畫面感。2.1 命令行級(jí)別從Injection到IAT Hook的思路轉(zhuǎn)變很多人一上來(lái)就喊“我要注入DLL”其實(shí)注入只是萬(wàn)里長(zhǎng)征第一步。你注入進(jìn)去的DLL得想辦法讓客戶端進(jìn)程執(zhí)行你寫的防撤回邏輯才行。這里面最經(jīng)典的做法就是IAT Hook導(dǎo)入地址表鉤子。Windows下的任何程序都離不開系統(tǒng)提供的API比如讀寫文件、彈出消息框、創(chuàng)建進(jìn)程等。這些API的調(diào)用地址會(huì)被記錄在程序進(jìn)程內(nèi)存的“導(dǎo)入地址表”里。IAT Hook的思路就是你寫一個(gè)DLL注入到微信或QQ進(jìn)程后偷偷把目標(biāo)API在IAT表里的地址改成你自己寫的函數(shù)的地址。這樣一來(lái)當(dāng)微信程序想調(diào)用那個(gè)API時(shí)就會(huì)先執(zhí)行你的代碼你的代碼可以在里面搞點(diǎn)小動(dòng)作比如攔截參數(shù)然后再跳轉(zhuǎn)回原來(lái)的API執(zhí)行。在防撤回場(chǎng)景里我們Hook的目標(biāo)通常不是哪個(gè)單一API而是一個(gè)內(nèi)部處理函數(shù)。比如微信在收到撤回指令后會(huì)調(diào)用某個(gè)內(nèi)部方法去修改數(shù)據(jù)庫(kù)狀態(tài)。你光Hook系統(tǒng)API是攔不住的因?yàn)樗叩氖蔷幾g器生成的虛表調(diào)用。這就引出了下一條更硬核的路子。2.2 二進(jìn)制級(jí)別內(nèi)存補(bǔ)丁的“外科手術(shù)”既然要攔截內(nèi)部函數(shù)那最直接的手段就是“改二進(jìn)制”。這也就是大家常說(shuō)的內(nèi)存補(bǔ)丁。以某版本PC微信為例當(dāng)撤回指令的JSON包到達(dá)客戶端后程序會(huì)走一個(gè)類似于HandleRevokeMsg的邏輯。我們?nèi)绻茉趦?nèi)存里定位到這個(gè)邏輯的機(jī)器碼然后把關(guān)鍵判斷跳轉(zhuǎn)改成無(wú)條件跳轉(zhuǎn)或者直接改成ret指令讓它提前返回就能做到“假撤回”。聽(tīng)起來(lái)挺爽對(duì)吧但這里面有兩個(gè)核心難點(diǎn)第一個(gè)難點(diǎn)是版本差異。微信和QQ幾乎每周都在更新每次更新函數(shù)偏移地址和指令位置都會(huì)產(chǎn)生變化。今天你辛辛苦苦定位到的偏移0x584F9E明天可能就變成了0x5A1B2C。所以所有靠譜的防撤回工具都需要一個(gè)特征碼匹配機(jī)制通過(guò)掃描文件特征字節(jié)來(lái)定位要Patch的地址而不是寫死偏移。第二個(gè)難點(diǎn)是指令級(jí)別的坑。你要把原來(lái)的匯編指令改掉但CPU執(zhí)行是流水線式的如果你Patch的字節(jié)數(shù)不對(duì)破壞了原有的完整指令或者修改后導(dǎo)致棧不平衡那程序分分鐘就會(huì)崩潰。比如原本是jnz short loc_xxx2字節(jié)指令你想改成無(wú)條件跳轉(zhuǎn)jmp需要5字節(jié)多出來(lái)的3個(gè)字節(jié)就會(huì)“吃掉”后面一條正常指令程序不亂才怪了。提示所以我個(gè)人并不建議新手一上來(lái)就玩純二進(jìn)制Patch。你可以先用x64dbg之類的調(diào)試器下斷點(diǎn)看調(diào)用棧理解了消息處理的整個(gè)函數(shù)調(diào)用鏈之后再動(dòng)手寫補(bǔ)丁。這屬于是“八珍玉食徐徐圖之”的功夫活。2.3 數(shù)據(jù)庫(kù)層面被忽略的“降維打擊”說(shuō)完了Hook和補(bǔ)丁再來(lái)聊一個(gè)思路清奇的野路子直接讀數(shù)據(jù)庫(kù)。不管程序怎么執(zhí)行消息要落在本地最終都是寫入本地?cái)?shù)據(jù)庫(kù)文件的。PC微信的歷史消息存儲(chǔ)在本地的MSG.db不同版本路徑和格式有差異QQ早期版本是MsgCenter.db之類的。防撤回想得很復(fù)雜但你可以換個(gè)角度我根本不讓它撤回成功但我也不攔它我直接把這個(gè)db文件定期復(fù)制一份不就行了更絕妙的是現(xiàn)代版本的微信數(shù)據(jù)庫(kù)雖然加了SQLCipher加密但在進(jìn)程運(yùn)行期間數(shù)據(jù)庫(kù)句柄是打開的你可以通過(guò)Hook拿到密鑰或者干脆用某些內(nèi)存技術(shù)DUMP出解密后的緩存數(shù)據(jù)。這條路的好處在于穩(wěn)定因?yàn)樗灰蕾囉陬l繁變化的功能流程代碼只要數(shù)據(jù)庫(kù)結(jié)構(gòu)不變你就能一直用。缺點(diǎn)也很明顯你得懂SQLite數(shù)據(jù)庫(kù)結(jié)構(gòu)還得會(huì)處理復(fù)雜的表關(guān)聯(lián)關(guān)系比如消息表、會(huì)話表、索引表并且要考慮數(shù)據(jù)庫(kù)文件被占用無(wú)法復(fù)制的鎖問(wèn)題。對(duì)于只是想“看看到底撤回了啥”的輕量級(jí)用戶這條路明顯太重了。3. 手把手實(shí)操打造一個(gè)穩(wěn)定不崩的防撤回小工具理論聊了那么多現(xiàn)在進(jìn)入正題。我在這里假設(shè)你已經(jīng)有一定Windows C/C或者Python的基礎(chǔ)我會(huì)從工具選型到代碼思路給你一套完整的落地參考方案。這個(gè)方案我管它叫“模擬項(xiàng)目X基于逆向注入的內(nèi)存補(bǔ)丁方案”它是目前兼容性最好也是代碼量相對(duì)可控的一條路線。3.1 環(huán)境準(zhǔn)備與工具選型做這種活兒工具鏈其實(shí)非常固定。我強(qiáng)烈建議你準(zhǔn)備以下東西開發(fā)語(yǔ)言C用于寫注入DLL Python用于寫偏移掃描器可選。分析工具Cheat EngineCE 或 x64dbg。CE用來(lái)小范圍搜內(nèi)存x64dbg用來(lái)上斷點(diǎn)動(dòng)態(tài)調(diào)試。注入工具一個(gè)成熟的進(jìn)程注入器或者你自己用CreateRemoteThreadLoadLibrary寫一個(gè)簡(jiǎn)單的。特征碼掃描器自己用Python寫一個(gè)用來(lái)在新版本客戶端里定位要Patch的字節(jié)序列。注意工具本身沒(méi)問(wèn)題但請(qǐng)一定在合規(guī)前提下研究。僅用于學(xué)習(xí)Windows消息機(jī)制和軟件逆向工程原理不要拿去做破壞別人軟件正常功能的事情也不要用在他人設(shè)備上。3.2 關(guān)鍵步驟一定位“撤回處理函數(shù)”的偏移地址這是整個(gè)環(huán)節(jié)最難也是最核心的一步。以某歷史版本PC微信為例版本號(hào)4.0.x我們可以這樣定位打開微信進(jìn)入某個(gè)聊天窗口讓一條消息的撤回按鈕呈可點(diǎn)擊狀態(tài)也就是發(fā)給自己的消息或?qū)Ψ絼偘l(fā)的消息。用x64dbg附加到微信進(jìn)程在下斷點(diǎn)前先給“撤回”這個(gè)動(dòng)作下斷點(diǎn)。但因?yàn)槟悴恢纼?nèi)部函數(shù)所以比較靠譜的辦法是切到CE搜索消息里的字符串比如消息內(nèi)容本身找到內(nèi)存地址然后在這個(gè)內(nèi)存地址上設(shè)置“訪問(wèn)斷點(diǎn)”。在微信里點(diǎn)擊撤回程序就會(huì)因?yàn)樵L問(wèn)這段內(nèi)存而觸發(fā)斷點(diǎn)停在某個(gè)函數(shù)指令處。這時(shí)候你查看調(diào)用棧就會(huì)看到一層層往上的調(diào)用關(guān)系最頂層那個(gè)或者倒數(shù)第二個(gè)處理回調(diào)大概率就是HandleRevokeMsg。記下這個(gè)函數(shù)的偏移地址模塊基址偏移然后用CE或工具把該函數(shù)的開頭前幾個(gè)字節(jié)DUMP出來(lái)記錄下來(lái)作為特征碼。3.3 關(guān)鍵步驟二編寫特征碼掃描與Patch邏輯拿到特征碼和偏移后你的DLL要做的事就很簡(jiǎn)單了在DLL入口處DllMain的DLL_PROCESS_ATTACH分支開啟一個(gè)新線程。在新線程里遍歷主模塊的.text節(jié)區(qū)用特征碼匹配的方式把那幾個(gè)字節(jié)找出來(lái)。找到后修改內(nèi)存頁(yè)保護(hù)屬性VirtualProtect然后把開頭改成mov eax,1; ret之類的指令具體改法取決于原函數(shù)的棧約定和調(diào)用約定。例如原函數(shù)是sub rsp, 28h開頭你為了讓它直接返回成功可以改成xor eax, eax; ret。這里必須提醒幾個(gè)超級(jí)容易踩的坑誤判特征碼不同OEM的編譯器可能生成不同字節(jié)你的特征碼不能包含全局變量地址或固定偏移最好用通配符比如??去匹配。比如48 8B 85 ?? ?? ?? ?? C3其中問(wèn)號(hào)部分就是可變的。多線程競(jìng)爭(zhēng)注入時(shí)機(jī)太早或太晚都不行。太早模塊可能還沒(méi)加載完成太晚撤回指令可能已經(jīng)執(zhí)行過(guò)了。一般監(jiān)聽(tīng)PE_PARSER或者輪詢模塊列表等主窗口加載完成后再Patch。冷卻時(shí)間Patch完成后如果程序有自校驗(yàn)惡意代碼保護(hù)可能會(huì)崩潰。這一點(diǎn)在QQ上尤其明顯。所以Patch成功后記得秒退出線程別留痕。3.4 關(guān)鍵步驟三消息數(shù)據(jù)的本地留存策略光攔截撤回指令還不夠因?yàn)槟阒皇亲屗帮@示還在”萬(wàn)一本地內(nèi)存被清理或者會(huì)話記錄被清空那還是丟了。所以我建議在防撤回的同時(shí)增加一個(gè)本地自動(dòng)備份邏輯用ReadProcessMemory讀取消息控件的文本內(nèi)容通過(guò)SendMessage獲取WM_GETTEXT或者直接從消息數(shù)據(jù)庫(kù)結(jié)構(gòu)中拿到明文內(nèi)容。在進(jìn)程退出前或者在每次收到新消息時(shí)把內(nèi)容以純文本方式追加寫入到你自己的指定文件里。這條路有個(gè)好處哪怕客戶端哪天崩潰了你的純文本備份文件還在真正的“防丟”。但注意這又涉及隱私和數(shù)據(jù)安全問(wèn)題你必須確保備份文件存在你自己的本地磁盤不要亂傳網(wǎng)盤也別用來(lái)干壞事。4. 常見(jiàn)問(wèn)題排查與實(shí)戰(zhàn)避坑速查表在實(shí)際折騰的過(guò)程中我相信你們遇到的問(wèn)題會(huì)比教程里寫的多得多。我把這些年見(jiàn)過(guò)的、以及我自己踩過(guò)的比較有代表性的問(wèn)題整理了一下直接給到排查思路。問(wèn)題現(xiàn)象可能原因排查與解決建議DLL注入后微信/QQ閃退Patch的字節(jié)數(shù)錯(cuò)了破壞了CPU指令流水線或者Patch的函數(shù)棧不平衡。先用x64dbg調(diào)試模式把Patch指令縮短到跟原指令等長(zhǎng)或者用jmp跳到一塊空內(nèi)存執(zhí)行完邏輯再jmp回來(lái)。防撤回偶爾生效偶爾失效特征碼定位的是多態(tài)代碼或者撤回消息類型分多種文字/圖片/文件你只Patch了其中一種。回調(diào)函數(shù)可能分支處理看下調(diào)用棧把不同消息類型對(duì)應(yīng)的Handler都Patch掉。明明看到消息還在重啟客戶端又沒(méi)了你只攔截了顯示邏輯但數(shù)據(jù)庫(kù)層面的刪除事務(wù)已經(jīng)提交了。必須同時(shí)Hook掉數(shù)據(jù)庫(kù)刪除相關(guān)函數(shù)或者直接做文本備份留存。注入工具被殺毒軟件攔了你可能用了公開的遠(yuǎn)線程注入工具特征碼被記錄。自己用NtCreateThreadEx或者APC注入繞開特征檢測(cè)同時(shí)務(wù)必僅限自用不要傳播。新版客戶端一更新就失效函數(shù)偏移變了特征碼沒(méi)匹配到。這是必現(xiàn)的。任何防撤回工具都無(wú)法做到永久通殺。需要重新定位特征碼或者等待工具作者更新。5. 防撤回工具的替代方案與邊界反思搞技術(shù)的人很容易陷入“手里有錘子看啥都是釘子”的誤區(qū)。防撤回技術(shù)雖然炫酷但咱也得想想它到底是不是解決“消息丟失”的最佳方案。我自己的觀點(diǎn)是工具永遠(yuǎn)是輔助核心還是習(xí)慣。你想防撤回根本上是因?yàn)槟阈枰舸嬷匾畔?。那為什么不把這活兒前置呢我在實(shí)際工作中一直堅(jiān)持一個(gè)很樸素的“不丟消息”原則重要的事情絕不讓它只存在于聊天記錄里。比如涉及錢款、賬目、項(xiàng)目節(jié)點(diǎn)的關(guān)鍵信息直接同步到自己的云筆記或項(xiàng)目管理工具里并設(shè)置提醒。需要對(duì)方確認(rèn)的內(nèi)容盡量用郵件這種具備歸檔和不可篡改屬性的溝通方式。如果是團(tuán)隊(duì)協(xié)作約定用協(xié)作平臺(tái)而非私人IM確認(rèn)結(jié)論。退一步說(shuō)就算你用了防撤回工具你也要明白一個(gè)邊界你只能防本地顯示的撤回?zé)o法阻止信息源頭被服務(wù)器的策略處理。有些消息在服務(wù)器端可能直接被判定違規(guī)而攔截根本就不會(huì)下發(fā)給客戶端這種情況下你連被撤回的消息影子都看不到談何“防”這個(gè)技術(shù)的真正價(jià)值在于幫你留住“已經(jīng)合法到達(dá)你本地”的信息讓你在事后追溯時(shí)有據(jù)可循。一旦想明白這點(diǎn)你就不會(huì)把它神話也不會(huì)拿它去干越界的事情。6. 實(shí)操心得我在研究過(guò)程中的三個(gè)親身感受文章寫到這里核心內(nèi)容差不多都在上面了。最后我再分享幾個(gè)個(gè)人感受比較深的小細(xì)節(jié)。第一個(gè)體會(huì)是逆向工程的耐心遠(yuǎn)比技術(shù)重要。那段時(shí)間為了定位一個(gè)特定版本的撤回函數(shù)我一晚上都在循環(huán)重復(fù)“點(diǎn)擊-下斷點(diǎn)-看堆棧-分析指令”的操作鼠標(biāo)都點(diǎn)壞了一個(gè)。遇到版本更新導(dǎo)致全盤掃不到特征碼那種挫敗感真的會(huì)讓人想砸鍵盤。但只要你穩(wěn)下來(lái)一步一步跟調(diào)用棧走最后總能在某個(gè)不起眼的偏移處找到那個(gè)藏得極深的函數(shù)。第二個(gè)體會(huì)是自己寫工具和用現(xiàn)成工具的感受完全不一樣。用別人的工具你就像個(gè)黑盒使用者不知道它為什么會(huì)崩也不知道它到底安不安全自己寫一套哪怕寫得丑但你心里門清每一個(gè)字節(jié)的作用遇到問(wèn)題也能快速定位。這對(duì)個(gè)人技術(shù)能力的長(zhǎng)進(jìn)絕對(duì)是質(zhì)的提升。第三個(gè)體會(huì)是代碼里的“道德開關(guān)”很重要。我后來(lái)在看自己工具代碼時(shí)特意加了一個(gè)限制只處理本地文本數(shù)據(jù)不涉及任何網(wǎng)絡(luò)抓包和竊取邏輯。這讓整個(gè)研究項(xiàng)目始終在合理的技術(shù)探索范疇內(nèi)。我也勸各位做這類研究時(shí)心里繃一根弦別輕易把它用在不該用的地方。如果你也是那種喜歡扒開軟件外殼看內(nèi)部構(gòu)造的人不妨從今天開始用那兩個(gè)調(diào)試工具去探索一下微信或QQ里面那些有趣的消息處理邏輯。不過(guò)記住看完這篇東西還是要把更多精力放在提升自己上消息撤回這東西看淡一點(diǎn)咱不慌。