上一篇讲解了纯静态解决 global-metadata.dat 的各类问题,并且提到了利用 il2cpp_api(未混淆)动态dump CS文件这一方法。
也就是下方这个项目: https://github.com/Perfare/Zygisk-Il2CppDumper ,所以这一篇就分享一下我解决 il2cpp_api 混淆的解决方法。
1. 了解Dumper原理
通过源码了解 Zygisk-Il2CppDumper 做了什么,因代码过长,所以后续仍旧截取片段。
打开 il2cpp_dump.cpp,找到 il2cpp_dump 函数。
我们可以看到几个由 il2cpp 命名的函数,il2cpp_domain_get、il2cpp_domain_get_assemblies、il2cpp_assembly_get_image 等等。
void il2cpp_dump(const char *outDir){
LOGI("dumping...");
size_t size;
auto domain = il2cpp_domain_get();
auto assemblies = il2cpp_domain_get_assemblies(domain, &size);
std::stringstream imageOutput;
for (int i = 0; i < size; ++i) {
auto image = il2cpp_assembly_get_image(assemblies[i]);
imageOutput << "// Image " << i << ": " << il2cpp_image_get_name(image) << "\n";
}
索引 il2cpp_domain_get 查看它的实现,会来到 il2cpp-api-functions.h。
会发现整个头文件是一个规范由 DO_API 宏定义的函数。
// domain
DO_API(Il2CppDomain*, il2cpp_domain_get, ());
DO_API(const Il2CppAssembly*, il2cpp_domain_assembly_open, (Il2CppDomain * domain, const char* name));
DO_API(const Il2CppAssembly**, il2cpp_domain_get_assemblies, (const Il2CppDomain * domain, size_t * size));
跳转到 DO_API 看看它都做了什么。
r:返回的类型
n:函数的名字
p:指针地址
#define DO_API(r, n, p) r (*n) p
#include "il2cpp-api-functions.h"
#undef DO_API
static uint64_t il2cpp_base = 0;
void init_il2cpp_api(void *handle){
#define DO_API(r, n, p) { \
n = (r (*) p)xdl_sym(handle, #n, nullptr); \
if(!n) { \
LOGW("api not found %s", #n); \
} \
}
#include "il2cpp-api-functions.h"
#undef DO_API
}
通过 xdl_sym 去获取符号名 n 的地址并定义成返回类型为 r 的函数。
DO_API 宏遍历了所有的API声明,并为每个API解析函数地址。
既然如此,我们打开任意一个 il2cpp.so 来查看一下。

打开导出表,搜索 il2cpp_,可以看见大量的函数名。
而这些函数有什么作用呢?我们截取 dump_method 看看 cs 文件中的方法是怎么导出的。
std::string dump_method(Il2CppClass *klass){
std::stringstream outPut;
outPut << "\n\t// Methods\n";
void *iter = nullptr;
while (auto method = il2cpp_class_get_methods(klass, &iter)) {
//TODO attribute
if (method->methodPointer) {
outPut << "\t// RVA: 0x";
outPut << std::hex << (uint64_t) method->methodPointer - il2cpp_base;
outPut << " VA: 0x";
outPut << std::hex << (uint64_t) method->methodPointer;
} else {
outPut << "\t// RVA: 0x VA: 0x0";
}
/*if (method->slot != 65535) {
outPut << " Slot: " << std::dec << method->slot;
}*/
outPut << "\n\t";
uint32_t iflags = 0;
auto flags = il2cpp_method_get_flags(method, &iflags);
outPut << get_method_modifier(flags);
//TODO genericContainerIndex
auto return_type = il2cpp_method_get_return_type(method);
if (_il2cpp_type_is_byref(return_type)) {
outPut << "ref ";
}
auto return_class = il2cpp_class_from_type(return_type);
outPut << il2cpp_class_get_name(return_class) << " " << il2cpp_method_get_name(method)
<< "(";
auto param_count = il2cpp_method_get_param_count(method);
for (int i = 0; i < param_count; ++i) {
auto param = il2cpp_method_get_param(method, i);
auto attrs = param->attrs;
if (_il2cpp_type_is_byref(param)) {
if (attrs & PARAM_ATTRIBUTE_OUT && !(attrs & PARAM_ATTRIBUTE_IN)) {
outPut << "out ";
} else if (attrs & PARAM_ATTRIBUTE_IN && !(attrs & PARAM_ATTRIBUTE_OUT)) {
outPut << "in ";
} else {
outPut << "ref ";
}
} else {
if (attrs & PARAM_ATTRIBUTE_IN) {
outPut << "[In] ";
}
if (attrs & PARAM_ATTRIBUTE_OUT) {
outPut << "[Out] ";
}
}
auto parameter_class = il2cpp_class_from_type(param);
outPut << il2cpp_class_get_name(parameter_class) << " "
<< il2cpp_method_get_param_name(method, i);
outPut << ", ";
}
if (param_count > 0) {
outPut.seekp(-2, outPut.cur);
}
outPut << ") { }\n";
//TODO GenericInstMethod
}
return outPut.str();
}
所以我们可以知道,Unity 底层就是通过这些 API 去获取各类 class、field 等信息。
因此,我们也同样可以通过这些 API 去获取它们。
2. il2cpp_api混淆例子
拿出例子,观看混淆后的样子:

我们去导出表搜索 il2cpp 字符串,发现没有一个相对应的 API 函数,函数名被混淆为长度 11 的乱序字符。
3. 解决思路发散
- 特征码。
- API 的顺序是否规律。
- 找到游戏上个未混淆的版本,进行对比。
这是作者最先想到,并且尝试的方法,先说第一种。
.text:00000000019047F8
.text:00000000019047F8
.text:00000000019047F8 EXPORT YMqyW_Qt_Ld
.text:00000000019047F8 YMqyW_Qt_Ld ; DATA XREF: LOAD:0000000000007578↑o
.text:00000000019047F8 ; __unwind {
.text:00000000019047F8 RET
.text:00000000019047F8 ; } // starts at 19047F8
.text:00000000019047F8 ; End of function YMqyW_Qt_Ld
.text:00000000019047F8
.text:00000000019047FC EXPORT yG_WSutBToN
.text:00000000019047FC yG_WSutBToN ; DATA XREF: LOAD:0000000000003B88↑o
.text:00000000019047FC ; __unwind {
.text:00000000019047FC RET
.text:00000000019047FC ; } // starts at 19047FC
看到以上两个函数,它们仅仅只有 RET 指令,除了这两个还有其余许多汇编一模一样的函数。因此,想通过特征码来区分是不行的。
第二种,API 的顺序是否规律?这里作者直接给出答案,大致是规律的。例如,如果你确认某个混淆函数是 il2cpp_property_get_name,那么它的下一个混淆函数大概率就是 il2cpp_property_get_get_method,以此类推。
.text:0000000001279314 EXPORT il2cpp_property_get_name
.text:0000000001279314 il2cpp_property_get_name ; DATA XREF: LOAD:00000000000048F0↑o
.text:0000000001279314 ; __unwind {
.text:0000000001279314 B il2cpp_field_get_type_0
.text:0000000001279318
.text:0000000001279318 EXPORT il2cpp_property_get_get_method
.text:0000000001279318 il2cpp_property_get_get_method ; DATA XREF: LOAD:0000000000005178↑o
.text:0000000001279318 ; __unwind {
.text:0000000001279318 B il2cpp_class_get_name_0
.text:000000000127931C EXPORT il2cpp_property_get_set_method
.text:000000000127931C il2cpp_property_get_set_method ; DATA XREF: LOAD:000000000000A788↑o
.text:000000000127931C ; __unwind {
.text:000000000127931C B il2cpp_class_get_namespace_0
.text:0000000001279320
.text:0000000001279320 EXPORT il2cpp_property_get_parent
.text:0000000001279320 il2cpp_property_get_parent ; DATA XREF: LOAD:000000000000E0E8↑o
.text:0000000001279320 ; __unwind {
.text:0000000001279320 B il2cpp_assembly_get_image_0
.text:0000000001279320 ; } // starts at 1279320
.text:0000000001279328 EXPORT il2cpp_object_get_class
.text:0000000001279328 il2cpp_object_get_class ; DATA XREF: LOAD:000000000000B928↑o
.text:0000000001279328 ; __unwind {
.text:0000000001279328 B il2cpp_assembly_get_image_0
这个方法的隐患就是,如果顺序有变动,你也不确定。当某个重要 API 地址错误时,程序会崩溃。
第三个方法,找到游戏上个未混淆的版本进行对比,这个方法在版本更新越接近时越好用。所以将以上三个方法一起搭配使用,应该是最有效的。
4. 利用libunity.so
不知道你是否注意到过这个 so 呢?任意将这个 libunity.so 名字丢入 AI 说明它的作用,它应该都会给你诸如此类的回答。
1. 引擎运行时核心
包含 Unity 游戏引擎的核心功能代码
处理游戏循环、内存管理、渲染管线等基础系统
提供 Unity C# 脚本与底层原生代码之间的桥梁
包含游戏的核心逻辑(特别是使用 IL2CPP 时)
我们可以特别注意到这两行回答:“提供 Unity C# 脚本与底层原生代码之间的桥梁” 和 “包含游戏的核心逻辑(特别是使用 IL2CPP 时)”。
我们任意打开一个未混淆 il2cpp_api 包的 libunity.so,发现导出表也没有跟 il2cpp 相关的函数啊?那试试字符串呢?

我们赶紧索引字符串,看看谁在调用它们。
result = sub_2931EC(a1, 0);
qword_122FDC0 = result;
if ( result )
{
off_122F698 = sub_2933C8(result, "il2cpp_init", 0);
if ( off_122F698 )
{
v2 = 1;
}
else
{
sub_819C78("il2cpp: function il2cpp_init not found\n");
v2 = 0;
}
qword_122F6A0 = sub_2933C8(qword_122FDC0, "il2cpp_init_utf16", 0);
if ( !qword_122F6A0 )
{
sub_819C78("il2cpp: function il2cpp_init_utf16 not found\n");
v2 = 0;
}
off_122F6A8 = sub_2933C8(qword_122FDC0, "il2cpp_shutdown", 0);
if ( !off_122F6A8 )
{
sub_819C78("il2cpp: function il2cpp_shutdown not found\n");
v2 = 0;
}
off_122F6B0 = sub_2933C8(qword_122FDC0, "il2cpp_set_config_dir", 0);
if ( !off_122F6B0 )
{
sub_819C78("il2cpp: function il2cpp_set_config_dir not found\n");
v2 = 0;
}
off_122F6B8 = sub_2933C8(qword_122FDC0, "il2cpp_set_data_dir", 0);
可以发现,所有的 il2cpp_api 相关的字符串都只索引了这一个函数,并且观察规律,这些 API 是按照一定顺序进行初始化的。
按照这个思路,难道说……?赶紧打开混淆的 libunity.so。
if(result)
{
v2 = sub_557770(result, "GdnPlVZEdxf", 0);
qword_119E3E8 = v2;
if( !v2 )
sub_B5BB5C(&unk_144CDD);
v3 = v2 != 0;
qword_119E3F0 = sub_557770(qword_119EB38, "xeESwBbuab_", 0);
if( !qword_119E3F0 )
{
sub_B5BB5C(&unk_FA8EA);
v3 = 0;
}
off_119E3F8 = sub_557770(qword_119EB38, "nKyghkcwxJe", 0);
if( !off_119E3F8 )
{
sub_B5BB5C(&unk_1372DB);
v3 = 0;
}
qword_119E400 = sub_557770(qword_119EB38, "XiKOzOdJsYE", 0);
if( !qword_119E400 )
{
sub_B5BB5C(&unk_FF214);
v3 = 0;
}
qword_119E408 = sub_557770(qword_119EB38, "AbJEjDq_zgF", 0);
if( !qword_119E408 )
{
Ohhhhhhhhhh! 通过比较我们可以知道 GdnPlVZEdxf 对应的就是 il2cpp_init,接着往下的函数都是一一对应的。
5. 编写脚本,快速映射
众所周知,我是一个懒狗。既然有这么好用的映照函数,当然是赶紧编写一些 IDAPython 脚本做一套映射表。
- 获取 il2cpp_api 对照表
ADRL X1, aGdnplvzedxf ; "GdnPlVZEdxf"
ADRP X1, #aXeeswbbuab@PAGE ; "xeESwBbuab_"
ADRL X1, aNkyghkcwxje ; "nKyghkcwxJe"
ADRL X1, aXikozodjsye ; "XiKOzOdJsYE"
通过汇编观察规律,字符串不是在 ADRL 就是 ADRP 语句上。众所周知,我是一个懒狗,懒狗就要有懒狗的样子!我都发现这个规律了,凭什么还要写脚本?派出 AI 大将。
一通批评 AI 后,哐当!我们得到一个 IDA 插件(顺便让它帮忙写了一个 UI,不然每次都要点加载脚本,累死哥们了),运行一下 DumpIl2cppFuction.py。

il2cpp_functions = [
'il2cpp_init',
'il2cpp_init_utf16',
'il2cpp_shutdown',
'il2cpp_set_config_dir',
'il2cpp_set_data_dir',
'il2cpp_set_temp_dir',
'il2cpp_set_commandline_arguments',
'il2cpp_set_commandline_arguments_utf16',
'il2cpp_set_config_utf16',
'il2cpp_set_config',
'il2cpp_set_memory_callbacks',
'il2cpp_get_corlib',
'il2cpp_add_internal_call',
'il2cpp_resolve_icall',
'il2cpp_alloc',
'il2cpp_free',
'il2cpp_array_class_get',
'il2cpp_array_length',
'il2cpp_array_get_byte_length',
'il2cpp_array_new',
'il2cpp_array_new_specific',
得到了一个按照正确顺序的 il2cpp 函数表。注意 Unity 的版本不同,这里的函数表也不一样。大家在分析游戏的时候,可以收集一下各个版本的 il2cpp_functions 表,其实也没几个版本差距特别大,也就 4-5 个。
function_addresses = {
'inbBTWllAfE': 0x019048A8,
'moZkrEOxnod': 0x01904948,
'UgeTybrtfli': 0x01903FE0,
'bETTzCiChhf': 0x01904800,
'dNQnXjbmJKE': 0x0190473C,
'vkWLZpdNfvo': 0x01903FF4,
'lQyvUscAGEB': 0x01904010,
'rpORpEUDUUG': 0x0190496C,
'DDuIaqSaJPb': 0x01904940,
'SCpPaWMGYuU': 0x019046D4,
'JerpDnxOVno': 0x01903F68,
'iL_PocEDOUL': 0x019046F4,
顺手再 dump 一个混淆的表。
il2cpp函数映射表
================================================================================
分析函数: 0x59D26C - 0x59FEA0
================================================================================
1. il2cpp_init -> GdnPlVZEdxf (0x01903EAC)
2. il2cpp_init_utf16 -> nKyghkcwxJe (0x01903F5C)
3. il2cpp_shutdown -> XiKOzOdJsYE (0x01903F60)
4. il2cpp_set_config_dir -> AbJEjDq_zgF (0x01903F64)
5. il2cpp_set_data_dir -> JerpDnxOVno (0x01903F68)
6. il2cpp_set_temp_dir -> NkyFkhKjXDP (0x01903F6C)
7. il2cpp_set_commandline_arguments -> UfNXctJaZQO (0x01903F7C)
8. il2cpp_set_commandline_arguments_utf16 -> MJdRIVcUmWY (0x01903F8C)
9. il2cpp_set_config_utf16 -> igjWeppGXcn (0x01903F90)
10. il2cpp_set_config -> NPuDEZOXejP (0x01903F94)
11. il2cpp_set_memory_callbacks -> EAFNJ_FIJYK (0x01903F98)
12. il2cpp_memory_pool_set_region_size -> gdwVBiglIWC (0x01903F9C)
13. il2cpp_memory_pool_get_region_size -> NGbgctWOdID (0x01903FA0)
14. il2cpp_get_corlib -> kdoNiyv_Qhd (0x01903FA4)
15. il2cpp_add_internal_call -> UWZdnspJd_I (0x01903FA8)
16. il2cpp_resolve_icall -> KaCKWSaBPCP (0x01903FAC)
17. il2cpp_alloc -> PRfrmVSTTrU (0x01903FB0)
18. il2cpp_free -> LAiuXZUDdNh (0x01903FB4)
19. il2cpp_array_class_get -> QRIjzueyGZT (0x01903FB8)
20. il2cpp_array_length -> jzmYhGGuWvJ (0x01903FBC)
最后再映射成一个表。
6. 修改Zygisk-Il2CppDump项目
怎么将我们的混淆函数名和代码中的函数名映射上呢?你问我?我怎么知道?
// 简单版混淆API映射 - 最小修改版本
// 1. 在文件顶部添加映射表
static std::unordered_map<std::string, std::string> g_apiNameMapping = {
{"il2cpp_init", "EEHqqEpNwom"},
{"il2cpp_init_utf16", "RZsQLagwTlx"},
{"il2cpp_shutdown", "FPBuKAnxJnW"},
{"il2cpp_set_config_dir", "gvRjOOFEQoD"},
{"il2cpp_class_get_methods", "yYjgwGcrPwX"},
{"il2cpp_class_get_field_from_name", "AXSPXatQQDi"},
{"il2cpp_class_from_name", "FYAdRLtSOwT"},
{"il2cpp_domain_get", "olpitYGBqDf"},
{"il2cpp_object_new", "rmxDRModSbB"},
{"il2cpp_runtime_invoke", "GMAl_ZIdVoF"},
{"il2cpp_string_new", "MzuQxUkMQUQ"},
{"il2cpp_type_get_name", "mZIXSsmWGNY"},
{"il2cpp_method_get_name", "UwPOCbVQdna"}
// 根据需要添加更多映射...
};
// 获取混淆后的API名称
const char* get_obfuscated_name(const char* original_name){
auto it = g_apiNameMapping.find(original_name);
return (it != g_apiNameMapping.end()) ? it->second.c_str() : original_name;
}
// 2. 修改你的 init_il2cpp_api 函数
void init_il2cpp_api(void *handle){
#define DO_API(r, n, p) { \
const char* symbol_name = get_obfuscated_name(#n); \
n = (r (*) p)xdl_sym(handle, symbol_name, nullptr); \
if(!n) { \
LOGW("api not found %s (tried: %s)", #n, symbol_name); \
/* 回退到原始名称 */ \
n = (r (*) p)xdl_sym(handle, #n, nullptr); \
if(!n) { \
LOGE("api not found %s", #n); \
} \
} \
}
#include "il2cpp-api-functions.h"
#undef DO_API
}
/*
使用方法:
1. 将这个代码片段复制到你现有的文件中
2. 确保包含了必要的头文件:
- #include <unordered_map>
- #include <string>
3. 编译测试即可
这样最小的修改就能让Dumper支持混淆API了!
*/
肯定是继续让 AI 写啊!
后续可以让 dump 脚本映射成 {"il2cpp_init", "EEHqqEpNwom"} 的样式。但是我太懒了,连让 AI 修改一下的动力都没有了……
最后在 game.h 填上你最爱的游戏包名,编译导入手机 Magisk 加载模块即可。
#define GamePackageName "com.game.packagename"
以上就是突破 Unity 游戏 Il2Cpp API 混淆,实现动态 Dump C# 文件的核心思路与实践。对于从事安全/逆向工程的技术人员而言,理解游戏引擎底层的交互机制是解决问题的关键。