Unity游戏逆向实战:如何用IDA Pro和il2cpp API动态调用你的C#脚本方法

张开发
2026/4/20 9:25:04 15 分钟阅读

分享文章

Unity游戏逆向实战:如何用IDA Pro和il2cpp API动态调用你的C#脚本方法
Unity游戏逆向实战从二进制到动态调用的深度探索在移动游戏安全领域Unity引擎构建的游戏因其特殊的编译机制成为逆向工程师关注的焦点。当我们需要分析一个已发布的Unity游戏时传统的静态分析方法往往难以触及核心逻辑而动态调用技术则能打开一扇全新的窗口。本文将带你深入libil2cpp.so的内部世界通过实战演示如何定位关键C#方法并实现运行时调用。1. 理解il2cpp运行机制Unity游戏的脚本后端从Mono迁移到il2cpp后整个执行模型发生了根本性变化。C#代码不再以中间语言形式存在而是被编译为C代码并最终生成原生二进制文件。这种转变带来了性能提升同时也引入了新的逆向挑战。关键文件组成libil2cpp.soAndroid/GameAssembly.dllWindows包含所有脚本逻辑的编译后代码global-metadata.dat保存类型系统元数据相当于il2cpp的地图libunity.soUnity运行时核心模块在逆向过程中这三个文件需要配合使用。以Android平台为例典型的文件路径结构如下/data/app/[package]/lib/arm64/ ├── libil2cpp.so ├── libunity.so └── assets/bin/Data/ └── Managed/ └── global-metadata.dat提示获取这些文件需要root权限或使用特殊工具从APK中提取本文不涉及具体提取方法。2. 逆向分析基础准备2.1 工具链配置进行il2cpp逆向需要准备以下工具组合工具名称用途推荐版本IDA Pro反汇编分析7.7Il2CppDumper元数据解析v6.7.0Frida动态注入框架15.1.0Python3脚本支持3.8安装完成后首先使用Il2CppDumper处理二进制文件Il2CppDumper libil2cpp.so global-metadata.dat output_dir这个命令会生成以下关键文件script.json所有C#类和方法的结构信息dump.cs伪代码形式的类型定义stringliteral.json字符串常量池2.2 IDA Pro分析技巧加载libil2cpp.so到IDA后需要特别注意以下几个关键点导出函数识别在Exports窗口搜索il2cpp_前缀的函数重点关注以下核心APIil2cpp_domain_get il2cpp_domain_assembly_open il2cpp_assembly_get_image il2cpp_class_from_name il2cpp_class_get_method_from_name字符串交叉引用查找Assembly-CSharp等关键字符串追踪对global-metadata.dat的访问路径函数特征识别il2cpp生成的函数通常有固定prologue/epilogue注意第一个参数通常是this指针即使静态方法3. 动态调用技术实战3.1 方法定位流程假设我们要调用游戏中的Player::CalculateDamage方法完整流程如下获取DomainIl2CppDomain* domain il2cpp_domain_get();加载Assemblyconst Il2CppAssembly* assembly il2cpp_domain_assembly_open(domain, Assembly-CSharp);获取Imageconst Il2CppImage* image il2cpp_assembly_get_image(assembly);定位目标类Il2CppClass* klass il2cpp_class_from_name(image, , Player);获取方法信息const MethodInfo* method il2cpp_class_get_method_from_name(klass, CalculateDamage, 2);3.2 参数处理技巧il2cpp方法调用有几个特殊之处需要注意隐式this指针即使是静态方法il2cpp也会添加额外参数调用约定通常是__fastcall参数顺序调整IDA中看到的参数顺序可能与C#声明不同需要通过调试确定实际布局结构体处理复杂值类型可能被拆分为多个寄存器引用类型始终以指针形式传递示例调用代码typedef int (*CalculateDamageFn)(void*, int, float); CalculateDamageFn pFunc (CalculateDamageFn)method-methodPointer; int damage pFunc(nullptr, 100, 1.5f);3.3 异常处理机制动态调用可能遇到的各种异常及应对方案异常类型可能原因解决方案段错误方法指针无效验证MethodInfo结构参数不匹配签名错误检查参数数量和类型空指针类未加载确保先初始化相关模块权限错误访问私有成员使用反射API绕过注意在实际游戏中关键方法可能被混淆或加密需要结合运行时分析确定真实地址。4. 高级技巧与应用场景4.1 方法替换技术通过修改MethodInfo结构可以实现运行时方法替换void ReplaceMethod(const MethodInfo* original, void* newFunc) { DWORD oldProtect; VirtualProtect(original-methodPointer, sizeof(void*), PAGE_READWRITE, oldProtect); original-methodPointer (Il2CppMethodPointer)newFunc; VirtualProtect(original-methodPointer, sizeof(void*), oldProtect, oldProtect); }典型应用场景修改游戏逻辑如无敌模式注入调试代码实现快速测试4.2 自动化分析框架结合Frida可以实现自动化方法追踪Interceptor.attach(Module.findExportByName(libil2cpp.so, il2cpp_runtime_invoke), { onEnter: function(args) { var method args[1]; var name Memory.readUtf8String( Memory.readPointer(method.add(0x10))); console.log(Calling: name); } });4.3 对抗混淆方案现代游戏常用的保护手段及其应对元数据加密动态解密global-metadata.dat解决方案内存dump或hook解密函数API隐藏抹去导出函数名解决方案特征码扫描或符号恢复代码混淆控制流平坦化解决方案动态去混淆或模拟执行5. 实战案例伤害计算修改以修改RPG游戏伤害公式为例完整步骤如下使用Il2CppDumper定位BattleSystem.CalculateDamage方法在IDA中找到对应函数地址例如sub_123456编写注入代码int Hooked_CalculateDamage(void* thisPtr, int baseDamage, float multiplier) { // 原函数调用 int original ((CalculateDamageFn)0x123456)(thisPtr, baseDamage, multiplier); // 修改逻辑伤害翻倍 return original * 2; }使用Frida或自定义so实现注入var target Module.findBaseAddress(libil2cpp.so).add(0x123456); Interceptor.replace(target, new NativeCallback( function(thisPtr, baseDamage, multiplier) { var original target.call(thisPtr, baseDamage, multiplier); return original * 2; }, int, [pointer, int, float] ));在实际项目中这种技术可以用来平衡测试时的数值调整快速验证游戏机制实现自动化测试脚本6. 安全与伦理考量虽然逆向工程是一项强大的技术能力但在实际应用中必须注意法律边界仅对自有或授权软件进行分析遵守最终用户许可协议道德准则不开发破坏游戏平衡的工具尊重开发者的劳动成果技术防护商业项目应使用专业混淆工具关键逻辑应放在native层实现反调试机制在过去的项目中我曾遇到过因过度依赖动态调用而导致的维护问题。当游戏更新频繁时基于内存地址的hook需要不断调整最终我们转向了更稳定的协议级分析方案。

更多文章