Skip to content
This repository was archived by the owner on Sep 30, 2026. It is now read-only.
NightDust981989Public archive

About

Forza Horizon 6 自由摄像机 mod —— 已归档的失败记录:偏移量已逆向出来,但没找到让画面动起来的写入路径

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

FH6 自由摄像机

Forza Horizon 6 的自由摄像机与多机位拍摄工具。

⚠️ 仅限单机 / 自由漫游使用。绝不要在联机模式(Rivals、Eventlab、 多机位赛事、排行榜)中使用。 这是注入型工具,反作弊能够检测到。 账号封禁风险由使用者自行承担。

📦 本项目已归档(2026-09-30)。目标未达成 —— 始终没找到能控制摄像机的写入路径。 保留下来作为一份失败记录:路线为什么走不通、以及过程中确认下来的结构性发现, 都在下面的「归档说明」里。


归档说明(2026-09-30)

项目停止在这一天。目标是自由摄像机 + 多机位拍摄,最终没有做到。

诚实结论:偏移量早就找到了(相机对象、位置、朝向全部确认), 缺的是"写进去之后画面会动"的那条写入路径。

三条路线各自的下场:

路线 做法 结果
hook 类工厂 挂接创建相机对象的工厂函数 证伪。6 个候选工厂在正常游戏过程中从不被调用;装了钩子 30 秒内必崩。
RendererClient vtable 0x6de0180(基类 IClientRendererInterface),槽 10/11/13 各持一份 64 字节变换 判死。游戏内 F11 实测 ×:本体里没有等于相机位置的三元组(该类只有 1 个实例,不存在"只试了第一个实例"的假阴性)。
AIO 字节补丁 沿用 Forza-Mods-AIO 的 FH5 签名在 FH6 上扫特征码 4/4 全废。3 条不命中;唯一一条"命中"(noheightlimit)是假阳性 —— 见下。

关于最后那条假阳性,值得单独记一笔,因为它能省掉后来人的时间: F6 报告说 noheightlimit 命中 2 处。挖到底以后,命中的是 .text 里 RVA 0x868C20 的一条 addsd xmm4, xmm2,属于一段双精度三维距离计算; 后面跟着 comiss xmm1, [rip+...] 与常量 0.006 比较, 距离小于 ε 时走备用分支调用 0x5e84ace —— 这是向量归一化前的零长度保护(一个数值 ε), 跟 FH5 上那条 movsd xmm3, [rsi+0x5C0] 毫无关系。 结论:FH5 的签名不能直接搬到 FH6 上用。

真正确认下来的东西(比失败的路线更有价值)

  • +0x310 = CCam 基类的位置偏移,由两个互不相关的子类独立确认: CCamPhotoMode(误差 0.0000 m)与 CCamFollowHigh(误差 0.2821 m)。朝向在 +0x320。
  • CamCopter 的位置在 +0xB20(误差 0.0000 m)。
  • 载具相机家族(CCamDriver / CCamHood / CCamBumperHigh / CCamWheel) 全部 ×,且"最接近"一律落在 +0x3E0。
  • 静态分析从原理上答不了"谁写了这个字段" —— 它只能告诉你"哪儿引用了这个地址"。 最后试过用 +0x310/0x320/0x330/0x340 四槽成组来定位"消费相机矩阵的代码", 177 条命中全是假的(0x310 只是"结构体偏移 300 出头"的普通偏移, 连 [rsp+0x310] 和存 bool 的 movzx 都混了进来)。这条路没有判别力。

如果后来者想接着做

不要再从静态分析入手。 缺的是"谁每帧把相机写进渲染用的结构体", 这个问题只有动态观测(内存断点)能回答。另有两处线索值得先查:

  • FrameStatistics(控制块 COL 0x79bb508)是引擎的逐帧记录, 它记着"当前渲染用的相机" —— 它是输出记录,不是相机本身。 这正是为什么往它、以及往 CCamPhotoMode+0x310 写值,画面都不动。 反过来想:谁每帧往 FrameStatistics 里填数据,谁就握着权威相机。
  • forzahorizon6.exe 正常退出时会重新封装自己(mtime 与最后一份 ShutdownReport.xml 的 DATE_TIME 精确到秒相同)⇒ 磁盘上的 .text 约有一半是密文, 在磁盘镜像上做交叉引用必然产生假阴性(见 docs/04)。 本文档里 0x868C20 之类的地址都来自运行时内存镜像,不是磁盘文件偏移。

