jshook 中文文档 下载 App

native 层 IL2CPP Hook——libil2cpp.so 模块地址与函数拦截

为什么 IL2CPP 特殊

Unity 游戏有两种脚本后端:Mono 和 IL2CPP。IL2CPP 会把 C# 代码编译成 native 机器码,打包进 libil2cpp.so。这意味着:

找到 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 函数的调用约定:

所以 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);
}

详见 内核模式内存读写。