RevokeMsgPatcher 核心机制深度解析:防撤回与多开原理
目录#
[整体架构概述](#1-整体架构概述)
[防撤回技术原理](#2-防撤回技术原理)
2.1 问题本质:DLL 字节级覆盖
2.2 双轨补丁匹配策略
2.3 特征码搜索算法
2.4 模糊匹配实现细节
2.5 补丁执行与状态检测
2.6 撤回功能阻断机制(目标行为)
[多开技术原理](#3-多开技术原理)
3.1 Windows 互斥对象机制
3.2 HANDLE 枚举与查询
3.3 互斥句柄的精确识别与关闭
3.4 Timer 驱动的主动清理模式
[数据模型详解](#4-数据模型详解)
[关键异常处理与边界条件](#5-关键异常处理与边界条件)
[两个模块的代码路径对比](#6-两个模块的代码路径对比)
1. 整体架构概述#
RevokeMsgPatcher 由两个独立但互补的程序集组成:
程序集 | 用途 | 核心技术 |
|---|---|---|
| 防撤回补丁注入 | PE DLL 字节级修改(精准 SHA1 + 模糊特征码) |
| 多开工具 | Windows HANDLE 级别操作(NtQuerySystemInformation + DuplicateHandle) |
二者作用层次完全不同:
防撤回:在进程外部,通过修改磁盘上的
.dll二进制文件来实现,需要目标应用处于**关闭状态**。多开:在进程运行期间,通过操作系统内核对象实现,目标应用**必须正在运行**。
2. 防撤回技术原理#
2.1 问题本质:DLL 字节级覆盖#
微信/QQ/TIM 等应用在收到"撤回消息"指令后,会在客户端侧执行以下逻辑链:
防撤回的本质是:找到控制这条逻辑链的关键函数调用点,将其目标指令替换为 NOP(空操作)或跳转指令,使函数执行时无害返回。
由于这些应用程序每次更新都会重新编译 DLL,内存中的函数偏移地址会变化,因此无法硬编码偏移量。RevokeMsgPatcher 采用的是运行时不变的字节序列特征(opcode pattern)来定位目标位置——这组 opcode 在执行相同逻辑功能的函数中保持稳定,即使应用升级也会保留。
2.2 双轨补丁匹配策略#
AppModifier.ValidateAndFindModifyInfo() 中的匹配流程是一个严格的多级决策树:
为何有两种策略?
策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
SHA1 精准匹配 | 已知确切版本的 DLL | 零误判、结果确定 | 需维护大量版本的 SHA1 记录 |
特征码模糊匹配 | 版本跨越但逻辑函数未变 | 覆盖版本区间 | 可能有多个匹配点,需额外校验 |
2.3 特征码搜索算法:Boyer-Moore#
BoyerMooreMatcher.MatchAll() 实现了经典的 **Boyer-Moore 字符串匹配算法**,用于在二进制文件(DLL 的字节数组)中高效查找所有符合 Search 模式的偏移位置。
预处理阶段(扫描模式串一次)#
Bad Character Heuristic(坏字符规则):
含义:当文本中某个字符与模式串不匹配时,将模式串向右移动,直到坏字符与模式串中最右侧的同字符对齐。若该字符在模式串中不存在,则移动 m 位(整个模式长度)。
Good Suffix Heuristic(好后缀规则):
含义:当已经成功匹配了模式串的后缀部分后发生失配,利用已匹配的好后缀信息来决定最大安全移位距离,避免遗漏可能的匹配位置。
匹配阶段(滑动窗口)#
关键在于每次失配都取 max(坏字符移位, 好后缀移位) —— 这是 Boyer-Moore 的标准优化:两条规则独立计算,取较大值可以跳得更远而不遗漏匹配。
时间复杂度:最坏情况 $O(nm)$,平均情况 $O(n/m)$,对于典型的短特征码(几十字节)在大 DLL(几 MB)中搜索效率极高。
2.4 模糊匹配实现细节:FuzzyMatcher.IsEqual()#
IsEqual() 用于判断某段文件内容是否"已经等于目标替换值",支持 通配符匹配:
这使得特征码定义时可以忽略不相关的寄存器值/立即数,只关注核心 opcode 序列。
在 ModifyFinder.FindChanges() 中,对每个匹配位置执行如下判断:
这一步至关重要:防止重复打补丁时产生破坏性写入。如果某位置已经是正确的替换值,就不再做无意义的写操作。
2.5 补丁执行与状态检测#
执行流程(AppModifier.Patch())#
状态检测(补丁已安装的判定)#
ValidateAndFindModifyInfo() 中存在三种状态检测:
状态 | 触发条件 | 处理方式 |
|---|---|---|
| FileSHA1 == SHA1After | 直接抛异常,告知用户已安装 |
| FindChanges 返回空 list(全部 IsEqual 为 true) | 抛异常,告知已安装 |
| matchNum > 0 但 changes.Count == 0(部分位置已被替换) | 抛异常,提示可能与其他补丁冲突 |
| matchNum < replacePatterns.Count 且 none replaced | 提示特征码数量不匹配,可能是最新版本 |
卸载还原(Restore())#
2.6 撤回功能阻断机制(目标行为)#
当补丁成功后,目标 DLL 中原本控制撤回行为的函数体被改写。以微信的 WeChatWin.dll 为例,典型的目标函数伪代码:
原始逻辑(有撤回):
补丁后逻辑(防撤回):
或者更常见的做法是将 je(条件跳转)替换为 jne 或无条件跳过整个撤回处理块。具体字节替换模式取决于 DLL 的实际汇编布局,由 patch.json 中定义的 ReplacePattern.Search 和 ReplacePattern.Replace 精确描述。
3. 多开技术原理#
3.1 Windows 互斥对象机制#
Windows 提供内核级同步原语——互斥体(Mutant / Mutex),用于确保某资源在任意时刻只被一个进程访问。
微信实现单实例限制的逻辑如下:
关键事实:互斥对象属于系统全局命名空间(\Sessions\X\BaseNamedObjects\),其句柄在进程内部持有,但从外部无法直接感知哪个句柄属于哪个互斥对象。
3.2 HANDLE 枚举与查询#
ProcessUtil.GetHandles() 通过调用 Windows 内部未导出 API NtQuerySystemInformation 来获取系统级句柄表:
返回值是一个 SYSTEM_HANDLE_INFORMATION 数组,每条记录包含:
字段 | 说明 |
|---|---|
| 持有该句柄的进程 ID |
| 句柄值(进程虚拟地址空间内的索引) |
| 内核对象地址(不可直接解引用) |
| 访问权限掩码 |
由于句柄是进程地址空间内的虚拟索引,要访问它需要先通过 DuplicateHandle() 将其复制到当前进程的句柄表:
然后对复制出的句柄调用 NtQueryObject(InformationClass = ObjectNameInformation = 1)获取对象的名称字符串。
3.3 互斥句柄的精确识别与关闭#
FindAndCloseWeChatMutexHandle() 的核心流程:
DUPLICATE_CLOSE_SOURCE 是关键标志:它让 DuplicateHandle 在执行时直接关闭源进程的句柄,而不是真正"复制"。这相当于向内核发送一个"关闭此句柄"的请求,而无需进程自身的参与。
3.4 Timer 驱动的主动清理模式#
FormMultiInstance 采用 Timer(Windows Forms System.Windows.Forms.Timer)周期性轮询:
用户点击【启动多开】即开始 Timer,此后每当有新的微信进程启动(或由用户手动启动第二个实例),Timer 检测到该进程尚未关闭互斥句柄,便立即执行关闭操作,使其能通过微信的单实例检查。
手动模式按钮:
按钮 | 操作 |
|---|---|
关闭所有微信进程 |
|
清理所有微信互斥句柄 | 一次性对当前所有 WeChat 进程执行 |
4. 数据模型详解#
4.1 补丁配置模型#
补丁信息以 JSON 形式内嵌于程序资源或从远程服务器动态获取,结构如下:
关键字段含义:
SHA1Before/SHA1After:补丁前/补丁后的文件 SHA1 哈希,用于精确身份验证。
Changes[].Offset:在 DLL 中的字节偏移量(十六进制)。
ReplacePatterns[].Search:特征码(支持通配符0x00匹配任意字节)。
ReplacePatterns[].Replace:替换为目标字节序列。
Category:功能类别标签("防撤回"、"多开"等),用于 UI 多选框筛选。
SimilarCategories:同类互斥标签,防止用户同时勾选互斥功能。
4.2 变更对象模型#
FileUtil.EditMultiHex() 将所有 Change 按 Position 升序排序后,依次对文件进行 FileStream.Seek + Write 操作,不使用临时文件,直接原地修改。
5. 关键异常处理与边界条件#
5.1 文件被占用(IOException)#
当目标 DLL 被微信/QQ 进程锁定时,File.ReadAllBytes() 或 File.Copy() 会抛出 IOException。此时必须:
确保目标应用已完全关闭
以管理员权限运行补丁工具
部分情况下需要进入安全模式或通过外部工具解锁句柄
5.2 版本范围边界#
IsInVersionRange() 使用字符串比较版本(而非数值比较),存在边界情况:
这是版本字符串比较的经典缺陷,但在实际应用中影响较小,因为版本号通常保持相同的位数格式。
5.3 特征码多匹配位置#
同一特征码在 DLL 中可能出现多次(例如同一个函数被内联展开到多个位置)。FindChanges() 会对**所有匹配位置**都执行替换,这意味着如果某些匹配位置不应被修改,会导致不可预期的行为。这也是为什么 ReplacePattern.Tips 和 SimilarCategories 字段存在——用于在 UI 层引导用户做出正确选择。
5.4 多开工具的进程生命周期管理#
wechatProcesses 列表在 Timer 运行时持续累积,只有当进程 HasExited == true 时才被移除。若用户手动终止了某个微信进程但 Timer 未检测到(极端情况),该条目会永久残留,直到工具重启。
6. 两个模块的代码路径对比#
6.1 防撤回模块主路径#
6.2 多开模块主路径#
附录:关键 API 对照表#
Windows API | 用途 | 所在模块 |
|---|---|---|
| 枚举系统级 HANDLE 表 | MultiInstance |
| 查询内核对象名称 | MultiInstance |
| 打开目标进程以复制句柄 | MultiInstance |
| 关闭目标进程中的指定句柄 | MultiInstance |
| 关闭当前进程中的句柄副本 | MultiInstance |
| 获取 DLL 文件版本 | RevokeMsgPatcher |
| 计算文件 SHA1 用于精准匹配 | RevokeMsgPatcher |
| 原地写入替换字节 | RevokeMsgPatcher |
