Lazy loaded image
Frida学习笔记(五):Java 层 API 全解
Words 9096Read Time 23 min
2026-7-7
2026-7-7
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,按功能可以分为六组:
notion image
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.performJava.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,被动模式可能要等用户点击登录按钮才能触发,主动模式两秒就能拿到。
notion image
被动 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 的兄弟,根本不在委派路径上。
notion image
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 进行。
notion image
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 还会出现在两个相对小众的场景:① 注册一个继承自系统抽象类(如 BaseAdapterBroadcastReceiver)的"占位实现",配合 $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.systemjava.lang.reflectcom.android.internal 等)。在实际使用中,你通常只关心 App 自己的代码。

8.6 在 Hook 中使用堆栈的最佳实践

💡
性能注意:创建 Exception 对象并获取堆栈是有开销的(需要遍历当前线程的整个调用栈帧)。如果被 Hook 的方法每秒调用几百次(比如 Socket.write),每次都打印堆栈会导致明显的性能下降。建议在高频 Hook 中,只在满足特定条件时才打印堆栈(比如密钥长度为 16 字节时,或者输入包含特定关键字时)。

九、Java.choose 高级用法

9.1 回顾:Java.choose 的基本工作原理

第04篇介绍了 Java.choose 的基础用法。这里补充它的底层原理和高级技巧。
Java.choose
上一篇
爬虫数据采集加密算法全景指南
下一篇
网络指纹篇:TLS指纹、HTTP2指纹、IP信誉与代理检测

Comments
Loading...
Catalog