当前状态

代码已完成并验证可编译、可安全运行,但始终没能控制摄像机。

(原以为卡在偏移量上 —— 实际上偏移量后来全部找到了。卡住的是写入路径: 值写进去了,画面不动。详见上「归档说明」。)

这不是半成品,是刻意的设计:所有偏移量默认为空/零, 未配置的 DLL 会拒绝安装钩子并打印操作指引,而不是崩溃或破坏游戏。

已验证:

  • ✅ Release x64 与 Debug x64 均编译通过,零警告(MSVC 14.51 / v145 工具集)
  • ✅ MASM 拦截器正常汇编(Interceptor.obj 已生成)
  • ✅ DLL 能加载、初始化、优雅失败,不崩溃(用 tools/selftest 验证)
  • ✅ 注入到错误进程时会被明确拒绝,不会静默扫描错误的内存范围
  • ✅ 拍照模式补丁的字节/INI 解析逻辑通过 26 项检查(tools/patchtest)
  • ✅ 已在真实游戏进程中实测过(2026-09-21 ~ 09-29,全部结果见上「归档说明」)
  • ❌ 从未成功控制摄像机 —— 三条路线全部实测失败

两条路线

反作弊/完整性校验这一层已经解决了(见 docs/03 E 节)。

下面这两条是当初设想的路线。两条最终都没走通(见上「归档说明」)—— 保留原文只是作为当时的计划记录,其中"缺的只是 FH6 上的具体字节"这类措辞已经过时。

路线 A:AIO 字节补丁(先走这条,风险低)

不接管摄像机,只用几个字节补丁去掉游戏自带拍照模式的限制 (穿墙、无高度限制、变焦范围、移动速度)。 改动小、风险低,但拿不到绝对坐标控制,因此做不了多机位。

代码(PatchManager)已经写好了,缺的只是 FH6 上的具体字节 —— 而且填字节不用重新编译:

  1. 注入 DLL,进游戏自由漫游
  2. 按 F6 打印扫描报告,看哪条特征码在 FH6 上命中、命中处是什么指令
  3. 用 Cheat Engine / IDA 判断该怎么改
  4. 把原字节和新字节填进 %LOCALAPPDATA%\FH6Camera\patches.ini (模板见 resources/patches.ini.example)
  5. 按 F7 重载,按 F5 启用

写入前会逐字节比对 expected(原字节),不一致就拒绝写入 —— 不会因为签名命中错位置而写坏游戏。

详见 docs/01-逆向工程步骤.md 第 12 节。

路线 B:IGCS 接管摄像机(做多机位必须走这条)

自己算位置和朝向并写回游戏结构体。这是唯一能做多机位的路,但工作量大得多。

跟着 docs/01-逆向工程步骤.md 的 1~11 节走, 那份文档是完整的分步指南(从"怎么用未知初始值扫描找到摄像机坐标" 到"怎么做出唯一的 AOB 特征码")。

逆向完成后:

  1. 把结果填进 src/FH6CameraDLL/GameConstants.h —— 需要的每一项都在 docs/03-偏移量清单.md 里列成了清单。

  2. 填 Interceptor.asm 里的 TODO 占位符,然后把该文件里的 g_interceptorPlaceholdersPresent 从 1 改成 0。

    这一步不能跳过。那些占位符是 nop,会静默汇编通过 —— 填好了特征码 却忘了替换它们的话,钩子会被装上去,然后每帧从游戏代码跳进一个 "什么都不做"的拦截器,破坏寄存器状态并崩溃。 DetourEngine::install() 会用那个哨兵拦住这种情况并明确报错。

然后重新编译,按 F9 开启自由摄像机。

为什么逆向这一步必须你自己做:它需要在本机对运行中的游戏用 Cheat Engine / x64dbg 动态调试,我无法代劳。


编译

需要 Visual Studio 2022 或更新版本(含 C++ 桌面开发工作负载与 MASM)。

MSBuild.exe FH6Camera.sln -p:Configuration=Release -p:Platform=x64 -m

产物在 build/x64/Release/:

文件 说明
FH6CameraDLL.dll 注入进游戏的自由摄像机本体
FH6CameraInjector.exe 注入器

用法

