.版本 2 '══════════════════════════════════════════════════════════════════ ' 内挂丸开关(置入代码版 v4 「崩游修复版」 2026-09-25) '────────────────────────────────────────────────────────────────── ' ★ v4 修的是一个必死循环,不是小 bug。看下面这段 —— ' ' 命令字 49(启动内挂丸)的真实 handler 在 RVA 0x683834,第一句就读空指针: ' 683834 mov edx, dword ptr [0x3c441bc] ; 管理器指针,实测恒 0 ' 68383A movzx eax, byte ptr [edx + 0x54c0] ; ← 读 [0 + 21600] = 0x54C0 ' ... ' 0x54C0 落在用户态 64KB 禁区内 → ACCESS_VIOLATION → 游戏进程当场合死。 ' 这不是代码写法问题,是命令本身踩了空指针。v3 没能拦住,v4 拦住了。 ' ' 顺带纠正一个之前搞错的结论: ' cmd 49 的 handler 不是 0x6828D6,cmd 50 的也不是 0x68291B。 ' 分派表 0x684818 / 0x684708 实测落点: ' cmd 0x0B(11) -> 0x6828D6 cmd 0x0C(12) -> 0x68291B ' cmd 49 (0x31) -> 0x683834 cmd 50 (0x32) -> 0x683E29 ' 控件 +0x364 里那个 49/50 是「菜单功能号」,跟命令字数值恰好相同但语义不同, ' 别再拿 +0x364 当命令字去推。 ' ' 命令字 50(停止)的 handler 0x683E29 全程不碰 0x3C441BC,是安全路径。 ' ' 用法: 开关内挂丸 (32) 关 ← 安全,随时可用 ' 开关内挂丸 (31) 开 ← 要过三道读门槛,任一不过就拒绝发命令 '══════════════════════════════════════════════════════════════════ ' 池基址 0x03C35758 = 63133528 ' 命令字 49 = 启动内挂丸 50 = 停止内挂丸 ' ecx = 控件对象本身(不是 控件对象+4) ' CALL = 0x682880 + 0x400000 = 0x0482880 '══════════════════════════════════════════════════════════════════ ' ▎机器码逐条对照(6 条,栈平衡、无 pushad/popad) ' 6A 00 push 0 ' 附参数(先压 → 落在 [ebp+0xC]) ' 8B 69 0C mov ecx,[ebp+0xC] ' this = 控件对象本身 ' 8B 45 08 mov eax,[ebp+8] ' 命令字(后压 → 落在 [ebp+8]) ' 50 push eax ' 把变量压进去 ' B8 80 88 82 04 mov eax,0x0482880 ' 0x682880 + ImageBase 0x400000 ' FF D0 call eax '══════════════════════════════════════════════════════════════════ ' ────────────────────────────────────────────────────────────────── ' 只读体检(外挂进程读内存,永远不会让游戏崩) ' 把「开」要过的三道门槛先量一遍,量完你看得很清楚 ' ────────────────────────────────────────────────────────────────── .子程序 内挂丸_体检, 逻辑型, 公开 .局部变量 句柄, 整数型 .局部变量 管理器, 整数型 .局部变量 原始值, 整数型 .局部变量 低字节, 整数型 句柄 = 全局_进程句柄 写日志 (“── 内挂丸体检(只读,不碰游戏)──” + #换行符) 管理器 = 内存_读整数型 (句柄, 124194508) ' 0x03C441BC + 0x400000 写日志 (“[0x3C441BC] 管理器指针 = ” + 到文本 (管理器) + #换行符) ' ★ 注意:-1 和 0 一样危险,别被 "-1 不是 0 所以能用"骗了 ' edx = 0xFFFFFFFF 时 [edx + 0x54C0] = 0xFFFFFFFF + 21600 ' = 0x1_000054BF → 32 位截断 = 0x54BF ' 0x54BF 仍在用户态 64KB 禁区 → 照样 ACCESS_VIOLATION .如果真 (管理器 = 0 或 管理器 = -1) 写日志 (“✗ 管理器指针不可用(0 或 -1)—— 这就是发 49 必崩的原因。” + #换行符) 写日志 (“ handler 0x683834 第一句就读 [edx+0x54C0]:” + #换行符) 写日志 (“ edx=0 → 读 0x54C0 踩 64KB 禁区,崩” + #换行符) 写日志 (“ edx=-1 → 读 0x54BF 同样踩禁区,一样崩” + #换行符) 写日志 (“ 结论:这个指针在当前游戏状态下拿不到有效值。” + #换行符) 写日志 (“ 所以「开」走命令字这条路是死路,别再试了。” + #换行符) 写日志 (“ 换思路:直接对池里的「启动内挂丸」控件做点击(见 内挂丸_转储)。 ” + #换行符) 返回 (假) .如果真结束 ' 低字节 = 值 - 值÷256 × 256(纯除法,避开 位与/取字节集 的兼容性问题) 原始值 = 内存_读整数型 (句柄, 管理器 + 21600) ' +0x54C0 低字节 = 原始值 - 原始值 ÷ 256 × 256 写日志 (“[mgr+0x54C0] = ” + 到文本 (低字节) + “(0 = 可以继续,非 0 = 已在运行,会被挡回)” + #换行符) 原始值 = 内存_读整数型 (句柄, 126781308) ' 0x03BF3834 + 0x400000 = 3 才继续 写日志 (“[0x3BF3834] = ” + 到文本 (原始值) + “(必须 = 3)” + #换行符) 写日志 (“体检结束。” + #换行符) 返回 (真) ' ────────────────────────────────────────────────────────────────── ' 机器码核心:给任意命令字 + 任意控件 ' 参数位固定:命令字 = [ebp+8],控件 = [ebp+0xC] ' 只有 2 个参数、0 个局部变量 → 不依赖局部变量排布 ' ────────────────────────────────────────────────────────────────── .子程序 内挂丸_发命令, 逻辑型, 公开, 传命令字与控件指针,调 0x682880 .参数 命令字, 整数型 .参数 控件地址, 整数型 .如果真 (控件地址 ≤ 65536 或 控件地址 = -1) 返回 (假) .如果真结束 置入代码 ({ 106, 0 }) ' 6A 00 push 0 附参数 置入代码 ({ 139, 105, 12 }) ' 8B 69 0C mov ecx,[ebp+0xC] this=控件 置入代码 ({ 139, 69, 8 }) ' 8B 45 08 mov eax,[ebp+8] 命令字 置入代码 ({ 80 }) ' 50 push eax 置入代码 ({ 184, 128, 136, 130, 4 }) ' B8 808882 04 mov eax,0x0482880 置入代码 ({ 255, 208 }) ' FF D0 call eax 返回 (真) ' ────────────────────────────────────────────────────────────────── ' 开关内挂丸:扫池找槽 → 过门槛 → 调上面的核心 '─────────────────────────────────────────────────────────────────── ' 状态 31 = 开(要过三道门槛) 32 = 关(安全,随时可用) '─────────────────────────────────────────────────────────────────── .子程序 开关内挂丸, 逻辑型 .参数 状态, 整数型 .局部变量 句柄, 整数型 .局部变量 池基址, 整数型 .局部变量 管理器, 整数型 .局部变量 原始值, 整数型 .局部变量 低字节, 整数型 .局部变量 S, 整数型 .局部变量 控件, 整数型 .局部变量 功能号, 整数型 .局部变量 计数49, 整数型 .局部变量 计数50, 整数型 句柄 = 全局_进程句柄 池基址 = 63133528 ' 0x03C35758 ' ═════════ 第 0 步:要开,先把三道门槛量清楚 ═════════ .如果真 (状态 = 31) 管理器 = 内存_读整数型 (句柄, 124194508) ' [0x3C441BC] .如果真 (管理器 = 0 或 管理器 = -1) 写日志 (“── 开关内挂丸:拒绝发命令 ──” + #换行符) 写日志 (“✗ [0x3C441BC] 管理器指针 = ” + 到文本 (管理器) + “(0 或 -1 都不可用)” + #换行符) 写日志 (“ 命令 49 的 handler 会读 [edx+0x54C0]:” + #换行符) 写日志 (“ edx=0 → 0x54C0 edx=-1 → 0x54BF” + #换行符) 写日志 (“ 两个地址都在 64KB 禁区 → 游戏必崩。已拦下。” + #换行符) 写日志 (“ 建议跑 内挂丸_体检 () 看详情,或改用 内挂丸_转储 () 找点击回调。” + #换行符) 返回 (假) .如果真结束 原始值 = 内存_读整数型 (句柄, 管理器 + 21600) ' [mgr + 0x54C0] 低字节 = 原始值 - 原始值 ÷ 256 × 256 .如果真 (低字节 ≠ 0) 写日志 (“── 开关内挂丸:拒绝发命令 ──” + #换行符) 写日志 (“✗ [mgr+0x54C0] = ” + 到文本 (低字节) + “,说明内挂丸已在运行,无需再开。” + #换行符) 返回 (假) .如果真结束 原始值 = 内存_读整数型 (句柄, 126781308) ' [0x3BF3834] 必须 = 3 .如果真 (原始值 ≠ 3) 写日志 (“── 开关内挂丸:拒绝发命令 ──” + #换行符) 写日志 (“✗ [0x3BF3834] = ” + 到文本 (原始值) + “,必须 = 3,场景不对。” + #换行符) 返回 (假) .如果真结束 写日志 (“── 开关内挂丸:开 三道门槛已过,允许发命令 ──” + #换行符) .否则 写日志 (“── 开关内挂丸:关(安全路径)──” + #换行符) .如果真结束 .如果真 (状态 = 31) 功能号 = 49 ' 启动内挂丸 .否则 功能号 = 50 ' 停止内挂丸(安全路径) .如果真结束 写日志 (“目标命令字 = ” + 到文本 (功能号) + #换行符) ' ═════════ 第 1 步:摸底,池里有没有这个命令字 ═════════ 计数49 = 0 计数50 = 0 S = 0 .判断循环首 (S < 14998) 控件 = 内存_读整数型 (句柄, 池基址 + S × 4) .如果真 (控件 > 0 且 控件 ≠ 65535) .如果真 (内存_读整数型 (句柄, 控件 + 860) = 49) ' 0x364 计数49 = 计数49 + 1 .如果真结束 .如果真 (内存_读整数型 (句柄, 控件 + 860) = 50) 计数50 = 计数50 + 1 .如果真结束 .如果真结束 S = S + 1 .判断循环尾 () 写日志 (“摸底:49(启动) × ” + 到文本 (计数49) + “ 50(停止) × ” + 到文本 (计数50) + #换行符) .如果真 (状态 = 31 且 计数49 = 0) 写日志 (“✗ 池里没有 49(啟動內掛丸),说明内挂丸面板从没打开过。” + #换行符) 写日志 (“ 请先在游戏里手动点开一次內掛丸界面,再回来调用。” + #换行符) 返回 (假) .如果真结束 ' ═════════ 第 2 步:扫池找槽(「关」在这里顺带校验 +0x3938 指针)═════════ S = 0 .判断循环首 (S < 14998) ' 池容量 0x3A98 控件 = 内存_读整数型 (句柄, 池基址 + S × 4) .如果真 (控件 > 0 且 控件 ≠ 65535) .如果真 (内存_读整数型 (句柄, 控件 + 860) = 功能号) ' 0x364 功能号 写日志 (“命中 槽” + 到文本 (S) + “ 控件=” + 到文本 (控件) + #换行符) .如果真 (功能号 = 50) ' handler 0x683E29 结尾会把 ecx = 控件+0x3938 传给 0x8fcca0 ' 那个指针为 0 的话同样会崩,先读出来看看 原始值 = 内存_读整数型 (句柄, 控件 + 14584) ' 0x3938 写日志 (“+0x3938 指针 = ” + 到文本 (原始值) + #换行符) .如果真 (原始值 ≤ 65536) 写日志 (“✗ +0x3938 指针非法(可能为 0),拒绝发关闭命令,避免二次崩游。” + #换行符) 返回 (假) .如果真结束 .如果真结束 .如果真 (内挂丸_发命令 (功能号, 控件) = 假) 写日志 (“发命令失败【控件指针非法】” + #换行符) 返回 (假) .如果真结束 写日志 (“命令已发出(命令字 ” + 到文本 (功能号) + “)” + #换行符) 返回 (真) .如果真结束 .如果真结束 S = S + 1 .判断循环尾 () 写日志 (“没找到命令字 ” + 到文本 (功能号) + “ 的控件。” + #换行符) 返回 (假) '══════════════════════════════════════════════════════════════════ ' 扫描内挂丸控件(v3 无数组版,本次未改动) '══════════════════════════════════════════════════════════════════ .子程序 扫描内挂丸 .局部变量 句柄, 整数型 .局部变量 池基址, 整数型 .局部变量 S, 整数型 .局部变量 控件, 整数型 .局部变量 功能号, 整数型 .局部变量 开关位, 整数型 .局部变量 类型字, 整数型 .局部变量 动作1, 整数型 .局部变量 非0数, 整数型 .局部变量 有49, 整数型 .局部变量 有50, 整数型 .局部变量 槽号, 整数型 .局部变量 名称, 文本型 .局部变量 命中, 整数型 .局部变量 打印数, 整数型 .局部变量 好, 逻辑型 句柄 = 全局_进程句柄 池基址 = 63133528 非0数 = 0 有49 = 0 有50 = 0 写日志 (“── 扫描内挂丸控件 ──” + #换行符) ' ── 第一遍:统计 ── S = 0 .判断循环首 (S < 14998) 控件 = 内存_读整数型 (句柄, 池基址 + S × 4) .如果真 (控件 > 0 且 控件 ≠ 65535) 非0数 = 非0数 + 1 功能号 = 内存_读整数型 (句柄, 控件 + 860) ' 0x364 .如果真 (功能号 = 49) 有49 = 有49 + 1 .如果真结束 .如果真 (功能号 = 50) 有50 = 有50 + 1 .如果真结束 .如果真结束 S = S + 1 .判断循环尾 () 写日志 (“全池有效控件数 = ” + 到文本 (非0数) + “ 扫描范围 14998” + #换行符) 写日志 (“功能号 49(啟動內掛丸) = ” + 到文本 (有49) + “ 个” + “ 50(停止內掛丸) = ” + 到文本 (有50) + “ 个” + #换行符) ' ── 第二遍:按名称找「丸 / 内挂 / 掛」── ' 9293 个控件都初始化完了,49 依然为 0 → 说明 +0x364 的 49/50 不是内挂丸。 ' 这一遍直接按名字搜,看池子里到底有没有内挂丸这个东西。 命中 = 0 写日志 (“── 按名称搜索池内控件 ──” + #换行符) 槽号 = 0 .判断循环首 (槽号 < 14998) 控件 = 内存_读整数型 (句柄, 池基址 + 槽号 × 4) .如果真 (控件 > 0 且 控件 ≠ 65535) 名称 = 编码_BIG5到简体 (内存_读文本型 (句柄, 控件 + 872, 40)) ' +0x368 名称 好 = 寻找文本 (名称, “丸”, 1, 真) > 0 .如果真 (寻找文本 (名称, “內挂”, 1, 真) > 0) 好 = 真 .如果真结束 .如果真 (寻找文本 (名称, “内挂”, 1, 真) > 0) 好 = 真 .如果真结束 .如果真 (寻找文本 (名称, “掛”, 1, 真) > 0) 好 = 真 .如果真结束 .如果真 (好) 功能号 = 内存_读整数型 (句柄, 控件 + 860) ' 0x364 开关位 = 内存_读整数型 (句柄, 控件 + 13513) ' 0x34C9 类型字 = 内存_读整数型 (句柄, 控件 + 8) ' 0x008 写日志 (“ ★槽” + 到文本 (槽号) + “ 控件=” + 到文本 (控件) + “ +0x364=” + 到文本 (功能号) + “ 开关位=” + 到文本 (开关位) + “ 类型=” + 到文本 (类型字) + “ 名称=” + 名称 + #换行符) 命中 = 命中 + 1 .如果真结束 .如果真结束 槽号 = 槽号 + 1 .判断循环尾 () 写日志 (“名称命中 = ” + 到文本 (命中) + “ 个(丸/内挂/掛)” + #换行符) ' ── 第三遍:看看 49/50 那个槽到底旁边是什么 ── 写日志 (“── +0x364 落在 30~70 的控件(最多 25 条,看 49/50 是不是内挂丸)──” + #换行符) 打印数 = 0 槽号 = 0 .判断循环首 (槽号 < 14998 且 打印数 < 25) 控件 = 内存_读整数型 (句柄, 池基址 + 槽号 × 4) .如果真 (控件 > 0 且 控件 ≠ 65535) 功能号 = 内存_读整数型 (句柄, 控件 + 860) .如果真 (功能号 ≥ 30 且 功能号 ≤ 70) 名称 = 编码_BIG5到简体 (内存_读文本型 (句柄, 控件 + 872, 40)) 写日志 (“ 槽” + 到文本 (槽号) + “ 控件=” + 到文本 (控件) + “ +0x364=” + 到文本 (功能号) + “ 名称=” + 名称 + #换行符) 打印数 = 打印数 + 1 .如果真结束 .如果真结束 槽号 = 槽号 + 1 .判断循环尾 () 写日志 (“扫描结束。” + #换行符) '══════════════════════════════════════════════════════════════════ ' ★ v5 新增:控件转储 —— 把内挂丸控件的原始字段全读出来 '══════════════════════════════════════════════════════════════════ ' 实测(2026-09-25):内挂丸的 4 个菜单项在控件池里,但 +0x364 = 0, ' → 说明 +0x364 根本不是「功能号」,前面几版全扫错了地方。 ' 这个子程序把控件对象从头到尾逐字读一遍,把「小数值」和「指针」 ' 都打出来 —— 功能号/状态位就藏在这一堆整数里,找到后一次定位。 ' ' 已知内挂丸菜单项(类型 34,控件地址步长 1416 = 0x588): ' 槽4231 控件=1335282848 启动内挂丸 ' 槽4232 控件=1335272936 停止内挂丸 ' 槽4233 控件=1335271520 设定内挂丸 ' 槽4234 控件=1335274352 关闭内挂丸 ' 槽4270 控件=1335323912 设定内挂丸的攻击行动范围。 ' ' 用法: 内挂丸_转储 (4231, 1536) 启动内挂丸 ' 内挂丸_转储 (4234, 1536) 关闭内挂丸 ' 两相对照,哪个字段不同,那个就是开关状态/功能号。 '══════════════════════════════════════════════════════════════════ .子程序 内挂丸_转储 .参数 槽号, 整数型 .参数 字节数, 整数型 .局部变量 句柄, 整数型 .局部变量 池基址, 整数型 .局部变量 控件, 整数型 .局部变量 偏移, 整数型 .局部变量 值, 整数型 .局部变量 是候选, 逻辑型 句柄 = 全局_进程句柄 池基址 = 63133528 控件 = 内存_读整数型 (句柄, 池基址 + 槽号 × 4) 写日志 (“── 转储 槽” + 到文本 (槽号) + “ 控件=” + 到文本 (控件) + “ 长度=” + 到文本 (字节数) + “ 字节 ──” + #换行符) .如果真 (控件 ≤ 65536) 写日志 (“✗ 该槽控件指针非法(” + 到文本 (控件) + “),读不出来。” + #换行符) 返回 .如果真结束 ' 逐个双字读,只打「小数值」和「指针」—— 功能号是整数,指针值很大 偏移 = 0 .判断循环首 (偏移 < 字节数) 值 = 内存_读整数型 (句柄, 控件 + 偏移) 是候选 = (值 ≥ 0 且 值 ≤ 400) ' 小枚举:功能号/状态 .如果真 (值 ≥ 65536 且 值 ≤ 0x7FFFFFFF) ' 看着像指针 是候选 = 真 .如果真结束 .如果真 (是候选) 写日志 (“ +” + 到文本 (偏移) + “ = ” + 到文本 (值) + #换行符) .如果真结束 偏移 = 偏移 + 4 .判断循环尾 () 写日志 (“转储结束。” + #换行符)