Yu-Ris 引擎识别
在 exe 上右键 → 属性 → 详细信息

可以知道这个游戏是 YU-RIS 引擎,版本 463
ypf 封包简介
封包文件是 ysbin.ypf,可以通过 Garbro 解压出一个 ysbin 文件夹,里面有很多子文件,每个子文件都是先加密,后压缩,然后拼在封包里。加密算法是异或一组 4 字节密钥,压缩算法是 zlib
顺便一提,一般我们关心的文本藏在 ybn 后缀的文件中。每个 ybn 都是一个自制虚拟机,类似于 kid 早期的 Seraphim 引擎或 Elf 的 AI2/3/5/Win 以及基于它改版的 Adv98 引擎,文本作为文本命令 op 的参数存在。但这不是本文的重点,不展开
ypf 封包结构解读(463 版本)

struct YpfHeader {
uint32_t mMagic; // 字符串 = YPF\00
uint32_t mVersionTmp; // 版本号,比如 1CF 对应 463
uint32_t mFileCount; // 子文件数
uint32_t mDataStart; // 子文件 Entry 数组结束地址,同时也是第一个子文件内容开始地址
uint8_t mZero[16]; // 固定 16 个 00
};
// 子文件描述区,是 mFileCount 个 YpfEntry 组成的字节区间
struct YpfEntry {
uint32_t nameHash; // 子文件名校验码,crc32
uint8_t nameLength; // 要解密,这个字节跟第一个密钥异或后得到文件名长度
uint8_t name[nameLength];// 要解密,这组字节跟同上密钥异或后得到文件名
uint8_t filetype; // 子文件类型,用 0-9 的枚举值代表不同类型
uint8_t isPacked; // 是否压缩
uint32_t unpackedSize; // 子文件解压后大小
uint32_t size; // 子文件当前大小(压缩状态)
uint32_t offset; // 子文件开始位置
uint32_t checkSum; // 子文件压缩内容的校验码,Adler32(上面的校验码对应文件名,这里的校验码对应文件内容)
};1. nameLength
这里的 nameLength 是一个加密的字节,需要解密后才知道文件名长度
解密方式是:先根据版本号查一个表,把查到的字节作为异或密钥,查不到就返回 0xFF
比如上图中的 0xED,版本 463 在表中没有对应值,所以得到 0xFF,跟 0xED 异或得 0x12,所以文件名长度为 18
2. nameHash
子文件名校验码 nameHash ,对应 010 Editor 默认的 crc32 对解密后的文件名运算后的结果,如图:

ypf 封包中的每个子文件项都按这个 hash 排序 (而不按文件名排序)
比如这里:
- 第 1 段: 22 CB BF 00,说明 nameHash = 00BFCB22 的是第 1 个文件
- 第 2 段: F7 97 03 03,说明 nameHash = 030397F7 的是第 2 个文件
- 第 3 段: 9D 66 11 03,说明 nameHash = 0311669D 的是第 3 个文件
- …
3. checkSum
既然讲了子文件名校验码,那就一起讲子文件压缩内容验码 checkSum
用的是初值为 0xFFFFFFFF,多项式为 0x04C11DB7 的 Adler32 校验算法(010 Editor 验证)
4. fieltype
文件类型 filetype 有以下几种
| 版本号 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| 0x0F7 | ybn | bmp | png | jpg | gif | avi | wav | ogg | psd | - |
| 0x122, 0x12C, 0x196 | ybn | bmp | png | jpg | gif | wav | ogg | psd | - | - |
| 0x1F4 | ybn | bmp | png | jpg | gif | wav | ogg | psd | ycg | psb |
举例:解读一个 ypf 封包
以下图的 ysbin.ypf 为例

这是文件头
59 50 46 00 // YPF\00
CF 01 00 00 // 版本号 1CF = 463
97 00 00 00 // 0x97 = 151 个子文件
33 18 00 00 // 子文件 Entry 数组到这里结束,或者说:第一个子文件内容从 0x1833 开始
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 // 固定 16 个 00子文件 Entry 数组是重复结构,这里以第一个子文件 Entry 结构体为例
22 CB BF 00 // 子文件名校验码 00BFCB22
ED // 如上所述,463 版本的异或密钥是 0xFF,ED 跟它异或是 0x12,所以后面 0x12=18 个字节代表子文件名
86 8C 9D 96 91 A3 86 8C 8B CF CF CF C7 C8 D1 86 9D 91 // 这 18 个字节一样跟 0xFF 异或,得到子文件名 ysbin\yst00087.ybn
00 // 子文件类型是 00,根据上表可知是 ybn
01 // 是否压缩 = 是
5C 08 00 00 // 子文件解压后大小 0x85C = 2140 字节
84 03 00 00 // 子文件当前大小(压缩状态) 0x384 = 900 字节
73 1F 09 00 // 子文件开始地址 0x91F73
E2 DE 50 C6 // 子文件压缩内容的校验码 C650DEE2根据封包里描述的子文件 Entry 信息,我们可以按区间和文件名提取出所有子文件,但这些子文件是先加密后压缩的,需要先解压后解密才能查看
ybn 文件解压与解密(版本 463)
解压
压缩算法是 Zlib,压缩内容也是非常标准的 Zlib 格式字节流,可以用 python 或 nodejs 的 Zlib 库直接读取解压
解密
加密算法是异或,密钥是 4 字节,每个游戏的密钥是不一样的。
但要注意,它不是从文件开始就异或,具体讲:
先看前 4 字节是否为 YSTB,是则解密,否则不用解密
需要解密的文件,虽然都是 4 字节密钥异或,但不同版本异或的区间不一样。对 463 版本的 Yu-Ris 引擎,每个 ybn 分为前 32 个字节的文件头和 4 个子文件区间,然后
- 文件的前 32 字节描述了多个地址区间,这些区间都是位于 32 字节后到文件末尾的子文件区间
- 解密算法是对第 32 字节后的每个子文件区间分别用同一个 4 字节密钥异或
解密举例
这是从 ypf 封包中解压后,我们拿到的 yst00092.ybn 结果

