Unity 应用存在两种差异很大的构建形态,判断属于哪一种应是第一步。Mono 构建会提供可反编译的 .NET 程序集;IL2CPP 构建则把 C# 编译为 C++ 再编译为原生代码,其 C# 元数据存放在单独文件中,必须与二进制重新组合后才可读。
适用场景
当 APK 中包含 assets/bin/Data/Managed/Metadata/global-metadata.dat 与 lib/arm64-v8a/libil2cpp.so;或 APK 中包含 assets/bin/Data/Managed/Assembly-CSharp.dll(Mono 构建);或 IPA 中包含 Frameworks/UnityFramework.framework;或 jadx 仅显示 UnityPlayerActivity 与 Unity 相关胶水代码;或需要还原游戏与应用逻辑、反作弊行为及 API 层时,可使用本技能。
不适用场景
Flutter 或 React Native 应用应改用 reversing-flutter-apps 或 reversing-react-native-apps;普通原生应用应改用 testing-mobile-applications;不含 Unity 标记的桌面游戏二进制应改用 analyzing-binaries。为在线多人服务制作作弊工具或绕过反作弊不在范围内,这属于服务滥用而非评估。
识别构建类型
命令示例:unzip -l target.apk | rg 'global-metadata|libil2cpp|Assembly-CSharp|libmono'。若存在 Assembly-CSharp.dll 与 libmono*.so,则为 Mono 构建,难度较低,可直接反编译 DLL;若存在 global-metadata.dat 与 libil2cpp.so,则为 IL2CPP 构建,适用下文主路径;若两者皆无但存在 UnityFramework,则为 iOS IL2CPP,处理方式同 IL2CPP,需从已解密的 IPA 中提取。
Mono 构建处理较快,可提取程序集并用 dnSpy、dnSpyEx、ILSpy 或 dotPeek 打开,其中 dnSpy 支持编辑。示例:unzip -j target.apk 'assets/bin/Data/Managed/Assembly-CSharp.dll' -d ./out;随后 ilspycmd ./out/Assembly-CSharp.dll -o ./decompiled。
将元数据与二进制重新组合
IL2CPP 将信息拆分:libil2cpp.so 保存编译后的代码,global-metadata.dat 保存 C# 类型系统,包括类名、方法名、字段名与字符串字面量,单独使用均无价值。工具的作用是匹配两者并生成符号。提取示例:unzip -j target.apk 'lib/arm64-v8a/libil2cpp.so' 'assets/bin/Data/Managed/Metadata/global-metadata.dat' -d ./in。随后执行 Il2CppDumper ./in/libil2cpp.so ./in/global-metadata.dat ./out。
典型输出用途:dump.cs 含每个类、方法与字段及地址,应首先阅读;DummyDll/ 为桩 .NET 程序集,可用 dnSpy 或 ILSpy 打开以浏览类型系统;script.json 为 IDA 或 Ghidra 的符号导入文件,用于给 libil2cpp.so 应用名称;stringliteral.json 含字符串常量,如 URL、密钥、消息。
当 Il2CppDumper 失败时,Il2CppInspector 是替代方案,它支持不同的 Unity 版本范围,并可生成 Ghidra 与 IDA 脚本及更丰富的 C++ 头文件。在断定某构建不受支持前应两者都试。
版本不匹配是常见失败原因。元数据格式随 Unity 版本变化,工具会要求或推断版本,应先读取 Unity 版本并显式传入:strings ./in/libil2cpp.so | rg -m3 '20[0-9]{2}.[0-9]+.[0-9]+';strings ./in/global-metadata.dat | head -5。
受保护的构建会加密或打乱 global-metadata.dat,文件头魔数会不对,转储工具将拒绝处理。迹象包括文件未以预期元数据魔数开头,或熵值整体偏高。可行路线是动态处理:让应用自行解密元数据,加载后从内存中转储。示例:frida -U -f com.target.app -l dump-metadata.js,再将转储文件按常规交给 Il2CppDumper。该模式与处理加壳恶意软件一致,通常比静态攻破保护更省力,可参考 analyzing-binaries。
处理还原后的代码
阅读 dump.cs 以获得类型映射,搜索关键类,如任何以 Manager、Service、Api、Network、Auth、Purchase、Config 命名的类。将 script.json 载入 IDA 或 Ghidra,使 libil2cpp.so 显示 C# 方法名而非 sub_1A2B3C;缺少这一步反汇编不可读,有了它便像反编译的 C#。检查 stringliteral.json 中的端点和密钥,与其他框架同理,字符串常量能保留下来。使用 dump.cs 中的地址通过 Frida 运行时 Hook:方法地址相对于 libil2cpp.so 基址,需解析模块基址并加上偏移。示例代码:const base = Module.findBaseAddress('libil2cpp.so'); Interceptor.attach(base.add(0x1A2B3C), { onEnter(args) { console.log('called with', args[1]); }, onLeave(ret) { console.log('->', ret); } });
安全相关发现
若用于评估而非单纯好奇,常见问题包括:stringliteral.json 中的硬编码凭据与端点,如后端密钥、分析令牌、云存储凭据;客户端权威逻辑,即货币、权益与校验在 C# 中完成且被服务器信任,可通过直接调用 API 验证;弱或缺失的证书绑定,Unity 网络层常配置宽松,应检查 UnityWebRequest 的使用;本地存档与配置篡改,PlayerPrefs 与本地存档中存放被服务器信任的值;应用内购买的收据校验在客户端完成。
应拒绝的合理化说法
“jadx 看不到东西,所以有保护。”Unity 应用的 dex 中按设计几乎什么都没有。“转储工具失败,所以无法分析。”应换另一个转储工具、显式提供 Unity 版本,若元数据被加密则从内存转储。“IL2CPP 是编译的,所以逻辑安全。”元数据随其一同分发,这正是该格式的意义。“客户端会校验购买。”那么购买就没有被校验。“我会直接读原始 arm64。”应先应用 script.json,同一函数在应用前后可读性差别巨大。
参考资料
testing-mobile-applications:平台存储、权限与原生层;testing-apis:后端,客户端权威逻辑在此成为发现项;analyzing-binaries:受保护构建的内存转储与反分析。相关工具包括 Il2CppDumper、Il2CppInspector、dnSpy/ILSpy、Frida、IDA/Ghidra。