Yuris 引擎有两个密钥,第一个密钥没难度。关键是第二个密钥,用 4 字节循环异或文件内容。没有这个密钥,就无法提取文本,也无法在封包的时候给 Garbro 提供正确的子文件(正确的子文件 = 解压且加密)

这个问题曾经卡过我一段时间,最后好一番折腾,直到阅读 Garbro 源码才找到问题的突破口,这也是我当初为什么写 Yu-Ris 引擎研究 的原因


已有解法

参考 [YU-RIS] 寻找脚本密钥ZQF-ReVN/RxYuris

原理是这样的:Yu-Ris 引擎加密的脚本里,在文件的特定位置,总会出现一处 00 00 00 00(代表某个数据块开始地址),这个位置跟 4 字节密钥一异或,还是密钥本身——相当于密文里必有一处地方连续 4 个字节就是密钥。只要推算这 4 个 00 在明文的位置,就能直接从密文的相同位置处读出密钥。而这 4 个 00 的位置总可以通过它前面的字节信息推算(根据 Yu-Ris 版本不同,计算方式略有差异)

这个方法,曾经解决了我的问题。但现在想想。它之所以能解出来,这两个条件缺一不可。一是明文里必出现 4 个连续的 00,二是我们知道这 4 个 00 的位置

这个方法有几个问题:

  • 如果以后引擎修改了数据结构,或者开发者修了这个漏洞。明文里不再必然出现 4 个连续的 00,方法就失效了
  • 不同 Yu-Ris 版本的数据结构不一样,导致不同版本这 4 个 00 出现的位置不固定,现在可以根据不同版本推算位置,以后出新版本可能得重新研究
  • 不只 Yu-Ris,其它类似加密方法的游戏也有不少,我们不可能每次都发现这么好的特征。假设我们以后遇到同类问题,在不知道明文特征的情况下,这个方法就失效了,还得回到动态调试找密钥


Yu-Ris 密钥新解

最近,沿着 用仿射模型破解一类文件加密算法 的方向思考,偶然发现了一个新的解法

首先明确一点,只有密文肯定还是不够的,这个方法需要提供一小段明文。不需要多,明文至少 8 字节就行(对应 4 个 sjis 字符),任意位置都可以

所幸作为一个 VN 游戏,我们获取一小段明文非常容易——点击一段对话,找 4 个字转 sjis 就有一段明文了,然后结合 FileMon 等工具就能知道它来自哪个游戏脚本(密文)

问题转换为:

已知一个游戏的密文是用 4 字节密钥异或加密,以及一段至少 8 字节明文,求游戏密钥

想象一个比较快乐的事情,我们知道这段明文在密文中对应的位置。那么,只要拿这 8 字节明文跟对应位置的 8 字节密文异或,直接就能读出密钥。因为在 GF(2) 里,若

$$ y = x + key $$

$$ x + y = x + (x + key) = key $$

因此,知道子明文在密文中的对应位置,就可以求密钥

可以把问题再转换为:

已知一个密文是用 4 字节密钥异或加密,以及一段至少 8 字节明文,求明文来自密文中的哪个位置

假设 Yu-Ris 的密钥是 b1 b2 b3 b4,明文是 x,密文是 y,它们每个字节分别是 x_i 和 y_i,那么在 GF(2) 里,可以表示为

$$ y_1 = x_1 + b_1 \\ y_2 = x_2 + b_2 \\ y_3 = x_3 + b_3 \\ y_4 = x_4 + b_4 \\ y_5 = x_5 + b_1 \\ ... $$

$$ y_{4k+i} = x_{4k+i} + b_i $$

(k 是自然数,i 是 0,1,2,3)

很容易发现,因为循环异或的周期性,密文中每个字节都跟 4 个位置前的字节异或的是同一个字节

利用 GF(2) 满足交换律和结合律,且异或跟自身互逆的特点,可以把密钥抵消

$$ y_1 + y_5 = (x_1 + b_1) + (x_5 + b_1) = x_1 + x_5 \\ y_2 + y_6 = (x_2 + b_2) + (x_6 + b_2) = x_2 + x_6 \\ y_3 + y_7 = (x_3 + b_3) + (x_7 + b_3) = x_3 + x_7 \\ y_4 + y_8 = (x_4 + b_4) + (x_8 + b_4) = x_4 + x_8 \\ ... $$

$$ y_i + y_{i+4} = x_i + x_{i+4} $$

这个恒等式非常好,好在左边只有明文,右边只有密文,都是已知的信息

它告诉我们:

  1. 假设把明文每个字节(从第 5 字节起)跟 4 个地址前的字节异或,去掉前 4 字节后,得到一组新字节(设为 Δy)
  2. 同样把密文每个字节(从第 5 字节起)跟 4 个地址前的字节异或,前 4 字节不变,得到一组新字节(设为 Δx)
  3. 那么 Δy 必在 Δx 里出现

这就纯粹是一个简单的搜索了,当然,既然是搜索就可能出现多个匹配结果,这时候就要一一试探了

为了保证搜索结果的唯一性,选一段明文的时候,最好选择不常见的文本,且明文越长越好。而 VN 游戏里最不缺的就是对话,一个字对应 2 字节明文,找十几个字就有二十几个字节的明文。搜索结果的唯一性很好保证

找到 Δy 在 Δx 里的位置,就知道明文在密文中的位置,拿明文跟这个位置 - 4 的密文异或,密钥也就破了


举例

以 Yu-Ris 引擎的游戏 mari_san 为例

收集密文和一段明文

打开 ProcMon 监控程序,打开游戏,点开第一句对话

