改一个字节就能防撤回?拆解 RevokeMsgPatcher 的特征码匹配定位术

发布时间:2026/8/14 18:05:24
改一个字节就能防撤回?拆解 RevokeMsgPatcher 的特征码匹配定位术 改一个字节就能防撤回拆解 RevokeMsgPatcher 的特征码匹配定位术【免费下载链接】RevokeMsgPatcher:trollface: A hex editor for WeChat/QQ/TIM - PC版微信/QQ/TIM防撤回补丁我已经看到了撤回也没用了项目地址: https://gitcode.com/GitHub_Trending/re/RevokeMsgPatcher凌晨两点你正被拉进一个灵感交流群有人发了一段话三秒后又撤回。大多数人只会骂一句但你想的却是另一件事能不能自己写个防撤回补丁你兴冲冲地找到微信安装目录打开那个传说中的WeChatWin.dll——100 多 MB 的二进制文件一屏密密麻麻的乱码那一刻你大概明白了什么叫大海捞针。这篇文章要讲的就是 RevokeMsgPatcher 这个开源防撤回补丁如何靠特征码匹配在这片字节海洋里精准定位到那一个需要修改的字节。为什么按文本搜索这条路注定走不通先说直觉方案微信的撤回功能是不是在 DLL 里存了撤回两个字我把它当成 Word 文档搜撤回不就行了答案是否定的原因有两条机器码里没有撤回。WeChatWin.dll是编译后的机器指令人类能读懂的字符串早就被拆散成一段段字节码。搜文本等于拿中文词典去查英文小说。就算记住了位置也没用。假设你在某次逆向时发现撤回判断在文件偏移3413977处微信一升级编译器稍微调一调代码顺序这个偏移就废了。固定地址 一次性补丁版本一更新就失效。所以问题的本质是你不能记住位置只能记住样子——去识别一段无论版本怎么变都相对稳定的字节模式。这就是特征码。特征码一串字节组成的指纹逆向工程师在反汇编里盯上一个关键分支——比如这条消息要不要允许撤回的je跳转指令——然后把它前后十几条指令的机器码抄下来这就成了特征码。你可以把它理解成人的指纹不靠名字认人靠纹路认人。通过调试器附加微信进程在模块列表里锁定 wechatwin.dll特征码匹配的源头就在这里特征码的形态长这样十进制数组// Search要查找的特征码 [15, 31, 68, 0, 0, 73, 139, 80, 8, 72, 133, 210, 116, 63, 72, 199, 193] // Replace匹配到之后替换成的字节 [15, 31, 68, 0, 0, 73, 139, 80, 8, 72, 133, 210, 117, 63, 72, 199, 193]这段代码到底解决了什么问题注意看第 13 个字节Search 里是116十六进制0x74对应JE条件跳转指令Replace 里变成了1170x75对应JNE。整个补丁的本质就是把如果满足条件就跳走改成如果不满足才跳走——撤回逻辑的分支方向被悄悄反转消息自然就撤不掉了。而63这个值0x3F不是指令它是一个通配符一会儿我们会专门讲它。上面这段特征码来自项目的RevokeMsgPatcher.Assistant/Data/下按版本号组织的patch.json文件。文件里还存着每个版本的SHA1Before/SHA1After用来精确校验这个 DLL 到底是不是我们认识的那个版本。那么问题来了特征码有了怎么在 100MB 的文件里高效地把它找出来通配符应对版本更新的填空模板如果你的补丁只服务一个版本的微信事情很简单直接比对整串字节就行。但现实是微信一个月更新好几次编译器一调整特征码里可能有几个字节会变。比如上面那串特征码指令的操作数是地址每次编译都可能变。硬匹配整串必然失败。RevokeMsgPatcher 的解法在RevokeMsgPatcher/Matcher/FuzzyMatcher.cs里把容易变的字节位置标成通配符0x3F匹配时跳过它。public const byte wildcard 0x3F; // 通配符 public static bool IsEqual(byte[] content, int start, byte[] whole) { int i 0; for (i 0; i whole.Length; i) { if (whole[i] wildcard) // 通配符不比较直接跳过 { continue; } if (content[start i] ! whole[i]) // 非通配符位置必须严格相等 { break; } } return i whole.Length; }这段代码到底解决了什么问题它实现了一个填空模板特征码里凡是遇到0x3F就当成这里随便是什么都行其余位置才严格比对。这样一条特征码就能覆盖字节有小幅变动的多个微信版本而不是每个版本单独维护一套。你可以理解为指纹比对时允许一两个细节位模糊大大提高了容错率。让搜索快起来的两个秘密Boyer-Moore 的跳跃光有模板还不够。你想过没有100MB 的文件假设特征码长 17 字节最笨的写法是逐字节挪动窗口比对最坏情况下要做几亿次字节比较。微信启动时卡几秒用户体验直接崩盘。RevokeMsgPatcher 的答案在RevokeMsgPatcher/Matcher/BoyerMooreMatcher.cs——经典 Boyer-Moore 算法。核心思路反直觉从模式串的末尾开始往前匹配一旦失配一次性跳好几格而不是老老实实挪一位。while (s (n - m)) { j m - 1; // 从模式串末尾开始 while (j 0 pattern[j] text[s j]) { j--; } if (j 0) // 全部匹配上 { firstShift s; return true; } else { // 坏字符规则 vs 好后缀规则取较大位移实现跳跃 s Max(goodSuffixShifts[j], badCharShifts[(int)text[s j]] - (m - 1) j); } }这段代码到底解决了什么问题它把每次挪一格变成每次挪很多格。两个预处理表坏字符规则 好后缀规则告诉算法当第 j 位失配时最少可以安全地跳过多少个字节而不会漏掉潜在的匹配点。理想情况下匹配一个几十字节的特征码每次失配都能跳过接近特征码长度的距离100MB 的扫描耗时从秒级降到毫秒级。实测里项目在ModifyFinder.FindChanges中打印的匹配耗时都是个位到几十毫秒。从 0 到 1一条真实特征码的完整旅行理论讲完我们实操一遍。假设你的微信是 3.9.x 版本现在要打防撤回老补丁完整流程是这样的第 1 步读取文件加载规则。ModifyFinder.FindChanges用File.ReadAllBytes把整个WeChatWin.dll读进内存同时从patch.json里取出该版本范围的Search/Replace特征码。第 2 步两段式定位。关键来了看FuzzyMatcher.MatchAll的实现public static int[] MatchAll(byte[] content, byte[] pattern) { byte[] head GetHead(pattern); // 取通配符之前的固定头串 int[] indexs BoyerMooreMatcher.MatchAll(content, head); // 先用 BM 快速定位 if (head.Length pattern.Length) { return indexs; // 没有通配符头串就是全串 } Listint res new Listint(); foreach (int index in indexs) // 对每个候选位置做全模板验证 { if (IsEqual(content, index, pattern)) { res.Add(index); } } return res.ToArray(); }这段代码到底解决了什么问题它把模糊匹配拆成了精确粗筛 模糊精验两步先用通配符前的固定头串交给 Boyer-Moore 快速粗筛出少数候选位置0x3F之前的字节是必须精确的所以 BM 能正常工作再逐个用IsEqual做全模板验证。这样既享受了 BM 的速度又保留了通配符的容错一举两得。第 3 步确认没有被补过。对每个命中位置ModifyFinder还会用Replace特征码再比一次如果命中位置已经和替换串相同说明补丁打过了跳过不加。第 4 步写回文件。拿到Change列表位置 新字节后交给RevokeMsgPatcher/Modifier/FileHexEditor.cs。它先备份出*.h.bak再调用FileUtil.EditMultiHex落盘foreach (Change change in changes) { stream.Seek(change.Position, SeekOrigin.Begin); foreach (byte b in change.Content) { if (b 0x3F) // 替换串里的通配符原样保留读跳过 { stream.ReadByte(); } else { stream.WriteByte(b); } } }这段代码到底解决了什么问题注意替换串里也可能带0x3F——这意味着这个位置保持原样不改。写回的时候遇到通配符就读一个字节跳过、不覆盖确保只有明确要改的位置被改写其余字节原封不动。整个流程走完你在调试器里看到的画面大概是这样的将条件跳转je修改为无条件跳转jmp这是防撤回补丁最典型的字节级操作补丁列表清晰地展示了两处字节替换74→EB、55→C3确认后即可修补并保存 DLL为什么反复打补丁不会把文件搞坏你可能已经想到一个坑补丁打了一次字节被改掉了下次启动软件再跑一遍匹配会怎样如果FindChanges直接报找不到特征码用户会一脸懵。RevokeMsgPatcher 的处理是双向匹配 归类上报核心逻辑在IsAllReplacedforeach (ReplacePattern pattern in replacePatterns) { int[] searchMatchIndexs FuzzyMatcher.MatchAll(partByteArray, pattern.Search); int[] replaceMatchIndexs FuzzyMatcher.MatchAll(partByteArray, pattern.Replace); // 查找串不存在、但替换串存在说明这个功能已经被补过了 if (searchMatchIndexs.Length 0 replaceMatchIndexs.Length 0) { alreadyReplaced.Add(pattern.Category); } }这段代码到底解决了什么问题它用替换后的样子去反向匹配搜原始特征码没找到但搜替换后特征码找到了就能确定这一项已经被打过补丁。程序会据此提示你该功能已安装或者告诉你是哪几项冲突了避免重复打补丁或者和其它来源的补丁互相覆盖把 DLL 搞坏。这就是ModifyFinder里那套match_already_replace、match_inconformity异常的来源——不是报错给你添堵而是把状态如实翻译成人话。拿到手之后你能用它做什么回到开头那个凌晨的场景。现在你知道原理了实际的用法其实非常朴素从项目仓库https://gitcode.com/GitHub_Trending/re/RevokeMsgPatcher克隆代码用 Visual Studio 打开RevokeMsgPatcher.sln编译。主程序RevokeMsgPatcher会自动探测微信/QQ/TIM 的安装路径显示匹配到的版本与可打的功能补丁防撤回、多开等勾选后一键备份并应用。如果你的版本较新、工具还没收录会得到明确的提示而不是静默失败——因为特征码匹配数对不上时程序宁可报错也不瞎改。更值得做的一件事是给这个项目贡献新版本的特征码。套路你已经会了——用调试器附加进程、找到撤回判断分支、抄下带通配符的字节序列、填写patch.json里的版本区间和Search/Replace。项目里RevokeMsgPatcher.Assistant/Data/下的目录0.7到2.1就是历代维护者一版一版积累下来的指纹库多一个版本就多一个用户能在升级后继续用上它。改一个字节的爽感只有亲自跑通一次才知道。下次有人在你面前秒撤回消息你大概不会再骂人了——你知道那串字节正躺在某个你闭着眼都能找到的位置上。【免费下载链接】RevokeMsgPatcher:trollface: A hex editor for WeChat/QQ/TIM - PC版微信/QQ/TIM防撤回补丁我已经看到了撤回也没用了项目地址: https://gitcode.com/GitHub_Trending/re/RevokeMsgPatcher创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考