内核模式可用与不可用 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 每次加载地址都不一样),记得每次切换都重新获取基址。