当前位置: 首页 > news >正文

逆向一个被遗忘的DVD游戏格式:从DES加密到Rust模拟器

缘起

前阵子翻出一台老的便携 DVD 播放器,发现里面居然有游戏菜单。七个分类,80 多款游戏。查了一下发现这东西叫 Native32,凌阳科技的芯片方案,2005-2011 年间大量用在 DVD 播放器和车载显示器上。

游戏由 Potatoo Multimedia Studio 开发,格式是私有的,没有公开文档。硬件淘汰后整个游戏库基本消失了。Internet Archive 上有人存了游戏文件,但原文写道"没有已知的方法可以在真实硬件之外运行它们"。

我用 Rust 逆向了这个格式并实现了模拟器。记录一下技术细节。

[图1: FHUI 主菜单界面 — 七个游戏分类]
FHUI 主菜单

文件格式

所有文件共享同一二进制格式,魔数头 _YUVGamemaker 1.3.12

偏移    内容
0x00    SWFT 缩略图(可选,跳过)
---     _YUV / ARGB 色彩空间标记
---     生成器字符串(48字节,如 "Resolution_320x240")
---     基础偏移量(colorspace + 0x60)
---     加密头部(32字节)  ← 资源偏移量藏在这里
---     光标数据
---     声音表 → 帧表 → 图像表 → 动作表 → 影片表 → 按钮表

32 字节加密头部是关键——里面藏了资源表的偏移量。不解密就什么都读不出来。

资源对象类型:Image(1)、Movie(2)、Button(3)、Action(4)、Sound(5)。

两种使用方式:

  • 独立游戏 — 单个 .smf 文件包含所有资源(如教育类游戏)
  • 跳板+场景 — 11KB 的 .smf 跳板文件 + 多个 .ssl 场景文件(如赤刃)

DES 加密

不是标准 DES

文件头 32 字节用 DES ECB 加密。我直接用了标准 DES 库,解出来全是乱码。又试了 3DES、DESede,都不对。

对比参考实现的输入输出后发现:凌阳用的是自定义 DES。算法框架和标准 DES 一样(初始置换 → 16 轮 Feistel → 最终置换),但所有置换表和 S-boxes 全部不同。

密钥来自常量 aber3801,和芯片型号 SPHE8202 相关。

需要手写的查找表

一共 8 组非标准查找表:

pub const INITIAL_MESSAGE_PERMUTATION: [u8; 64] = [0x3a, 0x32, 0x2a, 0x22, 0x1a, 0x12, 0x0a, 0x02,0x3c, 0x34, 0x2c, 0x24, 0x1c, 0x14, 0x0c, 0x04,// ...
];pub const FINAL_MESSAGE_PERMUTATION: [u8; 64] = [ /* 64个值 */ ];
pub const MESSAGE_SHUFFLE: [u8; 48] = [ /* 48个值 */ ];
pub const RIGHT_SUB_MESSAGE_PERMUTATION: [u32; 32] = [ /* 32个值 */ ];
pub const INITIAL_KEY_PERMUTATION: [u8; 56] = [ /* 56个值 */ ];
pub const SUB_KEY_PERMUTATION: [u8; 48] = [ /* 48个值 */ ];
pub const DES_SBOXES: [[u8; 64]; 8] = [ /* 8×64 = 512个值 */ ];
pub const KEY_SHIFT_SIZES: [u8; 16] = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1];

验证方式:对比参考实现的输入输出对,逐个表核对。任何一个值错了解密结果就是错的。

DES 解密是整个项目的"守门人"——解不开就什么都做不了。这一步花了我整个项目三分之一的时间。

Action 虚拟机

指令集

36 个操作码的栈式虚拟机。操作码值和 Flash ActionScript 高度重合(GotoFrame=0x81, Push=0x96, If=0x9d),但字节码编码不同。凌阳大概参考了 AS 的设计但自己搞了一套。

操作码分类:

帧控制:  NextFrame(0x04), PreviousFrame(0x05), Play(0x06), Stop(0x07)
算术:    Add(0x0a), Subtract(0x0b), Multiply(0x0c), Divide(0x0d)
比较:    Equals(0x0e), Less(0x0f), StringEquals(0x13), StringLess(0x29)
逻辑:    And(0x10), Or(0x11), Not(0x12)
变量:    GetVariable(0x1c), SetVariable(0x1d)
精灵:    CloneSprite(0x24), RemoveSprite(0x25)
跳转:    GotoFrame(0x81), Jump(0x99), If(0x9d), Call(0x9e)
宿主:    Push(0x96), GetUrl2(0x9a)

宿主调用

GetUrl2 操作码是 Native32 特有的扩展。Flash 中 GetUrl2 用于加载网页,Native32 用它来调用平台 API:

  • SSL+SSL_PlayNext+<路径> → 加载下一个场景
  • SSL+SSL_SaveSSLData+<变量> → 保存存档
  • SSL+SSL_GetSSLData+<变量> → 读取存档
  • LoadImage+<精灵>+D+<路径> → 加载 .dat 名称横幅
  • LoadImage+<精灵>+J+<路径> → 加载 .dat 预览截图
  • StartGame+<路径> → 启动游戏
  • GetFileNum+<目录> → 返回游戏数量
  • GetFirstFile+<目录> → 获取第一个游戏名
  • GetNextFile+<目录> → 获取下一个游戏名

需要实现 VmHost trait 来桥接 VM 和宿主系统。独立桌面端和 libretro 核心各自实现这个 trait。

VM 骨架

pub struct ActionVM {stack: Vec<Value>,vars: HashMap<String, Value>,pc: usize,bytecode: Vec<u8>,
}impl ActionVM {pub fn execute_frame(&mut self, host: &mut dyn VmHost) -> Result<()> {loop {let opcode = self.read_u8()?;match opcode {0x00 => return Ok(()),0x04 => host.next_frame(),0x96 => self.push()?,0x99 => self.jump()?,0x9a => {let url = self.pop_string()?;host.get_url2(&url)?;}0x9d => self.conditional_jump()?,_ => return Err(anyhow!("Unknown opcode: 0x{:02x}", opcode)),}}}
}

VM 本身不复杂——36 个 match 分支,一个栈,一个变量表。麻烦的是宿主调用的实现,每个调用都需要理解原机的行为语义。

图像解码

YUV 4:2:0

Y 通道全分辨率,U/V 半分辨率需 2x 垂直插值。压缩用 packbits + RLE 混合编码。

插值的边界处理有点 tricky——边缘像素不能简单复制,否则色块边界很明显。参考实现用了一种"借用"策略:U/V 值为 0 时借用相邻行的值。

fn interpolate_y(data: &[u8], w: usize, h: usize) -> Vec<u8> {let h1 = h * 2;let mut result = vec![0u8; w * h1];for y in 0..h {for dy in 0..2 {for x in 0..w {let val = if dy == 0 {if y == 0 || get(y * w + x) != 0 {get(y * w + x)} else {get((y - 1) * w + x)}} else {if y == h - 1 || get(y * w + x) != 0 {get(y * w + x)} else {get((y + 1) * w + x)}};result[(y * 2 + dy) * w + x] = val;}}}result
}

ARGB1555

16 位格式,直接位移解码。5 位分量乘 8 扩展到 8 位。

SSL 多文件系统

大游戏的加载流程:

EBBLADE.smf (11KB 跳板)→ NALOGO.mpg(Logo 视频)→ BBSTART.SSL(标题,1.8MB)→ BBMENU.SSL(主菜单,3.5MB)→ BBPLAY10.SSL(第1关,2.6MB)→ BBPLAY20 → BBPLAY30 → ...→ BBFINISH.SSL / BBOVER.SSL

.ssl.smf 用完全相同的二进制格式。区别仅在用途。

存档格式:.ssl_sav 文件,纯文本数字字符串。比如 1109600000002

NA32SSL 目录下有两套完全独立的游戏集——CHINESE(中文教育)和 ENGLISH(英文动作),不是翻译关系。

FHUI 菜单

FHUI.smf 是前端启动器,通过宿主调用枚举游戏列表:

  1. GetFileNum+EACT → 返回数量
  2. GetFirstFile+EACT → 第一个游戏
  3. GetNextFile+EACT → 下一个游戏
  4. LoadImage+<精灵>+D+<路径> → 从 .dat 解码名称横幅
  5. LoadImage+<精灵>+J+<路径> → 从 .dat 解码预览截图
  6. StartGame+<路径> → 启动游戏

.dat 文件魔数头 INFO,嵌入两块 YUV 图像(名称横幅 + 预览截图,描述文字已烘焙进图像里)。

MPEG-1 解码器

《钢铁风暴》《风暴之翼》有 3D 动画过场,MPEG-1 格式。我用纯 Rust 实现了解码器——零 C 依赖。

实现内容:

  • MPEG-1 系统流解复用(分离视频和音频)
  • I 帧解码(帧内编码)
  • P 帧解码(帧间编码,前向预测)
  • 宏块解码(16×16 块)
  • 运动补偿
  • DCT 反变换
  • VLC 变长编码解表
  • YCbCr → RGB 色彩空间转换

最大的坑是运动补偿——P 帧有 16 种运动矢量模式(16x16、16x8、8x16、8x8),最初只实现了 16x16,结果过场动画出现马赛克块。

MPEG-1 的文档质量参差不齐,ISO 11172 很多细节描述得含糊。真正有用的参考是 ffmpeg、mpeg_play 等开源实现的源码。

架构

native32emu-core      → 平台无关引擎(库)
native32emu           → 桌面端(minifb)
native32emu-libretro  → libretro 核心

