简介:Il2CppDumper是一款面向Unity逆向工程分析人员的经典工具,Windows版v6.7.46主要用于解析il2cpp生成的原生库,将其中元数据还原成可读的DLL文件(不含代码逻辑),用于定位和提取MonoBehaviour与MonoScript等关键结构,在游戏安全研究、模组制作与外挂分析等场景中应用广泛。工具覆盖ELF、ELF64、Mach-O、PE、NSO和WASM等多种可执行文件格式,兼容Unity 5.3至2020版本;既可直接解析文件,也能从内存dump所得libil2cpp.so入手,配合生成的IDA、Ghidra自动化脚本以及结构体头文件,显著提升静态分析效率,对于加固或混淆处理的样本也有一定绕过能力。压缩包共11个文件,其中8个Python脚本覆盖IDA/Ghidra脚本生成、结构体头文件输出与WASM辅助分析等步骤;2个exe分别为主程序与x86版本,可独立完成解析;另有1个config.json保存默认参数,整体体积约10.7MB,部署门槛较低。目前已有1050人学习,适合具备一定逆向基础的Unity开发者和安全研究人员,作为覆盖格式较全、上手快速的il2cpp还原与辅助分析工具集使用。
1. 深夜只能用 IDA 眼动翻汇编,直到我把 IL2CPP 的符号救回来
做手游性能分析或者崩溃排查的朋友大概率遇到过这种场景:拿到一个 Unity 的 so 文件,用 IDA 打开,函数列表清一色的sub_1234AB,字符串窗口能看到中文选项菜单,但就是不知道哪个函数对应哪个玩法逻辑。如果是普通 Mono 打包,至少还能找到 DLL 逐个看;一旦碰上 IL2CPP 打包,整套 C# 业务被编译成 C++ 静态函数,符号几乎全被剥离,没有一套可靠的符号恢复流程,分析效率基本靠猜。
Il2CppDumper-win-v6.7.46 就是用来解决这个问题的老牌工具,它不算新,界面也普通,但胜在稳。它干的事情很直接:把global-metadata.dat里的类型信息抽出来,再把 so 文件的二进制机制对齐,最后生成一份带命名空间和类名的 C# 结构描述文件dump.cs。有了它,你就能从"对着裸汇编猜逻辑"变成"对着函数名和参数类型做判断"。这篇文章我会从原理讲到命令,从参数设置讲到容易劝退的坑,最后收在一个验证偏移的技巧上,都是我自己在模拟项目上反复踩过的东西。
2. 先搞清楚 IL2CPP 的产物结构:为什么需要符号恢复
2.1 Unity 构建产物里那两个关键文件是怎么分工的
IL2CPP 的构建流程本质上是把 C# 代码先转成 IL,再由一套专门的编译器翻译成 C++ 源码,最后统一编进一个原生 so 里。正因为翻译过后类和方法都成了普通 C++ 函数,程序运行效率是上去了,但类名、方法名、字段名这些调试信息被全部剥掉,留给分析侧的只有libil2cpp.so和global-metadata.dat这两件套。
libil2cpp.so负责执行逻辑,它内部有几个关键的导出区域。il2cpp_init是入口之一,所有初始化都从那儿过;il2cpp_class_from_name这类 API 在运行时用来做类型查询,经常成为动态分析的挂钩点。另外就是那套虚方法表的结构,IL2CPP 靠它实现多态和接口分发,对还原调用关系很重要。
global-metadata.dat则是一本运行时的"户口本",Class 的嵌套关系、方法签名、字段偏移、字符串字面量,全都以二进制的形式存在这里面。它是 IL2CPP 世界观的静态描述层,没有它,光看 so 的汇编根本不知道一堆寄存器操作是为了什么字段。反过来,光有 metadata 没有 so,你也不知道方法实际编译出来的地址偏移是多少。Il2CppDumper 做的事情就是把这两个文件按版本规则做关联,把户口本里的名字填到二进制对应的地址上。
2.2 版本匹配是这套工具的命门
这个工具从命令行到输出都写着一股硬核味,命令很简单,真正头疼的是"版本"两个字。IL2CPP 的 metadata 格式每年都在变,Unity 每个大版本甚至小版本更新都可能改动字段结构、字符串常量表和类型初始化顺序。Il2CppDumper-v6.7.46 有一个内部维护的版本映射表,它能不能正确解析,完全取决于它认不认识你手上这个工程的 metadata 版本号。
我自己处理过的模拟项目里,最省心的是 2019 和 2020 时代的 Unity 工程,基本拖进去就能出结果;2021 往后的版本偶尔会遇到新字段,2022 的某些小版本需要用--force之类的强制参数才能跳过校验。这说明一个什么问题呢?拿到文件后不要先想着怎么破解逻辑,先确认版本支持度。
如果是新版 Unity 的游戏,建议先用工具包里自带的版本探测能力去读一下 metadata 的头部信息,确认兼容性再决定要不要继续。硬来的结果通常就是抛出异常,或者生成一个看起来正常但大量字段类型是错的 dump.cs,这个很隐蔽。
global-metadata.dat的位置一般比较固定,通常在游戏 APK 的assets/bin/Data/Managed/Metadata/目录下可以找到,iOS 版本则是在Data/Managed/Metadata/。libil2cpp.so正常在lib/arm64-v8a/下,需要注意不同渠道包 ABI 不同,尽量拿 arm64 的,和后面动态调试的思路也匹配。
2.3 什么样的分析场景适合用这个方案
第一类是性能分析和崩溃分析。这类需求不关心具体业务逻辑怎么策划,但需要快速定位到某个线程到底在跑什么函数、崩溃栈里那个地址对应的是哪个方法。手工在 IDA 里找符号无异于大海捞针,但有了 dump.cs 和符号文件,把偏移对回去就能还原调用栈。
第二类是安全评估。比如要检查某游戏有没有在客户端做了不应该的校验逻辑,或者要定位某个敏感字符串的引用位置。字符串本身可能被加密,但 dump.cs 能提供字段名和方法名的组合,缩小搜索范围。
第三类是灰度兼容性分析。运营团队想确认某个新版本更新后到底改了哪些核心类,直接做版本间 dump.cs 的 diff,改了什么一目了然。
需要有心理预期的是,这个方案不适合完全没有二进制分析基础的人做零基础入门。你得至少看得懂 IDA 的函数列表和几个基本汇编指令,否则 dump 出来的符号表也只是文档里的一堆名字,形不成真正的分析能力。
3. 把 Il2CppDumper 用起来:命令行、配置文件与产物地图
3.1 Windows 下跑通一次 Dump 的最小流程
假设你已经把 APK 解包拿到了libil2cpp.so和global-metadata.dat,工具解压后同目录放这两个文件,然后用命令行执行:
Il2CppDumper.exe libil2cpp.so global-metadata.dat dump_out逻辑上是从第二个参数读取 metadata 文件,解析出类型结构;从第一个参数读取可执行文件,建立 method token 到虚拟地址的映射;第三个参数指定输出目录。执行成功后可以预期得到三个主要文件:dump.cs是核心产物,script.json保存函数地址与签名信息,il2cpp.h是 C++ 风格的头文件描述。
参数说明:第一个文件路径填 so,后面的 metadata,最后是输出目录。如果不带输出目录,工具默认在自身目录下生成结果。我习惯每次指定独立目录,不然多个版本混在一起容易乱,血泪经验。另外某些环境需要把两个文件拷到工具同一目录下再执行,这是因为老版本的命令行解析在某些 Windows 语言环境下对路径空格处理不友好,简单规避即可。
执行之后如果看到版本号识别成功的日志,说明匹配上了。如果提示 metadata 版本不支持,不要慌,还有别的路子,后面避坑章节详细说。
3.2 配置文件里的几个关键开关
工具同目录下会生成一个config.json,它控制 dump 的精细程度。这里有几个开关值得关注:
DumpMethod、DumpField、DumpProperty:控制是否需要把方法、字段、属性都写进 dump.cs。默认全开,建议第一次不要关,全量导出后面再过滤。DumpAttribute:是否导出特性标注。部分游戏在运行时用特性做配置,这个对定位逻辑有帮助。GenerateDummyDll:是否生成程序集级别的 DLL 桩,普通分析用不上,除非你要用调试器做 C# 断点级联调。ForceIl2CppVersion和ForceMetadataVersion:强制指定版本号。这个后面排查章节会重点讲。
命令行如果需要指定版本,常见的调用形式是这样:
Il2CppDumper.exe libil2cpp.so global-metadata.dat dump_out --force 2021.4.18注意这个--force参数要配合 config 里的对应开关一起用,只传命令行不改配置文件,部分版本会直接忽略。遇到新版 Unity 项目,判断不了真实版本时就靠它先蒙一个兼容版本试跑,能出结果的概率大概有三成,剩下的就得处理更复杂的情况了。
3.3 产出文件的地图:先看什么、后看什么
拿到输出目录后,不要一头扎进 dump.cs 从头读到尾,那上万行的文件通读是没有意义的。我有固定的阅读顺序。
先看script.json。它的结构是字典,key 是 metadata 类型索引,value 包含地址偏移和方法签名。这个文件是给脚本用的机器语言,人眼读不友好,但它能判断一个关键问题:方法的虚拟地址是不是合理的代码段范围。如果某个方法的地址落到了 0x0 或者明显小于镜像基址,那说明本次解析大概率有个别地方错误,要格外谨慎。
然后看dump.cs里的全局命名空间分布。用字符串搜索能力搜几个和游戏包名相关的命名空间,确认主要业务类都已经恢复。正常工程里Assembly-CSharp.dll是主程序集,我们关注的是里面玩家飞机、战斗流程、副本管理这类核心类,如果这些类的方法签名都正确,就可以认为本次 dump 质量达标。
最后才是用il2cpp.h补细节,一般情况下我用不上这个文件,除非是要给 IDA 写加载脚本或给某跨平台系统做解析模块时才用。它和 dump.cs 描述的是同一套结构,多一个 C 头文件是为了方便 C/C++ 工具链直接引用。
3.4 第一次执行的完整命令可以让输出更规整
实际项目中会有大量重复跑的情况,把命令固定成一段能省很多事。我的习惯是写个批处理放在工具同目录:
@echo off Il2CppDumper.exe libil2cpp.so global-metadata.dat out_%date:~0,4%%date:~5,2%%date:~8,2%参数解释:输出目录按日期命名,每次执行自动生成新目录,避免上一次的结果被覆盖;前面两个路径固定为约定好的文件名,配合脚本批量替换文件就能连续处理多个版本。这个习惯在有多个渠道包要分析时特别有用。日志里如果输出dump completed就是成功了,其他结果都要检查下再继续。
4. 让 dump.cs 变成可用的分析线索:三个进阶处理技巧
4.1 用 dump.cs 快速画出目标模块的类图关系
dump.cs 本质上是一个完整的 C# 语法文本,类和接口的继承关系、方法重载、字段定义都写得很明确。可以直接写个小脚本把关键类的基类和它实现的接口抽出来,形成一张依赖继承的表格。比如想知道某个战斗系统类都依赖哪些组件,用正则把它的基类、接口和引用类型字段解析出来,就能很快看清边界。
我在实际项目里用一个很简单的方案:先把 dump.cs 里某个关键命名空间下的类摘出来,再统计这些类里方法名包含特定关键词的方法,比如Init、Update、Reset、OnClick这类映射到游戏生命周期的方法。这类方法往往对应着引擎回调或者 UI 事件绑定,是定位逻辑入口最强的信号。
这步做出来以后,一个普通的手游核心流程大概能还原出 60% 的调用骨架,剩下的 40% 就在 IDA 里对着地址看汇编了。有了符号名再去看汇编,事半而功倍。
4.2 script.json 配合 IDA 做批量函数重命名
手工在 IDA 里一个函数一个函数地改名字是最笨的,量大了也容易出错。常见做法是写一段 IDAPython 脚本,从script.json读键值对,把函数的地址和命名对应关系批量写进 IDA 的数据库里。
import json with open("script.json", "r", encoding="utf-8") as f: data = json.load(f) # data 内的 addr 字段是相对虚拟地址,需要加上镜像基址作为最终地址 # image_base 是 IDA 自动识别的加载基址,一般可以通过 idaapi.get_imagebase() 拿到 image_base = 0x1000000 # 遍历函数表,将脚本中记录的地址同步成当前数据库内的真实地址 # address 统一按整数处理,避免字符串拼接造成地址精度丢失 for item in data: addr = int(item["addr"], 16) + image_base name = item["name"] if idc.get_func_name(addr) != name: idc.set_name(addr, name, idc.SN_FORCE)这段脚本的逻辑很直白:从 json 读取每一个方法的相对偏移,加上 IDA 识别出的镜像基址得到实际虚拟地址,再用SN_FORCE强制改名。过程中要留意两个常见的翻车点,一是 json 里某些方法是泛型特化,名字里面带反引号和数字后缀,IDC 的命名规则可能不允许这种字符,需要先做一次字符清理;二是地址有可能重叠,同一个偏移被多个 token 指向,改名时后写的覆盖前面,结果导致 IDA 函数名混乱。
参数说明:image_base的值不要手填,用idaapi.get_imagebase()动态获取,避免不同镜像下直接改出无效地址。脚本里SN_FORCE是关键,没有它,IDB 里已有的分析结果会阻止改名,报了异常就中断了。
4.3 用脚本过滤掉无用噪音,留下核心方法
完整 dump 一个中型游戏,dump.cs 几十万行是很正常的,真正需要人肉阅读的核心逻辑可能只占 2%。常见做法是写个提取脚本,只保留特定命名空间的方法声明和字段声明:
import re # 读取 dump.cs 全文,按行为单位做过滤 # 保留条件和场景强相关:命名空间前缀、方法特征关键字都放在规则里 with open("dump.cs", "r", encoding="utf-8") as f: lines = f.readlines() keywords = ["BattleManager", "LoginHandler", "ItemStruct"] filtered = [] for line in lines: # 只保留包含关键字的类声明、字段声明和方法声明 # 避免把命名空间内部所有代码全留下 if any(k.lower() in line.lower() for k in keywords): filtered.append(line) with open("filtered_dump.cs", "w", encoding="utf-8") as f: f.writelines(filtered)这个脚本不复杂,但很实用。注意过滤规则要同时覆盖类名、接口名和字段类型,否则容易漏掉关键关联。另外过滤后的文件只适合人工阅读,别拿去做全文检索——因为大量关联类型已经被滤掉了,直接搜索会得出错误的结论。
这一步做完以后,分析效率会有本质提升。原来打开几十万行的文件不知道该看哪儿,现在整个关键路径上只留下几百行有效信息,配合前面的 IDA 改名脚本,整个分析流程就从"大海捞针"变成"顺着地图找节点"。
5. 避坑指南:Il2CppDumper 常见的五类翻车现场
5.1 提示 Unsupported metadata version,程序直接退出
现象是命令执行后立刻抛异常,日志里显示当前 metadata 版本不被支持,程序拒绝继续。
原因很简单:你的 Unity 版本比工具内置的版本映射表新,或者说这个工程的 metadata 数据结构已经调整过,工具不认识里面的字段排列。
解决路径有几个。首选是更新工具版本,看有没有新的 release 支持对应 Unity 版本;其次是用 config 里的强制版本参数,盲试相近版本。在模拟项目里我试过一次,Unity 2021 的工程用 2020 的强制版本跑,居然正常输出了,虽然个别接口类型解析有问题,但整体可用。需要注意强制版本跑出来的结果,新手期不建议直接拿去做动态分析,先做少量人工校验。
5.2 dump.cs 里类名正常,但方法体全是空的占位符
现象是类型结构、字段、属性都很完整,但方法声明只有签名和分号,没有方法体内的关键逻辑提示。
原因是某些新版本的 IL2CPP 引入了更激进的代码剥离策略,metadata 中依然保留方法签名,但方法体的原 IL 指令已经被移除,dump 出来的只是壳。这种情况下直接读 dump.cs 是看不出逻辑的,必须用 script.json 结合 so 的汇编分析。
解决方式是放弃对 dump.cs 方法体的依赖,转去 IDA 里看对应地址段的汇编,同时结合字段偏移和类型名称反推逻辑。这个方法曲线会陡,但确实是目前主流游戏都会遇到的方向。
5.3 Dump 正常,但反推出来的函数地址在 IDA 里对应到乱码数据
现象是 script.json 的地址段看起来正常,但用 IDA 定位后发现该地址根本不是代码段,而是只读数据或者完全无意义的字节。
原因是镜像基址对不上。这类工具默认按装载基址 0 处理,而真实进程镜像的基址在 Android 上通常是0x...某个固定值,不同 ABI 和系统版本还会有随机化偏移。尤其要注意模拟器和真机的差异。
解决方式就是在脚本里正确处理基址换算。前面给的示例里虽然在代码中说明了要用动态基址,但实际执行时还是建议大家手打一次校验,拿一个已知函数的地址做 sanity check,确认偏移方向对再去批量执行。这个检查步骤能救回很多白忙活的时间。
5.4 替换了不同渠道包的文件后,偏移全部错位
有些游戏的渠道包代码版本不同,同一个版本号下可能包含不同的热更和 A/B 实验开关。如果你拿着 A 渠道的libil2cpp.so去对 B 渠道的global-metadata.dat,那么类列表可读,但方法名和地址错位很常见。
原因在于两个文件必须严格来自同一个构建产物。代码经过混淆或条件编译后,函数顺序可能微调,metadata 里的类型索引就可能变化。
解决方式就是核对文件构建信息。一般可以从 so 里的Unity version字符串和 metadata 头部的版本号做双重比对,再不行就看 dump 日志里识别的版本号是否一致。
5.5 字符串全是乱码,dump.cs 里看到一堆奇怪的字节
这是加密字符串干扰,不是工具坏了。很多 App 能检测到非法篡改和使用调试器,字符串是运行时解密,静态 metadata 里只有密文。dump.cs 里的乱码实际上是密文经过 Unescape 后的形态,不是可读文本。
解决方式就是别在静态文件里找字符串,改到运行时动态获取。挂上调试器在解密函数下断点,或者用内存搜索去匹配解密后的明文,再把明文对应的相对偏移在 script.json 里定位函数,这样就能找到引用点了。这条经验只适合有动态调试基础的人使用,新手去搞容易被反调试机制带崩。
6. 最后教你一个验证偏移是否正确的日常习惯
很多新人做完 dump 之后最没底的一件事是:怎么知道这份脚本靠不靠谱?我每次都用一个很笨但有效的办法,拿il2cpp_class_from_name下手验证。
这个 API 在 IL2CPP 里是运行时把字符串形式的类名转换成类对象的入口,IDA 搜索到它的交叉引用后,能看到这个方法调用的调用方往往是一个静态类初始化函数。而 script.json 里对应这个类的init方法偏移如果和 IDA 里看到的吻合,那整条链路基本就通了。
更简单粗暴的验证是靠常见字符串定位。比如游戏里一定有个设置面板类的名字包含 Settings,全局搜 dump.cs 找到那个类,记下类构造函数的偏移,再回 IDA 里找到那个偏移地址看汇编前几条指令是不是标准的栈帧建立代码。是,就说明这个偏移有效,整个 dump 版本也可信。
还有一种情况是虚拟方法表偏移。IL2CPP 的类结构里虚表地址在 metadata 里存着,很多功能入口是通过虚函数调用的。针对一个 dump 成功的类,去对比它的虚表条目数和 dump.cs 里的方法列表数,数量一致且顺序对应,这份 dump 就是能用的。
这套方法每个工程只需要做一次,过程大概是十分钟,但带来的信任感是后面整个逆向分析的地基。我自己每次换新版本工具或者新游戏项目,都会先花这十分钟做校验,再开始正式的分析流程。这么多年养成的习惯,几乎没遇到过校验通过但实际分析时翻车的情况。可惜当时某开发者跟我说不用校验,直接信 dump.cs,结果那个项目白干了一个多星期,从那以后我就再没跳过这步。
希望这个技巧对你也有用,也希望这篇文章能帮你少走一段弯路。
本文还有配套的精品资源,点击获取