近日在逆向某安卓手游的数据时,发现其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的预操作
[ol]
[/ol]
使用IDA进行初步分析
经过一段时间的分析,发现目标程序对Unity.ResourceManager进行魔改,并加入了SecurityStreams类进行加密:
图片1.jpg (62.2 KB, 下载次数: 0)
下载附件
2024-2-21 12:45 上传
接着,打开IDA定位到此函数,并寻找其函数的调用,发现如下调用:
图片2.jpg (60.55 KB, 下载次数: 0)
下载附件
2024-2-21 12:46 上传
由此可知,IV通过文件名计算而来,Key通过ToArray函数而来,但此函数有5000多行,很难分析参数v307的结构和其偏移对应的值。
调用Decrypt函数的时候需要传入Key和IV,因此钩取该函数的调用参数值成为了最佳选择。
使用Frida和Frida Il2Cpp Bridge进行Hook
配置Frida环境
[ol]
[/ol]
配置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时,参数被打印于控制台:
图片3.jpg (127.55 KB, 下载次数: 0)
下载附件
2024-2-21 12:46 上传
由于读取多个bundle时的key一致,因此得出key,由于Unity asset bundle文件的前16字节是基本相同的,因此无需考虑iv造成的前16字节乱码,直接将其定义为可识别的头部即可通过AssetStudio等程序拆出资源文件。
举一反三
经过抓包,发现几乎所有网络请求都是被加密的。经过一段时间的搜寻,发现其网络请求函数SendTgServerEncrypted:
图片4.jpg (46.5 KB, 下载次数: 0)
下载附件
2024-2-21 12:46 上传
但此函数并没有传入key等,由此可见key应该在函数内部进行计算。
此时打开IDA,对此函数进行分析:
图片5.jpg (71.16 KB, 下载次数: 0)
下载附件
2024-2-21 12:46 上传
可见NetworkUtils被初始化后直接获取该类的key和otp字段,然后进行密钥计算。由此可知key应该是固定值并在cctor中被赋值。
因此,对初始化函数进行分析:
图片6.jpg (225.08 KB, 下载次数: 0)
下载附件
2024-2-21 12:46 上传
由此可见,一些编译器生成的PrivateImplementationDetails偏移值被sub_EDC5C8初始化后才能够变成有效偏移值,因此通过静态分析得出真正的key值极为困难。
不过我们知道,想要通过AES加密必须初始化一个AES加密类,我们在此发现了游戏函数对该类型的引用:
图片7.jpg (29.23 KB, 下载次数: 0)
下载附件
2024-2-21 12:47 上传
mscorlib中的System.Security.Cryptography.Aes看起来并没有什么有用字段,通过翻阅一些文档,发现其实际上使用System.Core中的System.Security.Cryptography.AesCryptoServiceProvider.CreateDecryptor创建解密器,而该类型是这样的:
图片8.jpg (73.79 KB, 下载次数: 0)
下载附件
2024-2-21 12:47 上传
由此可见,我们仅需钩取System.Security.Cryptography.AesCryptoServiceProvider类即可得知CreateDecryptor函数被调用时的参数。
尝试钩取,特意发送几个网络请求,同时获得以下信息:
图片9.jpg (290.81 KB, 下载次数: 0)
下载附件
2024-2-21 12:47 上传
使用该密钥,空IV解密,发现前16byte为乱码,但16byte后明文完整。合理猜测response前16byte为IV,后面为密文,使用该逻辑成功解密出明文。
尾声
Il2Cpp Bridge还有很多其它实用功能,比如直接定位某函数并通过invoke()调用,获取返回值,若对其感兴趣可以阅读官方wiki。
本人第一次在论坛发表文章,也是为了记录自己的逆向学习过程,若有不足请大佬多多指出!