从一次"眼睁睁看着消息被撤回"说起:RevokeMsgPatcher 特征码匹配机制拆解
【免费下载链接】RevokeMsgPatcher:trollface: A hex editor for WeChat/QQ/TIM - PC版微信/QQ/TIM防撤回补丁(我已经看到了,撤回也没用了)项目地址: https://gitcode.com/GitHub_Trending/re/RevokeMsgPatcher
你有没有过这样的经历:群里刚有人发了一段重要通知,你还没来得及点开,对方一个撤回收回去,内容就永远消失在了聊天记录里。气归气,可如果你动手研究过 PC 版微信、QQ 的防撤回补丁,就会发现这背后藏着一门挺有意思的功夫——如何在动辄上百 MB 的 DLL 文件里,精准找到那一两处控制"撤回"逻辑的字节,再把它改掉。
RevokeMsgPatcher 正是这样一个开源项目:它通过底层二进制修改,让 PC 版微信/QQ/TIM 的撤回功能形同虚设。而它最核心、也最容易出问题的部分,就是特征码匹配。这篇文章会从"为什么要匹配"讲到"具体怎么匹配",把源码里的关键思路用大白话捋一遍。
第一步,先弄懂特征码到底是个什么东西
你平时在文件里搜文字,用的是"关键词";而在二进制世界里,"关键词"就是一串字节。比如某条特征码写出来可能是这样:
15 31 68 00 00 00 73 80 08 48 85 D2 74 3F这串字节在反汇编后,可能对应的就是几条机器指令。防撤回的原理并不复杂:微信在收到"撤回"指令后,会走一个判断分支,决定要不要把消息从你本地删掉。这个分支的跳转指令(比如je,条件跳转)就是突破口——只要把"条件跳转"改写成"无条件跳转"或"不跳",撤回逻辑就被废掉了。
所以 RevokeMsgPatcher 干的事可以概括成三步:
- 准备一份"查找串"(Search),描述补丁前长什么样;
- 准备一份"替换串"(Replace),描述补丁后应该变成什么;
- 在 DLL 全文件范围内找到所有查找串出现的位置,把对应字节换成替换串。
听起来简单,真正的坑全藏在"查找"这两个字里。
通配符登场:一个特征码如何同时适配多个版本
软件一升级,编译结果就变。明明只是加了个小功能,函数开头可能就多插了几条指令,原本严丝合缝的特征码立刻对不上。要是每出一个版本就重新找一次特征码,维护成本能把人累死。
RevokeMsgPatcher 的解法很聪明:引入通配符0x3F。特征码里凡是"这里具体是什么字节不重要"的位置,就填上0x3F,匹配时跳过不比较。
// FuzzyMatcher.cs public static bool IsEqual(byte[] content, int start, byte[] whole) { int i = 0; for (i = 0; i < whole.Length; i++) { if (whole[i] == wildcard) // 0x3F,跳过比较 { continue; } if (content[start + i] != whole[i]) { break; } } return i == whole.Length; }这段代码的逻辑一句话就能说清:遇到0x3F就当没看见,其余位置必须严格相等。只要所有非通配符位置都匹配上了,就认为整条特征码命中。
比如微信某个版本的防撤回特征码里就出现过这样的配置:
Search: 133 192 116 50 185 63 63 63 63 138 Replace: 133 192 235 50 185 63 63 63 63 138中间连续四个63(即0x3F)通常对应一个 4 字节的地址参数,不同版本这个地址会变,但它不影响你判断"是不是这段逻辑"。用通配符把它们遮住,一条特征码就能横跨好几个小版本,这就是模糊匹配的威力。
Boyer-Moore:为什么它能在百 MB 文件里"跳着"找
有了通配符,下一步就是怎么找得快。微信的 WeChatWin.dll 动辄 100MB 以上,如果拿特征码从头到尾一格一格比,慢不说,还要反复比对大量无效位置。
BoyerMooreMatcher 用的是经典的 Boyer-Moore 算法,核心思想是从后往前匹配 + 失败时尽量多跳几步:
// BoyerMooreMatcher.cs,主循环 while (s <= (n - m)) { j = m - 1; // 从模式串最后一个字节开始比 while (j >= 0 && pattern[j] == text[s + j]) { j--; } if (j < 0) // 全部匹配成功 { shiftIndexes[c] = s; c++; s += goodSuffixShifts[0]; } else { // 坏字符规则与好后缀规则取较大值,保证单次跳跃最大 s += Max(goodSuffixShifts[j], badCharShifts[(int)text[s + j]] - (m - 1) + j); } }打个比方:普通匹配像逐字翻阅一本厚词典找某个词;Boyer-Moore 则像翻词典时先看词尾,对不上就直接往后甩好几页。它依赖两个预计算表:
- 坏字符表:记录模式串里每个字节最后一次出现的位置。文本里那个"对不上的字节"如果在模式串后面出现过,就把模式串对齐过去,一次跳过一大段;
- 好后缀表:利用"已经匹配上的后缀"信息,把模式串平移到下一个可能匹配的位置。
两个表取较大值作为跳跃步数,既保证不错过真正的命中点,又让扫描次数大幅下降。这就是为什么整个文件读完加匹配完,耗时通常只在毫秒级。
匹配与替换的完整流程:ModifyFinder 如何拍板
有了模糊匹配和快速检索,剩下的工作就是把它们串成一个可靠的流程。ModifyFinder 是这个环节的总调度,核心方法FindChanges做的事情如下:
// ModifyFinder.cs foreach (ReplacePattern pattern in replacePatterns) { int[] matchIndexs = FuzzyMatcher.MatchAll(fileByteArray, pattern.Search); if (matchIndexs.Length >= 1) { foreach (int index in matchIndexs) { matchNum++; // 该位置已经是替换后的样子,就不需要再改了 if (!FuzzyMatcher.IsEqual(fileByteArray, index, pattern.Replace)) { changes.Add(new Change(index, pattern.Replace)); } } } }每一步都有讲究:
先用"头串"过滤,再做全量验证。一条带通配符的特征码,前面通常有一小段固定字节。FuzzyMatcher 会先把这段"头串"提取出来,交给 Boyer-Moore 快速找出所有候选位置,再对每个候选位置用IsEqual做一次完整的通配符校验。头串和特征码等长时,说明没有通配符,直接返回头串的匹配结果即可——这一手把"快"和"准"兼顾了。
匹配计数与替换计数要严格对账。一个功能可能配置了多条特征码,每条又可能命中多处。matchNum统计的是总命中次数,changes里才是真正需要改的位置。如果某个命中位置已经和替换串一模一样,说明之前打过补丁,就不再重复加入修改列表——这是防止重复打补丁的第一道闸门。
版本库与冲突检测:优雅地告诉用户"你已经打过了"
特征码是跟着版本走的,所以项目在RevokeMsgPatcher.Assistant/Data/下按版本号维护了一套patch.json,里面按应用(WeChat、QQ、TIM、QQLite)区分,记录了每个版本的生效区间和对应的查找串、替换串。
但光有版本库还不够,用户可能用别的工具打过补丁,也可能一个特征码对应多个功能。ModifyFinder 里有一套完整的"对账"逻辑:
- 匹配总数少于预期时,用
IsAllReplaced做反向检查:如果查找串搜不到、但替换串能搜到,说明这个功能早就被改过了,于是抛出"已经安装过对应补丁"的提示,并列出具体是哪些功能; - 如果匹配总数大于等于预期、但真正需要改的数量和预期不符,就要警惕是不是被第三方补丁"部分修改"过,此时会提示"部分特征已被替换"。
// ModifyFinder.cs,反向检测 foreach (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); } }正是这层"双向匹配",让工具在异常场景下不是硬着头皮改文件,而是明确告诉用户发生了什么、该怎么处理——宁可多提示,也不在状态不明的文件上乱动。
特征码匹配失败的避坑清单
结合源码和实际使用,我把常见的坑按"原因 → 对策"整理成了一份清单,供你做类似工具时参考:
| 现象 | 大概率原因 | 对策 |
|---|---|---|
| 匹配数小于预期,报"不一致" | 目标软件升级,特征码漂移 | 确认版本号,升级配套的patch.json,或联系作者更新 |
| 提示"已安装补丁" | 之前打过同款补丁,但界面忘了显示 | 取消勾选对应功能,或使用"已安装功能查询" |
| 提示"部分特征被替换" | 用过第三方多开/防撤回工具 | 从官方渠道重装目标软件后重新打补丁 |
| 通配符位置写得不对 | 特征码开头就是可变字节,GetHead会直接抛异常 | 保证特征码开头保留足够长度的固定字节作"头串" |
| 杀毒软件拦截 | 修改了 WeChatWin.dll / IM.dll | 属于正常现象,校验过文件哈希后放行即可 |
另外有两个实践中的细节值得记下:
- "头串"必须足够长。FuzzyMatcher 的
GetHead在遇到第一个通配符时就截断,如果截出来的头串太短,候选位置会爆炸式增多,验证阶段的耗时也跟着上涨; - 通配符不要出现在特征码开头。开头就是
0x3F会让头串长度为 0,源码里会直接抛出"不正确的通配符位置"异常。
结语:一条特征码背后是整个匹配体系
回头再看,RevokeMsgPatcher 的匹配能力其实是一套组合拳:Boyer-Moore 负责在巨型文件里快速定位,通配符模糊匹配负责跨版本容错,ModifyFinder 负责把"匹配-对账-报错"整条链路串起来。也正是这套组合,让一个补丁工具能长期跟进微信、QQ 的频繁更新而保持可用。
如果你对逆向工程或二进制匹配感兴趣,这个项目(源码位于RevokeMsgPatcher/Matcher/目录)是个很好的入门范本。它的代码量不大,却能让你在真实场景里体会到"算法落地"的完整过程——从字节数组到跳转指令,从特征码到冲突检测。
遇到匹配失败或者想支持新版本?欢迎到项目的 Issue 区反馈,带上你的软件版本号和报错信息,作者和社区会帮你一起排查。下一次,当你在群里看到"对方撤回了一条消息"却依旧能读完全文时,不妨想想背后那条je变jmp的字节——技术的乐趣,往往就藏在这些不起眼的地方。
【免费下载链接】RevokeMsgPatcher:trollface: A hex editor for WeChat/QQ/TIM - PC版微信/QQ/TIM防撤回补丁(我已经看到了,撤回也没用了)项目地址: https://gitcode.com/GitHub_Trending/re/RevokeMsgPatcher
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考