ProcMon 过滤 CreateFile 函数且路径包含 ybn 的记录

可以发现,打印 “おきて。朝よ…” 这句话时,程序访问了文件 yst00123.ybn

那么 yst00123.ybn 就是密文

选 “おきて。朝よ” 转 sjis 编码,一共 12 字节:82 A8 82 AB 82 C4 81 42 92 A9 82 E6,作为子明文


计算密钥

子明文第 5 字节起,每个字节跟 4 个字节前的字节异或,去掉前 4 字节,得到

00 6C 03 E9 10 6D 03 A4

密文前 4 字节不变,第 5 字节起每个字节跟自己 4 字节前的字节异或,得到

在处理后的密文文件里搜索处理后的明文,找到唯一匹配位置 1440h

这就是关键位置。回到原密文文件,倒推 4 个地址,从 143Ch 开始取 12 字节:

51 C7 2E 3D 51 AB 2D D4 41 C6 2E 70

再跟原明文

82 A8 82 AB 82 C4 81 42 92 A9 82 E6

异或,结果是

D3 6F AC 96 D3 6F AC 96 D3 6F AC 96

显然,密钥是

D3 6F AC 96

或者分别向右旋转 1 到 3 字节的 6F AC 96 D3AC 96 D3 6F96 D3 6F AC 之一。看搜索结果的位置 mod 4 的余数决定,因为这里 143C mod 4 = 0,所以向右旋转 0 字节,第一个就是密钥


验证密钥

将求出的密钥 D3 6F AC 96 跟原密文异或验证

异或后发现原本无法解释的密文能看到大段有意义的 sjis 明文(中间非文本的地方是占位符),证明密钥确实是这 4 个字节

如果异或之后结果还是乱码,那就依次尝试 6F AC 96 D3,AC 96 D3 6F 和 96 D3 6F AC,直到密钥对齐,看到显著的明文为止


推广

虽然上文一直在解这个问题:已知一个用 4 字节密钥异或加密的密文,已知至少 8 字节连续明文,求异或密钥

但显然,这个方法不只 4 字节,任意字节都可以——

已知一个用 N 字节密钥异或加密的密文,求异或密钥? → 想办法收集至少 2N 字节连续明文就行,方法是类似的


代码

把整篇博客输入 AI,很快就把工具开发出来了。这是一个已知密文是 N 字节异或加密时,通过输入少量明文破解密钥的工具

https://github.com/dantecsm/xor-key-recovery

后续可能改进:

  • 提供自动枚举密钥周期破解的功能。假定密文是一组字节异或加密,但不知道字节长度,那就自动枚举 N = 2,N = 3 …… 各种周期,推算密钥
  • 最大周期根据明文解释能力上限决定,即输入的明文字节长度除以 2 向下取整


启发

凡是被周期影响的东西,一个解决方向是找两个被周期影响序列,把它们的相位对齐,然后做差分运算(或者说这个周期影响的逆运算),把这个影响抵消掉。

就像潮涨潮落,只要两片海域处在潮汐周期的同一相位,那么把它们的高度相减,总能拿到在没有潮汐影响下海平面的高度差。


更新:未知明文攻击

我们上面的方法,需要至少 8 字节明文,那没有明文怎么办?

→ 那就猜明文,把它转换成已知明文攻击

比如说文件协议里必出现字节,魔术字,还有游戏里大概率会出现的文本,大概率会出现的连续字节等,这些都可以试试作为特征。

一、常用语

比如说 VN 游戏,大概率会出现文本 “さようなら”(sa yo na ra),“ありがとう”(a ri ga to),“こんにちは”(kon ni chi wa)等,它们都是 5 字 10 字节,满足分析 Yu-Ris 引擎至少 8 字节的要求。

  1. 首先,对游戏里所有 ybn 后缀的文件做 4 字节滚动异或,每个文件按 4 字节对齐后合并成一个大文件,作为搜索空间。
  2. 然后,选一个文本对它做 4 字节滚动异或,拿到 6 字节,作为特征码。
  3. 最后,把特征码放到搜索空间去搜索,把密钥猜出来。

当然,这只是经验文本,如果要搞得更细一些。不妨用统计分析——可以用 Garbro 解包多部 VN 游戏文本,以游戏为单位搜索每个游戏里最常出现的连续 N 字文本,统计频率,归纳游戏中最常出现的连续明文作为已知明文假设。

二、连续 00

大多数游戏脚本,脚本语言 + 引擎实现方式决定了它本身容易出现大量连续 00,而且这还不是偶然,只要涉及到数据结构对齐,padding,未赋值字段,初始地址,默认数值等,成片的 0x00 就是高度可预期的特征。

比如上面游戏的脚本明文 yst000123.ybn,搜索 8 个以上连续 00 就有 19 个结果。旁边的 yst000124.ybn,一搜更是有 41 个结果。

所以连续 00,也是未知明文攻击相当具有判别力的一个锚点。

我用自己的工具实际解了一遍。只输入 8 个 00 作为明文破解,发现匹配结果不是太少,而是太多。

如图,处理后的文件搜索出 301 个匹配结果:

这也不是大问题。只要修改程序,让它继续用所有匹配结果算出 4 字节密钥,再按频率依次按最常出现的一一验证就行。(因为从搜索结果里明文至少有 19 处连续 8 字节 00,说明这些计算结果里必有正确密钥)

将推算的密钥去重得到 37 种密钥,其中重复出现最多的密钥是 D3 6F AC 96。跟上面的结果一样,正是正确密钥

可见,一路下来我们一不开游戏,二不起 OD,只知道文件是 4 字节密钥异或加密结果,用统计分析加一些计算,就能把密钥破了。