Back
Featured image of post 原神 2.8 KCP 验证密钥交互流程解析与流量解密

原神 2.8 KCP 验证密钥交互流程解析与流量解密

浅析原神的登录以及密钥交换流程

导言

自从原神更新 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 之类的字段,而服务器获取之后则对其进行解密和签名,发送带有 ServerSeedSeedSignatureGetPlayerTokenRsp

整个过程如下图所示:

所以很显然,这是一个标准的通过 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 位,而一个 WindSeedClientNotify5.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 + CmdId4567 0070 进行异或拿到 4 个 Bytes 长度的 key。接着我们需要找到一个 Metadata Length 始终为一个数的包,因为 WindSeedClientNotify 这个包的 Metadata Length 是动态的,因此我们需要获得到 Metadata Length XOR Key 以计算对应的偏移量。

我们选择了 WorldPlayerRTTNotify 这个包,因为它的 Metadata Length 始终为 0,于是我们又可以通过异或获取到总长度为 2*3 Byteskey。此时就可以计算 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 就不会消失,而更显然的是,这样的做法对于一家商业公司毫无意义。

comments powered by Disqus