多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Unity逆向实战:Il2CppDumper还原C#类型与避坑指南

Unity逆向实战:Il2CppDumper还原C#类型与避坑指南 简介面向从事Unity游戏逆向与安全研究的工程师这款Windows版工具专门解决IL2CPP编译后程序集结构难以还原的痛点。它能在缺少源码的情况下还原出DLL文件中的类型与元数据并提取MonoBehaviour和MonoScript支持常见的ELF、Mach-O、PE、NSO与WASM等文件格式可覆盖Unity 5.3至2020的多个版本。压缩包内共包含11个文件主要组件是两个可直接运行的执行程序、八个用于自动化分析的Python脚本以及一个JSON配置整体体积约10.7MB执行程序用于完成核心的还原与修复脚本用于对接IDA与Ghidra生成结构体头文件也能处理WASM等特殊格式。该版本还额外支持从内存转储绕过保护以及简单PE保护绕过方便直接分析从进程内存中导出的库文件或带壳可执行程序。截至目前已有1048人学习下载借助配套的IDA/Ghidra脚本和结构体头文件生成方案能够快速还原Unity工程的关键类、接口与脚本信息适用于游戏修改、安全审计和通信协议研究等实际场景。1. Il2CppDumper-win-v6.7.46 是什么当 Unity 游戏只剩 Dll 和 dat它帮你还原 C# 世界拿到一个 Unity 打包的样本翻遍整个目录只有两个文件最显眼一个叫GameAssembly.dll一个叫global-metadata.dat。前者是编译后的原生二进制后者是全局元数据文件。如果你试着用常见的反编译工具去打开那个 Dll看到的只有一堆没有符号的汇编指令C# 的类名、方法名、字段名早就被 AOT 编译吞掉了。Il2CppDumper-win-v6.7.46 就是专门干这个的工具把这两个文件喂给它它会恢复出完整的类型定义、方法签名、字段偏移甚至生成一个可以继续做 .NET 反编译的 DummyDll 目录。对于做安全分析、兼容性研究、或者自家产品排障的人来说这一步能省下至少三天的盲猜时间。这篇文章我会从原理、操作、产物使用到高频踩坑一步步把它跑通并用到自己的样本上。2. 先把原理走通il2cpp 二进制与 global-metadata.dat 到底在配合什么很多人一上来就双击 exe报错就懵。其实 Il2CppDumper 的成败九成取决于你认不认识这两个输入文件之间的关系。这章不涉及复杂数学只需要理解它内部在做一次“映射”。2.1 为什么 Unity 要抛弃 Mono把 C# 编成 native 代码早期的 Unity 使用 Mono 运行时C# 代码先编译成 IL然后由运行时通过 JIT 即时编译成机器码。JIT 的好处是跨平台方便坏处是首帧卡顿、内存占用高而且在 iOS 这类不允许动态申请可执行内存的平台上很受限。于是后来的 Unity 引入了 IL2CPP先把 C# 的 IL 转换成 C 源码再交给各平台的 C 编译器编译成原生二进制。这套路的直接结果是你拿到的GameAssembly.dll里已经完全没有 IL 了也没有.NET的元数据表。类名、方法名、字符串全都被压扁成一个个二进制结构体分散在两个地方一部分在代码段里函数体、虚表指针另一部分在global-metadata.dat里类型信息、名字和偏移表。如果我们只盯着二进制看确实像读天书。Il2CppDumper 做的事情并不是“解密”更不是暴力破解。它本质上是按照 Unity 在某个构建阶段写出的元数据布局规则把这些离散信息重新串起来。global-metadata.dat里存了足够多的名字和偏移索引而GameAssembly.dll里的代码段又有指向这些索引的引用。两者像一本字典和一套页码单独看都没用合在一起才能恢复出 API 结构。2.2 metadata 里有什么以及它和二进制是如何被绑定的global-metadata.dat是以二进制格式存储的内部包含字符串字面量、类型定义、方法定义、字段定义、属性、接口、虚表偏移等。可以说它就是一份压缩过的反射信息只不过不是标准的.NET元数据格式而是 Unity 自家的 il2cpp 结构体布局。这两个文件是通过“偏移”绑定的。举个例子二进制的代码段里有一处调用指令指向一个偏移地址这个偏移在 metadata 里对应某个方法名。Il2CppDumper 会先解析 metadata 里的方法表拿到每个方法的 token、参数类型、返回值类型然后再扫描二进制的虚表位置和函数引用点把 token 映射成实际的 RVA相对虚拟地址。最后把名字和地址拼在一起输出。所以这里有一个极其重要的结论metadata 和二进制必须来自同一次构建。只要你把某个版本的global-metadata.dat配到另一个版本的GameAssembly.dll上就算工具不报错生成的方法名和地址也会错得千奇百怪。后面第 5.1 节会专门说这个。2.3 动手前先核对版本、导出模式和符号表三者缺一不可我习惯在做任何解析前先花两分钟核对三件事。第一是文件完整性特别是 metadata 头部的魔数能不能读出来第二是架构32 位和 64 位的二进制解析逻辑差异很大第三是有没有被壳处理过带壳的文件直接喂工具通常只会得到崩溃或残缺结果。下面这段 PowerShell 脚本可以从GameAssembly.dll的 PE 头里读取机器类型快速判断是 x86 还是 x64。这是 Windows 上最省事的核对方式。$bytes [IO.File]::ReadAllBytes(GameAssembly.dll) $e_lfanew [BitConverter]::ToInt32($bytes, 0x3C) $machine [BitConverter]::ToUInt16($bytes, $e_lfanew 4) switch ($machine) { 0x14c { x86 (32-bit) } 0x8664 { x64 (64-bit) } default { unknown machine: 0x{0:X4} -f $machine } }逻辑说明PE 文件的 DOS 头偏移0x3C处保存了到 PE 头的偏移量e_lfanewPE 头的前两个字节是机器类型字段。0x14c是 x860x8664代表 x64。如果输出 “unknown”说明这个文件可能不是有效的 PE 格式或者被加壳后在头部做过手脚。对于 metadata 文件直接用十六进制编辑器打开看前四个字节比较直观。正常数据通常是一段特征性的魔数如果看起来是乱码、全零或者带有明文标记就要考虑是不是被加密过。另外从内存中 dump 出来的 metadata 可能带有额外的头需要先剥离。这个核对过程能帮你过滤掉一大半“工具不好用”的假象。3. Windows 上跑通 v6.7.46拖拽、交互式输入与最小操作步骤了解了原理接下来就是实际操作。这个工具是 Windows 下的 exe没有复杂的安装过程解压就能跑。但为了少翻车我建议你按下面顺序准备环境。3.1 解压后先看这些文件别急着开跑解压后目录里通常会有主程序 exe、说明文档和一两个配套文件。不要点开就看先把它放到一个没有中文、没有空格、不带特殊符号的路径下比如D:\il2cpp\。这看起来像强迫症但实际遇到过很多次因为路径里的中文字符导致文件路径解析失败的情况尤其是不同语言的 Windows 系统对编码的处理不一致。另外终端安全软件偶尔会把这个工具误判为风险程序。如果你用的是工作机放进白名单再跑否则可能在加载阶段直接被隔离。我见过不止一个人明明下载成功了一运行就闪退最后发现是安全策略拦住了进程。把GameAssembly.dll和global-metadata.dat放在同一个目录下命名尽量简单。我一般会建一个名为input的文件夹里面只放这两个文件。这样就算后续要批量跑多个版本也不容易混淆。3.2 命令行与交互模式第一次就成功的最小流程最稳妥、最不容易出错的运行方式是直接双击 exe然后按交互提示输入路径。这比背参数更靠谱因为不同版本的命令行参数差异不小。下面是一次完整的输入过程Il2CppDumper.exe # 控制台出现提示Input Il2Cpp binary path # 输入GameAssembly.dll # 控制台出现提示Input metadata path # 输入global-metadata.dat # 工具解析完成后会提示输出目录或直接在当前目录生成输出这条流程的优点是宽容。你不需要记住任何参数工具会一步一步问。如果文件路径太长可以先切换到文件所在目录再启动 exe这样直接用文件名就够了。注意这里的输入不带引号除非路径里真的有空格。部分版本也支持带参数直跑常见写法是Il2CppDumper.exe GameAssembly.dll global-metadata.dat -o dump其中-o dump指定输出目录为当前路径下的dump文件夹。如果你的版本直接报错不认-o就退回交互模式。命令行参数在不同构建之间并不完全一致所以不要硬背。跑完以后屏幕上应该会出现一行类似 “Done!” 的提示。这时打开输出目录你才能真正理解它做了什么。3.3 输出的四个产物该看哪些、能扔哪些正常完成后输出目录里通常会有四类东西dump.cs、il2cpp.h、script.py和一个DummyDll文件夹。我用一个表来说明它们的用途。产物用途处理建议dump.cs纯文本包含所有类型、方法、字段和 RVA 信息适合全文搜索保留当作索引il2cpp.hC 头文件给 NDK 层代码或原生调试用保留但日常不太看script.py用于反汇编器批量导入函数名的脚本按需使用DummyDll空壳 .NET 程序集集合能放进 .NET 反编译工具看结构保留下一章核心材料其中DummyDll里的每个 DLL 并不是真实可运行的程序集方法体都是空的。它的价值在于把global-metadata.dat里的类型信息翻译成了标准 .NET 元数据让常见的反编译工具能读出类名、方法签名、字段偏移。下一章我就用它来快速还原类结构。4. 从生成的 DLL 到可用类定义反编译、字符串定位与函数映射的落地路径工具跑完只是第一步真正的起点是把DummyDll变成你脑子里清晰的调用关系。这一章我会顺着一条实际分析路径走一遍先看类定义再拿地址最后落到 native 层定位函数。4.1 用 .NET 反编译工具打开 DummyDll还原方法和字段把DummyDll文件夹里的程序集比如最大的那一个拖进 .NET 反编译工具你会看到类似这样的结构public class PlayerController : MonoBehaviour { public int health; public string name; public void TakeDamage(int damage) { } }注意这个类的方法体是空的只有签名和字段。字段类型、方法参数都是真实的偏移量也会以某种形式保留。这就是你想要的地图它告诉你这个游戏里有哪些对象、它们能做什么、数据放在哪。看到类名后我通常会在反编译工具里先用“分析”或“搜索”功能把跟业务逻辑强相关的关键词过一遍比如TakeDamage、LevelManager、IsServer。这一步能快速判断样本里是否包含某个系统也方便后续去dump.cs里找对应的 RVA。4.2 用 dump.cs 当索引在反汇编器里找回本体知道类和方法名还不够你最终可能要定位这个函数在GameAssembly.dll里的实际位置。这时候dump.cs就是索引。打开它搜索方法名会看到类似下面这样的条目public void TakeDamage(int damage) { } // RVA: 0x12A3B4 Offset: 0x2A3B4RVA 是相对虚拟地址它不等于文件中的物理偏移。要在反汇编器里跳转需要算一下绝对地址。如果你已知加载基址是 0x140000000这是 64 位 PE 的默认 ImageBase用下面这段 Python 就能算出目标地址# 已知 PE 默认加载基址 0x140000000 base 0x140000000 rva 0x12A3B4 # 从 dump.cs 中复制的 RVA target base rva print(跳转地址:, hex(target))把输出地址填进反汇编器的跳转框按回车就能直接落到函数开头。如果你拿到的样本不是从磁盘静态加载的而是从内存中 dump 出来的那基址要以 dump 工具给的实际加载地址为准不能盲目用 0x140000000。这是后面避坑章节里最容易踩的点之一。4.3 一个最小验证流程从类名到调用点为了确认我们找到的函数真的是业务逻辑而不是某个公共库函数我习惯做一次最小闭环验证。步骤非常简单在dump.cs里搜索目标类名记录方法名和 RVA。在反汇编器中跳到ImageBase RVA。查看函数前几条指令是否与参数和局部变量特征匹配。查看该函数的交叉引用列表看看有哪些地方调用了它。如果交叉引用是空的回到 2检查函数是否被内联或 strip。这个流程通常只需要五分钟但它能帮你确认“名字对应代码”不是幻觉。很多人跑完 Il2CppDumper 就直接去搜字符串把思路带偏最后对着一个不相关的地址浪费一下午。先做验证再展开深入分析效率会高很多。5. 避坑指南v6.7.46 在真实样本上的 5 个高频问题工具本身是挺好用的但真实样本永远不会像示例那么干净。这一章我整理了 5 个我在不同样本上反复踩过的坑全部按“现象 → 原因 → 解决”的形式写你照着排查能省不少时间。5.1 错误提示 Magic number 不对先别怪工具多半是两个文件不匹配现象启动后直接提示Magic number错误或者类似Bad metadata的话程序根本不往下走。原因global-metadata.dat和GameAssembly.dll不配套。常见场景是样本网盘里文件名都对但其实本体是不同版本混搭的。另外有些分发者喜欢把 metadata 做一层压缩或者加前缀工具不认。解决从来源可靠的地方重新提取尽量用同一个安装包里的文件。如果文件是内存 dump 出来的用十六进制编辑器看看文件开头有没有非 normal 的头结构。如果发现了自定义头部先把这部分去掉再跑。千万不要自己去改魔数骗过校验那样只会得到一堆垃圾地址。5.2 生成到一半崩溃或卡死控制流指令识别失败的典型信号现象程序运行了几秒输出了一部分文件就崩溃或者 CPU 占用飙到 100% 后一直卡住。原因样本二进制被加壳保护或者带有针对静态分析的反模式。Il2CppDumper 在扫描虚表时依赖对指令边界的识别一旦遇到加密后的代码或者花指令解析器就会走进死胡同。解决不要拿壳文件直接喂工具。常见做法是在调试器里把程序跑起来从内存中转储已解密的实际镜像再用这个镜像做输入。注意转储时要保证内存页完整尤其是原始文件头部分。转储后如果还有问题优先试试往dump.cs输出前加一段手动修复指令把明显异常的字节段跳过。5.3 虚拟地址和文件偏移搞混脱壳后被坑现象工具生成的 RVA 已经算好了但在反汇编器里输入地址看到的却是一堆db数据完全不像函数开头。原因你混淆了两个概念。RVA 是相对虚拟地址文件偏移是磁盘上相对文件开头的偏移。PE 加载到内存时各 section 有内存对齐RVA与FileOffset之间相差一个固定的 delta。直接拿 RVA 当文件偏移用自然对不上。解决记住一个公式VA ImageBase RVA。反汇编器里输入 VA不是 FileOffset。如果你手里的二进制是从磁盘静态加载的就不要手动添加任何 section 偏移。如果你用的是从内存 dump 的镜像则要按照 dump 工具给出的 ImageBase 去加。这个原则放之四海而皆准。5.4 一堆__c前缀方法要不要开反混淆现象dump.cs里充满了__c_0_xxx这种奇怪名字看起来就像被混淆过。原因这其实不是混淆而是 il2cpp 编译器在生成 C 中间代码时对匿名闭包、自动生成的方法使用了这种命名规则。它只是丑陋并不代表原意丢失。解决先用默认配置跑不开任何反混淆选项。等类结构全部还原成功后再处理名字。如果你一上来就开启“反混淆非法字符”或“清理命名”很容易让工具在处理到某个版本的控制流时提前中断结果连基本结构都拿不到。实在介意名字可以后期用字符串交叉引用去重命名。5.5 老版本样本输出错位Unity 版本差异现象同一个 v6.7.46 工具跑一个新样本没问题跑一个老样本却生成一堆看似合理的类但地址完全对不上。原因不同 Unity 版本的 il2cpp 内部结构体布局不一致。Il2CppDumper 每个版本都会适配一批新加或者变更的结构但老版本的结构字段很可能已经被移除或改名。解决记录样本对应的 Unity 版本。这个信息通常可以在游戏引擎的版本文件里读到也可以在 metadata 尾部找到一些特征字符串。然后根据版本选择合适的工具版本。不要指望一个版本通吃所有样本。我在本地会按 Unity 版本号建目录放下对应版本的工具和已生成的索引需要时快速切换。6. 进阶技巧两个小脚本把 v6.7.46 变成批量还原流水线单个样本跑通之后下一个需求往往是批量处理。这里分享两个我实际在用的脚本一个负责批量调用工具一个负责验证输出是否完整。6.1 批量跑样本用批处理脚本实现一分钟一个如果你手头有几十份样本目录结构都是样本名/GameAssembly.dll加样本名/global-metadata.dat可以用这个批处理脚本循环处理echo off setlocal enabledelayedexpansion for %%f in (samples\*.dll) do ( set base%%~nf if exist samples\!base!\global-metadata.dat ( echo Processing !base! ... Il2CppDumper.exe %%f samples\!base!\global-metadata.dat out\!base! ) ) endlocal这段脚本遍历samples目录下所有的 DLL如果同目录下存在对应的 metadata 文件就调用 Il2CppDumper 输出到out下。用%%~nf取文件名主干确保每个样本的输出目录不冲突。注意如果输出目录不存在有的版本不会自动创建建议先mkdir out。6.2 用类数量做回归验证升级后遗漏了哪个程序集一眼可见批量跑完不能只看有没有报错还要看输出质量。我用一个简单的 Python 脚本统计dump.cs里的类数量然后与历史数据对比import re, sys with open(sys.argv[1], r, encodingutf-8, errorsignore) as f: content f.read() classes len(re.findall(r^public (?:sealed )?class , content, re.M)) print(class count:, classes)如果之前是 1200 个类这次只有 900那就说明 metadata 提取不完整或者二进制被清理过需要回到提取阶段检查。这个脚本也适合在升级工具版本后做回归——版本升级不一定全是改进偶尔也会因为适配新结构而丢掉老格式的兼容。我自己的习惯是把每次样本的类数量、dump.cs大小记在一个文本文件里积累几周后就能看出规律也能在工具更新时第一时间发现“意外惊喜”。希望帮到你。本文还有配套的精品资源点击获取
返回列表