---
title: 世界计划 缤纷舞台！feat. IL2Cpp
title_image: crop.png
subtitle: 从零开始解密 pjsk 的网络请求
tags:
  - ctf
  - 教程
---
玩了这么久プロセカ，有点想分析一下游戏的排行数据，做一些自动化，比如让自己一直停留在整两百名之类的。这种手游抓抓包模仿请求一定不会很难吧——装上 Reqable 连上模拟器抓抓包偷吃几块饼干之后发现，请求负载和返回内容都依然是乱码...

![[Screenshot 2024-10-24 062606.png|只偷吃到了饼干渣]]

看到乱码的第一反应是编码不对，或者过了某种基础混淆，总之先丢给 CyberChef 看看魔法糊糊器能不能把答案糊出来，发现不行。去群里问问之后 Menci 和我说是 AES 加密过的，还告诉我了加密密钥在 APIManager 里面，那就试着找找吧

## 1 从拆开 APK 开始

因为プロセカ的客户端能够解密，那么解密密钥一定就在客户端里。首先下载了プロセカ 4.0.0 的 XAPK 包。所谓的 XAPK 其实就是把多个 APK 压缩在一起啦，其中一个包含了所有架构共享的文件，另一个包里放了 arm64 平台专属的二进制（虽然这个游戏好像还没有编译过除了 arm64 以外的平台，不知道为什么还要这样分包）

解压缩之后单独用 apktool 把两个安装包的文件拆出来看一看吧。目录结构看上去就是一个一般的 IL2Cpp Unity 项目，没有什么特别的。编译过的游戏二进制在 lib/arm64-v8a/libil2cpp.so 里面，然后运行它、让它能够和 C# 那边的程序对接需要的元信息在 assets/bin/Data/Managed/Metadata/global-metadata.dat 里面。

![[2024-10-24 15-24.png|两个重要的文件]]

如果想要快速找到代码的话，只看 IL2Cpp 编译好的的二进制肯定不行。这个文件里一共有 44 万个没有名字的函数指针，IDA 把这些分析出来都花了快一个小时，从这里一个一个找肯定是不行的。所以我搜索了一下 IL2Cpp 反编译，找到了 Perfare/IL2CppDumper 这个工具，试了一下发现报错说 "Metadata file supplied is not valid metadata file"

![[2024-10-24 16-06 1.png|正常的元文件的文件头]]

为什么呢？看了一下，一般的 global-metadata 的文件头是 0xAF1BB1FA，而我拿到的这个不是，也就说明它很可能被加了壳。

## 2. 脱壳元文件

怎样解密呢？首先试了同一个作者的另一个工具 Zygisk-IL2CppDumper，是一个据说可以自动解密元文件的 Magisk 模块，但是构建好装上之后打开游戏发现什么都没有发生，说会解包到 /data/data 里面但是也没有，还是试试别的方法吧。

既然上次用 Cheat Engine 在电脑上从内存中脱壳了 maimai 的 Assembly-CSharp，我觉得应该也可以用同样的方式脱壳プロセカ的元文件，所以就下载了一份 Game Guardian 试了一下。打开游戏，挂载上 GG，内存搜索 "H AF1BB1FA" 之后马上就找到了！

![[Screenshot 2024-10-24 073615.png|搜索到了内存]]

把地址复制下来，去第四个 Tab 把地址粘贴进去之后点旁边的选择器选中这个区域的头到尾，导出成文件之后重命名成新的 global-metadata.dat 就可以了。之后重新试了一下用 IL2CppDumper 反编译就没有报错了！

![[2024-10-24 16-03.png|反编译成功了！]]

虽然还是有 "This file may be protected" 的报错，原本还以为 libil2cpp.so 也被套了壳，结果发现并没有，只是识别到了 JNI_OnLoad 预警性地报了一个错而已，也就是说已经成功啦。看了一眼 Dumper 输出的 `dump.cs`，里面确实有 APIManager，里面也确实用到了 AES 加密，但是因为实现的部分被转成二进制了，这里只能看到定义。

## 3. 找钥匙！

接下来就是找钥匙的环节，但是怎么找呢？如果要找到钥匙的话，大概需要看一下 APIManager 的代码实现，可是 `dump.cs` 里面只有空壳没有实现，可是只看 libil2cpp.so 又没办法知道 APIManager 在哪... 怎么办呢？看了一下 Dumper 仓库里面写的文档，看起来像是能和 IDA 联动让实现和空壳结合在一起的样子，那就下载一个 IDA 试试吧。

