前言
近日在逆向某安卓手游的数据时,发现其AssetBundle、网络请求均使用AES加密。
使用Frida Il2Cpp Bridge工具可在仅使用已知名称的情况下,方便地实现对某个可实例化类型的实例钩取,查看其内部函数的调用参数和结果。
工具
将il2cpp lib和metadata.dat dump为dll结构的工具:il2cpp-dumper
Frida以及Frida server:Github
Frida Il2Cpp Bridge:Github
dnSpy:Github
IDA Pro 7.7:百度网盘
NodeJS:官方网站
Python:官方网站
使用环境
PC:Windows 11
手机:Mi 8
Root:Magisk 27.0
实战
对APK的预操作
- 使用解压软件解压APK,并找到
/lib/arm64-v8a/libil2cpp.so
和/assets/bin/Data/Managed/Metadata/metadata.dat
。
- 启动
il2cpp-dumper
,依次选中libil2cpp.so
和metadata.dat
,等候其生成dump出的文件。我们在此仅关注DummyDll文件夹中的Dll结构。
- 使用IDA(在此选用7.7版本,因为最新的8.3版本并不支持对arm64的反汇编)导入
libil2cpp.so
,并使用工具栏中的File/Script file...
选取il2cpp-dumper
目录下的ida_with_struct_py3.py
,依次选择对应的文件即可将名称信息导入IDA。
使用IDA进行初步分析
经过一段时间的分析,发现目标程序对Unity.ResourceManager
进行魔改,并加入了SecurityStreams
类进行加密:
接着,打开IDA定位到此函数,并寻找其函数的调用,发现如下调用:
由此可知,IV通过文件名计算而来,Key通过ToArray函数而来,但此函数有5000多行,很难分析参数v307的结构和其偏移对应的值。
调用Decrypt函数的时候需要传入Key和IV,因此钩取该函数的调用参数值成为了最佳选择。
使用Frida和Frida Il2Cpp Bridge进行Hook
配置Frida环境
- 使用带有Root手机开启adb,使用
adb push frida-server-xx.x.x-android-arm64 /data/local/tmp
将其置入手机
- 使用
adb shell
进入命令行, su
命令获取root权限,进入路径cd /data/local/tmp
,为可执行文件添加权限chmod +x ./frida-server-xx.x.x-android-arm64
然后启动可执行程序./frida-server-xx.x.x-android-arm64
- 在电脑上安装Frida
pip install frida frida-tools
配置Il2Cpp Bridge环境
创建文件夹并创建文件index.ts
编写package.json
,使其识别index.ts
为源代码,编译出frida可用的js。
{
"main": "index.ts",
"scripts": {
"prepare": "npm run build",
"loop": "frida-compile index.ts -w -o frida.js"
},
"dependencies": {
"frida-il2cpp-bridge": "^0.8.8"
}
}
在此目录下打开命令行并运行npm run loop
会自动安装所需的库,该命令行会不断检测index.ts
是否被修改,然后重新编译。
接着编辑index.ts
,在首行加入import "frida-il2cpp-bridge"
,可成功编译,环境配置完毕。
编写Hook代码
编写一个Hook代码,使其定位Unity.ResourceManager
,并Hook默认命名空间中的SecurityStreams
类,对其进行Attach。
Il2Cpp.perform(() => {
const SecurityStreams = Il2Cpp.domain.assembly("Unity.ResourceManager").image.class("SecurityStreams");
Il2Cpp.trace(true).classes(SecurityStreams).and().attach();
});
进行Attach后,在该类被初始化、实例方法被调用时,能够自动在控制台输出详细参数。
进行Hook
当index.ts
被自动编译为frida.js
完成后,使用frida注入程序。
frida -U -f app.package.name -l .\frida.js
进入游戏,函数被自动Hook,在调用Decrypted时,参数被打印于控制台:
由于读取多个bundle时的key一致,因此得出key,由于Unity asset bundle文件的前16字节是基本相同的,因此无需考虑iv造成的前16字节乱码,直接将其定义为可识别的头部即可通过AssetStudio等程序拆出资源文件。
举一反三
经过抓包,发现几乎所有网络请求都是被加密的。经过一段时间的搜寻,发现其网络请求函数SendTgServerEncrypted
:
但此函数并没有传入key等,由此可见key应该在函数内部进行计算。
此时打开IDA,对此函数进行分析:
可见NetworkUtils被初始化后直接获取该类的key和otp字段,然后进行密钥计算。由此可知key应该是固定值并在cctor中被赋值。
因此,对初始化函数进行分析:
由此可见,一些编译器生成的PrivateImplementationDetails
偏移值被sub_EDC5C8
初始化后才能够变成有效偏移值,因此通过静态分析得出真正的key值极为困难。
不过我们知道,想要通过AES加密必须初始化一个AES加密类,我们在此发现了游戏函数对该类型的引用:
mscorlib
中的System.Security.Cryptography.Aes
看起来并没有什么有用字段,通过翻阅一些文档,发现其实际上使用System.Core
中的System.Security.Cryptography.AesCryptoServiceProvider.CreateDecryptor
创建解密器,而该类型是这样的:
由此可见,我们仅需钩取System.Security.Cryptography.AesCryptoServiceProvider
类即可得知CreateDecryptor
函数被调用时的参数。
尝试钩取,特意发送几个网络请求,同时获得以下信息:
使用该密钥,空IV解密,发现前16byte为乱码,但16byte后明文完整。合理猜测response前16byte为IV,后面为密文,使用该逻辑成功解密出明文。
尾声
Il2Cpp Bridge还有很多其它实用功能,比如直接定位某函数并通过invoke()调用,获取返回值,若对其感兴趣可以阅读官方wiki。
本人第一次在论坛发表文章,也是为了记录自己的逆向学习过程,若有不足请大佬多多指出!