← 返回总索引助手UI控件逆向 05_助手UI控件逆向/助手0x3F4分派机制与控件ID实现表.html
助手UI控件逆向生成:2026-09-18分析对象:D:\rxjh2025\client\YGOnline.exe

助手 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] 分派:

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 转发链原文

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 个参数):

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        ; 公共尾部出口

五、公共尾部 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~4E4F~52(互斥页签)、9D(真正空转)、A1 A2
超范围忽略 4 01(那 4 个 ID=01 槽)、4254DE1C9

几个关键 case 的实现位置:

ID 偏移 case 体 行为概要
0x31 3938 0xA406B4 开始挂机(先查 [0x3BF28CC]+0x54D8[0x3BA1F44]==3 等前置条件,不满足则弹提示 0x14D6 / 0xAD0)
0x32 393C 0xA40CA9 停止挂机:清 [this+0x34B8]0xA4A4B00xA3A870(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 弹窗取消

七、为什么 ②③ 能用而 ④ 不行(核心坑)

0xA3F3D0thisecx 决定,而 ②③④ 的机器码都用 mov ecx,[ebp-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 反作弊,以上动态验证仅限单机 / 私服环境。