首页 / 资讯中心 / 文章详情

IL2CPP符号恢复实战:用Il2CppDumper还原Unity函数名与分析方法

IL2CPP符号恢复实战:用Il2CppDumper还原Unity函数名与分析方法 ★ FEATURED ARTICLE
简介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, encodingutf-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, encodingutf-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, encodingutf-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结果那个项目白干了一个多星期从那以后我就再没跳过这步。希望这个技巧对你也有用也希望这篇文章能帮你少走一段弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站