从N1CTF2018 baby unity3d入门il2cpp逆向
字数 1491 2025-08-30 06:50:35
从N1CTF2018 baby unity3d入门il2cpp逆向分析
前置知识
il2cpp基础概念
- il2cpp是Unity引擎的脚本后端实现,将C#代码编译为C++代码
- 主要包含两个关键文件:
- libil2cpp.so:包含核心逻辑的二进制文件
- global-metadata.dat:包含元数据信息
必要工具
- Il2CppDumper:用于解析il2cpp结构的工具
- GitHub地址:https://github.com/Perfare/Il2CppDumper
- IDA Pro:用于逆向分析so文件
- dnSpy:用于查看C#反编译代码
- frida-il2cpp-bridge:用于动态分析
- GitHub地址:https://github.com/vfsfitvnm/frida-il2cpp-bridge
解题步骤
1. 文件提取与初步分析
-
解包APK获取关键文件:
- libil2cpp.so
- global-metadata.dat(通常被加密)
-
识别global-metadata.dat加密:
- 文件头不是正常的"AF 1B B1 FA"
- 需要从libil2cpp.so中寻找解密逻辑
2. 定位解密函数
调用链分析
il2cpp_init
-> il2cpp::vm::Runtime::Init
-> il2cpp::vm::MetadataCache::Initialize
-> il2cpp::vm::MetadataLoader::LoadMetadataFile
IDA分析技巧
- 从il2cpp_init开始跟踪
- 识别关键函数:
- sub_47C770对应Runtime::Init
- sub_4B5564对应MetadataCache::Initialize
- 关键字符串线索:
- "CLKFIL\rMETIDITI\nDIT"是加密后的"global-metadata.dat"
解密函数识别
- sub_512FDC是核心解密函数
- 解密算法:每4字节与key数组异或
- key数组位置:0x518A24
- key数组大小:0x84个DWORD
3. 解密global-metadata.dat
解密代码示例
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file;
unsigned char *buf;
size_t fileSize;
file = fopen("global-metadata.dat", "rb");
fseek(file, 0, SEEK_END);
fileSize = ftell(file);
rewind(file);
buf = (unsigned char *)malloc(fileSize);
fread(buf, 1, fileSize, file);
fclose(file);
unsigned int key[] = {0xF83DA249, 0x15D12772, /* ...完整key数组... */};
for (int i = 0; i < fileSize; i+=4){
*((unsigned int *)(buf + i)) ^= key[(i + i/0x84) %0x84];
}
FILE *file_write = fopen("output.bin", "wb");
fwrite(buf, 1, fileSize, file_write);
fclose(file_write);
return 0;
}
解密后处理
- 修复文件头为"AF 1B B1 FA"
4. 使用Il2CppDumper解析
-
运行Il2CppDumper:
- 输入:libil2cpp.so和解密后的global-metadata.dat
- 输出:
- Assembly-CSharp.dll(可用dnSpy查看)
- dump.cs(包含函数偏移信息)
- script.json(用于IDA脚本)
-
IDA符号恢复:
- 使用ida_py3.py脚本加载script.json
- 自动恢复函数和类名
5. 关键函数分析
CheckFlag函数
- 偏移:0x518A24
- 参数:
- text:a2
- password:TypeInfo+80 + 4000
- iv:TypeInfo+80 + 2364
- flag密文:StringLiteral_2814
加密流程
- 对输入进行AESEncrypt
- 与存储的flag密文比较
6. 动态分析技巧
使用frida-il2cpp-bridge
- Hook AESEncrypt获取参数:
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
const assembly = Il2Cpp.domain.assembly("Assembly-CSharp").image;
assembly.class('Check').method('AESEncrypt').implementation = function(){
console.log(arguments[0]); // 密文
console.log(arguments[1]); // password
console.log(arguments[2]); // iv
return this.method('AESEncrypt').invoke(arguments[0],arguments[1],arguments[2]);
}
});
- Hook字符串比较获取flag密文:
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
const SystemString = Il2Cpp.corlib.class("System.String");
var compare = SystemString.method('Equals');
compare.implementation = function(){
console.log(`arg[0]: ${arguments[0]}`);
console.log(`arg[1]: ${arguments[1]}`);
}
});
- 直接调用解密函数:
import "frida-il2cpp-bridge";
Il2Cpp.perform(() => {
function getBytes(str){
return Il2Cpp.corlib.class("System.Text.UTF8Encoding").new()
.method('GetBytes',1).invoke(Il2Cpp.string(str));
}
const assembly = Il2Cpp.domain.assembly("Assembly-CSharp").image;
const text = Il2Cpp.string('w0ZyUZAHhn16/MRWie63lK+PuVpZObu/NpQ/E/ucplc=');
const password = getBytes('91c775fa0f6a1cba');
const iv = getBytes('58f3a445939aeb79');
const val = assembly.class('Check').method('AESDecrypt').invoke(text,password,iv);
console.log(val); // 输出解密后的flag
});
关键数据
- flag密文:
w0ZyUZAHhn16/MRWie63lK+PuVpZObu/NpQ/E/ucplc= - password:
91c775fa0f6a1cba - iv:
58f3a445939aeb79
总结
- il2cpp逆向核心在于解析加密的global-metadata.dat
- 解密通常采用异或等简单算法,关键在定位解密函数
- 静态分析结合动态调试(frida)能有效提高效率
- Il2CppDumper是解析il2cpp结构的必备工具
- 注意关键字符串和函数调用链的分析