native 层 IL2CPP Hook——libil2cpp.so 模块地址与函数拦截
为什么 IL2CPP 特殊
Unity 游戏有两种脚本后端:Mono 和 IL2CPP。IL2CPP 会把 C# 代码编译成 native 机器码,打包进 libil2cpp.so。这意味着:
- 没有 .NET 运行时可 Hook
- 所有游戏逻辑都在 native 层
- 必须用 Frida 的 native 层 API(
Module、Interceptor、NativeFunction)
找到 libil2cpp.so 的基址
var il2cpp = Module.findBaseAddress('libil2cpp.so');
console.log('[+] libil2cpp.so base: ' + il2cpp);
Module.findBaseAddress 返回这个 SO 库在内存里的起始地址。函数的实际运行地址 = 基址 + 偏移量。
找函数偏移量
有几种方法:
方法一:用 dump.cs
用 Il2CppDumper 或类似工具从 APK 里导出所有 C# 类/方法的偏移量列表(dump.cs)。在 dump.cs 里搜索目标方法名就能找到偏移。
方法二:运行时扫描
如果知道函数的特征模式(字节序列),用 Memory.scanSync 扫描:
var results = Memory.scanSync(il2cpp, il2cpp.module.size, 'AA BB CC DD ...');
results.forEach(function (match) {
console.log('[+] 找到匹配: ' + match.address);
});
拦截函数
拿到基址和偏移后,用 Interceptor.attach:
var il2cpp = Module.findBaseAddress('libil2cpp.so');
if (il2cpp) {
// dump.cs 里查到 GameManager.AddGold 的偏移是 0x1A2B3C
var addGoldPtr = il2cpp.add(0x1A2B3C);
Interceptor.attach(addGoldPtr, {
onEnter: function (args) {
// args[0] = this(实例指针)
// args[1] = 第一个参数(比如金币数量)
var gold = args[1].toInt32();
console.log('[+] AddGold 被调用,数量: ' + gold);
},
onLeave: function (retval) {
// retval = 返回值
console.log('[+] 返回值: ' + retval);
}
});
}
修改参数和返回值
在 onEnter 里可以直接改 args 数组,在 onLeave 里可以改 retval:
Interceptor.attach(addGoldPtr, {
onEnter: function (args) {
// 把金币数量翻倍
var original = args[1].toInt32();
args[1] = ptr(original * 2);
},
onLeave: function (retval) {
// 强制返回 true
retval.replace(ptr(1));
}
});
用 NativeFunction 调用 native 函数
如果想主动调用 native 函数(而不是被动等它被调用),用 NativeFunction:
var myFunc = new NativeFunction(
il2cpp.add(0x123456), // 函数地址
'void', // 返回类型
['int', 'int'] // 参数类型列表
);
myFunc(100, 200); // 调用
支持的类型:void、int、long、float、double、pointer、uchar、bool 等。
实例 vs 静态方法
native 函数的调用约定:
- 实例方法:
args[0]是this指针,后续是实际参数 - 静态方法:
args[0]就是第一个参数,没有this
所以 dump.cs 里如果标了 static,拦截时要注意 args 索引偏移一格。
内核驱动模式下的简化
如果开了内核驱动模式,不需要注入目标进程,可以直接用 kernel API 读写内存:
var base = kernel.getModuleBase('libil2cpp.so');
if (base && base !== '0x0') {
var health = kernel.readFloat(
'0x' + (parseInt(base, 16) + 0x1A2B3C4).toString(16)
);
console.log('Health: ' + health);
}
详见 内核模式内存读写。