第6章 Java Hook 详解
适用版本:Frida 17.9.11(frida-java-bridge 7.0+)
6.1 基础概念
所有 Java API 操作必须在 Java.perform() 中执行,确保当前线程已附加到 Java VM。
可用性检查
Java.available // Boolean: Java 运行时是否已加载
Java.androidVersion // String: Android 版本 (如 "14", "15")
Java.isMainThread() // Boolean: 是否在主线程17.0+ 重要变更:Runtime Bridge 解耦
从 Frida 17.0 起,frida-java-bridge 不再内置于 Frida 核心,而是作为独立 ESM 模块分发。使用 frida-tools 14.0+(REPL / frida-trace)时 bridge 自动加载;编写自定义 Agent 时需在编译时显式引入:
// 自定义 Agent 需要在 package.json 中添加依赖
// "dependencies": { "@anthropic/frida-java-bridge": "^7.0.0" }
import { Java } from "frida-java-bridge";使用 frida CLI 或 frida-trace 时无需额外配置,bridge 已自动集成。
6.2 Java.perform() 和 Java.use()
Java.perform(function() {
// 所有 Java API 调用必须在这里面
const Activity = Java.use('android.app.Activity');
const String = Java.use('java.lang.String');
const Log = Java.use('android.util.Log');
});
// 同步变体(线程已就绪时使用,性能更好)
Java.performNow(function() {
// 不创建新线程,直接在当前线程执行
});6.3 Hook 方法
基本方法 Hook
Java.perform(function() {
const MainActivity = Java.use('com.example.app.MainActivity');
// 通过替换 .implementation 来 hook
MainActivity.secretFunction.implementation = function() {
console.log('secretFunction 被调用!');
// 调用原始方法
return this.secretFunction();
};
// 带参数的 hook
MainActivity.login.implementation = function(username, password) {
console.log('login(' + username + ', ' + password + ')');
return this.login(username, password);
};
});Hook 构造器 ($init)
Java.perform(function() {
const MyClass = Java.use('com.example.app.MyClass');
MyClass.$init.implementation = function(arg1, arg2) {
console.log('构造器: ' + arg1 + ', ' + arg2);
this.$init(arg1, arg2); // 调用原始构造器
};
});Hook 重载方法 (.overload())
Java.perform(function() {
const MyClass = Java.use('com.example.app.MyClass');
// 通过参数类型指定具体重载
MyClass.calculate.overload('int', 'int').implementation = function(a, b) {
console.log('calculate(int, int): ' + a + ', ' + b);
return this.calculate(a, b);
};
// 另一个重载
MyClass.calculate.overload('java.lang.String').implementation = function(s) {
console.log('calculate(String): ' + s);
return this.calculate(s);
};
});Java 类型签名参考
| Java 类型 | Frida 签名 |
|---|---|
int | 'int' |
boolean | 'boolean' |
byte | 'byte' |
short | 'short' |
long | 'long' |
float | 'float' |
double | 'double' |
char | 'char' |
String | 'java.lang.String' |
byte[] | '[B' |
int[] | '[I' |
Object[] | '[Ljava.lang.Object;' |
Context | 'android.content.Context' |
void | 'void' |
Hook 所有重载
Java.perform(function() {
const Cipher = Java.use('javax.crypto.Cipher');
// 列出所有重载数量
console.log('重载数: ' + Cipher.getInstance.overloads.length);
// Hook 所有重载
Cipher.getInstance.overloads.forEach(function(overload) {
overload.implementation = function() {
console.log('Cipher.getInstance(' +
Array.from(arguments).join(', ') + ')');
return overload.apply(this, arguments);
};
});
});6.4 字段访问
Java.perform(function() {
const MyClass = Java.use('com.example.app.MyClass');
MyClass.doSomething.implementation = function() {
// 通过 .value 访问实例字段
console.log('字段 m = ' + this.m.value);
this.m.value = 42; // 修改字段值
// 字段名与方法冲突时,加下划线前缀
console.log('字段 _count = ' + this._count.value);
// 静态字段
console.log('静态字段: ' + MyClass.CONSTANT.value);
return this.doSomething();
};
});6.5 Java.choose()(堆上搜索实例)
Java.perform(function() {
Java.choose('com.example.app.SecretManager', {
onMatch(instance) {
console.log('找到实例: ' + instance);
console.log('秘密值: ' + instance.getSecret());
// 调用实例方法
instance.setSecret('hacked');
},
onComplete() {
console.log('堆扫描完成');
}
});
});6.6 Java.enumerateLoadedClasses()
Java.perform(function() {
// 同步枚举(17.0+ 推荐方式)
const classes = Java.enumerateLoadedClassesSync();
classes.filter(c => c.includes('com.example'))
.forEach(c => console.log(c));
// 异步枚举(兼容旧版写法)
Java.enumerateLoadedClasses({
onMatch(name, handle) {
if (name.indexOf('com.example') !== -1) {
console.log('Class: ' + name);
}
},
onComplete() {}
});
});注意:17.0+ 已废弃回调风格的枚举 API(onMatch/onComplete 模式),推荐使用同步数组返回方式。旧写法仍可使用但不推荐。
6.7 ClassLoader 处理
当类由非默认 ClassLoader 加载时:
Java.perform(function() {
Java.enumerateClassLoaders({
onMatch(loader) {
try {
// 尝试用特定 classloader 加载
const factory = Java.ClassFactory.get(loader);
const MyClass = factory.use('com.example.app.HiddenClass');
console.log('通过 loader 找到隐藏类');
// Hook 该类
MyClass.secretMethod.implementation = function() {
console.log('隐藏类方法被调用');
return this.secretMethod();
};
} catch(e) {}
},
onComplete() {}
});
});6.8 创建实例 ($new)
Java.perform(function() {
const String = Java.use('java.lang.String');
const newStr = String.$new('Hello from Frida');
console.log(newStr.toString());
const File = Java.use('java.io.File');
const f = File.$new('/data/data/com.example.app/test.txt');
console.log('存在: ' + f.exists());
});6.9 Java.cast() 和 Java.array()
Java.perform(function() {
// 类型转换
const Activity = Java.use('android.app.Activity');
const MyActivity = Java.use('com.example.app.MyActivity');
Java.choose('android.app.Activity', {
onMatch(instance) {
const myInstance = Java.cast(instance, MyActivity);
console.log(myInstance.myCustomMethod());
},
onComplete() {}
});
// 创建数组
const byteArray = Java.array('byte', [0x48, 0x65, 0x6c, 0x6c, 0x6f]);
const intArray = Java.array('int', [1, 2, 3, 4, 5]);
const stringArray = Java.array('java.lang.String', ['a', 'b', 'c']);
});6.10 注册自定义类
Java.perform(function() {
const MyHook = Java.registerClass({
name: 'com.example.frida.MyHook',
superClass: Java.use('java.lang.Object'),
implements: [Java.use('java.lang.Runnable')],
methods: {
run() {
console.log('自定义 run() 被调用');
}
}
});
});6.11 获取调用栈
Java.perform(function() {
const Exception = Java.use('java.lang.Exception');
const Log = Java.use('android.util.Log');
const MyClass = Java.use('com.example.app.MyClass');
MyClass.targetMethod.implementation = function() {
// 打印完整 Java 调用栈
console.log(Log.getStackTraceString(Exception.$new()));
return this.targetMethod();
};
});6.12 在主线程执行
Java.perform(function() {
Java.scheduleOnMainThread(function() {
// 在 Android 主/UI 线程执行
const Toast = Java.use('android.widget.Toast');
const context = Java.use('android.app.ActivityThread')
.currentApplication().getApplicationContext();
Toast.makeText(context,
Java.use('java.lang.String').$new('Frida!'), 0).show();
});
});6.13 Hook 所有方法(一键 Hook 整个类)
Java.perform(function() {
const MyClass = Java.use('com.example.app.TargetClass');
const methods = MyClass.class.getDeclaredMethods();
methods.forEach(function(method) {
const methodName = method.getName();
try {
MyClass[methodName].overloads.forEach(function(overload) {
overload.implementation = function() {
console.log('[*] ' + methodName + '(' +
Array.from(arguments).map(a => a + '').join(', ') + ')');
return overload.apply(this, arguments);
};
});
} catch(e) {
console.log(' 跳过: ' + methodName + ' - ' + e.message);
}
});
});6.14 类包装器属性总结
| 属性/方法 | 说明 |
|---|---|
$init | 构造器引用 |
$new(args...) | 创建新实例(分配 + 初始化) |
$super | 访问父类 |
$dispose() | 释放 JS 引用 |
$isSameObject(other) | 身份比较 |
$className | 完全限定类名 |
.overload(types...) | 选择特定重载 |
.implementation = fn | 替换方法实现 |
6.15 Android ART 适配(17.5+ 重大变更)
Android 14/15/16 兼容性
Frida 17.x 对 Android ART 运行时进行了多项关键适配:
| 版本 | ART 相关变更 |
|---|---|
| 17.1.4 | frida-java-bridge 7.0.3:新增 Android 16 支持 |
| 17.2.12 | 运行时检测 ART class spec 偏移量,不再依赖 SDK 版本启发式推断 |
| 17.2.14 | Art::GetOsThreadStat 伪装支持,应对 Zygote 单线程检查 |
| 17.4.1 | 静态 trampoline 修复;GC 后 ArtMethod 类字段同步 |
| 17.6.0 | 轻量级 Zygote 注入:通过 setArgV0Native() 补丁替代完整 bridge 加载 |
| 17.6.1 | BTI(Branch Target Identification)兼容:arm64 注入代码包含 BTI 编译 |
| 17.8.0 | 进程内 ART/Dalvik VM 加载,消除 frida-helper.dex 边界问题 |
Zygote 注入机制变更(17.6+)
17.6.0 对 Android Zygote 注入进行了架构级重构:
旧方案(17.5 及之前):
- 依赖完整的 frida-java-bridge 加载到 Zygote 进程
- 使用 ptrace 进行进程注入
- 体积大,容易被检测
新方案(17.6+):
- “Zymbiote” 轻量载荷:仅 920 字节(arm64)
- 通过补丁
android.os.Process.setArgV0Native()方法指针实现 hook - 使用
/proc/$pid/mem替代 ptrace 注入 - 通过抽象 UNIX Socket 与宿主通信
- 不再在 Zygote 中加载 frida-java-bridge
// 17.6+ Zygote 注入流程示意
1. frida-server 通过 /proc/$zygote_pid/mem 写入 zymbiote 载荷
2. 补丁 setArgV0Native() 方法入口跳转到载荷
3. 载荷通过抽象 socket 通知 frida-server 新 fork 的进程
4. frida-server 对目标子进程执行完整注入
ART 偏移量动态检测(17.2.12+)
不再假设 ART 内部结构偏移量与 SDK 版本一致。这解决了 OEM 定制 ROM 或独立 libart.so 更新导致偏移量变化的崩溃问题:
// 旧方式(17.2.12 之前):基于 SDK 版本硬编码偏移量
// 新方式(17.2.12+):运行时探测 ART 类结构偏移量
// - 通过特征模式匹配定位关键结构
// - 兼容 OEM 魔改 ART(MIUI、ColorOS、OneUI 等)
GC 安全性改进(17.4.1+)
// 17.4.1 修复:ArtMethod class 字段在 GC 后的同步问题
// 影响场景:长时间运行的 hook 在 GC 触发后可能失效
// 修复后:Frida 自动在 GC 后重新同步 trampoline 引用6.16 Android 进程匹配改进(17.9.10+)
Frida 17.9.10 改进了 Android 自定义进程的匹配逻辑:
# 现在可以正确匹配使用 android:process 属性的自定义进程名
frida -U -n "com.example.app:service"6.17 最佳实践与注意事项
Hook 时机
// 推荐:使用 Java.perform 确保 VM 就绪
Java.perform(function() {
// hook 代码
});
// 对于需要等待特定类加载的场景
Java.performNow(function() {
// 确认类已加载后使用 performNow 避免额外线程开销
});避免内存泄漏
Java.perform(function() {
const instances = [];
Java.choose('com.example.Target', {
onMatch(instance) {
// 如需长期持有引用,使用 Java.retain()
instances.push(Java.retain(instance));
},
onComplete() {}
});
// 使用完毕后释放
instances.forEach(inst => inst.$dispose());
});检测规避技巧
Java.perform(function() {
// Hook 常见的 Frida 检测点
const Runtime = Java.use('java.lang.Runtime');
Runtime.exec.overload('java.lang.String').implementation = function(cmd) {
// 过滤掉检测命令
if (cmd.indexOf('frida') !== -1 || cmd.indexOf('27042') !== -1) {
console.log('[!] 拦截检测命令: ' + cmd);
return null;
}
return this.exec(cmd);
};
// Hook /proc/self/maps 读取
const BufferedReader = Java.use('java.io.BufferedReader');
BufferedReader.readLine.implementation = function() {
const line = this.readLine();
if (line !== null && line.indexOf('frida') !== -1) {
return this.readLine(); // 跳过含 frida 的行
}
return line;
};
});6.18 版本变更摘要(17.4.1 → 17.9.11)
| 版本 | Java/ART 相关变更 |
|---|---|
| 17.0.0 | frida-java-bridge 解耦为独立 ESM 模块;废弃回调风格枚举 API |
| 17.1.4 | frida-java-bridge 7.0.3,新增 Android 16 支持 |
| 17.2.12 | ART class spec 偏移量运行时检测,告别 SDK 硬编码 |
| 17.2.14 | Zygote 单线程检查伪装 |
| 17.4.1 | ArtMethod 字段 GC 后同步修复 |
| 17.6.0 | Zymbiote 轻量 Zygote 注入(920 字节 arm64) |
| 17.6.1 | arm64 BTI 兼容修复 |
| 17.8.0 | 进程内 ART VM 加载,消除 dex helper 边界问题 |
| 17.9.10 | Android 自定义进程名匹配改进 |