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