jshook 中文文档 下载 App

内核模式可用与不可用 API——观察者进程没有目标应用的上下文

为什么会有区别

内核模式的核心是:脚本在观察者进程里运行,不在目标应用进程里。观察者进程和目标应用完全隔离——没有目标应用的 Java 类加载器、没有目标应用的 Native 库、没有目标应用的 UI 线程。

所以 Frida 在普通模式下提供的很多 API,在内核模式下根本没有执行环境。

✅ 可用 API

kernel 模块(内核模式专属)

API说明
kernel.getModuleBase(libname)目标进程中 SO 库的基址
kernel.readDword(address)读 4 字节整数
kernel.readFloat(address)读 4 字节浮点数
kernel.writeDword(address, value)写 4 字节整数
kernel.writeFloat(address, value)写 4 字节浮点数

memory 模块(通过守护进程 IPC)

memory.* 系列可以读写任意进程内存,是内核模式下除了 kernel.* 之外另一个重要操作入口。

Frida 全局对象

API说明
console.log()日志输出
runtime.isKernel当前是否内核模式
runtime.packageName目标应用包名(部分可用)
storage.*本地存储
modmenu.*Mod Menu(通过守护进程 IPC 渲染)
moddraw.*ImGui 绘制

❌ 不可用 API

API原因
Java.use() / Java.perform()观察者进程没有目标应用的 Java 类
Interceptor.attach() / Interceptor.replace()不能 Hook 目标进程的函数
Module.findBaseAddress()观察者进程没有目标应用的 SO 库
Process.findRangeByAddress()观察者进程的地址空间和目标进程不同
app.*依赖目标应用的 Context
view.*依赖目标应用的 UI 线程
toast() / alert()依赖守护进程 Binder(内核模式下可能不可用)
http.* / curl.*依赖守护进程 IPC

脚本里处理环境差异

推荐在脚本开头就判断运行环境,按分支写逻辑:

if (runtime.isKernel) {
    // 内核模式:纯 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);
    }
} else {
    // 普通模式:完整 Frida + JsApi
    Java.perform(function () {
        var Player = Java.use('com.game.Player');
        Player.getHealth.implementation = function () {
            return 9999;
        };
    });
}

同一份脚本,两种模式下各走各的分支,互不干扰。

检测运行环境的好处

除了切换 API 分支,还可以在内核模式下跳过某些只在普通模式下执行的初始化逻辑:

// 这些初始化只在普通模式下有意义
if (!runtime.isKernel) {
    // 普通模式下的 Mod Menu 创建
    var menu = modmenu.create('辅助', [...], { ... });
}

// 公共逻辑:两种模式下都跑
if (runtime.isKernel) {
    // 内核模式的游戏逻辑
} else {
    // 普通模式的游戏逻辑
}

调试建议

在普通模式下写和调试脚本(Java 层 Hook、日志更丰富),确认逻辑没问题后再切到内核模式测试 kernel API 分支。两种模式的内存地址基址可能不同(动态链接的 SO 每次加载地址都不一样),记得每次切换都重新获取基址。