分析对象:
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 ; 公共尾部出口
0xA41698,跳转表 0xA41588(均在 .rdata)0xA412040xA41204:按区间批量处理它不是空转——按 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。
| 分类 | 数量 | 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] 取「第一个局部变量」:
ECX = 父窗口 是最后一次赋值,且 ECX 与 父窗口 两个变量持有同一个值(父窗口 = [按钮+4],实测就等于助手窗口本身)。所以无论易语言把第一个还是最后一个局部变量放在 [ebp-4],读出来都是父窗口 → 侥幸永远正确。[ebp-4] 未必是 控件。一旦落错格子,机器码会:ecx 从错误对象取 +4 得到垃圾指针call [垃圾+4] → 不崩,但什么也不做而日志行 使用[v3.2]【点控件 393C → 控件=… ID=32】 读的是具名变量,所以照常打印成功 —— 表现就是「助手开关正常,其他按钮点了毫无反应」。
.子程序 助手_发命令, 逻辑型, 公开, 传控件指针;命令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
关键点:
[ebp+8] 是调用约定的硬约定,与局部变量怎么排无关 → 彻底消除陷阱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 反作弊,以上动态验证仅限单机 / 私服环境。