FH6CameraInjector.exe                  自动查找游戏进程并注入
FH6CameraInjector.exe --list           列出候选进程
FH6CameraInjector.exe --pid <PID>      指定进程
FH6CameraInjector.exe --dll <路径>     指定 DLL

⚠ 需要管理员权限(OpenProcess 需要)
⚠ Microsoft Store / Xbox 版有沙箱保护,CreateRemoteThread 会失败 ——
  请使用 Steam 版

注入后注入器窗口会自己跟随显示游戏的日志(按回车停止跟随,游戏继续运行)。

⚠ 不要改成让 DLL 在游戏进程里开控制台窗口。游戏进程里多一个顶层窗口会 让它在处理焦点时判断错"我的窗口是不是前台窗口",表现为失焦进暂停菜单后 切回游戏就闪退(不走异常、没有 dump)。日志文件用 _SH_DENYWR 共享写, 所以要单独看也可以:

Get-Content "$env:LOCALAPPDATA\FH6Camera\fh6camera.log" -Wait -Tail 20

热键

键 功能
F9 开关自由摄像机
WASD 前后左右移动
空格 / X 上升 / 下降
Q / E 滚转
Shift / Ctrl 加速 / 减速
右键拖动 转视角
1~9 切换到机位
Ctrl+1~Ctrl+9 保存当前机位
F10 摄像机复位
F8 时间暂停(尚未实现)
F11 按 vtable 精确找相机对象(内存扫描器,先按这个)
Ctrl+F11 导出内存镜像(离线静态分析用 —— 磁盘上的 .text 有一批是密文,见下)
F5 一键开关所有拍照模式补丁(AIO 路线)
F6 打印补丁点扫描报告(找 FH6 补丁点用)
F7 重载 patches.ini 并重扫(调参用,不用重编译)
F1 开始新扫描(扫描器的后备路线)
F2 我移动了相机 → 只留变化过的候选
F3 出报告 + 开始实时跟踪
F4 清空扫描候选
F12 追指针 —— 在游戏模块里找指向相机的稳定指针(拿到之后用)
M 分段标记 —— 每做一件事前按一下,之后每条 [差异] 行都带上段号
N 状态差分 —— 按两次(中间用游戏自己的输入移动相机),报出所有变了的 float 字段

F1~F4 / F11 / F12 是内存扫描器,用来找出相机结构体的 字段偏移和稳定基址(也就是 GameConstants.h 里那几个还空着的常量)。 自由摄像机装上之前,先用它把偏移找出来 —— 在此之前 F9 是无效的。

先按 F11。 相机对象的布局已经查清楚了(见 相机对象布局):它是一个 240 字节的 C++ 对象, 头 8 字节是 vtable,运行时的值 = 模块基址 + 0x655C648。 F11 就是拿这个常量去堆里精确比对 —— 一次命中,通常只有个位数个结果, 不需要 AOB 特征码、不需要装钩子、不需要绕 CRC。 正常输出长这样:

[扫描] === F11:按 vtable 精确找相机对象 ===
[扫描] 模块基址 0x... + RVA 0x655C648 → 目标值 0x...
[扫描] ... 45%(... MB),命中 3
[扫描] ══ 找到 N 个相机视图对象 ══
[扫描]   [1] 对象@0x...  视口 1920 x 1080

命中 0 个说明游戏更新过了,vtable 的 RVA 变了 —— 那时再退回 F1~F4 那条路重跑一遍。

F11 上挂了三个动作(用修饰键分):

按键 动作
F11 按 vtable 找相机对象(几秒,常用)
Shift+F11 按精确字节找"还有谁持有这个坐标"(90 秒,重)
Ctrl+F11 导出宿主模块的内存镜像(几秒,写文件)

Ctrl+F11 是给"磁盘上的代码不可信"这件事准备的。 离线静态分析发现 .pdata 里 414,860 个函数有 65,291 个(15.7%),函数体在磁盘上是密文 —— 从函数头就解不出合法指令,而且这些函数的头 16 字节反复出现同一串 (最热的一串出现 3456 次;随机数据不可能重复几千次,只能是"相同明文加密出 相同密文")。节表里的 .detourc / .detourd 也印证了这一层。

