ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

补丁突然失效了:一次防撤回特征码匹配事故的完整排查手记

补丁突然失效了:一次防撤回特征码匹配事故的完整排查手记 补丁突然失效了一次防撤回特征码匹配事故的完整排查手记【免费下载链接】RevokeMsgPatcher:trollface: A hex editor for WeChat/QQ/TIM - PC版微信/QQ/TIM防撤回补丁我已经看到了撤回也没用了项目地址: https://gitcode.com/GitHub_Trending/re/RevokeMsgPatcher凌晨两点有用户发来一条反馈微信更新到新版本后RevokeMsgPatcher 报错了——特征比对匹配数不一致。这条报错信息背后正是 PC 端防撤回补丁最核心、也最脆弱的环节特征码匹配。RevokeMsgPatcher 通过底层二进制特征码定位需要修改的字节在微信 / QQ / TIM 等即时通讯工具的 dll 文件中找到撤回逻辑的入口再把它改写掉从而实现防撤回。而只要软件一更新这些二进制特征就可能位移、变形甚至消失。这篇文章以一次真实的事故排查为主线从特征码是怎么来的一直讲到匹配器是如何设计出来的希望能帮你在面对补丁失效时知道问题出在哪、为什么。第一步先搞清楚报错匹配数不一致是什么意思排查任何故障都得先读源码。在 RevokeMsgPatcher 中一次完整的打补丁流程由 Modifier/AppModifier.cs 驱动大致是定位安装目录 → 校验目标 dll 是否存在 → 确认版本与 SHA1 → 备份 → 写入修改。而匹配数不一致这个异常抛自 Matcher/ModifyFinder.cs 的FindChanges方法。要理解这条报错得先明白项目支持两种打补丁的方式打补丁方式依据优点弱点精准替换文件版本 SHA1 完全匹配直接按固定偏移写字节简单可靠版本一升级就失效特征码替换在 dll 里搜索一段特征字节序列找到后改写能覆盖一段版本区间需要一套匹配算法支撑用户反馈的报错走的是第二种。特征码替换会在文件里做搜索而搜索的命中数量必须与预期一致特征串没搜到说明特征码失效了特征串搜到了但替换串也搜到了说明补丁已经打过。这两种情况代码会用不同的异常消息告诉用户。用户遇到的匹配数不一致多半是前者——新版微信的二进制代码重排把特征码改掉了。第二步特征码到底是怎么挖出来的在写匹配器之前得先知道特征码长什么样。这通常要在调试器里做。项目里保存了一张完整的操作截图展示了用 x32dbg 附加WeChatWin.dll后右键搜索字符串revokemsg的经典动作——因为这个字符串几乎就是这个功能的门牌号顺着它的交叉引用就能找到处理撤回消息的函数。找到函数后重点来了在这段代码里通常有一个条件跳转指令比如je机器码0x74控制着是否执行撤回的分支。防撤回的本质就是把这个条件跳转改成无条件跳转jmp机器码0xEB让程序永远跳过撤回动作。图中的7_je_to_jmp.png正是这个操作在反汇编窗口找到jns原为je指令在下方十六进制视图中把74改成EB。改完之后通过补丁插件把这些修改一次性写回 dll 文件整个手工逆向流程就完成了——见14_patch_dll.png列表里的每一行都记录了哪个地址、从什么字节改成什么字节。手工做完一次就可以把搜索串 → 替换串这一对字节序列提取出来交给程序去自动完成。可问题来了手工逆向只需要几分钟程序自动匹配却要求又快又准尤其要能忍受版本之间的细微变化。第三步先实现笨但必要的一步——精确匹配最开始的需求很简单给一段固定的字节串在 dll 里找到它。朴素的做法是从头到尾逐字节比较但对于动辄上百 MB 的 WeChatWin.dll 来说这种 O(n×m) 的暴力搜索太慢了。项目选择了经典的Boyer-Moore 算法实现在 Matcher/BoyerMooreMatcher.cs。它的核心思想是匹配失败时尽可能跳得远一点依靠两条预处理出来的规则坏字符规则文本中导致匹配失败的那个字节如果在模式串中最后一次出现的位置已知那么直接把这个位置对齐过来一次跳过一大段。好后缀规则已经匹配上的那段后缀如果模式串别处也有同样的后缀就把它们对齐否则整个越过。// BoyerMooreMatcher.TryMatch 的核心循环 int[] badCharShifts PreprocessToBuildBadCharactorHeuristic(pattern); // 坏字符表 int[] goodSuffixShifts PreprocessToBuildGoodSuffixHeuristic(pattern); // 好后缀表 while (s n - m) // s 是模式串相对文本的偏移 { int j m - 1; // 每次都从模式串末尾开始往前比 while (j 0 pattern[j] text[s j]) j--; if (j 0) { firstShift s; return true; } // 全部比中 else { // 两个规则取最大值保证一次移动尽量远 s Max(goodSuffixShifts[j], badCharShifts[text[s j]] - (m - 1) j); } }注意这里的一个细节坏字符表初始值直接填了模式串长度m而不是常见的-1。这样即使某个字节根本没在模式串里出现过移动量也不会算出负数省掉一次Max(1, ...)的保护边界更干净。第四步版本一更新精确匹配就翻车了——引入通配符精确匹配很快但它有个致命伤太死板。微信升级后函数头部的栈分配指令sub rsp, 0x??里那个立即数可能从0xE0变成0xF0一个call指令后面的 4 字节相对地址几乎必然变化。于是项目引入了一个聪明的约定用0x3F作为通配符出现在特征串里的0x3F表示这个字节随便是什么都行。这个能力实现在 Matcher/FuzzyMatcher.cs。看一个真实例子微信 4.0.3 及以上版本的防撤回特征串节选自 Assistant/Data 的 patch.json117, 33, 72, 184, 114, 101, 118, 111, 107, 101, 109, 115, 72, 137, 5, 63, 63, 63, 63, 102, 199, 5, 63, 63, 63, 63, 103, 0, 198, 5, 63, 63, 63, 63, 1, 72, 141反汇编出来就是jne mov rax, revokemsg开头的序列其中连续四个63即0x3F是全局地址每次编译都会变所以必须用通配符放行。替换串只改第一个字节117jne→235jmp其余原样保留——这是典型的最小改动思路也是补丁安全性的来源。第五步两阶段匹配——先定位候选点再逐字节验证有了通配符匹配逻辑要重新设计。一个朴素想法是直接遍历全文、逐字节做带通配的比较但这样会把性能拖垮。项目用的策略是两阶段匹配代码很直观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; // 没有通配符直接返回 Listint res new Listint(); foreach (int index in indexs) { if (IsEqual(content, index, pattern)) res.Add(index); // 阶段二全模式验证 } return res.ToArray(); }两个阶段的职责分得很清楚阶段一粗筛取通配符前的固定字节段用 Boyer-Moore 高速搜索。Boyer-Moore 天生不认通配符但没关系头串是干净的。阶段二精验对每个候选位置跑IsEqual遇到0x3F就跳过比较其余字节严格相等才算通过。其中IsEqual是整个模糊匹配的最后一公里它的正确性直接决定误报率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; // 走完整个模式串才算匹配 }为什么这个设计兼顾了性能与鲁棒性因为通配符通常出现在特征串的中后段如函数地址、栈偏移头串过滤保证了绝大多数无关区域根本不会被第二阶段触及只有极少数看起来像的位置才做完整验证。这也是微信这种百 MB 级 dll 依然能秒级完成匹配的关键。第六步最头疼的 Bug——重复打补丁与混合补丁匹配算法解决了找不到但事故排查中还有一种更隐蔽的情况补丁其实已经打过了再打一次程序却把已经改过的字节当成目标特征又出现一次导致误判。ModifyFinder 对这个问题做了反向校验同时搜索查找串和替换串。如果查找串已经不存在、而替换串存在说明这个功能的补丁早已安装——这就是IsAllReplaced的逻辑// 反向匹配判断某个功能是否已经被替换过 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); } }而FindChanges则在此基础上做最终的数量仲裁命中数等于预期数且都不是替换串 → 正常返回修改点位命中数小于预期数且反向校验发现部分功能已装 → 提示以下功能补丁已经安装请取消勾选命中数小于预期数且没有已装特征 → 提示特征码可能已随版本变化请提交 Issue。这套正向找 反向验的双保险把版本失效和重复安装这两种几乎无法从用户侧区分的情况拆成了两个可以给用户明确指引的报错。第七步事故的答案——特征库是按版本区间组织的回到凌晨两点的那条反馈。排查到最后问题的答案其实藏在数据里项目的特征码全部集中在 RevokeMsgPatcher.Assistant/Data/ 下按版本号分目录的patch.json中。每个文件里特征串不是孤立的而是绑定了一段版本区间StartVersion~EndVersion例如微信 3.9.11.0 到 4.0.3.0 的区间内配了一套特征4.0.3.0 以上又配了另一套。程序会根据读到的WeChatWin.dll版本号用IsInVersionRange选出一套合适的特征去匹配。所以匹配数不一致这个报错90% 的可能是用户升级到了某个区间末尾的新版本而新版还没有收录特征。这种版本区间 通配符的架构就是项目对抗高频版本迭代的根本手段——它把维护成本从每个版本都要精确改降到了每隔一段版本区间补一套模糊特征。给后来者的三个优化方向缓存头串的匹配结果FindChanges里有一段标注了// TODO 该逻辑需要优化指的是它对每个功能都重复读取整份文件字节实际上可以先按字节流并行、或按功能合并搜索。更长更稳的特征设计通配符最好放在中后段头串越长、筛选越准同时特征长度不宜过短否则容易在百 MB 文件里误命中。把匹配失败自动上报像这次的事故如果程序能在报错的同时把版本号、SHA1 一并提交维护者定位特征失效会快得多。总结一次补丁失效的报错背后其实是三层设计在协作Boyer-Moore 负责快通配符负责稳ModifyFinder 负责对。如果你在升级软件后遇到 RevokeMsgPatcher 提示特征码匹配数不一致现在你应该知道这意味着新版本特征还没收录而不是程序坏了——欢迎到项目提交 Issue 附上你的版本号也可以参与维护 patch.json让防撤回补丁覆盖更多版本。掌握这套特征码匹配的思路无论对补丁开发、逆向分析还是安全研究都是一块非常实用的地基。【免费下载链接】RevokeMsgPatcher:trollface: A hex editor for WeChat/QQ/TIM - PC版微信/QQ/TIM防撤回补丁我已经看到了撤回也没用了项目地址: https://gitcode.com/GitHub_Trending/re/RevokeMsgPatcher创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表