geogeo28/atari_reverse

GitHub: geogeo28/atari_reverse

一套将 Atari ST 二进制程序逆向为经逐字节验证的可读 C 代码并制作像素一致重制版的完整工具链与方法论框架。

Stars: 0 | Forks: 0

# Atari ST — 从二进制到重制版 从一款失落的 1988 年 Atari ST 游戏发布的可执行文件中将其复原:反汇编它,为每一个 函数命名,将其重写为可读的 C 语言代码,并**证明其与原始机器码逐字节一致**,然后 构建一个在像素级别上与**原版绘制效果完全一致**的免费优化重制版——并将其重新 在 68000 上运行。

Buggy Boy in-race frame rendered by the C reconstruction

这不是原版程序的截图——该帧是由重构代码绘制的:其道路光栅化器、滚动 blitter、对象调度器和 HUD,均基于游戏自身的 COURSES.DAT 绘制。

**Buggy Boy**(Elite Systems,1988 年)是一个完整的实战案例,已端到端解决: **91/91 个函数已验证** · **约 20,000 行重构的 C 代码** · **69 个测试模块** · **可在 68000 上驾驶**。相关工具和 [文档](docs/README.md)均与具体游戏无关——将它们指向任何 GEMDOS 的 `.PRG` 文件即可。 ## 三个阶段 ``` BUGGYBOY.PRG ── the shipped 1988 binary (you supply it) │ │ tools/prg_dis.py · Ghidra headless · names.txt naming loop ▼ 1. DISASSEMBLE & NAME decomp.c — 91 named functions, anchored on OS traps + hardware regs │ │ rewrite as idiomatic C, then diff every function against a cycle-accurate 68000 ▼ 2. RECREATE recreate/ — readable C, each function byte-for-byte == the original │ (oracle: Musashi running the real machine code) │ rewrite freely for speed and clarity; the only rule is the frame must not change ▼ 3. REMASTER remaster/ — native structs, faster algorithms, pixel-identical output │ ▼ DEMO.PRG on a 68000 ── cross-compiled back to m68k, runs under Hatari ``` 每个阶段都由其上一阶段进行裁判,因此不会有任何问题被悄悄忽略。阶段 2 由运行原始机器码的精确周期仿真器进行评判;阶段 3 则逐帧与阶段 2 已验证的核心代码进行比对评判。 ## 图库 以下所有内容均**由重构代码渲染**,然后使用游戏自身的调色板从 Atari 的 4 位平面帧缓冲区中解码。通过 `projects/buggyboy/gen_readme_assets.py` 即可重新生成整个集合——每次运行生成的字节完全相同,请在 `recreate/` 的虚拟环境下执行。 ### 比赛帧——三条赛道,真实驾驶 基于真实的赛道数据生成,在已验证的 `game_update` 中保持全油门驾驶,然后由已验证的渲染管线(道路 → 滚动 → 物体 → HUD)绘制。 | `OFFROAD` | `NORTH` | `SOUTH` | |:---:|:---:|:---:| | ![](https://static.pigsec.cn/wp-content/uploads/repos/cas/68/683369defac4aaef61f7fc8ba13c0a27ff58cf6e354e4bc3c27352a1168788c7.png) | ![](https://static.pigsec.cn/wp-content/uploads/repos/cas/ea/eaa2bf408b37e55e7117ac7f5515c740fc58df19f0d82536c3121d16edba4157.png) | ![](https://static.pigsec.cn/wp-content/uploads/repos/cas/39/39e57fe348996d68583f0d8fd193b24f92eb7ef60df79286d85bc0cf356f002b.png) | 左上角的赛道地图由 `init_leg_dash` 根据 `COURSES.DAT` 按路段构建,并由 `draw_dashboard` 每一帧进行 blit 操作;其上的轨迹是玩家的实时进度。 ### 屏幕 ### 赛道数据与精灵 事实证明,`COURSES.DAT` 并不是一种脚本,而是道路切片的**位图**数据,通过环形缓冲区每次流式传输 8 个字节。遍历它可以恢复每一段赛道的形状: | `OFFROAD` | `WEST` | |:---:|:---:| | ![](https://static.pigsec.cn/wp-content/uploads/repos/cas/b6/b6b035de9c6b895a57695779f4ac3a48f577c2ac2df90f99f1a41598478db58c.png) | ![](https://static.pigsec.cn/wp-content/uploads/repos/cas/d4/d43e9e4cceb44522f5826d45d7e8ec56d0d8de97bff9d144b58d358faacec7c8.png) | `GRAPHICS.GRA` 是一个精灵表加上一个 RLE 数据流,可解包为八个 320×200 的 4 位平面图集: [`projects/buggyboy/docs/function_graph.html`](projects/buggyboy/docs/function_graph.html) 是一个独立的调用图浏览器,涵盖了全部 117 个函数——在浏览器中打开即可,无需服务器。 拥有你自己的游戏副本后,你可以走得更远:`gen_assets.py` 可以重新生成完整的媒体集(每一个精灵页,通过驱动真实的 blitter 切割出的逐对象裁剪图,Buggy 动画的 GIF,以及通过重构的 YM2149 驱动程序重新渲染的原声带),并且重新运行 `gen_graph.py` 可以将所有这些媒体附加到生成它们的函数上。生成的媒体不会存储在此存储库中。 ## 阶段 1 — 反汇编与命名 在已知的基地址加载 `.PRG`,让 Ghidra 进行分析,然后迭代一个纯文本的名称映射表,直到反编译结果读起来像源代码一样。 ``` bash tools/new_project.sh mygame path/to/GAME.PRG # scaffold projects/mygame/ bash projects/mygame/run.sh # import → analyze → annotate → decomp.c # 读取 decomp.c → 追加到 names.txt → 重新应用 → 重新读取 bash projects/mygame/reapply.sh ``` `names.txt` 是事实来源——每行一个指令,按照 Ghidra 所见的方式(镜像偏移 + 加载基地址)进行寻址: ``` fn 0x1555e draw_hud var 0x18c38 leg_index cmt 0x1110e game_update: input, integrate throttle->speed, steering->road_curve, stream course… ``` 该方法是**由锚点向外推演**:从仿真器无法反驳的基准事实(GEMDOS/BIOS trap 编号、DRI 符号、硬件寄存器地址、字符串字面量)开始,并沿着调用图进行传播。然后*通过阅读函数体进行验证*;在本项目中,有几个最初充满信心的猜测实际上是错误的,直到有人真正阅读了代码([`docs/methodology.md`](docs/methodology.md))。 **Buggy Boy 的结果:** 335 条命名指令,全部 91 个函数均被命名,外加解码出的 loader、赛道格式、精灵格式、事件跳转表和声音驱动程序。 ## 阶段 2 — 复现:证明它 Buggy Boy 是纯手写的汇编代码,因此没有原始源码可供重新编译并进行逐字节匹配。相反,我们要证明的是**行为等价性**:在完全相同的内存中运行真实的机器码和重构代码,然后进行对比(diff)。 ``` initial memory image + registers │ ┌────────────┴─────────────┐ ▼ ▼ ORACLE (real 68k) CANDIDATE (our C) Musashi via liboracle.so libbuggyboy.so via ctypes │ │ ▼ ▼ final memory + write-set final memory └──────────► diff ◄────────┘ green = byte-for-byte identical ``` 双方共享同一个大型大端序内存镜像,其索引就是游戏的真实地址。测试工具(harness)会对*整个*内存镜像进行 diff,因此只要原版写入了一个字节而重构版遗漏了,测试就会失败。 叶子函数还可以额外选择加入一次归因过程,该过程会首先对 oracle 写入的字节进行污染,这样就可以捕获那些*碰巧*匹配上的候选项,而不是让它们蒙混过关。 对于永不返回的函数(`_start`,交互式循环),会在检查点 PC(程序计数器)处进行验证,并且任何被排除的栈带都会针对 oracle 最深的栈指针进行检查,从而确保排除项不会掩盖任何分歧。 **状态:91/91 已验证。** 有关每个函数的表格及其各自的确认方法,请参阅 [`recreate/STATUS.md`](projects/buggyboy/recreate/STATUS.md)。 ## 阶段 3 — 重制:释放它 `recreate/` 证明了原版的运作机制。而 `remaster/` 完全可以自由地变得和 68000 汇编截然不同——原生的 struct 代替扁平的内存镜像、真正的数据类型、预计算的表格、更优的算法——只需遵循一条规则: 两者使用了刻意设计的不同内存布局,因此它们的内部状态无法进行 diff。它们唯一共享的表面就是玩家所看到的内容,而这正是比较的参考面。任何哪怕只移动了一个像素的优化都会判定失败。 - **阶段 A — 渲染管线:通过。** 道路几何、光栅化器、滚动 blitter、地面/地平线、精灵、缩放对象、精细的 X 轴 blit 引擎、对象列表调度器以及全部八个 HUD 阶段均已移植,并且在整个帧缓冲区上达到了字节级精确。 - **阶段 B — 游戏玩法:进行中。** 赛道流式传输、对象环、玩家物理特性以及碰撞/自动转向脚本均已移植且达到帧级精确;声音、碰撞探测和事件派发尚未开始。 - **目标平台:** `DEMO.PRG` 交叉编译回 m68k 并在 Hatari 下运行,启动时会加载未经修改的 `COURSES.DAT` 和 `GRAPHICS.GRA`。其第 0 段(leg-0)的起始帧与重构版完全一致(字节级相同),并且是可以驾驶的。 有关各子系统的表格,请参阅 [`remaster/STATUS.md`](projects/buggyboy/remaster/STATUS.md)。 ## 快速开始 **前提条件:** Ghidra 12(脚本使用 Java 编写——Ghidra 12 已放弃对 Jython 的支持)、JDK 21、Python 3.10+、一个 C 编译器,以及 Hatari 仿真器和你自己用于在目标平台上运行的 TOS ROM。 ``` # 1. 逆向一个你自己的 binary bash tools/new_project.sh mygame path/to/GAME.PRG bash projects/mygame/run.sh # 2. 复现 Buggy Boy reconstruction(需要你自己的游戏文件位于 projects/buggyboy/bin/) cd projects/buggyboy/recreate make venv && make test # builds the Musashi oracle + the C cores, runs the differential suite make bench # per-frame cost: original 68000 vs the reconstruction cd ../remaster make test # pixel-equivalence against the verified cores ``` ## 仓库布局 ``` reverse/ ├── docs/ transferable knowledge, one file per expertise domain ├── tools/ game-agnostic tooling │ ├── prg_dis.py GEMDOS .PRG analyzer + 68000 first-pass disassembler │ ├── extract_graphics.py ST 4-plane / RLE graphics → PNG │ ├── depack_gamex.py static depacker for the Gamex/"PP" LZSS cruncher │ ├── ghidra_scripts/ PrgLoader · AtariOsTrapAnnotate · ExportDecompC · ApplyNames · … │ ├── headless.sh bootstrap: import → load → analyze → annotate → export │ ├── reapply.sh fast naming loop: apply names.txt → re-export │ ├── hatari_run.sh run a game in Hatari (unpack in-place, then dump memory) │ └── new_project.sh scaffold projects// └── projects// per-game: names.txt · decomp.c · recreate/ · remaster/ · docs/ ``` ## 文档 十三份领域指南,每一份都基于 Buggy Boy 的真实证据编写,但旨在作为通用流程。请先从 [`docs/00-overview.md`](docs/00-overview.md) 开始,了解端到端的工作流程和“这是什么类型的文件?”决策树,然后阅读 [`docs/agent-playbook.md`](docs/agent-playbook.md),了解将所有内容串联起来的验证闭环。完整索引:[`docs/README.md`](docs/README.md)。 | | | |---|---| | [binary-formats](docs/binary-formats.md) | 解析 `.PRG`/`.TOS`/`.TTP`、其文件头、符号和重定位信息 | | [packed-executables](docs/packed-executables.md) | 入口处是乱码——在分析之前先通过 Hatari 解压 | | [m68k-disassembly](docs/m68k-disassembly.md) | 阅读 68000 汇编,避免大范围反汇编不同步,识别跳转表 | | [ghidra-pipeline](docs/ghidra-pipeline.md) · [ghidra-gui](docs/ghidra-gui.md) | 无头模式驱动 Ghidra,然后进行交互式探索 | | [tos-os-calls](docs/tos-os-calls.md) | GEMDOS/BIOS/XBIOS/GEM 调用、basepage、loader | | [hardware-map](docs/hardware-map.md) | 视频/声音/MFP/IKBD 寄存器、中断、Line-A | | [graphics](docs/graphics.md) · [sound](docs/sound.md) | 位平面位图、调色板、RLE;YM2149 驱动程序 | | [on-target-execution](docs/on-target-execution.md) | 在真实硬件上运行已验证的重构代码 | | [methodology](docs/methodology.md) | 真正的命名方式:锚点 → 向外推演、验证、迭代 | ## 将其用于其他二进制文件 除了 `projects/buggyboy/` 之外,上述内容均非 Buggy Boy 专属。`new_project.sh` 可以为新的目标文件搭建骨架,Ghidra 脚本和命名循环适用于任何 GEMDOS 可执行文件,并且差分测试工具模式可以整体迁移——只需替换入口地址和内存镜像即可。如果入口点反汇编出来的是乱码,说明该二进制文件是被压缩过的;`prg_dis.py` 会打印出信息熵,并且 [`docs/packed-executables.md`](docs/packed-executables.md) 介绍了如何在 Hatari 中对其进行实时解压,以及如何分析内存转储。
标签:JS文件枚举, 客户端加密, 逆向工具