拿到内存镜像实测之后,真实规模比那 15.7% 大得多(15.7% 只是"解出非法指令" 这个判据的检出率,短密文段检不出来):.text 磁盘与内存不同 68.1 % (67.78 MB),逐函数算是 201,128 / 414,860 个(48.5%)不同; 而磁盘上干净的函数在内存里逐字节相同,密文的那些在内存里是教科书级的 MSVC 序言。.rdata / .data 的不同最长只有 7 / 128 字节,是纯重定位, 没有密文。⇒ 约一半的代码在磁盘上读不到。

后果:凡是在磁盘映像上做的"谁调用了这个函数""哪个函数写了这个字段", 落在这约一半里就什么都看不见。早先那些"0 个调用点"的结论都有假阴性的 可能 —— 这恰好解释了代码路线为什么一路走不通。(实测印证: 0x28f9970 在磁盘上"0 个调用点",在内存镜像上有 6 个。)

游戏能跑起来,说明这些函数在内存里已经解密。所以不需要破它的加密, 把内存里那份抄下来就行:按一次 Ctrl+F11,会在 %LOCALAPPDATA%\FH6Camera\ 下写出 fh6_image.bin(整块镜像,按 RVA 索引) 和 fh6_image.txt(小抄:运行时基址等)。离线用 tools/staticscan/memimg.py 读它 —— 接口和 rtti.py 的 Image 一样,现有脚本改一行就能切过去; tools/staticscan/memxref.py 是在它上面做 交叉引用/反汇编的入口(camstore.py --mem 则把"谁写 +0x310"那条扫描 改到内存镜像上跑)。

这个键顺便还会把内存镜像和磁盘 exe 逐字节比一遍,把"有多少字节不同、 最长的一段在哪"打进日志 —— 也就是"磁盘 ≠ 内存"这件事的当场实证, 不用离线猜。

F1~F4 / F11 / F12 和上面那批不一样:它们不要求游戏在前台, 在注入器窗口上按也生效。 F1 是全量扫描,FH6 有近 10 GB 私有内存,要一两分钟(分片进行, 游戏全程不卡),每 2 秒打一行百分比就是它正常的标志。 想中途放弃按 F4。

必须在拍照模式里做 —— 别的地方车流/行人每帧都在写内存,F2 永远收敛不了。 F3 按完之后要继续平稳地飞 5 秒:那 5 秒在比"前 4 名里谁动得平顺", 停下了就选不出来。这一步是必需的,因为按得分排序会饱和、会挑错, 而"谁位移大"也是反的(会挑中被反复改写的噪声地址)。

F12 是在找到相机之后才按的:扫描器给的是堆地址、每次重启都变, 而 hook 需要一个重启后仍有效的基址。F12 在游戏模块里找指向它的指针, 输出模块内 RVA,那是能变成特征码的东西。它只扫模块(约 175 MB),几秒就完。 按 vtable 那条路走通的话就不需要它了。

机位存档在 %LOCALAPPDATA%\FH6Camera\presets.ini, 补丁配置在 %LOCALAPPDATA%\FH6Camera\patches.ini, 日志在 %LOCALAPPDATA%\FH6Camera\fh6camera.log。


项目结构

src/FH6CameraDLL/          注入进游戏的 DLL
  CameraSystem.*           编排器:生命周期 + 主循环
  CameraManipulator.*      按偏移量读写游戏摄像机结构体 ← 唯一知道偏移量语义的地方
  CameraController.*       摄像机状态机(不碰游戏内存)
  CameraMath.*             四元数 / 欧拉角 / 插值
  DetourEngine.*           代码补丁安装与 CRC 绕过
  AOBScanner.*             特征码解析与扫描
  MemoryUtils.*            进程内存读写
  InputManager.*           热键与鼠标视角
  InputHooker.*            用 MinHook 屏蔽游戏输入
  PatchManager.*           拍照模式字节补丁(AIO 路线,带原字节校验)
  MemScanner.*             ★ 只读内存扫描器 —— 找相机结构体字段偏移
  CameraProbe.*            相机类工厂探针(路线 A,已证伪,默认关闭,别开)
  Interceptor.asm          MASM64 拦截器 ← 需要填 TODO 的占位符
  GameConstants.h          ★ 逆向结果填这里
