使用frida获取unity il2cpp符号信息
字数 1623 2025-08-22 12:22:36
使用Frida获取Unity IL2CPP符号信息教学文档
1. 背景知识
1.1 Unity IL2CPP编译模式
- Unity游戏使用IL2CPP模式编译时,会将C#代码转换为C++代码再编译为原生二进制
- 游戏中的字符串信息保存在
global-metadata.dat资源文件中 - 运行时才会将这些字符串读入内存
1.2 常规分析方法
- 通常使用
Il2CppDumper工具读取global-metadata.dat文件中的信息 - 该工具能帮助还原符号信息,辅助反编译工作
1.3 保护与对抗
- 游戏厂商可能采取保护措施:
- 修改
global-metadata.dat文件结构 - 对文件进行加密
- 使
Il2CppDumper无法正常工作
- 修改
2. IL2CPP加载过程分析
2.1 原始加载流程
MetadataCache::Initialize()函数调用LoadMetadataFile()加载元数据LoadMetadataFile()函数执行流程:void* MetadataLoader::LoadMetadataFile(const char* fileName) { // 组合文件路径 std::string resourcesDirectory = utils::PathUtils::Combine(...); std::string resourceFilePath = utils::PathUtils::Combine(...); // 打开文件 FileHandle* handle = File::Open(resourceFilePath, ...); // 映射文件到内存 void* fileBuffer = utils::MemoryMappedFile::Map(handle); // 关闭文件句柄 File::Close(handle, &error); return fileBuffer; }
2.2 global-metadata.dat文件结构
struct Il2CppGlobalMetadataHeader {
int32_t sanity;
int32_t version;
int32_t stringLiteralOffset; // string data for managed code
int32_t stringLiteralCount;
int32_t stringLiteralDataOffset;
int32_t stringLiteralDataCount;
int32_t stringOffset; // string data for metadata
int32_t stringCount;
int32_t eventsOffset; // Il2CppEventDefinition
int32_t eventsCount;
int32_t propertiesOffset; // Il2CppPropertyDefinition
int32_t propertiesCount;
int32_t methodsOffset; // Il2CppMethodDefinition
int32_t methodsCount;
// ... 其他字段
}
2.3 某手游的特殊处理
- 在
LoadMetadataFile()映射文件到内存后进行了解密操作 MetadataCache::Initialize()中还有一次解密- 文件结构被修改,无法直接使用
Il2CppDumper
3. 关键Hook点选择
3.1 为什么选择SetupMethodsLocked
- 通过分析IL2CPP源码调用链:
il2cpp_class_get_methods -> Class::GetMethods -> Class::SetupMethods -> SetupMethodsLocked SetupMethodsLocked是最终设置方法信息的关键函数- 在此处Hook可以获取完整的
MethodInfo结构
3.2 SetupMethodsLocked关键代码
void SetupMethodsLocked(Il2CppClass* klass, const FastAutoLock& lock) {
// 分配内存
klass->methods = (const MethodInfo**)IL2CPP_CALLOC(klass->method_count, sizeof(MethodInfo*));
MethodInfo* methods = (MethodInfo*)IL2CPP_CALLOC(klass->method_count, sizeof(MethodInfo));
MethodInfo* newMethod = methods;
for (MethodIndex index = start; index < end; ++index) {
// 获取方法定义
const Il2CppMethodDefinition* methodDefinition = MetadataCache::GetMethodDefinitionFromIndex(index);
// 设置方法信息
newMethod->name = MetadataCache::GetStringFromIndex(methodDefinition->nameIndex);
newMethod->methodPointer = MetadataCache::GetMethodPointerFromIndex(methodDefinition->methodIndex);
newMethod->invoker_method = MetadataCache::GetMethodInvokerFromIndex(methodDefinition->invokerIndex);
newMethod->declaring_type = klass;
newMethod->return_type = MetadataCache::GetIl2CppTypeFromIndex(methodDefinition->returnType);
// 设置参数信息
ParameterInfo* parameters = (ParameterInfo*)IL2CPP_CALLOC(methodDefinition->parameterCount, sizeof(ParameterInfo));
// ... 参数设置代码
// 将方法添加到类中
klass->methods[index - start] = newMethod;
newMethod++; // 这是最佳Hook点
}
}
4. 关键数据结构
4.1 Il2CppClass结构
struct Il2CppClass {
const Il2CppImage* image;
void* gc_desc;
const char* name; // 类名
const char* namespaze; // 命名空间
// ... 其他字段
};
4.2 MethodInfo结构
struct MethodInfo {
Il2CppMethodPointer methodPointer; // 方法指针
InvokerMethod invoker_method;
const char* name; // 方法名
Il2CppClass* declaring_type; // 声明类
const Il2CppType* return_type; // 返回类型
const ParameterInfo* parameters; // 参数信息
// ... 其他字段
};
5. Frida Hook实现
5.1 定位Hook地址
- 反编译
libil2cpp.so - 找到
SetupMethodsLocked函数 - 确定
newMethod++指令的偏移地址(示例中为0x72F09EC + 0x204)
5.2 Frida脚本
// hook SetupMethodsLocked
var module = Process.findModuleByName("libil2cpp.so");
var p_size = 8; // 指针大小,64位为8
Interceptor.attach(ptr(module.base).add(0x72F09EC).add(0x204), {
onEnter: function(args) {
var newMethod = this.context.x20; // ARM64下x20寄存器保存newMethod指针
// 读取MethodInfo结构
var pointer = newMethod.readPointer(); // 方法指针
var name = newMethod.add(p_size * 2).readPointer().readCString(); // 方法名
var klass = newMethod.add(p_size * 3).readPointer(); // 声明类
// 读取Il2CppClass结构
var klass_name = klass.add(p_size * 2).readPointer().readCString();
var klass_paze = klass.add(p_size * 3).readPointer().readCString();
// 输出格式: 命名空间.类名:方法名 -> 相对偏移
send(klass_paze + "." + klass_name + ":" + name + " -> " + pointer.sub(module.base));
}
});
5.3 脚本说明
- 通过
Interceptor.attach附加到目标地址 - 在
onEnter回调中读取方法信息 - 从寄存器/内存中提取:
- 方法指针
- 方法名称
- 声明类信息(类名和命名空间)
- 输出格式为:
命名空间.类名:方法名 -> 相对偏移
6. 注意事项
6.1 混淆处理
- 目标应用可能使用混淆技术
- 类名、方法名可能被混淆
- 需要额外的反混淆处理(不在本文讨论范围)
6.2 平台差异
- 示例使用ARM64架构(x20寄存器)
- 不同平台需要调整:
- 寄存器使用(x86/ARM32/ARM64不同)
- 指针大小(32位为4字节)
6.3 偏移地址
SetupMethodsLocked中的newMethod++指令偏移需要根据具体目标确定- 需要通过反编译找到正确位置
7. 扩展应用
7.1 自动化脚本
- 可以扩展脚本自动记录所有类和方法
- 生成符号表供后续分析使用
7.2 结合其他技术
- 结合内存dump技术获取更多信息
- 配合IDA等工具进行静态分析
7.3 性能考虑
- 大量方法调用可能影响性能
- 生产环境应考虑过滤关键方法
8. 总结
本方法通过Hook IL2CPP内部函数SetupMethodsLocked,在运行时动态获取Unity游戏的符号信息,绕过了对global-metadata.dat文件的保护措施。关键点包括:
- 理解IL2CPP加载流程和文件结构
- 选择合适的Hook点(SetupMethodsLocked)
- 正确解析MethodInfo和Il2CppClass结构
- 处理平台差异和混淆情况
这种方法对于分析受保护的Unity游戏特别有效,即使global-metadata.dat被加密或修改也能正常工作。