从"特征码匹配失败"到逆向定位:剖析RevokeMsgPatcher的防撤回补丁查找机制
【免费下载链接】RevokeMsgPatcher:trollface: A hex editor for WeChat/QQ/TIM - PC版微信/QQ/TIM防撤回补丁(我已经看到了,撤回也没用了)项目地址: https://gitcode.com/GitHub_Trending/re/RevokeMsgPatcher
某个深夜,微信自动更新到了新版本,你重新打开RevokeMsgPatcher准备打上防撤回补丁,弹出的却是这样一段提示:"特征比对:当前特征码匹配数[0]和期望的匹配数[2]不一致"。翻译成人话就是:程序没有在微信的DLL里找到它要找的那串二进制特征,补丁打不上了。
RevokeMsgPatcher是一款针对PC端微信/QQ/TIM的防撤回补丁工具,它通过直接修改目标程序DLL中的机器码,绕过消息撤回逻辑。这听起来简单,但真正的难点在于:腾讯频繁更新版本、重排代码,固定偏移量式的暴力修改很快失效。这篇文章不介绍怎么点按钮,而是把项目RevokeMsgPatcher/Matcher/目录下的查找引擎拆开,讲清楚"特征码匹配失败"背后的原因,以及这套系统如何做到在数十个版本之间保持可用。
一、问题根源:为什么不能用固定偏移打补丁
先看一个历史事实。项目早期的补丁方式是"精准定位":在配置中记录某个版本的DLL的SHA1哈希和一组绝对文件偏移,打补丁时直接往这些偏移处写入新字节。在RevokeMsgPatcher.Assistant/Data/目录下的patch.json中,这种信息至今仍保留着。
{ "Name": "WeChatWin.dll", "Version": "3.3.5.25", "SHA1Before": "3e94753ccbc2799d98f3c741377e99bdae33b4cf", "Changes": [ { "Position": 3413977, "Content": [235] }, { "Position": 12159591, "Content": [235] } ] }注意这里Content: [235],十进制235对应的十六进制是EB,即x86指令集中的JMP短跳转。反撤回的核心手法就是把某条JE(条件相等跳转)改成JMP(无条件跳转),让"撤回"这条路径永远走不通——这也是wiki中那张7_je_to_jmp.png所演示的操作。
这种方案的缺陷一目了然:每一个新版本都要手工在调试器里重新定位偏移、重新计算SHA1,劳动密集且脆弱。只要微信编译时插入或删除几条指令,所有偏移全部失效。于是项目引入了第二套机制:基于特征码的模糊匹配。
二、核心机制一:通配符驱动的特征码匹配
特征码匹配的思路是:不再记"第3413977字节要改",而是记"这段机器码长什么样"。配置里每条规则是一个ReplacePattern,包含Search(查找串)和Replace(替换串)两组字节数组,0x3F(即?)作为通配符,代表"这个位置的值随版本变化,忽略不比较"。
以微信4.0.x的防撤回特征为例:
Search: 117 33 72 184 114 101 118 111 107 101 109 115 ... 63 63 63 63 ... Replace: 235 33 72 184 114 101 118 111 107 101 109 115 ... 63 63 63 63 ...117是JNZ,235是JMP,中间72 184 114 101 118 111 107 101 109 115正是ASCII字符串"Revokemsg"。整段特征既锁定了"撤回功能入口"的代码形态,又通过通配符容忍了地址字段的差异。
为什么通配符必须用0x3F而不是别的值?因为?在ASCII中就是0x3F,而这段特征码本身不会出现在合法指令中——0x3F对应的AAS指令在正常编译代码里几乎不出现,冲突概率极低。这是FuzzyMatcher类里public const byte wildcard = 0x3F;这一行的设计考量。
三、核心机制二:Boyer-Moore算法如何让百兆DLL秒级定位
有了特征码,接下来是搜索效率问题。微信的WeChatWin.dll动辄上百MB,用朴素逐字节比较法在最坏情况下要比较几十亿次。项目在Matcher/BoyerMooreMatcher.cs中实现了经典的Boyer-Moore算法。
这个算法的核心思想是"从后往前匹配 + 失败时尽可能多跳"。它通过预处理模式串,构建两个启发式表:
- 坏字符表:匹配失败时,根据文本中出错的那个字节在模式串里最后一次出现的位置,决定可以安全跳过多少字节。
- 好后缀表:当模式串后缀已经部分匹配时,寻找"这截后缀是否能在模式串更早的位置重新对齐"。
// Matcher/BoyerMooreMatcher.cs static int[] PreprocessToBuildBadCharactorHeuristic(byte[] pattern) { int m = pattern.Length; int[] badCharactorShifts = new int[AlphabetSize]; for (int i = 0; i < AlphabetSize; i++) badCharactorShifts[i] = m; // 默认可跳整个模式串长度 for (int i = 0; i < m; i++) badCharactorShifts[pattern[i]] = m - 1 - i; // 记录每个字节最后出现的位置 return badCharactorShifts; } public static bool TryMatch(byte[] text, byte[] pattern, out int firstShift) { firstShift = -1; int n = text.Length, m = pattern.Length, s = 0; int[] badCharShifts = PreprocessToBuildBadCharactorHeuristic(pattern); int[] goodSuffixShifts = PreprocessToBuildGoodSuffixHeuristic(pattern); while (s <= n - m) { int j = m - 1; while (j >= 0 && pattern[j] == text[s + j]) j--; // 从尾部向前比对 if (j < 0) { firstShift = s; return true; } // 两条启发式规则取较大者作为跳跃距离 s += Max(goodSuffixShifts[j], badCharShifts[text[s + j]] - (m - 1) + j); } return false; }工程上还提供了MatchAll变体,返回所有匹配位置——这一点很关键,因为同一个特征码可能在DLL里出现多次(微信的撤回校验在多个消息类型上各有一处)。
四、核心机制三:两阶段模糊匹配,把通配符开销降到最低
通配符不能直接喂给Boyer-Moore——算法要求精确字节比较。FuzzyMatcher的处理方式是两阶段匹配:
- 用
GetHead从特征码里截取第一个通配符之前的连续字节作为"头串",交给Boyer-Moore精确定位候选位置; - 对每个候选位置调用
IsEqual做全模式验证,跳过通配符位置,逐字节确认剩余部分。
// Matcher/FuzzyMatcher.cs public static int[] MatchAll(byte[] content, byte[] pattern) { byte[] head = GetHead(pattern); // 截取通配符前的定长头串 int[] indexs = BoyerMooreMatcher.MatchAll(content, head); if (head.Length == pattern.Length) return indexs; // 无通配符,直接返回精确结果 List<int> res = new List<int>(); foreach (int index in indexs) if (IsEqual(content, index, pattern)) // 候选位置做全模式验证 res.Add(index); return res.ToArray(); } 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; }这套设计把"带通配符的搜索"转化成了"普通精确搜索 + 少量逐字节确认",让Boyer-Moore的高效跳跃能力得以保留。有一个边界值得注意:头串不能以通配符开头,否则GetHead会直接抛出"不正确的通配符位置"异常——特征码设计规范里应把首个通配符尽量后置,既能保证头串长度、加快定位,也能降低误命中率。
五、实战落地:ModifyFinder如何把匹配结果变成补丁动作
匹配引擎之上是编排层Modifier/AppModifier.cs与Matcher/ModifyFinder.cs。补丁决策的完整链路是:
- 读取目标DLL,通过文件版本号在
FileCommonModifyInfos中按StartVersion~EndVersion区间选择适用的特征码规则组(如3.9.11.0 < 版本 <= 4.0.3.0使用"防撤回(老)"与"多开"两组特征); - 用户勾选需要的功能类别(防撤回/多开),按
Category过滤出ReplacePattern列表; - 调用
ModifyFinder.FindChanges得到所有需要写入的位置与字节; - 先备份DLL为
*.h.bak,再执行写入,失败则自动还原。
FindChanges里最容易被忽略的是它的自检逻辑:它统计匹配总数matchNum,如果小于期望的规则数replacePatterns.Count,并不直接失败,而是进入IsAllReplaced做一次"反向扫描"——查找串一个都搜不到,但替换串却能搜到,说明该功能已经被打过了,此时报错信息会区分"全部已安装"和"部分已安装,请取消勾选"两种情况。
// Matcher/ModifyFinder.cs private static Tuple<bool, SortedSet<string>> IsAllReplaced( byte[] partByteArray, List<ReplacePattern> replacePatterns) { SortedSet<string> alreadyReplaced = new SortedSet<string>(); 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); } return new Tuple<bool, SortedSet<string>>( matchNum >= replacePatterns.Count, alreadyReplaced); }这套双向匹配是防止"重复打补丁损坏文件"的关键防线。下图展示了从定位字符串到修改跳转指令的完整逆向过程,特征码正是从这类调试会话中提炼出来的:
六、三个最常见的匹配问题与排查思路
1. 版本更新后"特征码匹配数不一致"
这是本文开头的场景。原因通常是新版本里目标函数的指令序列变了。排查步骤:
- 用x64dbg附加进程,搜索
revokemsg等关键字符串,定位到新版本对应的函数入口,观察分支指令是否从JE变成了JNZ、TEST等形态; - 对比新旧特征:如果只是某个操作数变了,把该字节改为
0x3F通配符即可;如果是指令整体重排,需要重写整段特征; - 项目策略是按版本区间维护多组特征,新版本发布后由维护者追加一组
StartVersion/EndVersion范围,所以遇到此报错时,第一时间应确认使用的补丁配置是否已覆盖当前版本。
2. "你已经安装过此补丁"但明明没有打过
通常是残留的.h.bak备份或上一次失败操作导致的。可以手工校验:删除目标DLL同目录下的.h.bak文件,重新运行补丁程序让它重新备份;若仍报错,用原版安装包覆盖DLL后重试。不要直接手工改DLL字节,SHA1校验会立刻识破。
3. 通配符位置设计不当导致误匹配
通配符太多或太靠前,会让特征码退化成"只匹配几个固定字节",在百兆DLL里产生大量假阳性候选,拖慢匹配甚至写错位置。经验规则是:头串(第一个通配符前的字节)至少保留8~16字节;通配符只用于明显的地址/立即数区域,指令操作码部分必须精确。
七、局限与展望
这套系统已经经受住了微信从2.7到4.x、QQ从9.0到QQNT、以及TIM多个产品线的长期验证,其"精确SHA1定位"与"模糊特征码"双轨并行的设计,本质上是用配置驱动替代代码驱动,让新版本适配成本大幅下降。但它也有固有局限:特征码仍然依赖人工逆向提炼,维护者需要持续跟进每一个版本;File.ReadAllBytes将整个DLL读入内存,对超过100MB的文件仍有优化空间(源码中FindChanges的TODO 该逻辑需要优化!注释也证实了这一点)。
如果想要参与这个项目,最实际的入口就是从RevokeMsgPatcher.Assistant/Data/下的patch.json入手:读懂一段特征码的语义、为某个新版本补充一组特征,就是对这个工具最直接的贡献。
【免费下载链接】RevokeMsgPatcher:trollface: A hex editor for WeChat/QQ/TIM - PC版微信/QQ/TIM防撤回补丁(我已经看到了,撤回也没用了)项目地址: https://gitcode.com/GitHub_Trending/re/RevokeMsgPatcher
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考