src/FH6CameraInjector/     注入器
docs/                      逆向指南与架构说明
resources/                 patches.ini 模板
tools/selftest/            DLL 自测加载器
tools/patchtest/           patches.ini 解析逻辑自测
tools/staticscan/          离线静态分析(反汇编 / RTTI / 交叉引用)
  rtti.py                  从 vtable 反查类名(MSVC RTTI 重建)
  xdis.py                  按 .pdata 分函数反汇编 / 找交叉引用
  memimg.py                ★ 读 Ctrl+F11 导出的**内存镜像**(磁盘 .text 约一半是密文)
  memxref.py               ★ 在内存镜像上做交叉引用 / 反汇编(--all / --fn / --around)
  camstore.py              谁在写相机变换矩阵(加 --mem 就在内存镜像上扫)
third_party/minhook/       MinHook(BSD-2-Clause,已 vendor)

架构与设计取舍见 docs/02-架构说明.md。


自测

1. DLL 加载 / 优雅失败

验证 DLL 能加载并且不会在未配置时崩溃:

MSBuild.exe tools/selftest/selftest.vcxproj -p:Configuration=Release -p:Platform=x64
tools/selftest/selftest.exe build/x64/Release/FH6CameraDLL.dll

预期行为:DLL 被加载后检测到自己不在游戏进程里, 打印警告并干净退出,退出码 0。

2. patches.ini 解析

PatchManager 的字节解析和 INI 解析是手写的,而它们决定往游戏代码里写什么。 这个测试单独验证这套解析逻辑(26 项检查):

MSBuild.exe tools/patchtest/patchtest.vcxproj -p:Configuration=Release -p:Platform=x64
tools/patchtest/patchtest.exe

覆盖的关键用例:

  • 补丁字节里出现 ?? 必须被拒绝(否则写进游戏的是垃圾)
  • 行尾注释 patch = 90 90 ; 说明 不能污染补丁字节
  • 被注释掉的 expected = 行不能生效
  • 空 patch = 必须判定为"未配置"而不是写入全零
  • expected 与 patch 长度不一致必须能被检出

⚠ 该测试里的解析函数是逐字复制自 PatchManager.cpp 的。 改动那边的解析逻辑时,这里必须同步,否则测试就失去意义。


3. 游戏注入后静默退出

症状:注入成功,但游戏自己消失 —— 没有崩溃弹窗、没有 minidump、 Windows 事件日志里也没有 APPCRASH。

这类退出不留下任何指向原因的日志,只能靠二分定位:把 resources/debug.ini.example 复制成 %LOCALAPPDATA%\FH6Camera\debug.ini, 按里面的说明逐项关掉我们的改动,看关掉哪一项之后游戏能活下来。

排查时先分清两件事,这决定了往哪个方向查:

  • 存活时间固定(比如每次都 ~60 秒)→ 定时完整性校验
  • 跟着操作走(失焦、切窗口才崩)→ 交互触发,多半是窗口/消息路径

日志里的 [心跳] 已运行 N 秒 每 10 秒一行,用来判断是哪种。

已知踩过的坑: 在游戏进程里开控制台窗口会让"失焦后切回游戏"闪退 —— 详见上面「用法」里的说明。

不要打开 probe_camera。 那条路线(hook 反射系统的类工厂)已经证伪: 6 个工厂在正常运行期一次都没被调用过,而且装了钩子本身就会让游戏在 30 秒内 退出 —— 原因见下。

装在游戏自己的 .text 上的钩子会被完整性校验看到,唯一的遮蔽手段是让 CRC 心跳每 10 秒把钩子撤下再挂上。而 MinHook 每次 enable/disable 都要 挂起再恢复进程内所有线程 —— 挂起一个正持有堆锁的线程会让进程死锁。 实测:探针关闭时游戏活 250 秒,开着(哪怕钩子里只做一个原子自增、 不读内存不写日志)30 秒内必退。

现在的做法是只读的内存扫描器:不改一个字节、 不碰 MinHook、不参与 CRC 心跳,所以没有上面这套问题。


参考


尚未完成

  • 逆向(两条路线的前置条件,见上)
  • FH6 的拍照模式补丁字节 —— 框架已就绪,字节要对着反汇编填 (走路线 A 的话,填进 patches.ini 即可,不用重编译)
  • Interceptor.asm 里的 TODO 占位符 —— 仅路线 B 需要
  • 多机位 —— 必须走路线 B;路线 A 做不到
  • 时间暂停 / HUD 隐藏 —— 缺结构体指针
  • 图形界面 —— 目前只有控制台日志与热键

About

Forza Horizon 6 自由摄像机 mod —— 已归档的失败记录:偏移量已逆向出来,但没找到让画面动起来的写入路径

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages