GMS 1.4 YYC编译的游戏,如何安全地修改里面的文字和图片?(附UndertaleModTool实操)
GMS 1.4 YYC编译游戏逆向修改实战指南:从资源替换到字符串安全编辑
当你拿到一个用GameMaker Studio 1.4 YYC编译的游戏EXE文件,却发现没有熟悉的data.win文件时,是否感到无从下手?这种特殊的编译方式确实给游戏修改带来了挑战,但也并非无计可施。本文将带你深入理解YYC编译后的EXE结构,并手把手教你如何安全地修改其中的文字和图片资源。
1. 理解YYC编译的EXE三段式结构
YYC(YoYo Compiler)是GMS 1.4提供的一种特殊编译选项,它通过Visual Studio和Windows SDK将代码编译为原生机器码,同时将所有游戏资源嵌入到单个EXE文件中。这种编译方式移除了传统的data.win文件,大大增加了逆向工程的难度。要成功修改这类游戏,首先需要理解其内部的三段式结构:
解释器部分(文件开头到FORM标记)
- 包含GameMaker的运行时解释器
- 负责解析GMS特有的数据结构
- 由于是原生编译代码,这部分难以直接反编译
数据包部分(FORM到AUDO标记)
- 存储游戏资源:精灵、音效、着色器等
- 如果未被加密,FORM后通常跟着GEN8标记
- 可以使用专用工具提取和修改
代码部分(AUDO标记到文件结尾)
- 包含实际的游戏逻辑代码
- 由于是原生编译,直接修改极易导致崩溃
- 字符串资源特殊存储,可谨慎修改
提示:在开始任何修改前,务必备份原始EXE文件。错误的修改可能导致程序无法运行。
2. 提取和修改游戏资源
对于大多数修改需求(如替换UI图片、更改字体等),我们主要关注数据包部分。以下是使用UndertaleModTool处理嵌入资源的详细步骤:
2.1 准备工作
首先需要准备以下工具:
- 十六进制编辑器(如HxD、010 Editor)
- UndertaleModTool(最新版本)
- Python环境(用于运行提取脚本)
2.2 定位并提取数据包
- 用十六进制编辑器打开目标EXE文件
- 搜索十六进制序列
46 4F 52 4D(即"FORM") - 确认后面是否跟着
47 45 4E 38("GEN8") - 记录FORM的起始偏移量(通常在文件开头后0x200-0x1000字节处)
提取数据包的两种方法:
方法一:手动复制创建data.win
- 从FORM开始选择到AUDO前的所有字节
- 新建文件,粘贴这些字节并保存为data.win
- 用UndertaleModTool打开这个文件
方法二:使用提取脚本
# extract_gms_data.py import sys with open(sys.argv[1], 'rb') as f: data = f.read() form_start = data.find(b'FORM') audo_start = data.find(b'AUDO') if form_start != -1 and audo_start != -1: with open('extracted_data.win', 'wb') as out: out.write(data[form_start:audo_start]) print("数据包提取成功!") else: print("未找到FORM或AUDO标记")2.3 修改游戏资源
成功提取数据包后,可以在UndertaleModTool中进行各种修改:
替换图片资源:
- 在资源列表中找到目标精灵(Sprites)
- 右键选择"Replace"替换为新的PNG文件
- 注意保持图片尺寸相同或相近
修改字体设置:
- 定位到Font资源
- 可以修改字体名称、大小等属性
- 如需使用新字体,需确保目标系统已安装
调整音效:
- 找到Sound资源
- 可替换为相同格式的音频文件
- 注意保持采样率和位深一致
修改完成后,保存data.win文件。此时需要将修改后的数据重新注入回EXE:
# 使用dd命令合并(Linux/macOS) dd if=original.exe of=modified.exe bs=1 count=$FORM_OFFSET dd if=extracted_data.win of=modified.exe bs=1 seek=$FORM_OFFSET conv=notrunc dd if=original.exe of=modified.exe bs=1 skip=$AUDO_OFFSET seek=$AUDO_OFFSET conv=notrunc3. 安全修改游戏内文本
游戏中的字符串资源存储在代码段中一个特殊位置,通常位于SetEndOfFile函数引用之后。修改这些字符串需要格外小心,以下是具体方法和注意事项:
3.1 定位字符串区域
- 用十六进制编辑器搜索
53 65 74 45 6E 64 4F 66 46 69 6C 65(SetEndOfFile) - 字符串通常从该标记后几百字节处开始
- 查找可读的ASCII文本确认位置
3.2 字符串修改原则
| 修改类型 | 可行方案 | 风险等级 |
|---|---|---|
| 等长替换 | 直接替换相同字节数的文本 | 低 |
| 缩短文本 | 用空字节(00)填充剩余空间 | 中 |
| 增长文本 | 绝对禁止 | 高 |
关键规则:
- 绝不增加字符串总长度:这会破坏内存布局导致崩溃
- 保持文件大小不变:任何增加或减少文件大小的修改都会导致问题
- 空字节填充技巧:缩短字符串时,用00填充剩余空间
3.3 实际操作示例
假设要修改游戏中"Game Over"文本:
原始内容(十六进制):
47 61 6D 65 20 4F 76 65 72 00 (G a m e O v e r \0)安全修改方案:
- 改为"Level End"(等长替换):
4C 65 76 65 6C 20 45 6E 64 00 (L e v e l E n d \0)- 改为"End"(缩短并用空字节填充):
45 6E 64 00 00 00 00 00 00 00 (E n d \0 \0 \0 \0 \0 \0 \0)危险操作(绝对避免):
- 改为"Game Over!!"(增加长度)
- 删除末尾的空字节
- 在字符串区域外写入数据
4. 高级技巧与疑难解答
4.1 处理加密的EXE文件
如果FORM后不是GEN8标记,可能遇到了加密数据包。可以尝试以下方法:
查找解密函数:
- 使用IDA Pro或Ghidra分析EXE
- 查找对FORM数据的操作函数
- 可能需要进行动态调试
常见加密模式:
- 简单的XOR加密
- 字节位移
- 自定义加密算法
解密脚本示例:
def decrypt_data(encrypted): key = 0x55 # 示例密钥 return bytes([b ^ key for b in encrypted]) with open('encrypted.exe', 'rb') as f: data = f.read() form_start = data.find(b'FORM') decrypted = decrypt_data(data[form_start:])4.2 修改后的测试与验证
任何修改都应经过严格测试:
基础测试:
- 程序是否能正常启动
- 修改的资源是否显示正确
- 游戏逻辑是否受到影响
边界情况:
- 测试所有涉及修改资源的场景
- 检查内存使用是否异常
- 长时间运行稳定性
崩溃处理:
- 如果游戏崩溃,检查修改的字符串是否遵循规则
- 验证资源文件格式是否正确
- 使用调试工具分析崩溃点
4.3 保护自己的游戏
如果你是游戏开发者,想防止这类修改:
- 资源加密:对数据包部分使用强加密
- 代码混淆:使用商业混淆工具处理YYC输出
- 完整性检查:运行时验证关键代码段哈希值
- 字符串加密:不要直接存储明文字符串
// 示例:运行时字符串解密 const char* GetGameString(int id) { static const uint8_t encrypted[] = {0x12, 0x34, 0x56, ...}; static char buffer[256]; DecryptString(&encrypted[offsets[id]], buffer, keys[id]); return buffer; }修改YYC编译的游戏确实比标准GMS游戏更具挑战性,但通过理解其内部结构和使用正确的工具链,仍然可以实现安全的资源替换和文本修改。记住始终遵循"不改变文件大小"和"不增加字符串长度"这两条黄金规则,就能避免大多数崩溃问题。
