找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1823

积分

0

好友

226

主题
发表于 3 天前 | 查看: 9| 回复: 0

上一篇讲解了纯静态解决 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_getil2cpp_domain_get_assembliesil2cpp_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函数导出表截图

打开导出表,搜索 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. 解决思路发散

  1. 特征码
  2. API 的顺序是否规律
  3. 找到游戏上个未混淆的版本,进行对比

这是作者最先想到,并且尝试的方法,先说第一种。

.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 相关的函数啊?那试试字符串呢?

IDA Pro字符串窗口截图

我们赶紧索引字符串,看看谁在调用它们。

  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 脚本做一套映射表。

  1. 获取 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

IDA插件导出界面截图

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# 文件的核心思路与实践。对于从事安全/逆向工程的技术人员而言,理解游戏引擎底层的交互机制是解决问题的关键。




上一篇:MySQL SELECT FOR UPDATE锁机制详解:行锁、间隙锁还是表锁?
下一篇:高通骁龙8 Elite Gen 6芯片定价或超300美元,LPDDR6仅Pro版专属
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-9 17:59 , Processed in 0.186442 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表