自定义Il2CppDumper:逆向分析Unity应用的核心原理与实战

自定义Il2CppDumper:逆向分析Unity应用的核心原理与实战
1. 项目概述为什么我们需要自定义Il2CppDumper如果你在移动安全或者游戏逆向的圈子里待过一阵子肯定对Il2CppDumper这个名字不陌生。它几乎是处理Unity Il2Cpp打包应用的“瑞士军刀”能把加密的global-metadata.dat和编译后的二进制文件比如libil2cpp.so重新变回我们能读懂的DLL和脚本。但不知道你有没有遇到过这种情况官方版本的Il2CppDumper跑起来一切正常导出的DLL结构清晰可当你兴致勃勃地打开IDA或者dnSpy准备大干一场时却发现关键的字符串、函数名、甚至是某些类的结构要么是乱码要么干脆就是一堆无意义的符号。更让人头疼的是有些应用为了对抗分析会对metadata或者il2cpp二进制文件进行自定义的加密或混淆标准的Il2CppDumper脚本一跑就直接报错提示“Can‘t use auto mode to process file”或者“Invalid metadata”。这就是标准流程的局限性。它预设了一个“标准”的Il2Cpp结构但现实世界里的应用尤其是那些对安全性有要求的游戏或商业App早就不是标准模板了。开发者可以修改Unity引擎的Il2Cpp导出后端对元数据格式做自定义序列化或者在二进制文件中插入垃圾代码、修改函数序列表MethodPointers的排列规律。这时候如果你只会双击运行现成的exe或py脚本那你的逆向分析之路可能就卡死在这里了。所以“自定义逆向分析全流程”的核心不是教你如何使用Il2CppDumper这个工具而是教你如何“改造”和“驾驭”它。我们要做的是深入其工作原理根据目标文件的具体情况去修改它的解析逻辑让它能正确解读那些被“动过手脚”的文件。这个过程就像给你一把万能钥匙的毛坯你需要根据锁芯的独特纹路自己动手打磨出最终的齿形。掌握了这个你就不再是工具的简单使用者而是能解决实际复杂问题的分析师。2. 核心思路拆解从黑盒调用到白盒定制很多人把Il2CppDumper当作一个黑盒工具输入两个文件输出一堆结果。但要想实现自定义分析我们必须把它打开看成由几个关键环节组成的白盒流水线。整个自定义流程的核心思路就是针对这个流水线的每一个可能被目标应用“污染”的环节进行诊断和手术。2.1 标准流程的四个关键环节标准的Il2CppDumper工作流可以简化为四个顺序环节元数据Metadata解析读取并解析global-metadata.dat文件。这个文件包含了所有类型、方法、字段、字符串等信息的“目录”和“描述”。Il2CppDumper需要理解这个文件的格式读取字符串池、类型定义表、方法定义表等。二进制代码Binary解析读取并解析libil2cpp.soAndroid或GameAssembly.dllWindows等二进制文件。核心任务是定位几个关键的数据结构在内存中的地址特别是Il2CppCodeRegistration和Il2CppMetadataRegistration这两个结构体。它们包含了指向所有方法指针、类型信息、泛型实例等实际数据地址的指针。地址计算与关联Dump利用从Metadata中读出的信息如某个方法在元数据表中的索引和从Binary中定位到的地址指针如方法指针数组的基地址计算出每个方法、字段在内存中的实际地址VA虚拟地址或文件偏移量File Offset。脚本生成Script Generation根据上述关联好的信息生成可供IDA、Ghidra或dnSpy使用的脚本文件如ida.py、ida_with_struct.py或者直接还原出.cs伪代码文件。2.2 自定义的切入点与应对策略当目标应用进行了加固或混淆时问题通常出现在前两个环节。我们的自定义工作也主要围绕它们展开。情况一元数据global-metadata.dat被加密或混淆。现象Il2CppDumper在第一步就失败提示“Invalid metadata”或无法读取文件头。本质文件内容不是标准的Il2Cpp Metadata格式。可能被整体加密、压缩或者文件头被修改。自定义策略我们需要先对global-metadata.dat进行解密或解混淆还原出标准的格式再喂给Il2CppDumper。这通常需要逆向分析应用的解密逻辑可能涉及对App本身so库的调试如使用Frida、IDA动态调试找到解密函数和密钥。情况二二进制文件libil2cpp.so的结构被修改。现象Il2CppDumper能识别metadata但在自动模式Auto下无法定位到Il2CppCodeRegistration等关键结构手动模式也需要反复尝试且可能失败。本质Unity引擎在生成Il2Cpp代码时其内部数据结构如Il2CppCodeRegistration的布局、成员顺序、甚至成员变量类型被修改。或者这些结构体的符号信息被完全剥离并被打乱存放。自定义策略这是最常见的自定义场景。我们需要修改Il2CppDumper的源码调整其搜索和识别这些关键结构体的算法。这可能包括修改特征码Il2CppDumper通过特征码一串特定的字节序列来搜索结构体。如果特征码变了我们就需要从目标二进制文件中通过静态分析或动态调试找到新的特征码。调整结构体定义如果结构体的成员变了例如增加了字段我们需要在源码中同步修改对应的C#结构体定义如Il2CppCodeRegistration类。实现新的搜索算法在极端情况下可能需要自己编写逻辑通过交叉引用、字符串引用等启发式方法定位关键数据。情况三混合情况。现象上述两种问题同时存在。策略需要先解决元数据解密问题再解决二进制文件解析问题。顺序不能乱因为解密后的元数据是正确解析二进制文件的基础。理解了这些我们就知道所谓的“3分钟”并非指从零开始解决一个高度混淆的应用而是指在理清思路、准备好定制化方案后执行修改、编译、测试这一套流程可以非常高效。大部分时间其实花在了前期的逆向分析和诊断上。3. 环境准备与工具链选择工欲善其事必先利其器。自定义Il2CppDumper不是一个孤立的任务它依赖一个完整的逆向分析环境。下面是我个人经过多个项目验证后觉得最稳定高效的一套组合。3.1 核心工具Il2CppDumper源码与编译环境首先放弃直接下载编译好的Release版本。我们需要源代码。获取源码访问Il2CppDumper的GitHub仓库Perfare/Il2CppDumper使用Git克隆或直接下载ZIP包。建议使用较新的Release标签下的代码兼容性和社区支持更好。编译环境Il2CppDumper主程序是C#编写的。你需要安装.NET SDK建议使用.NET 6.0或8.0的LTS版本。在Windows上Visual Studio 2022社区版是绝佳选择它集成了所有必要组件。在Linux/macOS上安装好.NET SDK后使用dotnet build命令即可编译。注意确保安装的.NET SDK版本与项目文件.csproj中指定的目标框架TargetFramework匹配否则可能编译失败。备用选择除了官方的C#版本社区还有基于Python的Il2CppDumper实现如Il2CppDumper-py。对于快速修改和原型测试Python版本有时更灵活因为无需编译直接改脚本即可。但C#版本功能更全、更稳定。我通常以C#版本为主力Python版本作为辅助验证思路。3.2 辅助分析工具这些工具用于诊断目标文件为自定义提供依据。十六进制编辑器如010 Editor、HxD。用于直接查看和编辑global-metadata.dat和libil2cpp.so的原始字节分析文件头、魔数、寻找可能的加密区域或特征码。010 Editor的模板功能尤其强大可以解析已知文件格式。静态分析工具IDA Pro / Ghidra逆向分析的基石。用于深入分析libil2cpp.so通过反汇编寻找关键函数、交叉引用、字符串数据从而定位被修改的Il2CppCodeRegistration等结构。Ghidra免费且功能强大是IDA的优秀替代品。CFF Explorer / Detect It Easy用于快速查看PEWindows或ELFLinux/Android文件的基本信息如区段、导入导出表、字符串对文件有一个快速的初步认识。动态调试工具Frida在Android平台上进行动态插桩的神器。当元数据被加密时我们通常需要Hook应用启动时的解密函数将解密后的元数据从内存中dump出来。Frida脚本编写灵活是解决加密问题的首选。IDA Debugger / LLDB用于更底层的动态调试。比如当静态分析无法理清混淆逻辑时可以通过调试跟踪程序的执行流观察关键数据在内存中的变化。脚本处理工具Python环境安装好frida、hexdump、struct等常用库。用于编写Frida脚本、解密脚本、数据转换脚本等。Jupyter Notebook强烈推荐。它非常适合做探索性数据分析。你可以将读取文件、尝试解密算法、解析数据结构、验证结果等一系列步骤写成独立的代码块实时查看每一步的输出极大地提升了调试和实验效率。3.3 工作目录规划保持工作目录清晰能节省大量时间。我通常这样组织MyIl2CppProject/ ├── TargetApp/ # 存放目标应用相关文件 │ ├── base.apk │ ├── libil2cpp.so (从apk中解压) │ └── global-metadata.dat (从apk中解压可能是加密的) ├── Il2CppDumper-Src/ # Il2CppDumper源码 │ ├── Il2CppDumper.csproj │ └── ... ├── Scripts/ # 自定义脚本 │ ├── dump_memory.js (Frida内存dump脚本) │ ├── decrypt_metadata.py (元数据解密脚本) │ └── find_pattern.py (特征码搜索脚本) ├── Dumped/ # 输出目录 │ ├── Decrypted-metadata.dat │ ├── DummyDll/ │ └── ida.py └── Notes.md # 分析笔记4. 实战案例自定义解析被修改的libil2cpp.so让我们通过一个最常见的场景来贯穿整个流程目标游戏的libil2cpp.so被修改导致Il2CppDumper自动模式失效。4.1 第一步初步诊断与信息收集首先用标准流程测试确认问题。# 在Il2CppDumper编译输出目录 dotnet Il2CppDumper.dll libil2cpp.so global-metadata.dat如果输出错误提示“Can‘t use auto mode to process file”并建议使用Manual模式这就印证了我们的猜测。接下来使用Detect It Easy或file命令快速查看so文件信息确认它是ARM64还是ARMv7架构。这很重要因为指针大小64位/32位会影响后续分析。然后用十六进制编辑器打开global-metadata.dat查看文件开头。正常的Il2Cpp元数据文件通常以AF 1B B1 FA小端序的魔数开头。如果这里是一堆乱码那可能元数据也被加密了我们需要先处理它案例放在下一节。假设这里魔数正常我们暂时聚焦于so文件。4.2 第二步定位关键结构体的新特征Il2CppDumper的自动模式本质是在so文件中搜索代表Il2CppCodeRegistration和Il2CppMetadataRegistration这两个结构体的特征字节序列。我们需要找到目标文件中这些特征码变成了什么。使用IDA进行静态分析将libil2cpp.so拖入IDA。分析完成后在Strings窗口ShiftF12搜索一些Il2Cpp运行时肯定会出现的字符串例如“Il2CppCodeRegistration”、“MetadataRegistration”或者一些常见的运行时错误信息。如果能找到这些字符串查看它们的交叉引用Xrefs很可能就会引导你到初始化这些结构体的函数附近进而找到结构体本身在数据段.data的位置。分析初始化函数在IDA的Exports窗口查找像il2cpp_init、il2cpp::vm::MetadataCache::Initialize这样的函数名。如果符号没被剥离这是最快的途径。进入这些函数分析其汇编代码。它们通常会将一个地址即Il2CppCodeRegistration结构体的地址加载到寄存器或存入某个全局变量。这个地址就是我们寻找的目标。特征码提取假设我们在IDA的.data段找到了一个可疑的结构地址是0x6E2C00。我们用十六进制编辑器跳转到so文件中对应的偏移量注意文件偏移量Offset不等于虚拟地址VA。需要根据ELF文件的程序头Program Header计算。IDA的“Edit - Segments - Rebase program”设置为0可以方便地看到文件偏移或者直接用objdump -h libil2cpp.so查看区段信息。 在目标地址附近观察数据的排列。Il2CppCodeRegistration结构体通常第一个字段是methodPointersCount方法指针数量一个整数紧接着是methodPointers方法指针数组的地址。我们可以提取一段独特的字节序列作为新的特征码。例如假设我们观察到从0x6E2C00开始的数据是00 10 00 00 // methodPointersCount 0x1000 (4096) C0 35 6E 00 // methodPointers 指针指向 0x6E35C0那么特征码可以是00 10 00 00 C0 35 6E 00注意字节序这里是小端。我们需要记录这个特征码以及它距离结构体起始地址的偏移这里是0。4.3 第三步修改Il2CppDumper源码现在我们有了新的特征码需要让Il2CppDumper认识它。定位搜索代码在Il2CppDumper源码中搜索SearchCodeRegistration或SearchMetadataRegistration这类方法。通常位于Il2CppBinary的某个架构具体实现类中如Elf64PE32。添加自定义特征码找到存储特征码的数组或列表。例如在Elf64类中可能有一个CodeRegistrationFeature的字节数组列表。我们将自己找到的特征码以byte[]的形式添加进去。// 示例在Elf64类的Initialize方法或某个特征码集合中添加 private static readonly Listbyte[] CustomCodeRegistrationFeatures new Listbyte[] { // ... 原有的特征码 new byte[] {0x00, 0x10, 0x00, 0x00, 0xC0, 0x35, 0x6E, 0x00}, // 我们添加的自定义特征码 };注意特征码不能太短否则误匹配概率高也不能太长如果目标地址因ASLR等原因有偏移就会匹配失败。通常8-16个字节比较合适。另外如果结构体第一个字段不是methodPointersCount比如被插入了其他字段你需要计算正确的偏移来提取特征码。调整结构体定义如果需要如果通过IDA发现Il2CppCodeRegistration结构体增加了新的字段例如在methodPointers之前多了一个uint32_t customField那么你还需要修改源码中对应结构体的定义可能在Metadata目录下的某个.cs文件里确保字段顺序和偏移量与二进制文件对齐。否则后续读取指针数组时会发生错位。编译与测试保存修改使用dotnet build重新编译项目。将生成的新的Il2CppDumper.dll或exe用于测试。dotnet build -c Release dotnet path/to/new/Il2CppDumper.dll libil2cpp.so global-metadata.dat观察输出看是否成功进入了自动模式并正确输出了CodeRegistration : 0x6E2C00和MetadataRegistration : 0x...。如果成功恭喜你最核心的一步已经完成。4.4 第四步处理元数据解密如果存在如果第一步发现global-metadata.dat文件头魔数不对说明它被处理过。这时我们需要先解密它。动态提取这是最可靠的方法。使用Frida在游戏启动时Hook住Il2Cpp运行时加载元数据的函数。常见的函数名是il2cpp::vm::MetadataCache::Initialize。在这个函数内部元数据文件的内容会被读入内存并解密。我们可以编写Frida脚本在这个时间点将解密后的内存数据dump到文件中。// 示例Frida脚本框架 (需根据实际函数签名调整) Interceptor.attach(Module.findExportByName(libil2cpp.so, _ZN6il2cpp2vm13MetadataCache10InitializeEPKvj), { onEnter: function(args) { // args[0] 可能是解密后元数据的内存地址 // args[1] 可能是元数据的大小 var metadataPtr args[0]; var metadataSize args[1].toInt32(); if (metadataSize 100) { // 简单过滤 console.log([] Metadata found at:, metadataPtr, size:, metadataSize); var buffer metadataPtr.readByteArray(metadataSize); // 将buffer写入文件 // 需要配合Python脚本或使用frida的send()将数据传回PC send({dump: true, data: buffer}); } } });在PC端用Python运行Frida脚本接收数据并保存为global-metadata-decrypted.dat。静态分析与还原如果动态提取困难可以尝试静态分析。用IDA找到解密函数逆向其算法然后用Python重现代码对加密的global-metadata.dat文件进行解密。这一步难度较大需要较强的逆向工程能力。使用解密后的元数据得到解密后的global-metadata-decrypted.dat后将其与libil2cpp.so一起喂给我们修改好的Il2CppDumper。4.5 第五步生成脚本与验证当Il2CppDumper成功运行并输出结果后它会生成一个DummyDll文件夹包含还原的Assembly-CSharp等DLL和IDA脚本如ida.py。初步验证用dnSpy打开DummyDll/Assembly-CSharp.dll查看类名、方法名是否被成功恢复。如果能看到清晰的命名空间和类名而不是一堆Module或f__AnonymousType说明基本成功。应用IDA脚本将ida.py脚本在IDA中加载File - Script file。这个脚本会为函数、方法、字符串等重命名并应用结构体定义极大提升反汇编代码的可读性。在IDA中随机跳转到几个函数的地址检查是否被正确标注上了C#方法名。交叉检查在游戏运行时通过Frida Hook一个已知功能的方法获取其在内存中的地址。然后回到IDA中查看这个地址是否已经被我们的脚本正确命名。这是最直接的验证。5. 常见问题排查与深度优化技巧即使按照流程操作你也可能会遇到各种问题。下面是我踩过的一些坑和对应的解决方案。5.1 特征码匹配失败问题添加了自定义特征码但Il2CppDumper仍然找不到。排查字节序确认你提取特征码时使用的字节序Endianness与目标平台一致。ARM通常是小端Little-Endian但你提取时可能看反了。地址偏移你提取特征码的位置可能不是结构体的绝对起始地址。确保你从IDA中看到的Il2CppCodeRegistration结构体的第一个字节开始提取。文件偏移 vs 虚拟地址你从IDA中看到的地址是加载后的虚拟地址VA。而Il2CppDumper是在原始文件磁盘上的so文件中搜索。你需要将VA转换为文件偏移File Offset。在IDA中你可以通过“Edit - Segments - Rebase program”将基址设为0这样显示的地址就更接近文件偏移。或者使用readelf -l libil2cpp.so查看程序头计算VirtAddr到Offset的映射关系。特征码唯一性你的特征码可能在文件中多处出现导致匹配到了错误的位置。尝试提取更长、更独特的字节序列或者结合多个特征进行判断。5.2 生成的DLL结构混乱或IDA脚本报错问题Il2CppDumper运行没有报错但生成的DLL里类的方法全是乱的或者IDA脚本执行时提示“Invalid address”。原因这通常是因为Il2CppCodeRegistration和Il2CppMetadataRegistration的地址有一个找错了或者结构体定义不匹配导致元数据索引和实际地址的对应关系全部错位。解决双重验证地址不要完全依赖自动搜索。在IDA中手动确认你找到的CodeRegistration地址是否正确。查看该地址附近的数据是否有一个较大的计数methodPointersCount紧接着是一个指向方法指针数组的地址那个方法指针数组是否指向了代码段.text的大量函数入口使用Manual模式Il2CppDumper提供了手动模式--mode manual。你可以通过命令行参数直接传入你通过IDA确认的CodeRegistration和MetadataRegistration的地址注意是文件偏移地址或VA根据程序要求。这可以绕过自动搜索直接测试地址是否正确。dotnet Il2CppDumper.dll libil2cpp.so global-metadata.dat --mode manual --code 0x6E2C00 --metadata 0x7D4000调整结构体偏移如果地址正确但输出仍乱可能是结构体成员偏移计算错误。你需要仔细对照IDA中结构体的内存布局修改Il2CppDumper源码中对应结构体类的字段定义可能需要用到[FieldOffset]属性来精确控制。5.3 应对高级混淆动态计算的特征码有些加固方案会动态生成关键结构体的内容导致静态的特征码每次都不一样。策略这种情况下特征码搜索可能失效。需要更智能的定位方式。字符串引用追踪在IDA中搜索字符串“Il2CppCodeRegistration”如果没被抹去。追踪所有引用这个字符串的代码这些代码很可能在初始化或注册这个结构体。导出函数分析分析il2cpp_init等初始化函数的汇编代码。即使符号被剥离这些函数通常也是导出函数在Exports表里。通过分析其汇编逻辑找到它准备并传递Il2CppCodeRegistration地址给内部函数的指令从而定位地址。Frida动态拦截在游戏运行时用Frida Hookil2cpp_init直接打印出传入的Il2CppCodeRegistration和Il2CppMetadataRegistration的地址。这是最暴力但最有效的方法。// Frida脚本示例拦截il2cpp_init (假设它是导出函数) var il2cpp_init Module.findExportByName(libil2cpp.so, il2cpp_init); if (il2cpp_init) { Interceptor.attach(il2cpp_init, { onEnter: function(args) { // 根据函数签名第一个参数可能是CodeRegistration第二个可能是MetadataRegistration console.log([] il2cpp_init called!); console.log( CodeRegistration ptr:, args[0]); console.log( MetadataRegistration ptr:, args[1]); // 可以在这里将指针值发送到PC端保存 } }); }5.4 性能与自动化优化当你需要批量处理多个版本的游戏或者同一个加固方案下的多个应用时手动修改源码会很累。创建配置文件可以修改Il2CppDumper让其从一个外部JSON或TXT配置文件读取自定义的特征码、结构体偏移等信息而不是硬编码在源码里。这样针对不同目标只需更换配置文件即可。编写辅助脚本用Python写一个自动化脚本集成以下步骤解压APK、寻找so和metadata文件、尝试标准Dump、如果失败则调用你预定义的自定义配置进行Dump、最后自动应用IDA脚本。这能把你从重复劳动中解放出来。6. 从逆向分析到实际应用成功完成Il2CppDumper的自定义解析只是万里长征第一步。它为你打开了应用内部逻辑的大门。接下来你可以利用这些成果做很多事情游戏功能分析与修改通过dnSpy分析还原的C#代码逻辑理解游戏机制如伤害计算、物品掉落。结合IDA对so文件的分析可以定位到关键的计算函数进而使用Frida进行运行时修改如修改数值、调用特定函数或者制作Mod。安全审计检查应用是否存在逻辑漏洞、敏感信息如密钥硬编码、不安全的通信协议等。自动化测试通过逆向出的接口和数据结构编写自动化测试脚本模拟用户操作或进行协议测试。学习与研究研究优秀的商业应用或游戏是如何组织代码、设计架构的是提升自己开发能力的一个独特途径。整个过程从遇到一个无法直接Dump的应用到通过静态分析和动态调试定位关键点再到修改工具源码成功解析最后利用结果进行深入分析——这正是一个完整的逆向工程思维和实践的体现。它考验的不仅仅是工具的使用更是对系统原理的理解、对问题的排查能力和编写代码解决实际问题的能力。每一次成功的自定义破解都是对这些能力的一次扎实锻炼。