热血江湖 YGOnline.exe —— 「窗口内容面板偏移」是怎么遍历出来的 时间:2026-09-28 14:5x 目标:D:\rxjh2025\client\YGOnline.exe(32位,无 ASLR,模块基址恒 0x400000) 本次 PID:14256(活体扫描) IFManager 对象 = 1120125408 ================================================================================ 【一句话答案】 接口窗口没有"固定的面板偏移表"可以抄。每个窗口的"内容面板偏移"是**扫出来的**, 判别特征只有一条:在窗口对象 + 0x300 ~ +0x600 里逐个取 DWORD 指针, 若 `读整数(该指针 + 8) == 24`,这个指针就是一个「内容面板」(类型码 24 = InterWindow)。 指针相对窗口对象的字节偏移,就是该窗口的面板偏移。 换句话说: 面板偏移 = 找 窗口对象 里哪个槽放着个 类型码==24 的子对象,记下那个槽的偏移。 ================================================================================ 一、四步遍历流程 ================================================================================ 第 1 步 取 IFManager 对象 接口管理 = 内存_读整数型(句柄, 0x0188C61C) ' = 25740828 ' 读不到(≤ 65536)说明游戏没进 / 进程选错,直接放弃 第 2 步 枚举槽位,拿到每个接口窗口对象 .计次循环首 (192, i) 槽偏移 = 0x300 + (i-1)*4 ' 768 ~ 1532 窗口对象 = 内存_读整数型(句柄, 接口管理 + 槽偏移) ' 非 0 即视为该槽有窗口对象(约 80 个非空) .计次循环尾 () 第 3 步 ★ 在窗口对象内部扫"内容面板" .计次循环首 (192, j) 子偏移 = 0x300 + (j-1)*4 子对象 = 内存_读整数型(句柄, 窗口对象 + 子偏移) .如果真 (子对象 > 65536) .如果真 (内存_读整数型(句柄, 子对象 + 8) == 24) ' ← 唯一判据:类型码 24 ' 子偏移 就是这个窗口的一个"内容面板偏移" ' 读(子对象 + 0x40) 是它的显示标志:1 = 正在显示 .如果真结束 .如果真结束 .计次循环尾 () 第 4 步 判定"窗口是否弹出" 该窗口的任一"内容面板"的 +0x40 == 1 → 窗口正显示 扫描范围说明:+0x300 ~ +0x600 共 192 个 DWORD。定这个范围是实测出来的 —— 本次全量扫下来,面板偏移最小 +0x330、最大 +0x5CC,全部落在窗口内。 ================================================================================ 二、"类型码 24" 是怎么确认的 ================================================================================ 控件对象 +0x08 存的是一批固定的"控件类型码",本次实测出现的全部取值: 14 = 接口基类(每个接口窗口自己) 16 / 17 / 18 = 布局/容器类 20 / 21 / 22 / 23 = 滚动条、边框、标题栏一类 24 = InterWindow ← 内容面板,就是它 30 / 31 = ListCtrl(列表、物品栏) 32 = TextBox(文本框) 33 = 复数/数组控件 34 = ButtonClass(按钮) 84 / 85 / 86 = InterSTATIC(静态文字/图片) 验证手段(想自己复核时用): 面板对象 +0x00 = 虚表指针;虚表指针 - 0x04 处是 RTTI 完整类名指针, 读出来能看到 "?AVInterWindow@@" 这样的字符串。本次抽验过,与类型码 24 一一对应。 为什么不用别的特征? · +0x0C 控件ID —— 会话内递增,每次开客户端都变,无法当特征 · +0x28/+0x2C 位置、+0x30/+0x34 尺寸 —— 大量窗口是 0,0 / 0x0,区分不出来 · 类名字符串 —— 控件对象里不落地(文字走 ybmsg.cfg),读不到 → 只有 +0x08 类型码,是编译期定死的常量,最稳。 ================================================================================ 三、实测统计(为什么"面板偏移"必须扫,不能写死) ================================================================================ 本次全量扫:80 个非空槽,共 193 个"类型码24"面板实例。 ★ 193 个面板实例分布在 **78 种不同的偏移**上: 偏移 次数 偏移 次数 偏移 次数 +0x330 (816) 33 +0x344 (836) 5 +0x364 (868) 3 +0x334 (820) 20 +0x340 (832) 4 +0x3D8 (984) 3 +0x338 (824) 14 +0x348 (840) 4 +0x498 (1176) 3 +0x33C (828) 8 +0x350 (848) 7 +0x4B0 (1200) 3 只出现 1~2 次的偏移(共 66 种): +0x34C +0x354 +0x358 +0x35C +0x360 +0x36C +0x378 +0x37C +0x388 +0x38C +0x390 +0x394 +0x398 +0x3A4 +0x3A8 +0x3AC +0x3B8 +0x3BC +0x3C4 +0x3C8 +0x3CC +0x3D0 +0x3D4 +0x3E0 +0x3E8 +0x3EC +0x3F0 +0x3F4 +0x3FC +0x400 +0x408 +0x40C +0x414 +0x418 +0x41C +0x42C +0x438 +0x448 +0x450 +0x464 +0x46C +0x47C +0x490 +0x494 +0x49C +0x4A0 +0x4A4 +0x4A8 +0x4B4 +0x4C0 +0x4C8 +0x4D0 +0x4D4 +0x4D8 +0x4E4 +0x4E8 +0x4EC +0x4F0 +0x514 +0x540 +0x544 +0x548 +0x58C +0x5B4 +0x5C4 +0x5CC → 最常见的 +0x330 也只占 33/193 ≈ 17%。“写死 +0x330”只对约六分之一的窗口成立。 单槽面板个数分布(一个窗口有几个内容面板): 0 个: 19 槽 1 个: 17 槽 2 个: 19 槽 3 个: 9 槽 4 个: 3 槽 5 个: 4 槽 6 个: 3 槽 7/8/9/10 个: 各 1 槽 13 个: 1 槽 14 个: 1 槽(InterMAIN 主界面 HUD) → 所以「一个窗口 = 一个面板偏移」的想法也不成立,得"取或"多个偏移。 本次实测的已知窗口(对照表): 槽 0x3AC id=108 InterMAIN +344(vis=1) +494(1) +4A0(1) +4E8(1) 其余 10 个 vis=0 槽 0x3CC id=1654 InterOption +340(vis=1) ← 常驻竖条 槽 0x4D4 id=6695 InterExchange +330(vis=0) +334(vis=1) +3A4(0) ← 交易窗正开着(申请页) 槽 0x4E8 id=7029 InterParty +330(vis=0) +3D4(0) +3D8(0) ← 组队窗关着 槽 0x3B0 id=904 InterChat (无类型24面板) ← 判不出,常驻不必判 槽 0x404 id=7018 InterNotice (无类型24面板) ← 面板在孙辈,见第六节 ================================================================================ 四、易语言子程序(可直接粘贴) ================================================================================ 【常量】 .版本 2 .常量 IFManager基址, "25740828" .常量 窗口可见标志, "64" .常量 窗口子槽起点, "768" .常量 窗口子槽个数, "192" .常量 面板类型码, "24" 【① 取窗口面板偏移(扫描法)——这就是"偏移怎么遍历出来"的代码化】 .版本 2 .子程序 取窗口面板偏移, 整数型 .参数 槽偏移, 整数型 .参数 序号, 整数型, , 第几个内容面板,从 1 开始 .局部变量 接口管理, 整数型 .局部变量 窗口对象, 整数型 .局部变量 子对象, 整数型 .局部变量 k, 整数型 .局部变量 找到, 整数型 接口管理 = 内存_读整数型 (全局_进程句柄, #IFManager基址) .如果真 (接口管理 ≤ 65536) 返回 (0) .如果真结束 窗口对象 = 内存_读整数型 (全局_进程句柄, 接口管理 + 槽偏移) .如果真 (窗口对象 ≤ 65536) 返回 (0) .如果真结束 找到 = 0 .计次循环首 (#窗口子槽个数, k) 子对象 = 内存_读整数型 (全局_进程句柄, 窗口对象 + #窗口子槽起点 + (k - 1) × 4) .如果真 (子对象 > 65536) .如果真 (内存_读整数型 (全局_进程句柄, 子对象 + 8) = #面板类型码) 找到 = 找到 + 1 .如果真 (找到 = 序号) 返回 (#窗口子槽起点 + (k - 1) × 4) .如果真结束 .如果真结束 .如果真结束 .计次循环尾 () 返回 (0) 【② 取窗口内容面板个数】 .版本 2 .子程序 取窗口面板个数, 整数型 .参数 槽偏移, 整数型 .局部变量 接口管理, 整数型 .局部变量 窗口对象, 整数型 .局部变量 子对象, 整数型 .局部变量 k, 整数型 .局部变量 个数, 整数型 接口管理 = 内存_读整数型 (全局_进程句柄, #IFManager基址) .如果真 (接口管理 ≤ 65536) 返回 (0) .如果真结束 窗口对象 = 内存_读整数型 (全局_进程句柄, 接口管理 + 槽偏移) .如果真 (窗口对象 ≤ 65536) 返回 (0) .如果真结束 .计次循环首 (#窗口子槽个数, k) 子对象 = 内存_读整数型 (全局_进程句柄, 窗口对象 + #窗口子槽起点 + (k - 1) × 4) .如果真 (子对象 > 65536) .如果真 (内存_读整数型 (全局_进程句柄, 子对象 + 8) = #面板类型码) 个数 = 个数 + 1 .如果真结束 .如果真结束 .计次循环尾 () 返回 (个数) 【③ 窗口是否弹出(扫描版,与《窗口是否弹出_子程序_0928.txt》一致)】 .版本 2 .子程序 窗口是否弹出, 逻辑型 .参数 槽偏移, 整数型 .局部变量 接口管理, 整数型 .局部变量 窗口对象, 整数型 .局部变量 子对象, 整数型 .局部变量 i, 整数型 接口管理 = 内存_读整数型 (全局_进程句柄, #IFManager基址) .如果真 (接口管理 ≤ 65536) 返回 (假) .如果真结束 窗口对象 = 内存_读整数型 (全局_进程句柄, 接口管理 + 槽偏移) .如果真 (窗口对象 ≤ 65536) 返回 (假) .如果真结束 .计次循环首 (#窗口子槽个数, i) 子对象 = 内存_读整数型 (全局_进程句柄, 窗口对象 + #窗口子槽起点 + (i - 1) × 4) .如果真 (子对象 > 65536) .如果真 (内存_读整数型 (全局_进程句柄, 子对象 + 8) = #面板类型码) .如果真 (内存_读整数型 (全局_进程句柄, 子对象 + #窗口可见标志) = 1) 返回 (真) .如果真结束 .如果真结束 .如果真结束 .计次循环尾 () 返回 (假) 【④ 遍历全部窗口的面板偏移(诊断 / 游戏更新后重新定位用)】 .版本 2 .子程序 遍历全部窗口面板偏移 .局部变量 接口管理, 整数型 .局部变量 窗口对象, 整数型 .局部变量 子对象, 整数型 .局部变量 i, 整数型 .局部变量 j, 整数型 .局部变量 面板, 整数型 接口管理 = 内存_读整数型 (全局_进程句柄, #IFManager基址) .如果真 (接口管理 ≤ 65536) 挂机信息.加入文本 (自定义时间 () + “IFManager 未就绪” + #换行符) 返回 () .如果真结束 .计次循环首 (#窗口子槽个数, i) 窗口对象 = 内存_读整数型 (全局_进程句柄, 接口管理 + #窗口子槽起点 + (i - 1) × 4) .如果真 (窗口对象 > 65536) 挂机信息.加入文本 (自定义时间 () + “槽 ” + 到文本 (#窗口子槽起点 + (i - 1) × 4) + “ 对象 ” + 到文本 (窗口对象) + “ 控件ID ” + 到文本 (内存_读整数型 (全局_进程句柄, 窗口对象 + 12)) + #换行符) .计次循环首 (#窗口子槽个数, j) 子对象 = 内存_读整数型 (全局_进程句柄, 窗口对象 + #窗口子槽起点 + (j - 1) × 4) .如果真 (子对象 > 65536) .如果真 (内存_读整数型 (全局_进程句柄, 子对象 + 8) = #面板类型码) 面板 = #窗口子槽起点 + (j - 1) × 4 挂机信息.加入文本 (自定义时间 () + “ 面板偏移 ” + 到文本 (面板) + “ 面板ID ” + 到文本 (内存_读整数型 (全局_进程句柄, 子对象 + 12)) + “ 可见 ” + 到文本 (内存_读整数型 (全局_进程句柄, 子对象 + #窗口可见标志)) + #换行符) .如果真结束 .如果真结束 .计次循环尾 () 挂机信息.加入文本 (自定义时间 () + #换行符) .如果真结束 .计次循环尾 () 挂机信息.加入文本 (自定义时间 () + “------ 全部窗口面板遍历完成 ------” + #换行符 + #换行符) ================================================================================ 五、性能与调用建议 ================================================================================ · 单个窗口扫一遍 = 192 次读内存 ≈ 1ms,随便调,不用做"先试 +0x330 再回退"的快路径。 · 但**不要**拿 ① 去循环枚举全部 80 个槽(80 × 192 ≈ 1.5 万次读),会明显卡。 枚举用 ④ 那种"一次扫描同时出全部结果"的写法。 · 日常判定只用 ③ `窗口是否弹出(槽偏移)`,最省事,游戏更新换版本也不用改代码。 · 已知要判的交易窗 / 组队窗,槽偏移是: 交易窗 = 1236 (0x4D4) 组队窗 = 1256 (0x4E8) 直接 `窗口是否弹出 (1236)` 即可。 ================================================================================ 六、三个必须知道的坑 ================================================================================ 坑 1:窗口对象自身 +0x40 不能用 InterExchange 关闭后它的 +0x40 仍然是 1(粘滞,关窗不清零)。 只有**类型码 24 的子面板**的 +0x40 才真正反映"此刻在不在屏幕上"。 坑 2:不是所有槽拿到的都是窗口对象 本次 80 个非空槽里,有 13 个是"垃圾/非窗口"对象: +0x348 +0x36C +0x3C4 +0x3E0 +0x3E4 +0x3E8 +0x3EC +0x3FC +0x420 +0x514 +0x554 +0x558 +0x55C 体检方法:扫这些对象的子槽,类型码直方图里**看不到 14/24/32/34/85** 这些已知控件码, 控件ID 也异常(nil / 0 / 千万级)。 → 用 ③ 判定时它们自然返回"假"(扫不到类型码24的面板),不会误报,不用专门排除。 坑 3:两个例外窗口,扫描法判不出 · 0x3B0 InterChat —— 子对象类型码是 20/23,没有类型码24的面板。它是常驻聊天框,本来也不用判。 · 0x404 InterNotice —— 面板在**孙辈**,要走两级: 读(读(读(0x0188C61C) + 0x404) + 0x338) → 弹窗对象 → 其 +0x330 面板(类型码24)→ +0x40 注意同层的 +0x330/+0x334 是常驻竖条,它们的子面板 vis 恒为 1,拿它们判弹窗会永远判"弹着"。 ================================================================================ 七、游戏更新后怎么"重新定位" ================================================================================ 面板偏移 +0x330 这类值是**编译期常量**,随源码改动会变(本次实测 78 种偏移也说明它本身就不统一)。 版本更新后如果判定失灵,按下面三步重来: 1. 进游戏,把要判的那个窗口(比如交易窗)**打开** 2. 跑一次 ④ `遍历全部窗口面板偏移`,日志里找"可见 1"的那一行 3. 记下它所在的**槽偏移**(判定用)和**面板偏移**(想跳过扫描、直接读时用) 找不到就把窗口关掉再跑一次对比 —— 同一个槽里 vis 从 1 变 0 的那个面板,就是它。 ================================================================================ 附:本次活体扫描的原始出处 D:\ce_mcp\_t30.py → 全槽 × 全子槽类型码扫描(本文第一/三节数据来源) D:\ce_mcp\_t31.py → 偏移分布统计与垃圾槽体检(本文第三节) 输出:D:\ce_mcp\_out30.txt _out31.txt