# 助手 0x3F4 分派机制与控件 ID 实现表 > 分析对象:`D:\rxjh2025\client\YGOnline.exe`(ImageBase 0x400000,32 位,无重定位) > 生成日期:2026-09-18 | 方法:静态反汇编(pefile + capstone)+ 实机内存遍历交叉验证 --- ## 一、结论速览 | 问题 | 结论 | |---|---| | `push 0x3F4` 是什么 | **不是消息号,是「点击」动作码**。助手窗口虚表槽#1(`0xA3F3D0`)只认 6 个动作码 | | 控件 ID 去哪了 | 被**原样当作「选项码」**转发给 `0xA3F700`——控件 ID 与选项码是同一套编号 | | 为什么只有开关能用 | 不是游戏不支持:74 个控件里 70 个都有实现。**是上一版 ④ 的机器码取错了值**(详见第六节) | | 修法 | 机器码核心抽成**只有 1 个参数、0 个局部变量**的子程序,只读 `[ebp+8]`,与局部变量排布彻底无关 | --- ## 二、`0xA3F3D0`:虚表槽#1 只认 6 个动作码 用户实机日志给出 `虚表=010A20F8 槽1=A3F3D0`。查镜像中值为 `00A3F3D0` 的 dword,唯一命中 `0x010A20FC`,其前一格 `0x010A20F8 = 00A2FBF0`,正是虚表首地址。 `0xA3F3D0` 开头即按 `[ebp+8]` 分派: ```asm 0xA3F3D0 push ebp / mov ebp,esp / sub esp,0x20 / push esi 0xA3F3D7 mov [ebp-4], ecx ; this = 助手窗口 0xA3F3DA mov eax, [ebp+8] ; 动作码 0xA3F3E0 cmp eax, 8 / jg 0xA3F40F 0xA3F3E6 cmp eax, 8 / je 0xA3F42E ; 8 → 每帧刷新 0xA3F3EC cmp eax, 3 / je 0xA3F61A ; 3 → 转发 0xA41740 0xA3F3F6 cmp eax, 6 / je 0xA3F630 ; 6 → 转发 0xA427E0 0xA3F400 cmp eax, 7 / je 0xA3F646 ; 7 → 布局刷新(用 +0x36C0 / +0x34B4 / +0x3A64) 0xA3F40F cmp eax, 0x3EE / je 0xA3F6C4 ; 0x3EE → 需 arg3==1 才转发(状态通知) 0xA3F41C cmp eax, 0x3F4 / je 0xA3F6DB ; ★ 0x3F4 → 点控件专用 0xA3F429 jmp 0xA3F6EC ; 其余一律忽略,直接 ret 0xC ``` **动作码表(`0xA3F3D0`)** | 动作码 | 目标 | 作用 | |---|---|---| | 3 | `0xA41740` | 转发(arg2, arg3) | | 6 | `0xA427E0` | 转发(arg2, arg3) | | 7 | `0xA3F646` | 布局刷新 | | 8 | `0xA3F42E` | 每帧刷新(长分支,含计时/自动逻辑) | | 0x3EE | `0xA3F6C4` | `arg3==1` 时才 `push arg2` 转发 | | **0x3F4** | **`0xA3F6DB`** | **点控件:转发(控件ID, lParam)给 `0xA3F700`** | 返回约定:`ret 0xC`(3 个参数,`__thiscall` 风格)。 --- ## 三、`0x3F4` 转发链原文 ```asm 0xA3F6DB mov edx, [ebp+0x10] ; arg3 = lParam 0xA3F6DE push edx 0xA3F6DF mov eax, [ebp+0xc] ; arg2 = 控件ID 0xA3F6E2 push eax 0xA3F6E3 mov ecx, [ebp-4] ; this = 助手窗口 0xA3F6E6 call 0xA3F700 ; ★ 与旧辅助直调的是同一个函数 0xA3F6EC mov eax, 1 / pop esi / mov esp,ebp / pop ebp / ret 0xC ``` **这就是为什么「旧代码直接 call 0xA3F700 + push 0x31」和「点开始按钮」效果一致**——两者最终都进 `0xA3F700`,只是旧路少了一层转发。 参数顺序与易语言侧完全对应: ``` push 0 → [ebp+0x10] lParam push 控件ID → [ebp+0xc] wParam / 选项码 push 0x3F4 → [ebp+8] 动作码 ecx = 父窗口(= 助手窗口) call [ecx虚表+4] ``` --- ## 四、`0xA3F700`:154 项跳转表全解 函数入口结构(`ret 8`,2 个参数): ```asm 0xA3F700 push ebp / mov ebp,esp / sub esp,0x1ac 0xA3F714 mov [ebp-0xf8], ecx ; this = 助手窗口 0xA3F71A mov eax, [ebp+8] ; 选项码 = 控件ID 0xA3F729 sub eax, 0xb 0xA3F732 cmp eax, 0x99 ; 合法范围 0x0B ~ 0xA4 0xA3F748 movzx eax, byte [eax+0xA41698] ; 字节索引表 0xA3F74F jmp [eax*4+0xA41588] ; 跳转表(154 项 / 68 个不同目标) 0xA41575 ... pop esi / ret 8 ; 公共尾部出口 ``` - **索引表** `0xA41698`,**跳转表** `0xA41588`(均在 `.rdata`) - 跳转表共 **68 个不同目标**,其中命中最多的一组(87 个码)全部指向 **公共尾部 `0xA41204`** --- ## 五、公共尾部 `0xA41204`:按区间批量处理 **它不是空转**——按 6 个区间批量处理勾选框 / 互斥页签: | 区间 | 处理对象 | 逻辑 | |---|---|---| | `0x1C~0x27`(12 个) | 勾选框 | `[this+(id-0x1C)*4+0x38F0]` 刷新控件 → `[this+(id-0x1C)+0x355C] ^= 1` | | `0x46~0x52`(13 个) | 互斥页签 | `[this+(id-0x46)*4+0x36F4]` 刷新 → 翻转 `+0x37A6` → 归 0 时联动 `+0x3794` | | `0x65~0x6D`(9 个) | 勾选框 | `[this+(id-0x65)*4+0x3450]` / `+0x33A0` / `+0x33AD` / `+0x347C` | | `0x80~0x88`(9 个) | 页签(按槽位 `0x5C4*slot`) | 翻转 `[... +0x559]`,刷新 `[this+(id-0x80)*4+0x470]` | | `0x89~0x90`(8 个) | 同族 | 翻转 `[... +0x562]`,刷新 `[this+(id-0x89)*4+0x494]` | | `0xA1 / 0xA2` | 双选按钮 | 翻转 `[this+(id-0xA1)+0x39DC]` 并同步到 `+0x3568` | 尾部完整 `cmp [ebp+8],imm` 序列:`0x1C / 0x27 / 0x46 / 0x52 / 0x65 / 0x6D / 0x80 / 0x88 / 0x89 / 0x90 / 0xA1 / 0xA2`。 --- ## 六、实机 74 个控件 ID 分类 | 分类 | 数量 | ID 列表 | |---|---|---| | **有独立实现** | 38 | `0B 0C 0D 0E 12 13 14 15 28 29 2A 2B 2C 2D 2F 31 32 33 34 35 36 37 39 3B 53 54 55 56 98 99 9A 9B 9C 9E 9F A0 A3 A4` | | **走公共尾部**(其中 `0x1C~0x27` 是真实现) | 28 | `1C~27`(12 个,勾选框)、`46~4E`、`4F~52`(互斥页签)、`9D`(真正空转)、`A1 A2` | | **超范围忽略** | 4 | `01`(那 4 个 ID=01 槽)、`425`、`4DE`、`1C9` | 几个关键 case 的实现位置: | ID | 偏移 | case 体 | 行为概要 | |---|---|---|---| | `0x31` | 3938 | `0xA406B4` | 开始挂机(先查 `[0x3BF28CC]+0x54D8` 与 `[0x3BA1F44]==3` 等前置条件,不满足则弹提示 0x14D6 / 0xAD0) | | `0x32` | 393C | `0xA40CA9` | 停止挂机:清 `[this+0x34B8]` → `0xA4A4B0` → `0xA3A870(0)` → 提示 0xBF6 → 刷新 `[this+0x3938]` | | `0x33` | 37F8 | `0xA40D4E` | 开/关设置面板:按 `[[this+0x34B4]+0x40]` 选模式,调 `0xA3F0B0(mode,1)` | | `0x34` | 37FC | `0xA40DA5` | 收起面板:`0xA3F0B0(0,0)` | | `0x35` | 3940 | `0xA40D28` | 保存 | | `0x36` | 3800 | `0xA40D39` | 取消 | | `0xA3` | 39E8 | `0xA410C4` | 弹窗确定 | | `0xA4` | 39EC | `0xA4119D` | 弹窗取消 | --- ## 七、为什么 ②③ 能用而 ④ 不行(核心坑) `0xA3F3D0` 的 `this` 由 `ecx` 决定,而 ②③④ 的机器码都用 `mov ecx,[ebp-4]` 取「第一个局部变量」: - **②③(3 个局部变量)**:`ECX = 父窗口` 是**最后一次赋值**,且 `ECX` 与 `父窗口` 两个变量**持有同一个值**(父窗口 = `[按钮+4]`,实测就等于助手窗口本身)。所以无论易语言把第一个还是最后一个局部变量放在 `[ebp-4]`,读出来都是父窗口 → **侥幸永远正确**。 - **④(5 个局部变量)**:`[ebp-4]` 未必是 `控件`。一旦落错格子,机器码会: 1. 把某个无关值当成 wParam 压栈(命令 ID 错) 2. `ecx` 从错误对象取 `+4` 得到垃圾指针 3. `call [垃圾+4]` → 不崩,但什么也不做 而日志行 `使用[v3.2]【点控件 393C → 控件=… ID=32】` 读的是**具名变量**,所以照常打印成功 —— 表现就是**「助手开关正常,其他按钮点了毫无反应」**。 --- ## 八、v3.2 修法:把机器码和栈布局解耦 ``` .子程序 助手_发命令, 逻辑型, 公开, 传控件指针;命令ID自动从 [控件+0x364] 读 .参数 控件地址, 整数型 ' ← 只有 1 个参数、0 个局部变量 置入代码 ({ 96 }) ' pushad 置入代码 ({ 106, 0 }) ' push 0 lParam 置入代码 ({ 139, 69, 8 }) ' mov eax,[ebp+8] 控件(唯一参数) 置入代码 ({ 139, 128, 100, 3, 0, 0 }) ' mov eax,[eax+0x364] 命令ID 置入代码 ({ 80 }) ' push eax wParam 置入代码 ({ 104, 244, 3, 0, 0 }) ' push 0x3F4 msg 置入代码 ({ 139, 69, 8 }) ' mov eax,[ebp+8] 置入代码 ({ 139, 72, 4 }) ' mov ecx,[eax+4] 父窗口 置入代码 ({ 139, 17 }) ' mov edx,[ecx] 置入代码 ({ 255, 82, 4 }) ' call [edx+4] 置入代码 ({ 97 }) ' popad ``` 关键点: 1. **参数位置 `[ebp+8]` 是调用约定的硬约定**,与局部变量怎么排无关 → 彻底消除陷阱 2. ⑤ **只有一个参数、零局部变量**,参数序号无歧义(多参数才有「谁在前」的问题) 3. ④ 只做「偏移 → 控件指针 → 校验 → 调 ⑤」,本身不含机器码 4. ②③ 保持 v3.1 原样(实机已验证),不动工作代码 5. 兜底对照:若 ⑤ 仍无反应,把两条 `139, 69, 8` 换成 `139, 69, 252`,并给 ⑤ 加一个局部变量 `控件 = 控件地址`,即切回局部变量版 **字节已用 capstone 逐条复核**(含故意写错 `106` 的对照组,确认会退化成 `push -0xc` + `add eax,[eax]` 整段错位)。 --- ## 九、地址速查 | 项 | 地址 / 偏移 | 说明 | |---|---|---| | 助手窗口虚表 | `0x010A20F8` | 实机日志一致 | | 虚表槽#0 | `0x00A2FBF0` | 构造/`ret 0xC` | | 虚表槽#1 | `0x00A3F3D0` | **命令分派入口** | | 动作码 0x3F4 分支 | `0xA3F6DB` | 转发给 `0xA3F700` | | 选项处理函数 | `0xA3F700` | 旧辅助直调目标,`ret 8` | | 索引表 | `0xA41698` | 154 字节 | | 跳转表 | `0xA41588` | 154 项 | | 公共尾部 | `0xA41204` | 区间批处理 | | 助手窗口成员 | `+0x34B4` | 对话对象里存助手窗口指针 | | 控件命令 ID | `+0x364` | 三种变体:`+0x364 / +0x370 / +0x440` | | 控件父指针 | `+0x4` | 实测 == 助手窗口 | > ⚠️ 客户端带 XIGNCODE 反作弊,以上动态验证仅限单机 / 私服环境。