type
Post
status
Published
date
Jul 7, 2026
slug
summary
本篇目标:系统掌握 Frida 提供的全部 Java 层 API。第三、四篇教会了你用
Java.use + implementation 做被动拦截(别人调用时你看一眼),但 Frida 的能力远不止于此——你可以主动调用 App 的任意方法、创建 Java 对象、注册全新的 Java 类、在多个 ClassLoader 之间切换、处理 Java 与 JavaScript 之间的类型转换。这些能力组合起来,才构成了完整的"在别人的进程里执行你的代码"的工具箱。tags
frida
Android
逆向工程
category
公众号
icon
password
网址
上次编辑时间
Jul 7, 2026 09:32 AM
comment
AI 总结
本篇目标:系统掌握 Frida 提供的全部 Java 层 API。第三、四篇教会了你用
Java.use + implementation 做被动拦截(别人调用时你看一眼),但 Frida 的能力远不止于此——你可以主动调用 App 的任意方法、创建 Java 对象、注册全新的 Java 类、在多个 ClassLoader 之间切换、处理 Java 与 JavaScript 之间的类型转换。这些能力组合起来,才构成了完整的"在别人的进程里执行你的代码"的工具箱。
一、Java 桥接 API 全景图
在写具体代码之前,先建立一个全局视图。Frida 的
Java 命名空间下大约有 20 多个 API,按功能可以分为六组:
Frida Java API 全景图
功能分组 | API | 一句话说明 |
环境控制 | Java.available | 当前进程是否有 Java 运行时(ART/Dalvik) |
ㅤ | Java.androidVersion | Android 版本号字符串 |
ㅤ | Java.perform(fn) | 确保当前线程已附加到 ART VM 后执行回调 |
ㅤ | Java.performNow(fn) | 同上,但 VM 未就绪时直接报错而不等待 |
类操作 | Java.use(className) | 获取 Java 类的 Wrapper 对象 |
ㅤ | Java.cast(handle, klass) | 将一个 Java 对象转换为指定类型的 Wrapper |
ㅤ | Java.array(type, values) | 创建 Java 类型的数组 |
ㅤ | Java.registerClass(spec) | 在运行时注册一个全新的 Java 类 |
堆搜索 | Java.choose(className, callbacks) | 遍历堆上所有指定类型的活跃实例 |
ㅤ | Java.retain(obj) | 防止 Java 对象被 GC 回收(创建全局引用) |
类加载器 | Java.enumerateLoadedClasses(callbacks) | 枚举所有已加载的 Java 类 |
ㅤ | Java.enumerateLoadedClassesSync() | 同上,同步版本,返回类名数组 |
ㅤ | Java.enumerateClassLoaders(callbacks) | 枚举所有活跃的 ClassLoader |
ㅤ | Java.enumerateClassLoadersSync() | 同上,同步版本 |
ㅤ | Java.classFactory | 当前使用的 ClassFactory(绑定到特定 ClassLoader) |
调度 | Java.scheduleOnMainThread(fn) | 在 Android 主线程(UI线程)上执行代码 |
ㅤ | Java.isMainThread() | 当前是否在主线程上 |
高级 | Java.openClassFile(filePath) | 打开一个 DEX 文件并加载其中的类 |
ㅤ | Java.deoptimizeEverything() | 强制 ART 关闭所有 JIT 编译,回退到解释执行 |
ㅤ | Java.deoptimizeBootImage() | 仅对 boot image 中的方法关闭 JIT |
ㅤ | Java.vm | 底层 JavaVM 对象,可直接调用 JNI 函数 |
第03-04篇已经深入讲过
Java.perform、Java.use(基础用法)、Java.choose(基础用法)、Java.enumerateLoadedClasses。本篇不重复这些基础,而是聚焦于:主动调用、ClassLoader 切换、Java.cast / Java.array / Java.registerClass 这些进阶能力,以及贯穿整个逆向工作流的类型转换和堆栈打印技巧。二、主动调用:不只是旁观,而是亲自动手
2.1 被动 Hook vs 主动调用
前两篇教的都是「被动拦截」——你设置一个 Hook,然后等 App 自己触发那个方法,你才能看到参数和返回值。这就像在公路上装了一个摄像头:车来了你能看到,车不来你就只能等。
主动调用则完全不同——你可以直接从 Frida 脚本中调用 App 的任意 Java 方法,不需要等 App 自己触发。
下图把两种模式画在同一时间轴上:被动模式是 App 在主线程触发 → Frida 在 JS 线程被动接到回调(红色数据流方向 App→Frida);主动模式是 App 闲置不动 → Frida 在 JS 线程主动发起 JNI 调用(绿色数据流方向 Frida→App)。同样要拿一个 token,被动模式可能要等用户点击登录按钮才能触发,主动模式两秒就能拿到。

