
1. 项目概述为什么我们需要这个集成方案如果你在Unity逆向这个圈子里摸爬滚打过一阵子尤其是面对那些用了il2cpp后端编译的现代Unity游戏或应用那你一定对“Il2CppDumper”这个名字不陌生。它几乎是所有逆向工程师打开il2cpp黑盒的第一把钥匙。但拿到钥匙只是第一步怎么高效地使用钥匙打开门并在门后的复杂迷宫里也就是反汇编后的二进制代码快速找到宝藏才是真正的挑战。这就是我今天想跟你聊的如何把Il2CppDumper这把钥匙和Binary Ninja这个强大的导航仪无缝集成起来彻底告别那种“脚本生成一堆文件还得手动导入、对不上号、满世界找符号”的糟心困境。简单来说这个方案的核心价值在于自动化与可视化。传统的流程是用Il2CppDumper处理游戏文件生成一堆.cs、.h、.py脚本文件然后你手动把这些脚本加载到IDA Pro、Ghidra或者Binary Ninja里祈祷一切顺利。这个过程不仅繁琐而且容易出错特别是当游戏版本更新或者有自定义运行时结构时。我们的目标是打造一个“一键式”或至少是“流程化”的解决方案让Il2CppDumper的分析结果能直接、准确、实时地反映在Binary Ninja的反汇编视图中让你能像分析一个有完整调试符号的本地程序一样去分析一个被混淆和保护的Unity il2cpp应用。这适合谁呢首先是游戏安全研究员、外挂检测与对抗工程师你需要深入理解游戏逻辑的实现机制。其次是想要学习Unity引擎内部机制或者对某些闭源Unity应用进行功能分析与复现的开发者。当然也包括CTF中遇到Unity题目的逆向爱好者。无论你是哪一类一个流畅的逆向分析环境都能让你的效率提升数倍。2. 核心工具链解析Il2CppDumper与Binary Ninja的定位与协同在深入集成细节之前我们必须先厘清这两个核心工具各自扮演的角色以及它们为什么要“在一起”。这就像组装一台精密仪器你得先了解每个零件的功能。2.1 Il2CppDumperil2cpp世界的“地图生成器”Il2CppDumper不是一个动态调试器也不是一个反汇编器。它的核心任务非常专一解析il2cpp的元数据Metadata和全局元数据注册表GlobalMetadata.dat并重建出原始C#代码的类、方法、字段等结构信息。你可以把它想象成一个考古学家通过挖掘遗址游戏二进制文件中的碑文元数据还原出古代城市原始C#项目的地图、建筑名称和功能说明。它的工作输入通常是两个核心文件GameAssembly.dll(或 libil2cpp.so)这是由C编译而成的核心逻辑库包含了所有游戏逻辑的实现但所有的函数名、类名都已被抹去只剩下内存地址。global-metadata.dat这是il2cpp编译过程中生成的“字典”或“索引”它记录了原始C#类型系统与编译后C函数地址之间的映射关系但本身是加密或编码的。Il2CppDumper的魔法就在于它能解密并解析这个global-metadata.dat然后将其与GameAssembly.dll中的地址关联起来最终输出一份“地图”。这份地图通常包括IDA Python / Ghidra Script / Binary Ninja Script用于直接向这些反汇编工具中导入符号函数名、类名。DummyDll一组空的.NET程序集DLL用于在dnSpy等.NET反编译器中提供类型信息方便查看代码结构虽然没实现但有签名。Script.json一个结构化的JSON文件包含了所有解析出的类型、方法、字段及其地址信息。关键点Il2CppDumper生成的是“静态”的映射信息。它不负责动态分析、不修改二进制文件、也不处理代码混淆。它的输出质量直接取决于元数据文件的完整性和版本兼容性。2.2 Binary Ninja现代化的“交互式导航仪”Binary Ninja简称BN是一个相对较新的反汇编平台以其现代化的UI、强大的中间语言MLIL, HLIL分析和出色的API与插件生态著称。相比于老牌的IDA ProBN在自动化分析、脚本编写和用户体验上往往更胜一筹。在il2cpp逆向的上下文中BN的角色是二进制加载与反汇编正确加载GameAssembly.dll进行初步的代码分析识别函数边界虽然一开始都是sub_XXXXXX这样的匿名函数。符号与类型信息承载平台接收并应用Il2CppDumper生成的“地图”符号信息将成千上万个sub_XXXXXX重命名为有意义的ClassName::MethodName。高级静态分析环境利用重命名后的符号结合BN的交叉引用XREFs、数据流分析、结构体Structure识别和类型传播Type Propagation功能深入分析游戏逻辑。BN的插件架构是其最大优势之一。我们可以通过Python插件动态地读取Il2CppDumper的输出特别是script.json并在BN分析数据库BNDB中批量创建函数、重命名、应用类型签名甚至自动识别和创建il2cpp运行时特有的数据结构如String, Array, List等。这实现了从“地图”到“导航仪内置三维模型”的飞跃。2.3 协同工作流从混沌到清晰理想的集成工作流应该是线性的、可重复的数据提取从目标Unity应用中获取GameAssembly.dll或.so和global-metadata.dat。这可能涉及APK解包、IPA解压或直接从游戏目录获取。地图生成使用Il2CppDumper处理上述两个文件指定输出格式包含Binary Ninja脚本script.py和关键的script.json。导航仪加载用Binary Ninja打开GameAssembly.dll等待其完成初始自动分析这可能会花点时间因为文件通常较大。地图导入不是简单地运行生成的script.py就完事了。我们需要一个更智能的插件或脚本它能解析script.json处理可能存在的地址偏移Image Base并优雅地将所有符号导入到当前BN的数据库中同时处理命名冲突和无效地址。后期分析与标注在符号导入的基础上利用BN的API进一步自动化为Il2CppString、Il2CppArray等常见运行时类型创建并应用自定义结构体Structure让反编译视图HLIL更易读标记虚函数表vtable识别并注释重要的游戏管理器类如GameManager,PlayerController的实例。这个流程的难点和传统方法的痛点主要在第4步。简单的脚本可能会因为基址不对齐、版本差异、脚本逻辑不完善而导致大量符号导入失败或导入错误位置。我们需要一个健壮的集成方案。3. 无缝集成实战构建你的自动化逆向管道理论说再多不如动手做一遍。下面我将以一个假设的Android Unity游戏libil2cpp.soglobal-metadata.dat为例详细拆解从环境准备到完美集成的每一步。我会解释每一步的意图并提供可操作的命令和代码片段。3.1 环境准备与工具获取工欲善其事必先利其器。你需要准备以下工具Il2CppDumper前往其GitHub发布页下载最新版本的预编译可执行文件。通常是一个ZIP包包含Il2CppDumper.exeWindows或Il2CppDumperLinux/macOS。备用方案如果你需要处理特定版本或修改逻辑可以克隆源码并用.NET SDK或Rider自行编译。Binary Ninja从官方购买并下载安装。确保你使用的是相对较新的版本如3.0以上以获得最稳定的Python插件API支持。重要安装时注意其Python插件目录的位置。在Windows上通常是%APPDATA%\Binary Ninja\plugins\在macOS/Linux上是~/.binaryninja/plugins/。这是我们放置自定义插件的地方。目标文件提取Android APK使用任何你喜欢的APK解包工具如apktool或者直接将.apk重命名为.zip后解压。libil2cpp.so通常在lib/架构目录/下global-metadata.dat在assets/bin/Data/Managed/Metadata/下。iOS IPA同样解压IPA文件在Payload/AppName.app/目录下寻找同名的文件。PC Standalone直接在游戏安装目录的GameName_Data/下寻找。实操心得建议建立一个清晰的工作目录结构。例如MyReverseProject/ ├── target_game/ │ ├── libil2cpp.so │ └── global-metadata.dat ├── tools/ │ └── Il2CppDumper/ ├── output/ │ ├── dummydll/ │ ├── ida_py/ │ ├── bn_script.py │ └── script.json # 最重要的文件 └── bn_plugin/ # 我们的自定义插件3.2 运行Il2CppDumper并理解输出打开命令行进入Il2CppDumper目录运行它。这是一个交互式命令行工具。# Windows示例 Il2CppDumper.exe ../target_game/libil2cpp.so ../target_game/global-metadata.dat ../output运行后它会提示你选择模式Auto, Manual, etc.通常选择Auto即可。如果自动模式失败比如元数据版本无法识别你可能需要尝试Manual模式并输入正确的CodeRegistration和MetadataRegistration地址这需要一些额外的侦查工作有时可以从游戏日志或通过内存扫描获得。成功执行后在output目录下你会看到一系列文件。对我们而言最关键的是script.json这是所有解析信息的结构化汇总是集成插件的“黄金数据源”。BinaryNinja.py这是Il2CppDumper自带的、面向Binary Ninja的脚本。它是个很好的起点但往往不够用。我们可能需要基于它进行增强。打开script.json你会看到一个巨大的JSON对象。其结构大致如下{ ScriptMethod: [...], // 方法列表每个元素包含地址、名称、签名等 ScriptString: [...], // 字符串常量的地址和内容 ScriptMetadata: {...}, // 元数据信息如版本、基址 ScriptMethod: [...], // ... 还有其他如 ScriptType, ScriptField 等 }ScriptMethod里的每个条目就对应着一个待恢复的C#方法其中Address字段就是它在二进制文件中的偏移通常是RVA相对虚拟地址。3.3 开发Binary Ninja增强插件Il2CppDumper自带的BinaryNinja.py脚本通常只是简单地遍历ScriptMethod然后调用bv.define_auto_symbol和bv.define_user_symbol来创建符号。在实际复杂场景中这可能会遇到问题基址Image Base问题script.json中的地址可能是RVA而BN加载文件后的实际基址可能不同尤其是PIE enabled的SO文件。插件需要能自动计算或允许用户指定正确的基址。函数识别失败BN可能没有成功分析出某个地址是一个函数的开头。直接创建符号会导致符号挂在代码块中间破坏分析。批量操作性能与反馈处理数万个方法需要进度提示并且要能优雅地处理错误避免一个失败导致整个脚本停止。类型信息应用除了函数名我们还想恢复参数类型、返回值类型甚至为this指针应用对应的类结构体。因此一个健壮的插件是必要的。下面是一个增强版插件的核心框架思路# il2cpp_bn_enhanced.py import json import binaryninja as bn from binaryninja import interaction def apply_il2cpp_symbols(bv: bn.BinaryView): # 1. 让用户选择 script.json 文件 json_path interaction.get_open_filename_input(Select Il2CppDumper script.json) if not json_path: return # 2. 加载并解析JSON with open(json_path, r, encodingutf-8) as f: data json.load(f) # 3. 获取关键信息并处理基址 image_base bv.start # 检查script.json中是否有SuggestedBaseAddress并进行调整 metadata data.get(ScriptMetadata, {}) suggested_base metadata.get(SuggestedBaseAddress, 0) # 计算偏移调整量。这是一个关键点需要根据实际情况调整逻辑。 # 例如如果script.json中的地址是绝对地址针对特定基址而bv的基址不同则需要调整。 # 更通用的做法script.json中的地址很可能是RVA相对于Image Base的偏移。 # 假设我们确认是RVA那么 # actual_address image_base rva_from_json # 但有时Il2CppDumper输出的是绝对地址需要减去它当时假定的基址可能在SuggestedBaseAddress里。 # 这里提供一个可配置的选项。 offset image_base - suggested_base if suggested_base else 0 # 或者更简单让用户输入一个修正值 offset interaction.get_int_input(Address offset (hex), Enter offset to apply (e.g., 0x1000):, 0) # 4. 处理方法符号 methods data.get(ScriptMethod, []) total len(methods) for i, method in enumerate(methods): # 进度反馈 if i % 1000 0: bn.log_info(fProcessing methods: {i}/{total}) addr method.get(Address, 0) if addr 0: continue # 应用地址偏移 actual_addr addr offset # 检查该地址是否在BinaryView的有效范围内并且是否已被识别为函数 if not bv.is_valid_offset(actual_addr): continue # 确保该地址被定义为函数如果BN还没分析出来我们强制创建 func bv.get_function_at(actual_addr) if func is None: # 尝试分析该地址为函数 # 先检查该地址是否在已有的代码段内并且指令看起来合理 # 简单做法直接创建函数BN内部会进行分析 func bv.create_user_function(actual_addr) if func is None: # 创建失败可能地址不对跳过 continue # 获取方法名 name method.get(Name, fsub_{actual_addr:x}) # 清理名称确保符合BN的符号命名规范 # 例如将 :: 替换为 .因为BN常用 . 作为命名空间分隔符 safe_name name.replace(::, .) # 定义符号 # 使用 define_auto_symbol 让BN管理它 symbol bn.Symbol(bn.SymbolType.FunctionSymbol, actual_addr, safe_name) bv.define_auto_symbol(symbol) # 高级可以尝试解析方法签名并应用到函数类型上 # signature method.get(Signature) # if signature: # # 解析signature字符串生成BN的Type对象并应用到func.type # pass bn.log_info(fIl2Cpp symbols applied from {json_path}) # 将函数注册为BN的插件菜单项 bn.PluginCommand.register_for_address( Il2Cpp\\Apply Symbols from script.json, Apply Il2CppDumper symbols to the current binary view, apply_il2cpp_symbols )注意事项基址修正这是集成中最容易出错的一环。你需要理解目标二进制文件的加载基址bv.start和script.json中地址的基准。对于Android的libil2cpp.so如果它是PIE位置无关代码那么每次加载的基址都不同但script.json中的地址很可能是基于一个假设基址如0的RVA。此时actual_addr bv.start rva。对于Windows的GameAssembly.dll情况可能类似。最稳妥的方法是先用BN打开文件记下bv.start在BN的线性视图左下角可以看到“Image Base: 0xXXXXXX”。然后在script.json里找一个你知道的、肯定存在的函数地址比如通过字符串交叉引用找到的某个已知函数计算差值作为偏移量。函数创建直接bv.create_user_function可能不够精确。更好的做法是先让BN完成自动分析然后再运行脚本。对于BN未自动识别为函数的地址可以尝试先将其所在区域定义为代码bv.add_user_code_ref或者使用更高级的API如bv.add_function。性能处理数万符号时频繁的UI更新如进度日志可能会卡顿。可以考虑使用后台线程或者分批处理。3.4 插件安装与使用将上述Python脚本如il2cpp_bn_enhanced.py保存到Binary Ninja的插件目录。重启Binary Ninja或使用Plugins - Manage Plugins - Reload Plugins。用BN打开你的libil2cpp.so或GameAssembly.dll。等待初始分析完成进度条走完。在反汇编视图或线性视图任意位置右键选择Il2Cpp - Apply Symbols from script.json。在弹出的文件对话框中选择Il2CppDumper生成的script.json文件。根据提示输入地址偏移量如果之前计算过。如果不确定可以先输入0尝试然后去检查几个已知的函数名是否正确。如果一切顺利你会看到左侧的“Functions”列表和反汇编视图中的调用从一堆sub_xxxxxx瞬间变成了有意义的Player::Update、GameManager::GetInstance等。世界顿时清晰了。4. 高级技巧与深度优化超越基础符号导入基础的符号导入只是第一步。要真正实现“无缝”分析我们还需要做更多工作让Binary Ninja的理解能力更接近原始的C#源代码。4.1 自动识别并应用il2cpp运行时结构体il2cpp运行时有很多内部使用的数据结构比如表示字符串的Il2CppString表示数组的Il2CppArray以及各种泛型容器。在反编译视图HLIL中如果我们能将这些内存布局定义为结构体Structure那么代码的可读性将极大提升。例如一个常见的Il2CppString在32位ARM上可能布局如下struct Il2CppString { void* klass; // 指向Il2CppClass的指针 void* monitor; // 同步锁 int32_t length; // 字符串长度字符数 char16_t chars[1]; // 柔性数组存放UTF-16字符 };我们可以在插件中在导入符号后自动创建这些结构体并查找使用了这些结构体的函数为其局部变量或参数应用正确的类型。def define_il2cpp_common_structures(bv): # 定义 Il2CppString 结构体 if Il2CppString not in bv.types: string_struct bn.StructureBuilder.create() # 假设是32位架构 ptr_type bn.Type.pointer(bv.arch, bn.Type.void()) int32_type bn.Type.int(4) # 添加字段 string_struct.append(ptr_type, klass) string_struct.append(ptr_type, monitor) string_struct.append(int32_type, length) # chars字段是可变长度的在BN中通常用数组或后续处理 # 我们可以先定义一个占位符或者不定义在分析时具体处理 # 这里我们定义一个指向wchar_t的指针来表示字符串内容 wchar_ptr_type bn.Type.pointer(bv.arch, bn.Type.wide_char_type()) string_struct.append(wchar_ptr_type, chars) # 将结构体注册到BV的类型系统中 bv.define_type(Il2CppString, string_struct.immutable_copy(), Il2Cpp String Object) # 类似地可以定义 Il2CppArray, Il2CppObject 等 # ...然后你可以编写启发式规则例如如果一个函数的第一个参数this指针类型是某个类且该类名包含“String”那么在HLIL视图中可以将该参数的类型设置为Il2CppString*。这通常需要结合script.json中的类型信息ScriptType进行更精细的匹配。4.2 利用字符串交叉引用快速定位关键逻辑script.json中的ScriptString部分包含了游戏中所有硬编码的字符串常量及其地址。在BN中我们可以快速找到这些字符串的引用从而定位到使用它们的函数这是逆向中非常高效的切入点。插件可以增强这一过程def highlight_and_xref_strings(bv, data): strings data.get(ScriptString, []) for s in strings: addr s.get(Address, 0) value s.get(Value, ) if addr and value: # 将地址处的数据定义为字符串 # 注意需要根据字符串编码通常是UTF-16来定义 # 这里简化处理BN可能会自动识别 # 更关键的是我们可以记录下这个字符串和地址方便后续搜索 # 可以在BN中创建一个“书签”或者自定义的数据段注释 bv.set_comment_at(addr, fIl2Cpp String: {value[:50]}...) # 只显示前50字符 # 查找并跳转到引用此字符串的代码 # xrefs bv.get_code_refs(addr) # 可以手动调用但通常BN会自动分析在实际分析中你可以直接在BN的“Strings”视图中搜索游戏内的UI文本、调试日志、错误信息等然后双击跳转到其地址再查看是哪个函数引用了它从而快速切入核心逻辑模块。4.3 处理虚函数表VTable与多态il2cpp的C实现中C#类对应的C结构体第一个成员通常是一个指向Il2CppClass的指针即klass其中包含了虚函数表vtable。理解vtable对于分析面向对象的代码至关重要。一个进阶的插件功能是自动识别并标注vtable。思路是从script.json的ScriptType中可以获取类的元数据。在二进制中寻找包含大量连续函数指针的数据区域这些区域很可能就是vtable。将这些vtable的地址与类关联起来并在反汇编中将其类型定义为函数指针数组。这需要对il2cpp内部对象模型有更深的理解实现起来更复杂但一旦完成对于分析继承、接口调用等场景帮助巨大。4.4 版本兼容性与错误处理不同Unity版本生成的il2cpp二进制和元数据格式可能有细微差别。你的插件应该具备一定的容错能力。字段解析失败如果script.json的某些字段缺失或格式不符插件应记录警告并跳过而不是崩溃。地址有效性二次校验在应用符号前可以检查目标地址是否位于可执行代码段.text段内。提供回退方案如果自动基址计算失败提供手动输入偏移量的界面。甚至可以尝试几种常见的偏移量进行试探性应用并让用户选择效果最好的一个。5. 常见问题排查与实战心得即使有了完善的工具链在实际操作中你依然会遇到各种“坑”。下面是我总结的一些典型问题及其解决方案。5.1 Il2CppDumper运行失败或输出不完整问题现象可能原因解决方案运行后提示“无法找到元数据注册信息”或直接退出。1.global-metadata.dat文件损坏或加密方式特殊。2. Unity版本太新或太旧Il2CppDumper尚未支持。3. 游戏使用了自定义的il2cpp运行时或进行了元数据混淆。1. 确认文件是从正确位置提取的完整文件。尝试用十六进制编辑器查看文件头看是否有明显的魔数。2. 检查Il2CppDumper的GitHub Issues或Release Notes看是否支持该Unity版本。尝试使用开发中的最新代码编译。3. 尝试使用Manual模式并配合其他工具如游戏运行时内存Dump获取CodeRegistration和MetadataRegistration地址。输出的script.json中方法地址大量为0或方法名缺失。元数据解析不完整可能遇到了未知的数据结构。1. 尝试Il2CppDumper的不同模式Auto, Manual, Old。2. 使用--dummy等命令行参数尝试不同的输出选项。3. 考虑使用其他辅助工具如Il2CppInspector进行交叉验证。实操心得对于某些加固严重的游戏global-metadata.dat可能被加密或压缩。有时需要先进行脱壳或内存Dump获取解密后的元数据文件。动态调试如使用Frida在游戏运行后Dump内存中的il2cpp模块和元数据往往是解决这类问题的终极手段。5.2 Binary Ninja中符号导入后函数名显示不正确或混乱问题现象可能原因解决方案所有导入的函数名都带有一个奇怪的偏移前缀或者地址完全不对。基址Image Base未正确对齐。这是最常见的问题。script.json中的地址与BN中加载的实际地址存在固定偏移。1.手动计算偏移在BN中找到一个你知道的、肯定存在的函数例如通过搜索字符串“Awake”或“Start”的引用来定位MonoBehaviour生命周期函数。记下BN中该函数的地址A。在script.json中搜索该函数名找到其记录的地址B。偏移量 A - B。在插件运行时输入此偏移量。2.使用插件交互好的插件应该提供输入偏移量的对话框并允许预览几个符号的映射结果。部分函数成功重命名但大量函数仍然是sub_xxxxxx。1. 对应地址未被BN识别为函数。2. 该地址可能位于只读数据段或未分析的区域。3.script.json中该方法地址本身就是0或无效。1. 确保在运行符号导入插件之前让BN完成自动分析。你可以点击“Analysis” - “Complete Analysis”。2. 对于未被识别为函数的地址插件应尝试强制创建函数如我们插件代码中所做。但需谨慎避免在数据区创建函数。3. 检查script.json确认这些方法是否确实有有效地址。可能是Il2CppDumper解析失败。函数名包含非法字符如::,,导致BN显示异常。C#中的泛型、命名空间分隔符等在转换为符号名时未正确处理。在插件中进行名称清洗。将::替换为.BN常用将和替换为_或其他合法字符。例如System.Collections.Generic.List1::Add清洗为System.Collections.Generic.List_1.Add。5.3 分析性能与大型二进制文件处理GameAssembly.dll或libil2cpp.so动辄几十MB甚至上百MB包含数万函数。一次性导入所有符号可能导致BN暂时卡顿。策略一分批导入。修改插件允许用户按命名空间或类名前缀过滤只导入当前关心的部分。例如只导入UnityEngine.或GameLogic.相关的符号。策略二延迟分析。先导入符号名称但不立即进行深度的类型传播或结构体应用。等需要深入分析某个模块时再针对性地运行更耗时的分析脚本。策略三使用BN的数据库BNDB。在完成符号导入和初步分析后立即保存为.bndb数据库文件。下次直接加载数据库无需重新分析速度会快很多。5.4 与动态分析Frida/游戏调试器结合静态分析Binary Ninja和动态分析Frida, LLDB, dnSpy附加调试是互补的。集成方案可以为此提供便利。地址同步在静态分析中你通过插件知道了Player::TakeDamage函数的地址是0x12345678文件偏移。在动态调试时你需要将其转换为运行时地址。如果ASLR/PIE启用运行时地址 模块基址 文件偏移RVA。你可以在游戏运行时用Frida的Module.findBaseAddress(libil2cpp.so)获取基址然后加上静态分析中的RVA就能直接下断点或Hook。类型信息共享在静态分析中定义好的Il2CppString结构体其内存布局与运行时是一致的。你可以将这个结构体的定义用于Frida的Memory.readUtf16String()等操作或者用于解释调试器中的内存数据。我个人在实际操作中的体会是这套集成方案的价值在于建立了一个可靠的静态分析基线。一旦符号导入成功后续的分析工作就变成了熟悉的“读代码”过程你可以用BN强大的交叉引用、图形视图和反编译功能像阅读有符号的库一样去理解游戏逻辑。这比在IDA中面对数万个无名函数要高效得多。最后再分享一个小技巧定期维护你的插件随着Il2CppDumper和Binary Ninja的更新以及你对il2cpp运行时理解的加深不断加入新的自动化分析规则如自动识别常见的游戏模式类、资源管理器等你的逆向效率会像滚雪球一样越来越快。