从libil2cpp.so到Frida脚本:一次完整的Unity手游内存修改逆向分析记录

张开发
2026/4/15 6:11:37 15 分钟阅读

分享文章

从libil2cpp.so到Frida脚本:一次完整的Unity手游内存修改逆向分析记录
从libil2cpp.so到Frida脚本Unity手游内存修改实战解析当你在玩一款单机手游时是否曾想过那些看似简单的数值背后隐藏着怎样的代码逻辑作为一名移动安全研究员我最近对一款采用Unity IL2CPP模式构建的热门单机手游进行了逆向分析目标是理解其内存管理机制并实现自定义数值修改。本文将完整记录从静态分析到动态注入的全过程特别关注那些容易被忽略的技术细节和思维路径。1. 环境准备与初步分析逆向工程的第一步永远是搭建合适的工作环境。我选择了以下工具链组合Android设备Root过的真机或模拟器推荐Genymotion分析工具IL2CPPDumper v6.6.3dnSpy v6.1.8Frida 15.1.17IDA Pro 7.6可选提示不同版本的IL2CPP引擎可能产生不同的反编译结果建议记录游戏使用的Unity版本号解压APK后在lib/armeabi-v7a目录下发现了关键的libil2cpp.so文件这确认了游戏确实采用IL2CPP编译模式。有趣的是文件大小达到了37MB暗示着这是一个相对复杂的项目。2. 静态反编译与符号恢复使用IL2CPPDumper处理libil2cpp.so时遇到了第一个技术难点——如何正确配对global-metadata.dat文件。经过多次尝试发现必须使用assets/bin/Data/Managed/下的原始文件而非任何缓存版本。成功反编译后DummyDll目录下生成了多个DLL文件其中Assembly-CSharp.dll包含了大部分游戏逻辑。用dnSpy加载时注意这些关键点// 反编译后的典型C#代码片段 public class PlayerStatus { public void Refresh(int health, int mana) { this.health health; this.mana mana; UpdateDisplay(); } }通过交叉引用分析我定位到了负责更新生命值和魔法值的Refresh方法。但IL2CPP的特殊性在于这些C#方法最终会被编译为Native代码需要找到对应的Native函数地址。3. 地址计算与函数定位IL2CPP函数的Native地址计算遵循特定模式获取libil2cpp.so的基地址加上函数在文件中的偏移量考虑ASLR带来的随机化偏移使用Frida的Module.findBaseAddress()可以动态获取基地址。而函数偏移则需要通过IL2CPPDumper生成的dump.cs文件来查找// dump.cs中的关键信息 // Address : 0x10781D90 // Name : PlayerStatus$$Refresh实际操作中我发现偏移量0x781D90需要加上模块基地址才能得到正确的内存位置。以下是验证地址正确性的Frida脚本片段const refreshAddr Module.getBaseAddress(libil2cpp.so).add(0x781D90); console.log(Refresh function at: refreshAddr.toString());4. Frida Hook实现与参数修改完整的Hook脚本需要考虑以下几点参数类型转换JavaScript到Native类型调用约定ARM vs x86线程安全确保在主线程执行以下是经过优化的Frida脚本Java.perform(function() { const libil2cpp Process.getModuleByName(libil2cpp.so); const refreshAddr libil2cpp.base.add(0x781D90); Interceptor.attach(refreshAddr, { onEnter: function(args) { console.log(原始参数: health${args[1]}, mana${args[2]}); // 修改参数值 args[1] ptr(9999); args[2] ptr(9999); console.log(已修改为最大值); }, onLeave: function(retval) { // 可在此处验证修改结果 } }); });在实际测试中发现游戏对数值范围有客户端校验超过9999会被重置。于是调整策略改为注入一个中间值8000成功绕过了校验机制。5. 逆向工程中的常见陷阱与解决方案在整个分析过程中遇到了几个典型问题混淆处理现象反编译后的类名和方法名变为无意义的字符串解决方案通过字符串引用和调用关系图重建符号表代码加密现象IL2CPPDumper无法解析global-metadata.dat应对尝试使用--version参数指定Unity版本或手动提取metadata反调试检测现象游戏在注入后立即崩溃对策使用Frida的anti-anti-debugging脚本如// 绕过常见反调试检测 var pthread_create Module.findExportByName(null, pthread_create); Interceptor.replace(pthread_create, new NativeCallback(function() { return 0; }, int, [pointer, pointer, pointer]));6. 进阶技巧持久化修改与自动化对于需要频繁修改的场景可以开发更智能的Hook方案条件修改只在特定情况下修改数值UI集成通过Frida的RPC功能创建控制界面签名扫描当地址随版本变化时使用特征码定位函数以下是一个特征码定位的示例function findPattern(module, pattern) { const ranges module.enumerateRanges(r-x); let result null; ranges.forEach(range { const matches Memory.scanSync(range.base, range.size, pattern); if (matches.length 0) { result matches[0].address; } }); return result; } const refreshPattern 01 00 A0 E3 1E FF 2F E1; // ARM汇编特征码 const refreshAddr findPattern(Module.load(libil2cpp.so), refreshPattern);7. 安全研究与道德考量在进行此类分析时务必注意仅用于学习研究和安全评估目的避免破坏游戏平衡或影响其他玩家尊重软件开发者的知识产权技术本身是中性的关键在于使用者的意图。通过这次逆向工程我不仅深入理解了Unity IL2CPP的工作机制还积累了宝贵的ARM架构调试经验。

更多文章