# IFManager 单例结构、0x0397xxxx 数据区与通用调用范式活体验证 > 对象:`D:\rxjh2025\client\YGOnline.exe` + CE 活体(PID 5488)|生成:2026-09-18 > 三件事:① 挖 0x0183C520 对象结构 ② 挖 0x0397xxxx 表结构 ③ CE 活体验证第 22 篇 §5.3 通用调用范式 ## 一、结论速览 | 项 | 结论 | |---|---| | 0x0183C520 | = ProcControl 系**配置单例指针**(构造序列 0x826B20/0xC531E0,读 ybmsg.cfg) | | 0x183C52C | = **IFManager** 单例指针(虚表 0x10A05EC),**大对象 ≥0x1088 字节**,全部 UI 的总管理器 —— 真正的主控 | | 构造函数 | IFManager = 0x9E6740;ProcControl = 0x826B20;析构置零 0x9EC122 / 0x826E1A | | ★ 窗口注册表 | IFManager +0x390~+0x444 每槽 = 一类窗口的当前实例指针(+0x414=助手)。**活体一次读出 7 个窗口并全部 RTTI 定名** | | 0x0397xxxx | 不是一张表,是「角色运行时状态区」:3 个变量簇 + 2 类索引表(详见 §五) | | ★ 活体验证 | 槽1 动作分派范式在 **InterInventory(背包)**与 **InterStatus(状态)**上 `thiscall slot1(this,0x3F4,1,0)` 调用成功(返回 0,无崩溃,指针无扰动)—— §5.3 推论升级为实测事实 | ## 二、两个单例的身份 capstone 逐指令扫全 .text:0x183C520/52C 直接引用 **16253 处**,取值后二次解引用(字段访问)**4949 处**。构造点: ``` 009E6782 mov [eax], 0x10A05EC ; 构造函数 0x9E6740 → RTTI = .?AVIFManager@@(对象 ≥0x1088 字节) 00826B4B mov [eax], 0x10956A0 ; 构造函数 0x826B20 → RTTI = .?AVProcControl@@ 00826BB0 push 0x10956A4 ; "ybmsg.cfg"(紧贴 ProcControl 虚表,同一构造序列) ``` | 地址 | 类 | 虚表 | 规模 | 语义 | |---|---|---|---|---| | [0x183C520] | ProcControl 系 | 0x10956A0(本体;此槽存其配置子对象) | 小(≈0x20+) | ybmsg.cfg 配置/消息流程控制 | | [0x183C52C] | IFManager | 0x10A05EC | **≥0x1088** | **UI 总管理器(主控单例)** | > 精化上一轮结论:主控是 0x183C52C 指向的 **IFManager**(引用量、对象规模、字段数都大一个数量级);0x183C520 只是旁边的小弟。 ### IFManager 字段地图(引用数 Top) | 字段 | 访问 | 语义 | |---|---|---| | +0x39C | R×620 | 背包窗口实例(InterInventory)—— 最热字段 | | +0x390 | R×511 | 主界面(InterMAIN),收消息码 0x46/0x3C/0xD… | | +0x3EC | R×465 | **当前活动窗口**(弹窗/NPC 对话) | | +0x394 / +0x398 | R×287/121 | 聊天 / 状态窗口实例 | | +0x414 | R×244 | **助手窗口(InterAutoPlay)** | | +0x444 | R×61 | 夫妻窗口(InterCouple);助手分发函数 0xA3F700 也读它 4 次 | | +0xD48/+0xD49 | W×62/34 | 状态字节区(少量写点) | 规律:**+0x390~+0x3C0 每 4 字节一个指针槽 = 「每类窗口的当前实例」注册表**。找任何窗口对象直接 `[[0x183C52C]+槽]` 一步到手,不必再从 HWND 爬。 ## 三、CE 活体验证:窗口注册表当场闭合 CE 附加 YGOnline.exe(PID 5488,32 位),一次读出整条链,静态 RTTI 定名: | IFManager 槽 | 活体实例 | 活体虚表 | 类名(RTTI) | 状态 | |---|---|---|---|---| | +0x390 | 0x407F45D0 | 0x10A7D40 | InterMAIN | 打开 | | +0x394 | 0x45375BE8 | 0x10A30B4 | InterChat | 打开 | | +0x398 | 0x44FFD3C8 | 0x10AE084 | InterStatus | 打开 | | +0x39C | 0x35EB0D68 | 0x10A5890 | InterInventory(背包) | 打开 | | +0x3EC | 0x3E075788 | 0x10AFDB8 | InterNpc | 当前活动窗 | | +0x414 | 0x20121C50 | 0x10A20F8 | InterAutoPlay(助手) | 已注册,slot1=0xA3F3D0 ✓ | | +0x444 | 0x456D2720 | 0x10B0B34 | InterCouple | 已注册 | 静态字段地图与活体 **100% 吻合**。[0x183C520] 活体 = 0x05EC9708,但其 [+0] 读回为堆指针(0x90C7020)而非虚表 —— 它存的是 ProcControl 初始化流程(0xC531E0,读 ybmsg.cfg)产出的**配置子对象**,不是带虚表的 ProcControl 本体。 ## 四、§5.3 通用调用范式 · 实测 ### 4.1 槽1 活体反汇编(InterInventory slot1 = 0xAE3760) ``` 00AE3760 push ebp / mov ebp,esp / sub esp,0x40 00AE3766 mov [ebp-08], ecx ; this 从 ECX 取 00AE3769 mov eax,[ebp+08] ; 第一个栈参 = 动作码 00AE376F cmp [ebp-0C], 0x3EE → je 0xAE3834 00AE37A2 cmp [ebp-0C], 0x3F4 → je 0xAE3A64 00AE37AF cmp [ebp-0C], 0x44A → je 0xAE3A44 00AE37BC cmp [ebp-0C], 0x46E → je 0xAE3B58 00AE378E …其余 0x03~0x08 走 jmp [edx*4+0xAE3C20] 跳转表 ``` 与助手槽1 0xA3F3D0 的分派形态**逐条同款**。 ### 4.2 范式调用实测(thiscall) 按引擎自己的手法(0x75A6FF 处 `push 0 / push 1 / push 0x3F4`),对两个**非助手**窗口执行: ``` thiscall slot1( this, 0x3F4, 1, 0 ) ① InterInventory this=0x35EB0D68 slot1=0xAE3760 → 返回 0 ✓ ② InterStatus this=0x44FFD3C8 slot1=0xC291E0 → 返回 0 ✓ 调用后复核:IFManager 及 7 个窗口指针、HP、MAPID 全部无扰动,游戏稳定 ``` **结论:§5.3 推论「助手那套手法对其它 Inter* 窗口同样适用」升级为实测事实。** ### 4.3 两个教训 - **不要手工换算十六进制传 this**:本轮两次手转出错(0x35C27D68、0x44FFF9C8),侥幸无害但属高危。正确做法:实例地址用 **CE 地址表达式字符串**传,或先用 Lua readInteger 取好。 - CE 批量读的 `[符号]+off` 解引用语法不可用 → 走 Lua `readInteger` 一次读整条链最稳。 ## 五、0x0397xxxx 数据区结构 重扫 0x03970000~0x0397FFFF:约 240 个被引用目标 = **「角色运行时状态区」**,由三个变量簇 + 两类索引表拼成: | 区块 | 形态 | 关键证据 / 语义 | |---|---|---| | 0x3970940~0x3970B90 | 玩家状态变量簇 | 0x3970A70=当前血量(写者 0x6DBC8F,旁有 `imul ×0x64` 百分比);0x3970A20=状态码;0x3970ADC/AE0=float 对;0x3970A9C=对 0x1DCD6500(5e8) 比较的计时器;0x3970AB4=对 −500 比较的阈值 | | 0x3970988/098C | **记录表索引** | `imul edx,[0x397098C],0x354` —— **852 字节记录 × 索引** | | 0x39741B8/1BC | 指针表 `[reg*8+表]` | 8 字节 stride 对象指针数组(读者 0x6BAxxx~0x6BCxxx 家族) | | 0x3970C60/0CF8、0x39747A8/4840 | 字数组表 `[reg*2+表]` | movsx word,stride=2(id→值映射) | | 0x3970F40/0F44 | 成对 8 字节表 `[reg*8+表]` | 读-改-写成对(or/mov),读者 0xAC88xx | | 0x39741xx~47xx | 属性变量簇 | 写者高度集中:0x9EE1xx~0x9F56xx 家族(属性包处理),含 idiv×0x64、movss float 段(0x3974B88/B90) | | 0x39780xx~82CC | 第三变量簇 | 读者 0x79Exxx/0x79Fxxx 家族,含 float 三连(0x3978084~808C)与 [reg*4+0x39782CC] 指针表 | | 0x3971A11…(长串) | 等距字读序列 | 读者 0xACCxxx 家族,目标间距 ≈0x5C(92字节) —— 另一张固定 stride 记录表被逐字段遍历 | 对辅助的意义: - 血量继续用 0x3970A70(活体=482 与 UI 一致);0x3970A7C 同值同写者,应为最大/基准血,CE 改血测试即可分辨。 - `imul ×0x354` 说明存在 **852 字节的角色/对象记录** —— 顺 `[0x397098C]×0x354+记录首` 可挖完整记录结构体(下一轮)。 - 0x39741B8/1BC 两张指针表就是「对象实例数组」,配合 stride 可直接遍历周围玩家/怪物。 ## 六、易语言调用模板(范式落地) ``` .版本 2 .子程序 Inter_发动作, 整数型, 公开, 通用:对任意 Inter* 窗口发动作码(实测通过) .参数 窗口槽偏移, 整数型, , 如 0x39C=背包 0x398=状态 0x414=助手 .参数 动作码, 整数型, , 如 0x3F4 .参数 参数一, 整数型, 可空, 默认 1 .参数 参数二, 整数型, 可空, 默认 0 .局部变量 管理器, 整数型 .局部变量 窗口, 整数型 .局部变量 虚表, 整数型 .局部变量 槽1, 整数型 管理器 = 读整数 (十六到十 ("0x183C52C")) .如果真 (管理器 = 0 或 窗口槽偏移 < 0) 返回 (-1) .如果真结束 窗口 = 读整数 (管理器 + 窗口槽偏移) .如果真 (窗口 = 0) 返回 (-2) .如果真结束 虚表 = 读整数 (窗口) .如果真 (虚表 = 0) 返回 (-3) .如果真结束 槽1 = 读整数 (虚表 + 4) .如果真 (槽1 = 0) 返回 (-4) .如果真结束 ' thiscall:ECX=窗口对象,栈参 = 动作码, 参数一, 参数二 返回 (调函数_ThisCall (槽1, 窗口, 动作码, 参数一, 参数二)) ``` 要点:**窗口对象一律从 IFManager 注册表槽取**(`[[0x183C52C]+槽]`);动作码先用 0x3F4(刷新)验证通路,再逐个试目标窗口认识的码。 ## 七、脚本与产物清单 | 文件 | 作用 | |---|---| | scan_183c.py | 0x183C520/52C 直接引用全表 + 寄存器追踪字段地图 + 写入点上下文(16253/4949) | | scan_183c2.py | 构造函数反汇编(拿虚表立即数)+ 关键字段用户上下文 | | scan_397.py | 0x0397xxxx 全区重扫(普通/索引/写点/样本)+ RTTI 定名 | | rtti_live.py | 活体虚表 → RTTI 类名(7 窗口全部定名) | | CE 工具链 | attach → Lua 链读 → 槽1 反汇编 → thiscall ×3 → 稳定性复核 |