前 32 (0x20) 字节如下
59 53 54 42 CF 01 00 00 61 00 00 00 84 01 00 00
18 0C 00 00 66 0B 00 00 84 01 00 00 00 00 00 00
...首先,前 4 字节 59 53 54 42 = YSTB,说明需要解密
接着,前 32 字节按每 4 个为一组,得到 8 组。对于 463 版本而言,从 0 算起,则第 3 到第 6 组就是 4 个子文件区间长度
59 53 54 42 0, magic = YSTB
CF 01 00 00 1, version = 1CF = 463
61 00 00 00 2
84 01 00 00 3
18 0C 00 00 4
66 0B 00 00 5
84 01 00 00 6
00 00 00 00 7第 3 到 6 组分别为 0x184, 0xC18, 0xB66, 0x184
那么,子文件区间为
- 区间 1: [0x20, 0x20+0x184]
- 区间 2: [0x20+184, 0x20+0x184+0xC18]
- 区间 3: [0x20+0x184+0xC18, 0x20+0x184+0xC18+0xB66]
- 区间 4: [0x20+0x184+0xC18+0xB66, 0x20+0x184+0xC18+0xB66+0x184]
若本游戏的密钥为 D3 6F AC 96,就用它分别异或这 4 段子文件区间,可得解密后的结果
再举一个例子,这时从 ypf 封包解压后,我们拿到的 yscfg.ybn

前 4 字节 ≠ YSTB,说明不需要解密,已处理完毕
Garbro 封包
封包就是修改游戏文件后,用 Garbro 把 ysbin 文件夹打包回 ysbin.ypf 的过程。准备替换原文件

如图,在软件里对 ysbin 文件夹右键 → 创建压缩文件,压缩文件格式选择 YPF 后,填这两个参数
- 压缩文件版本:就是 exe 属性中版本信息的十六进制,或者找游戏原版的
ysbin.ypf第 5-8 字节 - 8 位密钥:就是一个字节,这个字节是该版本对应的单字节密钥,也是原 ypf 封包中,在文件 Entry 项里加密文件名长度和文件名用的单字节密钥
注意: 打包对象的 ysbin 文件夹中的 ybn 文件不应该是解压解密后的状态,应该是解压且加密的状态。比如说:你能在待压缩的文件里本该加密的 ybn 文件中看到明文,那就错了,你需要把它用 4 字节密钥加密后再封包
不难看出,封包过程我们需要 2 个密钥,一个是加密子文件名的单字节密钥,一个是加密 ybn 文件内容的 4 字节密钥
找子文件名的单字节密钥方法
从 ypf 封包文件入手,找 0x25 开始 0x20 个字节,尝试跟 00-FF 异或,找到异或后出现 ysbin, yst, ybn 之类的结果,出现了就代表参与异或的字节可能是密钥

从第 0x25 字节开始,是因为前 0x20 字节是 ypf 的头部,跳过后开始子文件第 1 个 Entry,Entry 固定在 4 字节 nameHash 和 1 字节文件名长度后,开始出现被该密钥异或得到的子文件名
或者,假定解包后的文件夹名是 ysbin,那么简单的方式是:第 0x25 开始的 5 个字节跟 ysbin 的 ascii 码做异或,即跟 79 73 62 69 6E 异或
找 4 字节密钥方法一
这个方法建立在 Garbro 解包文件后,得到的是解压且解密的结果,如果只解压不解密(即打开解包结果看不到明文)就用不了
先用 Garbro 解包,解包后得到一批解压解密的 ybn 文件
然后分析原游戏的 ypf 封包文件,找到其中一个子文件的压缩内容(找一个子文件 Entry 的 offset 和 size,共同定位该子文件的压缩内容区间),把它复制出来,它就是标准的 Zlib 字节流,可用
const fs = require('fs');
const zlib = require('zlib');
// 解压文件函数
function decompressFile(inputFile, outputFile) {
const compressedData = fs.readFileSync(inputFile);
const decompressedData = zlib.inflateSync(compressedData); // 核心语句
fs.writeFileSync(outputFile, decompressedData);
}把它解压,得到

然后,解压内容跟 Garbro 解包的同文件内容做一次逐字节异或,得到密钥

显然,这个游戏的子文件密钥是 D36FAC96
找 4 字节密钥方法二
跟上一步一样,得到解压后的一个子文件后

用 ZQF-ReVN/RxYuris 的 Release 里提供的 YSTBGuessKey.exe,跑这个文件解密,原理是 ybn 明文中可预测 4 个 00 出现的位置,如果对它们异或,那么密文中相同位置就是密钥

这里 0x96AC6FD3 是写为大端序的,如果是实际 XOR 顺序,则是 D3 6F AC 96
免封包
正确封包后的 ysbin.ypf 覆盖原文件,游戏可以打开
但不止如此,如果我们直接把解包但未解密的 ysbin 文件夹放到程序同目录下,也可以打开游戏(这里用到了免封包的知识,参考 https://www.cnblogs.com/Dir-A/p/18096964)
这个游戏可以免封包,是因为 ypf 封包里的 yscfg.ybn 文件,把 filePriorityDev, fileProrityDebug 和 filePriorityRelease 三个值都设为 1。这意味着它先读取封包里的子文件 yscfg.ybn 后,后续每个子文件都优先从当前目录下找,因此免封包生效
暂无评论