导言
自从原神更新 2.8 以后,引入了 RSA 和 Seed Exchange 机制,导致了传统的抓包解析工具统统失效,在万众以为只能依靠 Akebi-GC 来解析的时代,来自 Sorapointa 的 WetABQ 为我们带来了无需 Cheat 或 Patch 的抓包工具 MagicSniffer。都什么年代了,还在用传统 Iridium,Sorapointa 写的 MagicSniffer,包含了 WindSeed 的魅力。
Seed Exchange
原神在 2.8 之后,米哈游通过引入 RSA 和 Seed Exchange
机制,实现了一种在提前分发好 RSA
密钥的情况下进行类似「服务器验证」的流程,用以阻止 PS
的进一步扩张。自此拿不到 RSA Key 的我们,只能对客户端进行 Patch
才能进行原神 PS 的开发和游玩,对于抓包更是如此,暴力破解 客户端的
UInt64
Seed 大概有 \(2^{64}
\thickapprox \pu{1.8e19}\)
种可能,几乎不现实(当然或许有矿老板可以破解)。
客户端先向服务器发送 GetPlayerTokenReq
包,其中包含了一个经过 RSA 加密的 ClientSeed
与其它诸如
KeyId
之类的字段,而服务器获取之后则对其进行解密和签名,发送带有
ServerSeed
和 SeedSignature
的
GetPlayerTokenRsp
。
整个过程如下图所示:
所以很显然,这是一个标准的通过 RSA 建立安全通信信道的流程,作为中间人我们最多只能获取 Server Seed 以及验证其签名,即便使用 Proxy 篡改 Client Seed 也无法做到不 Patch 客户端的 Sniffer。
但是可惜的是原神因为性能原因,基于 RSA 分发的不是 AES Key,而是 XOR Key。
WindSeedClientNotify
我们仍旧不清楚为什么一个大部分时间都是单机,联机偏向合作的一个神笔大世界探索游戏,签名反作弊驱动两代同堂,外加一个远程执行的
Lua 反作弊脚本(即大名鼎鼎的 WindSeedClientNotify
,RCE
WARNING 😱 )在每一次进入游戏的时候都会被客户端从服务器上加载。
尽管米哈游在反作弊上魔怔至极,但正因如此,其非常长的反作弊 Lua 脚本却成了今天 MagicSniffer 的突破口。
Hoyoanticheat ultimately fucked Hoyocryptology themselves 🤡
众所周知,米哈游的 XOR Key 长达 4096
位,而一个
WindSeedClientNotify
有 5.7+W
长度,并且几乎每个版本每个账号甚至于每个系统的反作弊 Lua
都长的几乎一致,相当于我们清楚了这个 Packet 对应的明文。
已知明文攻击
对于异或,一个显而易见的性质是
\[ \text{Message} \oplus \text{Key} = \text{Encrypted Message} \to \text{Message} \oplus \text{Encrypted Message} = \text{Key} \]
基于这一点,我们可以很容易地进行一次已知明文攻击,因为我们清楚 \(\text{Message}\)(明文)和 \(\text{Encrypted Message}\)(密文),前者可以通过上一个版本或者 Akebi-GC 的抓包器来获取。
原神 KCP 协议包结构如下:
首先我们需要从一堆包中获取
WindSeedClientNotify
,所以我们需要知道 CmdId
的 XOR Key(对应 4 Bytes
,其中 Magic Start
是明文且不会变动,因此可以直接进行已知明文攻击,获取前
4 Bytes
的 XOR Key 用于定位包的次序)。
基于观察,我们清楚在进行 GetPlayerTokenRsp
之后一定是
PlayerLoginReq
,而前者使用 dispatchKey
进行异或,与后者,以及后面所有的 Packets 使用的 key
被更新成了密钥交换后的新 XOR Key,所以我们可以通过对比
Magic Start
是否变化,来确定 PlayerLoginReq
的位置。
在 2.8,PlayerLoginReq
的 CmdId 是
0x70
,我们可以对这个变换后(新 XOR Key)的
Magic Start + CmdId
和 4567 0070
进行异或拿到
4 个 Bytes 长度的 key
。接着我们需要找到一个
Metadata Length
始终为一个数的包,因为
WindSeedClientNotify
这个包的 Metadata Length
是动态的,因此我们需要获得到 Metadata Length
XOR Key
以计算对应的偏移量。
我们选择了 WorldPlayerRTTNotify
这个包,因为它的
Metadata Length
始终为
0
,于是我们又可以通过异或获取到总长度为
2*3 Bytes
的 key
。此时就可以计算
WindSeedClientNotify
的偏移量:
\[ \text{Offset} = \text{Metadata Length} + \frac{4 + 4 + 4 + 8}{2} \]
此时我们就可以拿到 Body Data 了。
将数据按照 4096
的长度分段,每段异或一次,就可以得到
key
,但是有一些 Block 中的数据因设备而异,我们可以将得到的
key
保存到一个表里面,将所有的 key
按照出现次数进行排序,出现最多的那个即为真实的 key
。
结语
即便米哈游魔改了 WindSeedClientNotify
我们也可以通过
DummyClient 等方式获取一些 payload
非常大的包进行保存,同样进行已知明文攻击,即使最终米哈游还是魔改了协议,实在不行用
Akebi-GC 也一样抓包。
2.8 的更新主要是为了阻止 PS 扩散和降低 PS 的热度(Server Validation),防抓包只是这个过程中的副产品,我们不认为米哈游会采取进一步的措施继续阻止 PS Dev,只要米哈游不重头重构原神代码 PS 就不会消失,而更显然的是,这样的做法对于一家商业公司毫无意义。