作者:danielrh

童年时期,作者与弟弟帕特里克曾梦想驾驶《银河飞将》(Wing Commander)中的太空战斗机,并渴望成为“机长”系列中的双座战机伙伴,而非单一飞船内的两名乘员。这一愿景最终通过技术手段得以实现。
定位内存结构:从手动调试到自动化分析
2015年10月4日,作者在 Dosbox 模拟器中运行经典版《银河飞将》,试图通过网络数据包控制翼员(wingman)。由于 DOS 可寻址内存仅约一百万字节,且包含64K数据段,攻击者面临巨大的搜索空间问题。
为简化问题,作者尝试清除其他实体干扰。停止的玩家不应改变坐标,因此可以通过两次快照识别背景内存变化。若鼠标被禁用导致无法转向,则可能仅改变一个轴。初始阶段需移除翼员帕拉丁(Paladin),以免其移动干扰静止状态下的内存比对。在一次任务失败后,邪恶的太空猫击败了帕拉丁,作者利用 DOSBox 的 MEMDUMPBIN 调试命令进行内存快照。
作者需要在原始位置的记忆快照中寻找无变化的区域。通过对比静止与移动状态的快照,发现253个字节在静止时转换,650个字节在移动时转换,其中413个字节同时发生两种变化。结合追踪摄像头的交叉引用,剩余可疑字节缩减至132个。
数据段中有35个不同的字节集群发生变化。通过视觉检查浮动或小整数变化,搜索范围缩小至地址 13D3:A9C2。使用 DOSBox 调试器的 SM 命令在该地址添加整数,导致飞船向前冲击到新位置。此技术也成功识别了速度变量,位于 CAE2,因为当飞船停止时,三个32位整数归零。
此时,作者已能通过内存编辑器驾驶飞船,并在爪子及其他NPC船只上复制该行为。
深入逆向工程:寻找旋转与向量存储
2016年9月,作者编写工具自动比较 DOSBox 快照,取代了 vbindiff GUI。数月搁置后于11月重启项目。
为了找到旋转记忆位置,作者进行了更细致的搜索。游戏使用4字节标量表示向量,其中256代表1.0,正向量位置在0到255之间。这意味着大多数小旋转只改变一个字节。然而,程序员的一个“笑话”提供了线索:机组指挥官为实体属性保留了64个插槽(0x00至0x3F)。程序员将临时变量放在0x3F,而摄像机置于0x3D。鉴于当时3D图形尚处萌芽阶段,这可能是一个关于“3D”的双关语。
这种布局使得临时变量与摄像机变量相邻,扩大了单字节变化的搜索区域。作者首先识别出右向量的Y分量,随后通过重写实验发现,打印操作导致相邻3个字节从0x00切换至0xFF。虽然注意到临时和摄像机字节在六合倾倒中重叠,但额外写入的字节影响了下一个向量(向前[0]),导致星空场明显变化,从而开启了代码调查。
通过在地址上设置内存观察点,作者追踪了每船物理函数调用的矢量加函数。
随后,作者在 IDA Pro 中找到字节序列并开始注释游戏代码。多人游戏部分主要重复这些技巧,以操纵另一艘船的输入。
实现动作注入:处理 Borland 覆盖加载器
2016年12月,直接通过地址操控激光火焰或热探寻器等复杂功能变得不可行。为了获得准确体验,需要调用原始游戏功能,但这要求插入参数并从原位置返回。此外,许多函数涉及间接调用。Borland 编译器在每个编译单元(C++文件)上使用不同的覆盖(overlay),并根据玩家路径动态加载。
作者选择在字符串 "Loading WING COMMANDER" 附近(地址 13D3:0187 和 13D3:0395)插入指令指针转移代码。以下是用于激活激光炮的汇编片段:
00 terminates any string pointing here. pushf; pusha the game is mid-frame; leave registers as we found them xor si,si; push si argument: ship 0. Later thunks use mov si,imm16 for each argument call far 12D7:0156 through the stub, so it works wherever the overlay is loaded pop si; popa; popf cdecl: caller cleans the arguments retf back to the CS:IP DOSBox pushed
2017年3月,作者在主循环中找到可预测的连接点(0560:20E3),避免在游戏不可预测部分执行代码。7月,找到 do_damage 函数并阻止游戏将其称为次要玩家。
网络同步架构:TCP 客户端/服务器与 Protobuf
系统采用简单的 TCP 客户端/服务器架构,使用 protobuf 游戏消息。领导者发送每艘船的位置、火灾事件、失踪和损害信息。若发射激光或发生灾难性事件(弹射、降落、死亡),客户端回复自己的位置。
每个玩家拥有独立的实体0,另一玩家为实体1,接收其他模拟器数据包时需进行交换。
数据包直接修改内存,或被添加到必须在那个时刻发生的事件的任务队列中。初始床代码非常简单:
pushf pusha nop popa popf retf
当 Borland 覆盖加载器交换代码页面时,它会遍历堆上的 BP 链路并修补指向覆盖的返回地址。上述手工制造的卡片没有框架,导致行走读数为垃圾,游戏随机死亡。这一问题困扰团队从10月到12月。
修复方案是使床代码看起来像标准的 Borland 函数:
push bp mov bp, sp nop (0x90) ; welcome to the 0x90's pop bp retf
配置 DOSBox 检测程序计数器何时达到 NOP,并从任务队列注入应执行的调用。工作排队对于多人场景正常运行至关重要。
2017年10月,发现短暂物体在不同玩家的不同位置。通过 DOSBox 的新子模块确定函数返回时间。短暂物体有自己的发育规则,占据61个实体槽的共享区域。作者修改短暂物体位置,将其限制在12到15位。
12月,客户端更改为存储服务器船号码与本地号码的映射,停止强制生成函数返回,允许数据包在本地和主机ID之间翻译。小行星开始在多人游戏中正常工作。
2018年,重点转向动画爆炸、导弹和自动驾驶。
胜利条件与聊天功能的逆向
2019年6月,任务胜利计算不稳定。通过二进制搜索整个64K数据段复制到网络中,逐步切割,最终在 13D3:D248 找到80个字节。10月,确定总胜利积分计数在 13D3:C280,任务号码在 13D3:C255 和 C256。
2020年初,确定了负责通信文本的功能。这允许通过网络发送聊天内容,并将键入的信息直接接入系统,通过队友的视频显示单元(VDU)播放,实现了沉浸式聊天体验。
尽管2020年底游戏处于可玩状态,但分享乐趣仍困难重重:用户需寻找旧游戏副本、编译或下载特殊 DOSBox 版本、共享公网 IP 并穿透防火墙。
2026年:WebRTC 与 Lobbylink 实现的浏览器端联机
随着 WebRTC 的普及和脚本支持的增强,作者构建了新系统 Lobbylink,实现了真正的在线合作。
Lobbylink 系统
作者开发了 Lobbylink,这是一个基于 Go 语言的简单大厅系统,仅依赖 websocket 库,用于编排点对点 WebRTC 连接。它可以在任何小型服务器上运行,允许玩家通过共享房间代码连接。服务器维护静态来源允许列表,支持浏览器或几乎任何语言的客户端软件连接。房间代码用于定位同房间的其他玩家,并通知所有加入者。服务器还运行 coturn 并铸造 TURN 凭证,帮助受限防火墙后的玩家建立连接。
传输层已移植到 Lobbylink。作者请求 LLM 配置构建脚本以针对 emscripten,提取 GOG Inno Setup 安装包中的 Wing Commander 作为数据文件。这使得用户可以从 GOG 下载游戏,拖放到浏览器存储,输入房间代码即可游玩。
游戏可在 https://fireflyk64.github.io/dosbox/ 游玩,模拟器代码位于 https://github.com/danielrh/wcmulti。
此外,作者请求 LLM 添加了语音聊天功能,使用第二个 WebRTC 对等连接传输音频,让玩家能够一起飞行并欢笑。