被动 Hook vs 主动调用 时序对比
这在实际逆向中极其有用。几个典型场景:
场景一:验证你的理解。 你通过 Hook 观察到
EncryptUtils.encrypt("hello", "key123") 返回了 "a1b2c3"。为了确认你理解了参数格式,你想主动调用一次:传入 "test" 和 "key123",看看输出是什么。如果输出符合你的预期(比如 AES-CBC 的特征),说明你的理解正确。场景二:提取动态数据。 很多 App 有一个
getToken() 或 getDeviceId() 方法,返回运行时动态生成的值。你不需要等 App 自己调用这个方法——直接主动调用,立刻拿到结果。场景三:触发隐藏功能。 有些 App 内部有调试方法或管理员功能,正常使用流程中不会被触发。通过主动调用,你可以直接执行这些方法。
2.2 调用静态方法
静态方法是最简单的主动调用——因为不需要对象实例,直接通过类 Wrapper 调用即可。
这里有一个容易忽略的细节:**
.call(EncryptUtils, ...) 中第一个参数是类 Wrapper 本身**。对于静态方法,这个参数其实没有实际作用(静态方法不依赖实例),但 Frida 的 API 设计要求你传入它。与 Hook 回调中调用的区别:在
implementation 回调中,你用 this.method(args) 来调用原始方法,这里的 this 是被 Hook 的那个具体对象实例。而在主动调用中,你用 ClassName.method(args) 直接调用,这是在你选择的任意时刻发起调用,与 App 的执行流完全独立。
2.3 调用实例方法:先找到对象
实例方法需要一个对象实例才能调用。问题是:对象在 App 的堆内存中,你怎么拿到它?
有三种方式:
方式一:用 Java.choose 搜索堆上的活跃实例。
这是最常用的方式。
Java.choose 遍历堆上所有指定类型的对象,每找到一个就通过 onMatch 回调交给你。方式二:在 Hook 回调中保存 this 引用。
如果你已经 Hook 了某个方法,可以在回调中把
this(当前对象)保存到全局变量中,后续就可以随时调用它的方法。Java.retain 的重要性:当
implementation 回调结束后,Frida 会释放回调中 Java 对象的局部引用(JNI Local Reference)。如果你把 this 存到全局变量但不调用 Java.retain,后续使用时可能遇到"stale reference"错误——因为对象的 JNI 引用已经失效了。Java.retain 的底层是调用 JNI 的 NewGlobalRef,将局部引用升级为全局引用,防止被 GC 回收。当你不再需要这个引用时,可以对 wrapper 调用 obj.$dispose() 主动释放底层 JNI 引用;脚本卸载时 Frida 也会自动清理,所以实际逆向中很少有人显式 dispose。
方式三:通过
$new() 自己创建一个新对象。2.4 $new():在 Frida 中创建 Java 对象
$new() 是 Java.use 返回的 Wrapper 上的特殊方法,用于创建该类的新实例。它等同于 Java 中的 new ClassName(args)。$new 同样支持 overload,当构造函数有多个重载版本时:**init 再辨析**:第04篇提过这个区别,这里从 JNI 角度加深理解。`$new()
底层调用的是 JNI 的NewObject(env, clazz, methodID, args),它会:①分配内存 → ②调用构造函数 → ③返回对象引用。而
的替换的是步骤②中的构造函数本身。所以
new 是从外部创建新对象,$init` Hook 是拦截内部的对象创建过程。
2.5 主动调用的线程安全问题
主动调用 Java 方法时,你的代码运行在 Frida 的 JS 引擎线程上。而 App 的业务逻辑可能运行在主线程或其他工作线程上。如果你调用的方法不是线程安全的(比如它访问了一个只在主线程初始化的 Handler,或者它操作了 UI 元素),可能会遇到 crash。
对于必须在主线程执行的操作,使用
Java.scheduleOnMainThread:**Java.isMainThread()**:在执行代码前可以先检查当前是否在主线程上。在 Hook 回调中,你的代码运行在调用被 Hook 方法的那个线程上——如果 App 在主线程调用了被 Hook 的方法,你的回调也在主线程;如果 App 在后台线程调用,你的回调也在后台线程。而在
Java.perform 的直接回调中(非 Hook 回调),你运行在 Frida 的 JS 引擎线程上。
三、ClassLoader 切换:解决「类找不到」的终极方案
3.1 为什么 Java.use 会找不到类
第03篇讲过
ClassNotFoundException 的常见原因。其中有一种情况特别棘手:类确实存在于 App 中,但默认的 ClassLoader 找不到它。这在以下场景中经常发生:
加固/加壳 App:壳会创建自定义的 ClassLoader 来加载解密后的 DEX 文件。App 的业务类通过这个自定义 ClassLoader 加载,而 Frida 默认使用的是系统 ClassLoader,自然找不到这些类。
插件化/热修复框架:如 VirtualApk、RePlugin、Atlas、Tinker 等,每个插件模块都有独立的 ClassLoader。你要 Hook 的类可能在某个插件的 ClassLoader 中。
多 DEX App:虽然 Android 5.0+ 的 ART 原生支持 MultiDex,但某些场景下(如动态加载的 DEX)仍可能需要切换 ClassLoader。
下面这张图把"为什么默认找不到"画清楚——核心原因是 Java 的双亲委派只向上找父,不横向找兄弟:当目标类被 DexClassLoader(插件/加固)加载时,Frida 默认绑定的 PathClassLoader 调用
loadClass 一路向上委派到 BootClassLoader 都找不到,最后回落自查也不行,于是抛 ClassNotFoundException——而真正持有目标类的 DexClassLoader 是 PathClassLoader 的兄弟,根本不在委派路径上。
ClassLoader 双亲委派与 loadClass 委派链
理解了这张图,下面三节(枚举 → 切换 → 多工厂并存)就是顺理成章的解决步骤。
3.2 Java.enumerateClassLoaders:找到所有 ClassLoader
3.3 切换 ClassLoader:Java.classFactory
找到正确的 ClassLoader 后,需要告诉 Frida 使用它。方法是设置
Java.classFactory.loader:重要提醒:切换
Java.classFactory.loader 会影响后续所有的 Java.use 调用。如果你需要同时操作不同 ClassLoader 中的类,有两种做法:一是切换 → use → Hook → 切回,二是创建独立的 ClassFactory(下面讲)。
3.4 多 ClassLoader 并存:创建独立的 ClassFactory
当你需要同时操作来自不同 ClassLoader 的类时(比如同时 Hook 宿主 App 和插件模块的方法),频繁切换
Java.classFactory.loader 容易出错。更优雅的做法是创建独立的 ClassFactory:Java.ClassFactory.get(loader) 返回一个绑定到指定 ClassLoader 的 ClassFactory 对象。它拥有和 Java 命名空间类似的 API(.use()、.choose() 等),但所有类查找操作都通过指定的 ClassLoader 进行。
ClassLoader 切换与多 ClassFactory 工作模式
3.5 实战模式:通用的 ClassLoader 搜索 + Hook 模板
在实际逆向中,你经常需要"先找到 ClassLoader,再 Hook"。下面是一个可复用的模板:
四、Java.cast:类型转换的瑞士军刀
4.1 为什么需要 cast
在 Java 中,一个对象可以被声明为它的父类或接口类型。比如一个
ArrayList 对象可以被声明为 List 类型,一个 OkHttpClient$Builder 可以被作为 Object 传递。当你在 Frida 中通过 Hook 拿到一个参数时,它的 Wrapper 类型是方法签名中声明的类型,而不是对象的实际类型。如果方法签名是
void process(Object obj),你拿到的 args[0] 是 Object 类型的 Wrapper——你只能调用 Object 上定义的方法(toString()、hashCode() 等),无法直接调用 obj 真实类型上的方法。Java.cast 解决这个问题:它把一个 Java 对象的 Wrapper 转换为指定类型的 Wrapper,让你可以调用目标类型上的所有方法。4.2 基本用法
**obj.
的妙用:任何对象都有
className
属性,返回对象的实际运行时类名(等同于 Java 的obj.getClass().getName()`)。这在 cast 之前非常有用——先看看实际类型是什么,再决定 cast 成什么。
4.3 cast 到接口类型
Java 中的接口很常见。比如
Interceptor 接口在 OkHttp 中用于拦截网络请求。如果你通过 Java.choose 找到了一个实现了 Interceptor 接口的对象,但它的具体类名被混淆了(比如是 a.b.c),你可以 cast 为接口类型来调用接口方法:4.4 cast 在加密分析中的经典应用
在 Hook
Cipher.init 时,密钥参数类型是 java.security.Key 接口。你需要 cast 才能获取密钥的具体信息:五、Java.array:创建 Java 数组
5.1 为什么需要专门的数组 API
JavaScript 的数组(
[1, 2, 3])和 Java 的数组(int[]、byte[]、String[])是完全不同的东西。Java 数组有固定的元素类型和固定的长度,是 JVM 中的一等公民对象。当你需要主动调用一个接受 Java 数组参数的方法时,不能直接传 JS 数组——你需要用 Java.array 创建一个真正的 Java 数组。5.2 基本语法
类型标识符规则:
Java 类型 | Java.array 类型标识符 | 示例 |
byte[] | "byte" | Java.array("byte", [0x01, 0x02]) |
int[] | "int" | Java.array("int", [1, 2, 3]) |
long[] | "long" | Java.array("long", [100, 200]) |
float[] | "float" | Java.array("float", [1.0, 2.0]) |
double[] | "double" | Java.array("double", [1.0, 2.0]) |
boolean[] | "boolean" | Java.array("boolean", [true]) |
char[] | "char" | Java.array("char", [65, 66]) |
short[] | "short" | Java.array("short", [1, 2]) |
String[] | "java.lang.String" | Java.array("java.lang.String", ["a"]) |
Object[] | "java.lang.Object" | Java.array("java.lang.Object", [...]) |
5.3 实战:主动调用接受 byte[] 参数的加密方法
byte 类型的坑:Java 的
byte 是有符号的(-128 到 127),而 JavaScript 的数字没有这个限制。当你在 Java.array("byte", [...]) 中传入大于 127 的值(如 0xff),Frida 会自动处理符号转换。但在读取 Java byte[] 时,返回的值可能是负数——要转成无符号值需要 & 0xff。这是一个非常常见的坑:
六、Java.registerClass:在运行时注册全新的 Java 类
6.1 什么时候需要创建新类
这是一个高级功能,在大多数常规 Hook 场景中用不到——但有一类问题除了它没有别的解法:当 App 接受一个接口/抽象类参数,而你想注入自己的实现时。
最典型的就是替换回调。比如 App 有这样一个调用:
你想拦截
callback.onSuccess(data) 时拿到的 data。直接 Hook ResponseCallback.onSuccess 看似可行,但 ResponseCallback 是接口——Frida 的 Java.use("...ResponseCallback") 只能拿到接口 Wrapper,没法 Hook 接口方法本身(要 Hook 实现类才有意义)。而具体实现类可能是匿名内部类(ApiClient$1),名字混淆后还可能每次构建都变。Java.registerClass 解法是:自己注册一个实现 ResponseCallback 接口的代理类,然后 Hook apiClient.request(...) 把第三个参数换成代理实例。代理在 onSuccess 里既能打印 data,又能转发给原 callback——下面 6.3 节就是这个场景的完整代码。除了"替换回调",
registerClass 还会出现在两个相对小众的场景:① 注册一个继承自系统抽象类(如 BaseAdapter、BroadcastReceiver)的"占位实现",配合 $new 用来满足某个 API 的形参要求;② 配合 superClass 字段做"加一层 wrapper" 拦截父类受保护方法。这两种用法占比不高,看完 6.3 后举一反三即可,本篇不专门展开。6.2 基本语法
6.3 实战:替换网络回调拦截响应
七、类型转换工具箱:Java ↔ JavaScript 的桥梁
在 Frida 逆向中,你会不断在 Java 类型和 JavaScript 类型之间来回转换。这些转换操作虽然简单,但如果不熟练,会在每个脚本中反复耗费时间。本节整理一个完整的工具箱。
7.1 Frida 的自动类型转换
Frida 在 Java 和 JavaScript 之间提供了一些自动转换:
Java 类型 | JavaScript 接收类型 | 说明 |
String | JS string | 自动转换,双向 |
int / Integer | JS number | 自动转换 |
long / Long | JS number(注意精度) | 超过 2^53 会丢精度 |
boolean / Boolean | JS boolean | 自动转换 |
float / Float | JS number | 自动转换 |
double / Double | JS number | 自动转换 |
byte[] | JS array-like | 可用下标访问,但不是真正的 JS Array |
其他对象 | Java Wrapper | 需要调用 .toString() 转字符串 |
long 精度陷阱:JavaScript 的 Number 类型是 64 位 IEEE 754 浮点数,安全整数范围是 ±2^53。而 Java 的
long 是 64 位有符号整数,范围是 ±2^63。当一个 Java long 值超过 2^53(约 9007 万亿)时,传到 JavaScript 中会丢失精度。在处理时间戳(毫秒级)时通常没问题,但处理数据库 ID 或加密用的大整数时要注意。对于需要精确的场景,用 .toString() 将 long 转为字符串传输。
7.2 byte[] 处理:逆向中最高频的转换
Android 逆向中,你和
byte[] 打交道的频率极高——加密的输入/输出、密钥、IV、哈希值、签名、证书……全都是 byte[]。7.3 智能打印 byte[]:自动判断是文本还是二进制
在实际 Hook 中,你经常需要打印
byte[] 的内容,但不知道它是可读文本还是二进制数据。下面这个函数会自动判断:这个函数在第14篇(算法自吐脚本)中会被大量使用——它让加密监控的输出一目了然:文本明文直接显示文本,二进制密文显示十六进制。
7.4 Java 集合类型的遍历
除了
byte[],你还经常遇到 Java 的集合类型。以下是常见集合的遍历方式:性能提醒:遍历大型集合(如上千个元素的 List 或 Map)时要注意,每次
.get(i) 或 .next() 都涉及一次 JNI 调用,开销不小。在 Hook 高频方法(如网络拦截器每次请求都触发)时,建议只在确实需要时才遍历,或者加一个条件过滤。
八、堆栈打印:定位「谁调用了这个方法」
8.1 为什么堆栈如此重要
当你 Hook 到一个加密方法被调用了,看到了密钥和明文——接下来最重要的问题是:谁调用了它? 是登录模块?支付模块?还是设备指纹采集?
堆栈(Stack Trace)给你答案。它展示了从当前方法一路向上的完整调用链,比如:
一看就知道是「登录页面点击登录按钮 → 调用认证服务 → 执行 AES 加密」。
8.2 方式一:Exception + Log(最常用)
这是 Frida Java 层堆栈打印最标准、最可靠的方式:
原理:在 Java 中,
new Exception() 的构造函数会调用 fillInStackTrace(),记录当前线程的完整调用栈。Log.getStackTraceString() 将这个堆栈信息格式化为人类可读的字符串。为什么不直接用
exception.getStackTrace()? 你当然可以——它返回一个 StackTraceElement[] 数组,每个元素包含类名、方法名、文件名、行号。但 Log.getStackTraceString 会帮你格式化成整齐的多行字符串,在控制台输出中更容易阅读。
8.3 方式二:Thread.currentThread().getStackTrace()
这种方式的优势是可以编程式地访问堆栈的每一层(类名、方法名、行号),方便做过滤和条件判断。
8.4 方式三:Throwable(最精简)
8.5 过滤堆栈:只看有用的部分
原始堆栈通常很长,包含大量框架代码(
dalvik.system、java.lang.reflect、com.android.internal 等)。在实际使用中,你通常只关心 App 自己的代码。8.6 在 Hook 中使用堆栈的最佳实践
性能注意:创建
Exception 对象并获取堆栈是有开销的(需要遍历当前线程的整个调用栈帧)。如果被 Hook 的方法每秒调用几百次(比如 Socket.write),每次都打印堆栈会导致明显的性能下降。建议在高频 Hook 中,只在满足特定条件时才打印堆栈(比如密钥长度为 16 字节时,或者输入包含特定关键字时)。
九、Java.choose 高级用法
9.1 回顾:Java.choose 的基本工作原理
第04篇介绍了
Java.choose 的基础用法。这里补充它的底层原理和高级技巧。Java.choose- Author:24th
- URL:https://24th.top/article/396e5b08-46db-8061-b5a2-fbe89f19a413
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!