下载了 IDA 9.0，其实不太会用，我只懂得基础操作比如用 F5 切换反编译代码和汇编之类的，但是总之先打开二进制 libil2cpp.so 看看吧。刚打开的时候什么都没有，只有不到一百个函数，我还在怀疑是不是打开错文件了，但是等了十分钟又出现了很多函数，发现原来它只是还没有分析完... 可能 150MB 大的二进制对于 IDA 来说也太大了吧，完整分析完花了差不多一个小时，存的数据库有 2 GB 大 (╯’ – ‘)╯︵ ┻━┻

![[2024-10-24 16-26.png|好大文件]]

分析完之后依然没有函数名，全部都是 sub_1234567 这样的数字编号。看着 Dumper 的文档试着用 File > Script file 跑了一下那个 ida_py3 的脚本，选中了 script.json 之后函数名确实出现了！不过这只是第一步。

看了 `dump.cs` 里面的函数定义之后，我觉得 CreateCrypt 这个函数比较像是我们要找的，因为是一个静态函数，调用了之后会凭空造出来一个 FastAES 的实例，而 FastAES 需要传入两个钥匙才能创建实例，所以无论怎样这里都会用到钥匙吧？

![[2024-10-24 16-45.png|APIManager 的函数定义]]

看看反汇编的二进制代码发现确实用到了密钥。虽然变量全都被打乱了，但是从 FastAES 的构造器参数可以看出来 v3 v4 是 AES128 的两个钥匙 (key, iv)，然后规格是 AES128，密文模式 1 (CBC)，补格模式 2 (PKCS7)。那么这两个变量是从哪里来的呢？从代码里也能看到，v2 是从 v1 + 184 的指针地址读过来的，v3 是 v2 的值，v4 是 v2 的下一个值也就是 v1 + 192，而 v1 是 APIManager_TypeInfo 的地址。

![[2024-10-24 16-48.png]]

从 C# 的逻辑上想的话，TypeInfo 应该是一个类附带的常量或者变量类型的地方，从 `dump.cs` 里面可以看到它定义的变量指针偏差从 0 加到 8 加到 0x20, 0x28... 可是这里面没有 +184 = 0xb8 这个位置，为什么呢？

![[2024-10-24 19-27.png|APIManager 类的变量偏差最高到了 0x38]]

我在这里卡了超级久，然后看了一下构造器发现 +184 这个偏差原来是在调用构造器的时候动态写进去的，是代码里面没有定义的，也许是某种编译器优化吧。打开 `APIManager$$.cctor` 构造器首先发现的是它写入了指向 +184 的地方 (L23 行)，写入的是 v3 的值。向前读可以看到 v3 就是 v1，是一个空的 16 字节数组，然后把 v2 的数据复制到了 v1，而 v2 是 `Field__` 开头的这一长串。

![[2024-10-24 19-32.png|cctor 构造器的反编译代码]]

回到 `dump.cs` 里面查找这个字段，发现它是一个编译器生成的只读项，上面下面也有类似的东西，看这个 "Static Array Init Type Size" 的话这些应该就是代码里用到的数组常量被转移到的位置了。但是这一长串的十六进制似乎只是某种指针，数值似乎不在这里——因为如果是数值的话，数组的长度对不上——我们要找的 AES128 密钥是 128 位也就是 16 字节，但是这一长串每个都有 32 字节，和前面的 Array Size 也对不上。那么数值在哪呢？

![[2024-10-24 19-37 1.png|十六进制 offset 在编辑器里五颜六色]]

看到后面五颜六色的 offset 那里写了注释 "Metadata offset"，我就在想难道它会在 global-metadata 里面吗？用 hexed.it 打开了这个文件然后把 offset 复制到 Go to 栏里面转向，发现那里真的有一个 16 位的钥匙！接下来又按同样的方式找到了 16 位的 iv，整个 AES 加密就被破解啦（在这里打一下码，需要的话看完这篇肯定就能够自己找到了 quq）

![[2024-10-24 20-03 1.png|找到了 16 位的钥匙]]

## 4. 可以解密了！

接下来就是解密的过程了，把请求内容和返回内容都导出成文件，再随手用搓两行 python 脚本就可以解密啦：

```python
import sys
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from pathlib import Path

aes = AES.new(b"- key here qwq -", AES.MODE_CBC, b"- iv here meow -")
dec = lambda x: unpad(aes.decrypt(x), AES.block_size, style='pkcs7')

[f.with_suffix(".dec").write_bytes(dec(f.read_bytes())) 
    for f in Path(sys.argv[1]).glob("*")]
```

解密之后似乎是一个 pjsk 自己的二进制序列化格式，看起来还需要一些时间研究怎样把它转换成 json 呢，不过今天就到这里好啦，累累睡睡（x）

![[2024-10-24 21-02.png|解密之后的明文]]



