Forza Horizon 6 的自由摄像机与多机位拍摄工具。
⚠️ 仅限单机 / 自由漫游使用。绝不要在联机模式(Rivals、Eventlab、 多机位赛事、排行榜)中使用。 这是注入型工具,反作弊能够检测到。 账号封禁风险由使用者自行承担。
📦 本项目已归档(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(控制块 COL0x79bb508)是引擎的逐帧记录, 它记着"当前渲染用的相机" —— 它是输出记录,不是相机本身。 这正是为什么往它、以及往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 上的具体字节"这类措辞已经过时。
不接管摄像机,只用几个字节补丁去掉游戏自带拍照模式的限制 (穿墙、无高度限制、变焦范围、移动速度)。 改动小、风险低,但拿不到绝对坐标控制,因此做不了多机位。
代码(PatchManager)已经写好了,缺的只是 FH6 上的具体字节 ——
而且填字节不用重新编译:
- 注入 DLL,进游戏自由漫游
- 按
F6打印扫描报告,看哪条特征码在 FH6 上命中、命中处是什么指令 - 用 Cheat Engine / IDA 判断该怎么改
- 把原字节和新字节填进
%LOCALAPPDATA%\FH6Camera\patches.ini(模板见 resources/patches.ini.example) - 按
F7重载,按F5启用
写入前会逐字节比对 expected(原字节),不一致就拒绝写入 ——
不会因为签名命中错位置而写坏游戏。
详见 docs/01-逆向工程步骤.md 第 12 节。
自己算位置和朝向并写回游戏结构体。这是唯一能做多机位的路,但工作量大得多。
跟着 docs/01-逆向工程步骤.md 的 1~11 节走, 那份文档是完整的分步指南(从"怎么用未知初始值扫描找到摄像机坐标" 到"怎么做出唯一的 AOB 特征码")。
逆向完成后:
-
把结果填进 src/FH6CameraDLL/GameConstants.h —— 需要的每一项都在 docs/03-偏移量清单.md 里列成了清单。
-
填
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。
验证 DLL 能加载并且不会在未配置时崩溃:
MSBuild.exe tools/selftest/selftest.vcxproj -p:Configuration=Release -p:Platform=x64
tools/selftest/selftest.exe build/x64/Release/FH6CameraDLL.dll预期行为:DLL 被加载后检测到自己不在游戏进程里, 打印警告并干净退出,退出码 0。
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的。 改动那边的解析逻辑时,这里必须同步,否则测试就失去意义。
症状:注入成功,但游戏自己消失 —— 没有崩溃弹窗、没有 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 心跳,所以没有上面这套问题。
- InjectableGenericCameraSystem —— 注入式摄像机的经典架构(ASM 拦截器、写阻断、CRC 绕过)
- Chaarkors-FH6-Trainer —— FH6 的偏移量与 CRC 签名参考(注意:其 Camera 功能并未实现)
- UnknownCheats: FH6 逆向数据结构与偏移 —— 偏移量讨论帖(需要登录访问)
- 逆向(两条路线的前置条件,见上)
- FH6 的拍照模式补丁字节 —— 框架已就绪,字节要对着反汇编填
(走路线 A 的话,填进
patches.ini即可,不用重编译) Interceptor.asm里的 TODO 占位符 —— 仅路线 B 需要- 多机位 —— 必须走路线 B;路线 A 做不到
- 时间暂停 / HUD 隐藏 —— 缺结构体指针
- 图形界面 —— 目前只有控制台日志与热键