Emulator 结构体零平台依赖,持有所有游戏状态:

pub struct Emulator {pub reader: Native32Reader,pub sprites: SpriteSystem,pub frame_player: FramePlayer,pub vm: ActionVM,pub audio: AudioEngine,pub renderer: Renderer,pub input: InputHandler,pub save_manager: SaveManager,pub content_loader: ContentLoader,pub file_browser: FileBrowser,pub video_player: Option<VideoPlayer>,pub pending_videos: Vec<String>,pub auto_skip_cutscenes: bool,
}

libretro 核心的 retro_run() 就是一个 emu.tick() 调用加上视频/音频回调。

兼容性

84 款游戏全部通过测试。几个有意思的:

[图2: 赤刃 (Bloody Blade)]
赤刃

[图3: 钢铁风暴 (Metal Storm)]
钢铁风暴

[图4: 枪火 (Gun Fire)]
枪火

[图5: 风暴之翼 (Storm Wind)]
风暴之翼

[图6: 符文之语 (Rune Word)]
符文之语

  • 《风暴之翼》过场动画里有人骂 "Shit"——儿童设备
  • 《符文之语》文件名 ERuneWod.smf 少了个 r
  • 中英文两套 SSL 游戏是完全不同的内容
  • 部分 8Bit Game 里的 NES ROM 是不当 ROM hack

后续

  • MP3 音频(PCM 可用,MP3 待实现)
  • Save states
  • RetroArch Online Updater 一键安装

链接

  • GitHub
  • libretro 文档
  • BootlegGames Wiki
  • 游戏下载
http://www.jsqmd.com/news/1048715/

相关文章:

  • 百考通智能化AI,赋能文献综述,精准破解文献梳理难题
  • 2026年宁波AI搜索优化公司全面权威横向评测与选型决策指南 - 品牌报告
  • 嵌入式GUI开发实战:深度解析emWin三大数值调节控件
  • Ludusavi游戏存档备份工具深度指南:跨平台游戏进度保护实战
  • 3分钟搞定:PC版微信QQ防撤回补丁终极应用指南
  • 权威核验|2026 年卡地亚官方售后网点专项核验报告出炉,全国 60 余家正规授权维修点完整地址公布 - 卡地亚中国服务中心
  • [实战] 制造业质量控制:CAD图纸气泡图自动化标注与检验计划实操指南(2026版)
  • 如何用BiliTools解决B站视频下载与管理的三大难题
  • emWin内存设备16位位图绘制优化:GUI_MEMDEV_SetDrawMemdev16bppFunc实战指南
  • 邢台厨卫屋顶防水修缮三家对比测评 吉修匠 99.8 分 - 吉修匠
  • 零投诉零纠纷!2026沈阳黄金回收标杆品牌合扬实力认证 - 奢侈品交易观察员
  • HDRI转立方体贴图终极指南:3步让3D场景光照真实感翻倍
  • Qwen3-VL技术报告实操解剖:双塔对齐、动态桥接与工业级微调指南
  • 【图像隐写】基于DWT、SVD和扩频技术混合可见-隐形水印系统(将彩色标志和强大的隐藏水印嵌入图像中附Matlab代码
  • 2026 年重庆厨卫屋顶防水修缮三家对比测评 吉修匠 99.8 分稳居榜首 - 吉修匠
  • 夸克网盘链接解析直链链接_在线解析网盘链接
  • 2026年卡地亚官方售后服务体系升级公告:全新官方维修门店新址升级落地,最新服务热线同步开通 - 卡地亚中国服务中心
  • 2026年成都GEO优化机构综合测评及选择指南 - 刘向阳而生
  • 5分钟搞定M3U8视频下载:免费工具让加密视频轻松保存
  • ComfyUI-MultiGPU深度优化指南:释放GPU显存的分布式计算解决方案
  • 终极游戏分屏指南:让任何PC游戏都能本地多人对战
  • 本地代码AI工作流:Ollama+VSCode替代Codex实战指南
  • 2026:宁波专业除甲醛怎么选?避开低价陷阱,从服务落地能力对比,推荐宁波博豪环保 - 专注室内空气检测治理
  • 算法复杂度的多尺度建模与性能分层研究的技术8
  • 沧州家长口碑优选!2026单招择校高满意度机构,差异对比一目了然 - 快乐的大脚123
  • 终极Windows 11任务栏拖放修复指南:如何快速恢复高效操作体验 [特殊字符]
  • 2026年煲仔饭机深度测评:如何为餐饮场景匹配最佳方案? - 速递信息
  • 沈阳本地黄金回收24小时咨询,2026高价变现无忧 - 奢侈品交易观察员
  • 嵌入式GUI开发中位图资源优化:emWin转换器格式选择与性能调优实战
  • 2026 年邯郸厨卫屋顶防水修缮三家对比测评 吉修匠 99.8 分 - 吉修匠