当前位置:首页>学习笔记>Frida学习笔记(五):Java 层 API 全解

Frida学习笔记(五):Java 层 API 全解

  • 2026-05-11 21:42:47
Frida学习笔记(五):Java 层 API 全解

本篇目标:系统掌握 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.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,被动模式可能要等用户点击登录按钮才能触发,主动模式两秒就能拿到。

被动 Hook vs 主动调用 时序对比

这在实际逆向中极其有用。几个典型场景:

场景一:验证你的理解。 你通过 Hook 观察到 EncryptUtils.encrypt("hello", "key123") 返回了 "a1b2c3"。为了确认你理解了参数格式,你想主动调用一次:传入 "test" 和 "key123",看看输出是什么。如果输出符合你的预期(比如 AES-CBC 的特征),说明你的理解正确。

场景二:提取动态数据。 很多 App 有一个 getToken() 或 getDeviceId() 方法,返回运行时动态生成的值。你不需要等 App 自己调用这个方法——直接主动调用,立刻拿到结果。

场景三:触发隐藏功能。 有些 App 内部有调试方法或管理员功能,正常使用流程中不会被触发。通过主动调用,你可以直接执行这些方法。

2.2 调用静态方法

静态方法是最简单的主动调用——因为不需要对象实例,直接通过类 Wrapper 调用即可。

// active_call_static.js
// 主动调用 App 的静态方法

Java.perform(function() {
// 假设 App 有一个工具类,提供静态的加密方法
var EncryptUtils = Java.use("com.example.app.crypto.EncryptUtils");

// 直接调用静态方法,就像在 Java 中调用 EncryptUtils.md5("hello") 一样
var hash = EncryptUtils.md5("hello");
console.log("[主动调用] md5('hello') = " + hash);

// 调用有多个参数的静态方法
var encrypted = EncryptUtils.aesEncrypt("plaintext""secretkey");
console.log("[主动调用] aesEncrypt result = " + encrypted);

// 如果方法有重载,需要用 overload 指定参数类型
var result = EncryptUtils.encrypt
        .overload("java.lang.String""int")
        .call(EncryptUtils, "data"1);
console.log("[主动调用] encrypt('data', 1) = " + result);
});

这里有一个容易忽略的细节:**.call(EncryptUtils, ...) 中第一个参数是类 Wrapper 本身**。对于静态方法,这个参数其实没有实际作用(静态方法不依赖实例),但 Frida 的 API 设计要求你传入它。

与 Hook 回调中调用的区别:在 implementation 回调中,你用 this.method(args) 来调用原始方法,这里的 this 是被 Hook 的那个具体对象实例。而在主动调用中,你用 ClassName.method(args) 直接调用,这是在你选择的任意时刻发起调用,与 App 的执行流完全独立。

2.3 调用实例方法:先找到对象

实例方法需要一个对象实例才能调用。问题是:对象在 App 的堆内存中,你怎么拿到它?

有三种方式:

方式一:用 Java.choose 搜索堆上的活跃实例。

这是最常用的方式。Java.choose 遍历堆上所有指定类型的对象,每找到一个就通过 onMatch 回调交给你。

// active_call_instance.js
// 通过 Java.choose 找到实例,然后主动调用实例方法

Java.perform(function() {
    Java.choose("com.example.app.auth.UserSession", {
onMatchfunction(instance{
// instance 就是堆上找到的 UserSession 对象
// 现在可以调用它的任意实例方法
var token = instance.getToken();
var userId = instance.getUserId();
var isVip = instance.isVipUser();

console.log("[UserSession 实例]");
console.log("  Token: " + token);
console.log("  UserID: " + userId);
console.log("  VIP: " + isVip);

// 甚至可以修改对象状态
// instance.setVipUser(true);  // 把自己变成 VIP(慎用)
        },
onCompletefunction() {
console.log("[*] 堆搜索完成");
        }
    });
});

方式二:在 Hook 回调中保存 this 引用。

如果你已经 Hook 了某个方法,可以在回调中把 this(当前对象)保存到全局变量中,后续就可以随时调用它的方法。

// active_call_saved_ref.js
// 在 Hook 回调中保存对象引用,稍后主动调用

var savedSession = null;

Java.perform(function() {
var UserSession = Java.use("com.example.app.auth.UserSession");

    UserSession.getToken.implementation = function() {
var token = this.getToken();
console.log("[Hook] getToken: " + token);

// 保存这个对象的引用到全局变量
// Java.retain 防止被 GC 回收(重要!)
        savedSession = Java.retain(this);

return token;
    };
});

// 稍后可以通过 savedSession 主动调用(比如在 RPC 中)
// savedSession.refreshToken();
// savedSession.getUserProfile();

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() 自己创建一个新对象。

Java.perform(function() {
var StringBuilder = Java.use("java.lang.StringBuilder");

// $new() 相当于 Java 中的 new StringBuilder("Hello")
var sb = StringBuilder.$new("Hello");
    sb.append(" ");
    sb.append("World");
console.log(sb.toString());  // 输出: Hello World
});

2.4 $new():在 Frida 中创建 Java 对象

$new() 是 Java.use 返回的 Wrapper 上的特殊方法,用于创建该类的新实例。它等同于 Java 中的 new ClassName(args)

Java.perform(function() {
// 创建基础类型的包装对象
var Integer = Java.use("java.lang.Integer");
var num = Integer.$new(42);
console.log("Integer: " + num.intValue());  // 42

// 创建字符串(虽然 Frida 会自动转换,但有时你需要显式创建)
var JavaString = Java.use("java.lang.String");
var str = JavaString.$new("Hello from Frida");

// 创建 ArrayList 并添加元素
var ArrayList = Java.use("java.util.ArrayList");
var list = ArrayList.$new();
    list.add("item1");
    list.add("item2");
    list.add("item3");
console.log("List size: " + list.size());      // 3
console.log("List[0]: " + list.get(0));         // item1
console.log("List: " + list.toString());        // [item1, item2, item3]

// 创建 HashMap
var HashMap = Java.use("java.util.HashMap");
var map = HashMap.$new();
    map.put("key1""value1");
    map.put("key2""value2");
console.log("Map: " + map.toString());

// 创建 File 对象(不会真的创建文件,只是一个路径引用)
var File = Java.use("java.io.File");
var file = File.$new("/data/data/com.example.app/shared_prefs/config.xml");
console.log("File exists: " + file.exists());
console.log("File path: " + file.getAbsolutePath());
});

$new 同样支持 overload,当构造函数有多个重载版本时:

Java.perform(function() {
var File = Java.use("java.io.File");

// File(String pathname)
var f1 = File.$new("/sdcard/test.txt");

// File(String parent, String child)
var f2 = File.$new("/sdcard""test.txt");

// 如果自动匹配失败,手动指定重载
var f3 = File.$new
        .overload("java.lang.String""java.lang.String")
        .call(File, "/sdcard""test.txt");
});

**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.perform(function() {
// 这段代码会在 Android 主线程(UI 线程)上执行
    Java.scheduleOnMainThread(function() {
// 安全地调用需要主线程上下文的方法
var activity = Java.use("android.app.ActivityThread")
            .currentApplication()
            .getApplicationContext();

// 弹一个 Toast(必须在主线程)
var Toast = Java.use("android.widget.Toast");
var toastObj = Toast.makeText(
            activity,
            Java.use("java.lang.String").$new("Hello from Frida!"),
            Toast.LENGTH_LONG.value
        );
        toastObj.show();
console.log("[*] Toast 已显示");
    });
});

**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

// enumerate_classloaders.js
// 枚举所有活跃的 ClassLoader,找到能加载目标类的那个

Java.perform(function() {
var targetClassName = "com.example.app.core.SecretManager";

console.log("[*] 开始枚举 ClassLoader...\n");

    Java.enumerateClassLoaders({
onMatchfunction(loader{
try {
// 尝试用这个 ClassLoader 加载目标类
// loader.loadClass 就是 Java 的 ClassLoader.loadClass 方法
var klass = loader.loadClass(targetClassName);
console.log("[FOUND] " + targetClassName);
console.log("  ClassLoader: " + loader);
console.log("  ClassLoader 类型: " + loader.$className);

// 打印这个 ClassLoader 的父级链
var parent = loader.getParent();
var depth = 1;
while (parent !== null) {
console.log("  " + "  ".repeat(depth) + "└ parent: " + parent.$className);
                    parent = parent.getParent();
                    depth++;
                }
            } catch (e) {
// 这个 ClassLoader 找不到目标类,跳过
// 不打印错误信息,因为大多数 ClassLoader 都会失败
            }
        },
onCompletefunction() {
console.log("\n[*] ClassLoader 枚举完成");
        }
    });
});

3.3 切换 ClassLoader:Java.classFactory

找到正确的 ClassLoader 后,需要告诉 Frida 使用它。方法是设置 Java.classFactory.loader

// switch_classloader.js
// 切换 ClassLoader 后 Hook 加固 App 中的类

Java.perform(function() {
var targetClassName = "com.example.app.core.SecretManager";

    Java.enumerateClassLoaders({
onMatchfunction(loader{
try {
                loader.loadClass(targetClassName);
console.log("[*] 找到 ClassLoader: " + loader);

// 关键步骤:切换 Frida 的 ClassFactory 使用这个 ClassLoader
                Java.classFactory.loader = loader;

// 现在 Java.use 会通过新的 ClassLoader 查找类
var SecretManager = Java.use(targetClassName);
console.log("[*] 成功获取 " + targetClassName);

// 正常 Hook
                SecretManager.getSecret.implementation = function() {
var secret = this.getSecret();
console.log("[SecretManager] secret: " + secret);
return secret;
                };

console.log("[*] Hook 成功!");
            } catch (e) {
// 不是这个 ClassLoader,继续找
            }
        },
onCompletefunction() {}
    });
});

重要提醒:切换 Java.classFactory.loader 会影响后续所有的 Java.use 调用。如果你需要同时操作不同 ClassLoader 中的类,有两种做法:一是切换 → use → Hook → 切回,二是创建独立的 ClassFactory(下面讲)。

3.4 多 ClassLoader 并存:创建独立的 ClassFactory

当你需要同时操作来自不同 ClassLoader 的类时(比如同时 Hook 宿主 App 和插件模块的方法),频繁切换 Java.classFactory.loader 容易出错。更优雅的做法是创建独立的 ClassFactory:

// multi_classloader.js
// 同时操作多个 ClassLoader 中的类

Java.perform(function() {
var targetClassA = "com.example.app.host.HostManager";      // 宿主类
var targetClassB = "com.example.plugin.pay.PayProcessor";   // 插件类

var factoryA = null;  // 宿主的 ClassFactory
var factoryB = null;  // 插件的 ClassFactory

    Java.enumerateClassLoaders({
onMatchfunction(loader{
try {
                loader.loadClass(targetClassA);
// 为这个 ClassLoader 创建独立的 ClassFactory
                factoryA = Java.ClassFactory.get(loader);
console.log("[*] 宿主 ClassLoader: " + loader.$className);
            } catch(e) {}

try {
                loader.loadClass(targetClassB);
                factoryB = Java.ClassFactory.get(loader);
console.log("[*] 插件 ClassLoader: " + loader.$className);
            } catch(e) {}
        },
onCompletefunction() {
if (factoryA) {
// 通过 factoryA 操作宿主类
var HostManager = factoryA.use(targetClassA);
                HostManager.init.implementation = function() {
console.log("[Host] init 被调用");
returnthis.init();
                };
            }

if (factoryB) {
// 通过 factoryB 操作插件类
var PayProcessor = factoryB.use(targetClassB);
                PayProcessor.pay.implementation = function(amount{
console.log("[Plugin] pay: " + amount);
returnthis.pay(amount);
                };
            }
        }
    });
});

Java.ClassFactory.get(loader) 返回一个绑定到指定 ClassLoader 的 ClassFactory 对象。它拥有和 Java 命名空间类似的 API(.use().choose() 等),但所有类查找操作都通过指定的 ClassLoader 进行。

ClassLoader 切换与多 ClassFactory 工作模式

3.5 实战模式:通用的 ClassLoader 搜索 + Hook 模板

在实际逆向中,你经常需要"先找到 ClassLoader,再 Hook"。下面是一个可复用的模板:

// classloader_hook_template.js
// 通用模板:自动搜索 ClassLoader 并 Hook 目标类

functionhookWithClassLoader(targetClass, hookSetup{
    Java.perform(function() {
// 先尝试默认 ClassLoader
try {
var klass = Java.use(targetClass);
console.log("[*] 默认 ClassLoader 即可找到 " + targetClass);
            hookSetup(klass);
return;
        } catch(e) {
console.log("[*] 默认 ClassLoader 找不到,开始搜索...");
        }

// 遍历所有 ClassLoader,找到第一个能加载目标类的就停下
var done = false;
        Java.enumerateClassLoaders({
onMatchfunction(loader{
if (done) return'stop';
try {
                    loader.loadClass(targetClass);
                    Java.classFactory.loader = loader;
var klass = Java.use(targetClass);
console.log("[*] 通过 " + loader.$className + " 找到 " + targetClass);
                    hookSetup(klass);
                    done = true;
return'stop';
                } catch(e) {}
            },
onCompletefunction() {
if (!done) console.log("[!] 所有 ClassLoader 中均未找到 " + targetClass);
            }
        });
    });
}

// 使用方式
hookWithClassLoader("com.example.app.core.CryptoEngine"function(CryptoEngine{
    CryptoEngine.encrypt.implementation = function(data{
console.log("[CryptoEngine] encrypt: " + data);
returnthis.encrypt(data);
    };
});

四、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 基本用法

Java.perform(function() {
var SomeClass = Java.use("com.example.app.SomeClass");

// 假设这个方法接收 Object 类型的参数,但实际传入的是 JSONObject
    SomeClass.process.overload("java.lang.Object").implementation = function(obj{
// obj 的 Wrapper 类型是 Object,只能调用 toString()
console.log("[process] obj.toString(): " + obj.toString());

// 先检查实际类型
console.log("[process] 实际类型: " + obj.$className);
// 输出: org.json.JSONObject

// cast 为实际类型
var JSONObject = Java.use("org.json.JSONObject");
var jsonObj = Java.cast(obj, JSONObject);

// 现在可以调用 JSONObject 的方法了
console.log("[process] keys: " + jsonObj.keys());
console.log("[process] username: " + jsonObj.getString("username"));
console.log("[process] password: " + jsonObj.getString("password"));

returnthis.process(obj);
    };
});

**obj.className属性,返回对象的实际运行时类名(等同于 Java 的obj.getClass().getName()`)。这在 cast 之前非常有用——先看看实际类型是什么,再决定 cast 成什么。

4.3 cast 到接口类型

Java 中的接口很常见。比如 Interceptor 接口在 OkHttp 中用于拦截网络请求。如果你通过 Java.choose 找到了一个实现了 Interceptor 接口的对象,但它的具体类名被混淆了(比如是 a.b.c),你可以 cast 为接口类型来调用接口方法:

Java.perform(function() {
// 枚举所有实现了 okhttp3.Interceptor 接口的对象
    Java.choose("okhttp3.Interceptor", {
onMatchfunction(instance{
// instance 是某个具体实现类的对象,但类名可能被混淆
console.log("找到 Interceptor: " + instance.$className);

// cast 为 Interceptor 接口类型(虽然 choose 已经返回了正确的 Wrapper,
// 但在某些场景下你可能需要显式 cast 到特定接口)
var Interceptor = Java.use("okhttp3.Interceptor");
var interceptor = Java.cast(instance, Interceptor);

// 调用接口方法
// (Interceptor 只有一个 intercept(Chain) 方法,这里只是展示 cast 用法)
console.log("  类名: " + interceptor.$className);
        },
onCompletefunction() {}
    });
});

4.4 cast 在加密分析中的经典应用

在 Hook Cipher.init 时,密钥参数类型是 java.security.Key 接口。你需要 cast 才能获取密钥的具体信息:

Java.perform(function() {
var Cipher = Java.use("javax.crypto.Cipher");
var SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec");
var IvParameterSpec = Java.use("javax.crypto.spec.IvParameterSpec");

// init(int opmode, Key key, AlgorithmParameterSpec params)
    Cipher.init.overload("int""java.security.Key""java.security.spec.AlgorithmParameterSpec")
        .implementation = function(opmode, key, params{

var mode = opmode === 1 ? "ENCRYPT" : "DECRYPT";
console.log("\n[Cipher.init] 模式: " + mode);

// key 的声明类型是 java.security.Key(接口)
// 实际类型通常是 SecretKeySpec
console.log("[Cipher.init] Key 实际类型: " + key.$className);

if (key.$className === "javax.crypto.spec.SecretKeySpec") {
// cast 为 SecretKeySpec 以访问 getEncoded() 方法
var keySpec = Java.cast(key, SecretKeySpec);
var keyBytes = keySpec.getEncoded();
console.log("[Cipher.init] 密钥(hex): " + bytesToHex(keyBytes));
console.log("[Cipher.init] 密钥算法: " + keySpec.getAlgorithm());
        }

// params 的声明类型是 AlgorithmParameterSpec(接口)
// 实际类型通常是 IvParameterSpec
if (params !== null && params.$className === "javax.crypto.spec.IvParameterSpec") {
var ivSpec = Java.cast(params, IvParameterSpec);
var ivBytes = ivSpec.getIV();
console.log("[Cipher.init] IV(hex): " + bytesToHex(ivBytes));
        }

returnthis.init(opmode, key, params);
    };
});

functionbytesToHex(bytes{
if (!bytes) return"null";
var hex = [];
for (var i = 0; i < bytes.length; i++) {
var b = ((bytes[i] || 0) & 0xff).toString(16);
        hex.push(b.length === 1 ? "0" + b : b);
    }
return hex.join("");
}

五、Java.array:创建 Java 数组

5.1 为什么需要专门的数组 API

JavaScript 的数组([1, 2, 3])和 Java 的数组(int[]byte[]String[])是完全不同的东西。Java 数组有固定的元素类型和固定的长度,是 JVM 中的一等公民对象。当你需要主动调用一个接受 Java 数组参数的方法时,不能直接传 JS 数组——你需要用 Java.array 创建一个真正的 Java 数组。

5.2 基本语法

Java.perform(function() {
// 创建基本类型数组
var intArray = Java.array("int", [12345]);
var byteArray = Java.array("byte", [0x480x650x6c0x6c0x6f]);  // "Hello"
var longArray = Java.array("long", [100200300]);
var floatArray = Java.array("float", [1.02.53.14]);
var boolArray = Java.array("boolean", [truefalsetrue]);

// 创建对象类型数组
var stringArray = Java.array("java.lang.String", ["Hello""World"]);

// 读取数组元素
console.log("intArray[0] = " + intArray[0]);     // 1
console.log("intArray.length = " + intArray.length);  // 5

// 遍历数组
for (var i = 0; i < byteArray.length; i++) {
console.log("byte[" + i + "] = 0x" + (byteArray[i] & 0xff).toString(16));
    }
});

类型标识符规则:

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[] 参数的加密方法

// active_call_encrypt.js
// 主动调用 App 的 AES 加密方法,传入自定义的 byte[] 参数

Java.perform(function() {
var AesUtils = Java.use("com.example.app.crypto.AesUtils");

// 准备参数:16 字节的密钥和 16 字节的 IV
var key = Java.array("byte", [
0x010x020x030x040x050x060x070x08,
0x090x0a0x0b0x0c0x0d0x0e0x0f0x10
    ]);

var iv = Java.array("byte", [
0x000x000x000x000x000x000x000x00,
0x000x000x000x000x000x000x000x00
    ]);

// 要加密的明文
var plaintext = Java.array("byte", [
0x480x650x6c0x6c0x6f// "Hello"
    ]);

// 主动调用加密方法
// 假设方法签名: public static byte[] encrypt(byte[] data, byte[] key, byte[] iv)
var encrypted = AesUtils.encrypt(plaintext, key, iv);
console.log("[主动调用] 加密结果: " + bytesToHex(encrypted));
});

byte 类型的坑:Java 的 byte 是有符号的(-128 到 127),而 JavaScript 的数字没有这个限制。当你在 Java.array("byte", [...]) 中传入大于 127 的值(如 0xff),Frida 会自动处理符号转换。但在读取 Java byte[] 时,返回的值可能是负数——要转成无符号值需要 & 0xff。这是一个非常常见的坑:

var b = byteArray[0];          // 可能是 -1(Java 的有符号 byte)
var unsigned = b & 0xff;       // 255(无符号值)
var hex = unsigned.toString(16); // "ff"

六、Java.registerClass:在运行时注册全新的 Java 类

6.1 什么时候需要创建新类

这是一个高级功能,在大多数常规 Hook 场景中用不到——但有一类问题除了它没有别的解法:当 App 接受一个接口/抽象类参数,而你想注入自己的实现时

最典型的就是替换回调。比如 App 有这样一个调用:

apiClient.request(url, params, callback);  // callback 是 ResponseCallback 接口

你想拦截 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 基本语法

// register_class.js
// 注册一个新的 Java 类

Java.perform(function() {
// 注册一个实现了 Runnable 接口的新类
var MyRunnable = Java.registerClass({
name"com.frida.MyRunnable",  // 新类的全限定名

// 实现的接口列表
implements: [Java.use("java.lang.Runnable")],

// 方法实现
methods: {
// 实现 Runnable.run() 方法
runfunction() {
console.log("[MyRunnable] run() 被调用了!");
            }
        }
    });

// 创建新类的实例
var runnable = MyRunnable.$new();

// 传给需要 Runnable 的地方
var Thread = Java.use("java.lang.Thread");
var thread = Thread.$new(runnable);
    thread.start();
});

6.3 实战:替换网络回调拦截响应

// replace_callback.js
// 用自注册的类替换网络请求的回调,拦截响应数据

Java.perform(function() {
// 假设 App 有这样一个回调接口:
// public interface ResponseCallback {
//     void onSuccess(String data);
//     void onError(int code, String message);
// }

var ResponseCallback = Java.use("com.example.app.net.ResponseCallback");

// 保存原始回调引用
var originalCallback = null;

// 注册一个代理回调类
var ProxyCallback = Java.registerClass({
name"com.frida.ProxyCallback",
implements: [ResponseCallback],
fields: {
// 可以声明字段(可选)
"original""java.lang.Object"
        },
methods: {
onSuccessfunction(data{
// 拦截成功回调,打印响应数据
console.log("[ProxyCallback] onSuccess: " + data);

// 转发给原始回调
if (originalCallback) {
                    originalCallback.onSuccess(data);
                }
            },
onErrorfunction(code, message{
console.log("[ProxyCallback] onError: " + code + " - " + message);

if (originalCallback) {
                    originalCallback.onError(code, message);
                }
            }
        }
    });

// Hook 发送请求的方法,将回调替换为我们的代理
var ApiClient = Java.use("com.example.app.net.ApiClient");
    ApiClient.request.implementation = function(url, params, callback{
console.log("[ApiClient] request: " + url);

// 保存原始回调——必须 Java.retain,否则当 App 在异步线程
// 真正回调时,这里的 JNI 局部引用早已失效(参见 2.3 节关于
// Java.retain 的说明)。不 retain 会偶发 "stale local
// reference" 崩溃,特别在网络请求耗时较长时几乎必现。
        originalCallback = Java.retain(callback);

// 创建代理回调并替换
var proxy = ProxyCallback.$new();
returnthis.request(url, params, proxy);
    };
});

七、类型转换工具箱: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[]

// ====== byte[] → 十六进制字符串 ======
functionbytesToHex(bytes{
if (bytes === null || bytes === undefinedreturn"null";

var hex = [];
for (var i = 0; i < bytes.length; i++) {
// & 0xff 将有符号 byte 转为无符号
var b = (bytes[i] & 0xff).toString(16);
        hex.push(b.length === 1 ? "0" + b : b);
    }
return hex.join("");
}

// ====== byte[] → UTF-8 字符串 ======
functionbytesToUtf8(bytes{
if (bytes === null || bytes === undefinedreturn"null";

// 方法一:用 Java 的 String 构造函数(最可靠)
var JavaString = Java.use("java.lang.String");
return JavaString.$new(bytes, "UTF-8");
}

// ====== byte[] → Base64 字符串 ======
functionbytesToBase64(bytes{
if (bytes === null || bytes === undefinedreturn"null";

var Base64 = Java.use("android.util.Base64");
// Base64.NO_WRAP = 2,不添加换行符
return Base64.encodeToString(bytes, 2);
}

// ====== 十六进制字符串 → byte[](Java array)======
functionhexToBytes(hexStr{
var bytes = [];
for (var i = 0; i < hexStr.length; i += 2) {
        bytes.push(parseInt(hexStr.substr(i, 2), 16));
    }
return Java.array("byte", bytes);
}

// ====== UTF-8 字符串 → byte[] ======
functionutf8ToBytes(str{
var JavaString = Java.use("java.lang.String");
return JavaString.$new(str).getBytes("UTF-8");
}

// ====== Base64 字符串 → byte[] ======
functionbase64ToBytes(b64Str{
var Base64 = Java.use("android.util.Base64");
return Base64.decode(b64Str, 0);
}

7.3 智能打印 byte[]:自动判断是文本还是二进制

在实际 Hook 中,你经常需要打印 byte[] 的内容,但不知道它是可读文本还是二进制数据。下面这个函数会自动判断:

// smart_print_bytes.js
// 智能打印 byte[]:如果内容是可读文本则显示文本,否则显示十六进制

functionsmartPrintBytes(bytes, maxLen{
if (bytes === null || bytes === undefinedreturn"null";
if (bytes.length === 0return"(empty)";

    maxLen = maxLen || 256;  // 默认最多显示 256 字节

// 统计可打印 ASCII 字符的比例
var printableCount = 0;
var checkLen = Math.min(bytes.length, maxLen);

for (var i = 0; i < checkLen; i++) {
var b = bytes[i] & 0xff;
// 可打印 ASCII 范围:0x20-0x7E(空格到波浪号)
// 加上常见的控制字符:\t(0x09), \n(0x0A), \r(0x0D)
if ((b >= 0x20 && b <= 0x7e) || b === 0x09 || b === 0x0a || b === 0x0d) {
            printableCount++;
        }
    }

var printableRatio = printableCount / checkLen;
var truncated = bytes.length > maxLen;
var suffix = truncated ? "...(" + bytes.length + " bytes total)" : "";

if (printableRatio > 0.85) {
// 大部分是可打印字符,按 UTF-8 显示
try {
var JavaString = Java.use("java.lang.String");
var text = JavaString.$new(bytes, "UTF-8").toString();
if (truncated) text = text.substring(0, maxLen);
return'"' + text + '"' + suffix + " | (UTF8)";
        } catch(e) {
// UTF-8 解码失败,fallback 到十六进制
        }
    }

// 二进制数据,显示十六进制
var hex = bytesToHex(bytes).substring(0, maxLen * 2);
return hex + suffix + " | (HEX)";
}

这个函数在第14篇(算法自吐脚本)中会被大量使用——它让加密监控的输出一目了然:文本明文直接显示文本,二进制密文显示十六进制。

7.4 Java 集合类型的遍历

除了 byte[],你还经常遇到 Java 的集合类型。以下是常见集合的遍历方式:

// collection_traversal.js
// 遍历 Java 集合类型

Java.perform(function() {

// ====== 遍历 List(ArrayList / LinkedList 等)======
functionprintList(list{
if (list === null) { console.log("null"); return; }
console.log("List (size=" + list.size() + "):");
for (var i = 0; i < list.size(); i++) {
console.log("  [" + i + "] " + list.get(i));
        }
    }

// ====== 遍历 Map(HashMap / TreeMap / LinkedHashMap 等)======
functionprintMap(map{
if (map === null) { console.log("null"); return; }
console.log("Map (size=" + map.size() + "):");

// 通过 entrySet 遍历
varSet = Java.use("java.util.Set");
var Iterator = Java.use("java.util.Iterator");
var MapEntry = Java.use("java.util.Map$Entry");

var entrySet = map.entrySet();
var iterator = entrySet.iterator();
while (iterator.hasNext()) {
var entry = Java.cast(iterator.next(), MapEntry);
console.log("  " + entry.getKey() + " => " + entry.getValue());
        }
    }

// ====== 遍历 Set(HashSet / TreeSet 等)======
functionprintSet(set{
if (set === null) { console.log("null"); return; }
console.log("Set (size=" + set.size() + "):");

        var iterator = set.iterator();
        while (iterator.hasNext()) {
console.log("  " + iterator.next());
        }
    }

// ====== 遍历 Bundle(Android 特有,Activity 间传递数据)======
functionprintBundle(bundle{
if (bundle === null) { console.log("null"); return; }
console.log("Bundle:");

var keys = bundle.keySet();
var iterator = keys.iterator();
while (iterator.hasNext()) {
var key = iterator.next().toString();
var value = bundle.get(key);
console.log("  " + key + " => " + (value !== null ? value.toString() : "null"));
        }
    }

// ====== JSON 对象/数组遍历 ======
functionprintJSONObject(jsonObj{
if (jsonObj === null) { console.log("null"); return; }
// 最简单的方式:直接 toString(2) 格式化输出
console.log(jsonObj.toString(2));
    }

functionprintJSONArray(jsonArr{
if (jsonArr === null) { console.log("null"); return; }
console.log("JSONArray (length=" + jsonArr.length() + "):");
for (var i = 0; i < jsonArr.length(); i++) {
console.log("  [" + i + "] " + jsonArr.get(i));
        }
    }
});

性能提醒:遍历大型集合(如上千个元素的 List 或 Map)时要注意,每次 .get(i) 或 .next() 都涉及一次 JNI 调用,开销不小。在 Hook 高频方法(如网络拦截器每次请求都触发)时,建议只在确实需要时才遍历,或者加一个条件过滤。


八、堆栈打印:定位「谁调用了这个方法」

8.1 为什么堆栈如此重要

当你 Hook 到一个加密方法被调用了,看到了密钥和明文——接下来最重要的问题是:谁调用了它? 是登录模块?支付模块?还是设备指纹采集?

堆栈(Stack Trace)给你答案。它展示了从当前方法一路向上的完整调用链,比如:

at com.example.crypto.AesUtils.encrypt(AesUtils.java:42)
at com.example.api.AuthService.login(AuthService.java:128)
at com.example.ui.LoginActivity.onLoginClick(LoginActivity.java:56)

一看就知道是「登录页面点击登录按钮 → 调用认证服务 → 执行 AES 加密」。

8.2 方式一:Exception + Log(最常用)

这是 Frida Java 层堆栈打印最标准、最可靠的方式:

// 获取 Java 层调用堆栈
functiongetJavaStack() {
var Exception = Java.use("java.lang.Exception");
var Log = Java.use("android.util.Log");

// 创建一个 Exception 对象(不需要 throw),它在创建时就捕获了当前堆栈
var exception = Exception.$new();
// getStackTraceString 将堆栈格式化为可读字符串
var stackTrace = Log.getStackTraceString(exception);

return stackTrace;
}

原理:在 Java 中,new Exception() 的构造函数会调用 fillInStackTrace(),记录当前线程的完整调用栈。Log.getStackTraceString() 将这个堆栈信息格式化为人类可读的字符串。

为什么不直接用 exception.getStackTrace() 你当然可以——它返回一个 StackTraceElement[] 数组,每个元素包含类名、方法名、文件名、行号。但 Log.getStackTraceString 会帮你格式化成整齐的多行字符串,在控制台输出中更容易阅读。

8.3 方式二:Thread.currentThread().getStackTrace()

// 通过当前线程获取堆栈
functiongetJavaStackViaThread() {
var Thread = Java.use("java.lang.Thread");
var stackElements = Thread.currentThread().getStackTrace();

var lines = [];
for (var i = 0; i < stackElements.length; i++) {
var elem = stackElements[i];
        lines.push("  at " + elem.getClassName() + "." +
                    elem.getMethodName() + "(" +
                    elem.getFileName() + ":" +
                    elem.getLineNumber() + ")");
    }
return lines.join("\n");
}

这种方式的优势是可以编程式地访问堆栈的每一层(类名、方法名、行号),方便做过滤和条件判断。

8.4 方式三:Throwable(最精简)

functiongetJavaStackShort() {
var Throwable = Java.use("java.lang.Throwable");
return Throwable.$new().getStackTrace()
        .map(function(elem{
return elem.toString();
        })
        .join("\n");
}

8.5 过滤堆栈:只看有用的部分

原始堆栈通常很长,包含大量框架代码(dalvik.systemjava.lang.reflectcom.android.internal 等)。在实际使用中,你通常只关心 App 自己的代码。

// filtered_stack.js
// 过滤堆栈,只保留 App 业务代码

functiongetAppStack(maxLines, packageFilter{
    maxLines = maxLines || 8;
    packageFilter = packageFilter || null;  // 如 "com.example.app"

try {
var Exception = Java.use("java.lang.Exception");
var Log = Java.use("android.util.Log");
var stack = Log.getStackTraceString(Exception.$new());

var lines = stack.split("\n");
var filtered = [];

for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line.startsWith("at ")) continue;

// 跳过 Frida 自身的调用帧
if (line.indexOf("frida") !== -1continue;

// 跳过 Java 标准库和 Android 框架
if (line.indexOf("java.lang.reflect") !== -1continue;
if (line.indexOf("dalvik.system") !== -1continue;
if (line.indexOf("com.android.internal") !== -1continue;

// 如果指定了包名过滤,只保留匹配的
if (packageFilter && line.indexOf(packageFilter) === -1continue;

            filtered.push("  " + line);
if (filtered.length >= maxLines) break;
        }

return filtered.join("\n");
    } catch(e) {
return"(堆栈获取失败: " + e.message + ")";
    }
}

// 使用示例
// 打印最多 5 层,只看 com.example 包下的调用
// console.log(getAppStack(5, "com.example"));

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

// stack_best_practice.js
// 在加密 Hook 中使用堆栈追踪

Java.perform(function() {
var Cipher = Java.use("javax.crypto.Cipher");

    Cipher.doFinal.overload("[B").implementation = function(input{
var result = this.doFinal(input);

console.log("\n========== [Cipher.doFinal] ==========");
console.log("算法: " + this.getAlgorithm());
console.log("输入: " + smartPrintBytes(input, 64));
console.log("输出: " + smartPrintBytes(result, 64));
console.log("调用链:");
console.log(getAppStack(5));
console.log("=======================================\n");

return result;
    };
});

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


九、Java.choose 高级用法

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

第04篇介绍了 Java.choose 的基础用法。这里补充它的底层原理和高级技巧。

Java.choose 的底层实现依赖于 ART VM 的堆遍历接口。在 Android 的 ART 运行时中,所有 Java 对象都分配在受管理的堆(Managed Heap)上。ART 提供了遍历堆上所有存活对象的能力(类似于 GC 标记阶段的遍历)。Java.choose 利用这个能力,遍历堆上的每一个对象,检查它是否是指定类型的实例(包括子类实例),如果是就通过 onMatch 回调交给你。

这意味着 Java.choose 的性能开销与堆上的总对象数量成正比,而不是与目标类型的实例数量成正比。如果堆上有 100 万个对象,即使你要找的类型只有 1 个实例,它也要遍历全部 100 万个对象。

Java.choose 堆遍历原理

图里那个"扫描线穿过整个堆"的视觉对应的就是 ART 的堆遍历——每个对象都做一次 instanceof 判断,没有按类型建立索引可以走捷径。这也直接解释了下面 9.2 的两条优化策略:既然遍历不可省,那就尽早终止 + 不要重复触发

9.2 性能优化策略

策略一:尽早终止遍历。 如果你知道目标类型是单例(堆上只有一个实例),找到后立刻终止枚举即可。Java.choose 的 onMatch 回调返回字符串 'stop' 会让 Frida 停止后续遍历——这是官方支持的早终止机制:

Java.choose("com.example.app.AppConfig", {
onMatchfunction(instance{
console.log("[AppConfig] server: " + instance.getServerUrl());
console.log("[AppConfig] debug: " + instance.isDebugMode());
return'stop';  // 找到第一个就停止遍历
    },
onCompletefunction() {}
});

同样的早终止约定也适用于 Java.enumerateLoadedClasses 和 Java.enumerateClassLoaders 的 onMatch 回调。

策略二:避免在 Hook 回调中使用 Java.choose。 Hook 回调可能被频繁触发(每次方法调用都会触发),如果每次都执行一次堆遍历,性能影响会很大。更好的做法是在初始化时用 Java.choose 找到对象并保存引用,后续直接使用保存的引用。

// 不好的做法:每次 Hook 触发都做堆遍历
SomeClass.someMethod.implementation = function() {
    Java.choose("com.example.TokenManager", {  // 每次调用都遍历堆,性能差
onMatchfunction(tm/* ... */ },
onCompletefunction() {}
    });
returnthis.someMethod();
};

// 好的做法:初始化时找到引用,后续直接使用
var tokenManager = null;
Java.choose("com.example.TokenManager", {
onMatchfunction(tm{
        tokenManager = Java.retain(tm);  // 保存引用
    },
onCompletefunction() {}
});

SomeClass.someMethod.implementation = function() {
if (tokenManager) {
console.log("token: " + tokenManager.getToken());  // 直接使用
    }
returnthis.someMethod();
};

9.3 搜索子类实例

Java.choose 会匹配指定类型及其所有子类的实例。这在搜索接口实现时很有用:

// 搜索所有 Activity 实例(包括 MainActivity、LoginActivity 等所有子类)
Java.choose("android.app.Activity", {
onMatchfunction(activity{
console.log("活跃 Activity: " + activity.$className);
// 输出可能包括:
//   com.example.app.ui.MainActivity
//   com.example.app.ui.LoginActivity
//   com.example.app.ui.SettingsActivity
    },
onCompletefunction() {}
});

9.4 结合 cast 和 choose

搜索到的对象可能是某个子类的实例,但 onMatch 中的 Wrapper 类型是你传给 Java.choose 的那个类型。如果你需要调用子类特有的方法,需要先 cast:

Java.choose("android.app.Activity", {
onMatchfunction(activity{
if (activity.$className === "com.example.app.ui.LoginActivity") {
// activity 的 Wrapper 是 Activity 类型
// cast 为 LoginActivity 才能调用 getUsername() 等子类方法
var LoginActivity = Java.use("com.example.app.ui.LoginActivity");
var loginAct = Java.cast(activity, LoginActivity);

console.log("[LoginActivity]");
console.log("  username field: " + loginAct.mUsername.value);
console.log("  password field: " + loginAct.mPassword.value);
        }
    },
onCompletefunction() {}
});

十、Java.enumerateLoadedClasses:在代码海洋中搜索

10.1 枚举所有已加载的类

// enumerate_classes.js
// 枚举所有已加载的类,搜索包含特定关键词的类名

Java.perform(function() {
var keyword = "encrypt";  // 搜索关键词(不区分大小写)
var matches = [];

    Java.enumerateLoadedClasses({
onMatchfunction(className{
if (className.toLowerCase().indexOf(keyword.toLowerCase()) !== -1) {
                matches.push(className);
            }
        },
onCompletefunction() {
console.log("[*] 包含 '" + keyword + "' 的类 (" + matches.length + " 个):\n");
            matches.sort();
for (var i = 0; i < matches.length; i++) {
console.log("  " + matches[i]);
            }
        }
    });
});

同步版本更简洁(但会阻塞直到枚举完成):

Java.perform(function() {
var classes = Java.enumerateLoadedClassesSync();
console.log("[*] 共加载了 " + classes.length + " 个类");

// 搜索加密相关的类
var cryptoClasses = classes.filter(function(name{
return name.toLowerCase().indexOf("crypto") !== -1 ||
               name.toLowerCase().indexOf("cipher") !== -1 ||
               name.toLowerCase().indexOf("encrypt") !== -1;
    });

console.log("[*] 加密相关类 (" + cryptoClasses.length + " 个):");
    cryptoClasses.forEach(function(name{
console.log("  " + name);
    });
});

10.2 搜索技巧

在实际逆向中,搜索类的常见目的和对应的关键词:

目的
推荐关键词
找加密相关
encrypt
decryptciphercryptoaesrsasignhmac
找网络相关
http
okhttpretrofitrequestresponseurlapi
找登录相关
login
authtokensessionpasswordcredential
找数据库相关
database
sqliteroomdaocursor
找 Root 检测
root
detectsafetyintegritycheck
找 WebView 相关
webview
bridgejavascriptjsbridge

混淆代码的搜索策略:如果 App 经过了 ProGuard/R8 混淆,类名可能变成了 a.b.ca.b.d 这样无意义的名字。此时不能靠类名搜索,需要换一种思路:先用 jadx 的交叉引用功能从已知的 Android Framework 类(如 javax.crypto.Cipher——这个类名不会被混淆)出发,顺着调用链找到 App 的业务类。


十一、Java.deoptimizeEverything:对付 Hook 失效的杀手锏

11.1 ART 的 JIT 编译与 Hook 冲突

Android ART 运行时使用 JIT(Just-In-Time)编译器将热点 Java 方法编译为 Native 机器码。被 JIT 编译后的方法直接以 Native 代码执行,绕过了解释器。

Frida 的 Java 方法 Hook 是通过修改方法的 ART 内部数据结构(ArtMethod 的入口点)来实现的。但如果一个方法已经被 JIT 编译为 Native 代码,调用时可能走的是 JIT 编译后的路径,绕过了 Frida 修改的入口点——导致 Hook 设置成功了,但不触发回调

ART JIT 编译与 Frida Hook 入口点冲突

图里那个"两个 entry_point 字段"是关键:ArtMethod 同时持有 entry_point_from_interpreter(解释器入口)和 entry_point_from_quick_compiled_code(JIT/AOT 入口)。Frida 早期版本只改前者,方法被 JIT 后 ART 会走后者——Hook 就静默失效。Frida 16.x 已经改为同时覆盖两个入口点,所以现代版本遇到此问题的概率比 12-14 时代低很多;但在 Hook 框架类(boot image)或第三方加固自定义编译流程时仍可能命中。

11.2 使用 deoptimize

Java.perform(function() {
// 强制关闭所有 JIT 编译,回退到解释执行
// 这样 Frida 的 Hook 一定会被触发
    Java.deoptimizeEverything();

console.log("[*] JIT 已关闭,所有方法回退到解释执行");

// 现在设置的 Hook 一定生效
var TargetClass = Java.use("com.example.app.TargetClass");
    TargetClass.criticalMethod.implementation = function() {
console.log("[*] criticalMethod 被调用");
returnthis.criticalMethod();
    };
});

性能代价deoptimizeEverything 会显著降低 App 的运行速度(所有 Java 代码都以解释器模式运行,性能通常下降 5-10 倍)。在生产环境的 App 中,你可能会注意到明显的卡顿。但对于逆向分析来说,正确性远比性能重要——如果 Hook 不触发,分析就无法进行。建议的做法是:先不加 deoptimizeEverything,如果发现 Hook 不触发,再加上它。

**Java.deoptimizeBootImage()**:比 deoptimizeEverything 更精准。它只对 boot image 中的方法(Android Framework 的核心类,如 ActivityCipherPackageManager 等)关闭 JIT,对 App 自身的代码不影响。当你的 Hook 目标是 Framework 类时,用这个方法对性能影响更小。


十二、实战:通用类方法枚举器

把本篇学到的 API 综合起来,写一个实用工具:输入一个类名,自动列出它的所有方法签名、字段信息,并支持一键 Hook 所有方法。

// class_inspector.js
// 通用类方法枚举器 - 输入类名,自动列出所有方法和字段
// 用法: frida -U -f com.example.app -l class_inspector.js --no-pause

// ====== 配置 ======
var TARGET_CLASS = "com.example.app.crypto.EncryptUtils";  // 修改为你要分析的类名
var HOOK_ALL = false;  // 设为 true 则自动 Hook 所有方法并打印参数
var MAX_ARG_LENGTH = 200;  // 参数打印的最大长度

// ====== 辅助函数 ======
functionbytesToHex(bytes{
if (bytes === null || bytes === undefinedreturn"null";
var hex = [];
for (var i = 0; i < bytes.length; i++) {
var b = (bytes[i] & 0xff).toString(16);
        hex.push(b.length === 1 ? "0" + b : b);
    }
return hex.join("");
}

functionformatArgValue(arg{
if (arg === null || arg === undefinedreturn"null";
try {
// 尝试检测 byte[] 类型
if (arg.length !== undefined && typeof arg.length === "number" && arg.$className === undefined) {
// 可能是基本类型数组
if (arg.length > 0 && arg.length < 1024) {
try {
var hex = bytesToHex(arg);
if (hex.length > MAX_ARG_LENGTH) hex = hex.substring(0, MAX_ARG_LENGTH) + "...";
return"[byte[" + arg.length + "]] " + hex;
                } catch(e) {}
            }
return"[array, length=" + arg.length + "]";
        }

var str = arg.toString();
if (str.length > MAX_ARG_LENGTH) {
            str = str.substring(0, MAX_ARG_LENGTH) + "...(" + str.length + " chars)";
        }
return str;
    } catch(e) {
return"(无法转为字符串: " + e.message + ")";
    }
}

functiongetAppStack(maxLines{
try {
var Exception = Java.use("java.lang.Exception");
var Log = Java.use("android.util.Log");
var stack = Log.getStackTraceString(Exception.$new());
return stack.split("\n")
            .filter(function(l{
var t = l.trim();
return t.startsWith("at ") &&
                       t.indexOf("frida") === -1 &&
                       t.indexOf("java.lang.reflect") === -1;
            })
            .slice(0, maxLines || 5)
            .map(function(lreturn"    " + l.trim(); })
            .join("\n");
    } catch(e) { return""; }
}

// ====== ClassLoader 搜索 + 枚举 ======
functioninspectClass(className{
// 尝试获取类引用(支持自动搜索 ClassLoader)
var klass = null;
try {
        klass = Java.use(className);
    } catch(e) {
console.log("[!] 默认 ClassLoader 找不到 " + className + ",搜索其他 ClassLoader...");
        Java.enumerateClassLoaders({
onMatchfunction(loader{
if (klass) return;
try {
                    loader.loadClass(className);
                    Java.classFactory.loader = loader;
                    klass = Java.use(className);
console.log("[*] 通过 " + loader.$className + " 找到");
                } catch(e) {}
            },
onCompletefunction() {}
        });
    }

if (!klass) {
console.log("[!] 无法找到类: " + className);
return;
    }

// ====== 获取类信息 ======
var javaClass = klass.class;

console.log("\n╔══════════════════════════════════════════════════════════════╗");
console.log("║  Class Inspector                                            ║");
console.log("╚══════════════════════════════════════════════════════════════╝");
console.log("\n[类名] " + className);

// 父类
try {
var superClass = javaClass.getSuperclass();
if (superClass) {
console.log("[父类] " + superClass.getName());
        }
    } catch(e) {}

// 实现的接口
try {
var interfaces = javaClass.getInterfaces();
if (interfaces.length > 0) {
console.log("[接口]");
for (var i = 0; i < interfaces.length; i++) {
console.log("  - " + interfaces[i].getName());
            }
        }
    } catch(e) {}

// ====== 枚举字段 ======
try {
var fields = javaClass.getDeclaredFields();
if (fields.length > 0) {
console.log("\n[字段] (" + fields.length + " 个)");
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
var modifiers = Java.use("java.lang.reflect.Modifier")
                    .toString(field.getModifiers());
console.log("  " + modifiers + " " +
                           field.getType().getName() + " " +
                           field.getName());
            }
        }
    } catch(e) {}

// ====== 枚举方法 ======
try {
var methods = javaClass.getDeclaredMethods();
console.log("\n[方法] (" + methods.length + " 个)");

for (var i = 0; i < methods.length; i++) {
var method = methods[i];
var modifiers = Java.use("java.lang.reflect.Modifier")
                .toString(method.getModifiers());
var returnType = method.getReturnType().getName();
var methodName = method.getName();

// 获取参数类型列表
var paramTypes = method.getParameterTypes();
var paramList = [];
for (var j = 0; j < paramTypes.length; j++) {
                paramList.push(paramTypes[j].getName());
            }

console.log("  " + modifiers + " " + returnType + " " +
                        methodName + "(" + paramList.join(", ") + ")");

// 如果 HOOK_ALL 开启,自动 Hook 这个方法
if (HOOK_ALL) {
                hookMethod(klass, methodName, paramList);
            }
        }
    } catch(e) {
console.log("[!] 枚举方法失败: " + e.message);
    }

// ====== 枚举构造函数 ======
try {
var constructors = javaClass.getDeclaredConstructors();
if (constructors.length > 0) {
console.log("\n[构造函数] (" + constructors.length + " 个)");
for (var i = 0; i < constructors.length; i++) {
var ctor = constructors[i];
var paramTypes = ctor.getParameterTypes();
var paramList = [];
for (var j = 0; j < paramTypes.length; j++) {
                    paramList.push(paramTypes[j].getName());
                }
console.log("  " + className + "(" + paramList.join(", ") + ")");
            }
        }
    } catch(e) {}

console.log("\n[完成] 类检查结束\n");
}

// JVM 内部签名 → Frida overload 期望的可读类型名
//   "[B"                         → "[B"             (基本类型数组保持 JNI 形式)
//   "[I"                         → "[I"
//   "[Ljava.lang.String;"        → "[Ljava.lang.String;"
//   "java.lang.String"           → "java.lang.String"
// 其实 Frida overload 同时接受 "[B" 和 "byte[]"、"java.lang.String[]"
// 两种写法;但 Java 反射 getName() 在二维数组、嵌套数组上返回的是 JNI 形式,
// 这里统一规范化为 Frida 更稳妥的写法。
functionjvmSigToFridaName(name{
if (!name) return name;
// 基本类型数组:[B / [I / [J / [F / [D / [S / [C / [Z
var primMap = { 'B''byte''I''int''J''long''F''float',
'D''double''S''short''C''char''Z''boolean' };
var depth = 0;
while (name.charAt(depth) === '[') depth++;
if (depth === 0return name;  // 不是数组
var elem = name.substring(depth);
var elemName;
if (elem.length === 1 && primMap[elem]) {
        elemName = primMap[elem];
    } elseif (elem.charAt(0) === 'L' && elem.charAt(elem.length - 1) === ';') {
        elemName = elem.substring(1, elem.length - 1);
    } else {
return name;  // 解析不出,原样返回
    }
var suffix = '';
for (var i = 0; i < depth; i++) suffix += '[]';
return elemName + suffix;
}

functionhookMethod(klass, methodName, paramTypes{
try {
var overloadArgs = paramTypes.map(jvmSigToFridaName);

var method = klass[methodName];
if (!method) return;

var overloaded = paramTypes.length > 0
            ? method.overload.apply(method, overloadArgs)
            : method.overload();

        overloaded.implementation = function() {
var args = [].slice.call(arguments);
var argStr = args.map(function(areturn formatArgValue(a); }).join(", ");

console.log("\n>>> " + methodName + "(" + argStr + ")");

var result = this[methodName].apply(this, args);

console.log("<<< " + methodName + " => " + formatArgValue(result));
console.log("  调用链:\n" + getAppStack(3));

return result;
        };
    } catch(e) {
// Hook 失败(可能是抽象方法、接口方法等),静默跳过
    }
}

// ====== 入口 ======
Java.perform(function() {
    inspectClass(TARGET_CLASS);
});

运行效果示例:

╔══════════════════════════════════════════════════════════════╗
║  Class Inspector                                            ║
╚══════════════════════════════════════════════════════════════╝

[类名] com.example.app.crypto.EncryptUtils
[父类] java.lang.Object
[接口]
  - (无)

[字段] (3 个)
  private static final java.lang.String SECRET_KEY
  private static final java.lang.String IV
  private static boolean initialized

[方法] (5 个)
  public static java.lang.String encrypt(java.lang.String)
  public static java.lang.String decrypt(java.lang.String)
  public static byte[] encryptBytes(byte[], byte[])
  private static void init()
  public static java.lang.String md5(java.lang.String)

[构造函数] (1 个)
  com.example.app.crypto.EncryptUtils()

[完成] 类检查结束

将 HOOK_ALL 设为 true,可以自动 Hook 所有方法并打印参数和返回值——这在快速分析一个未知类时非常高效。

Class Inspector 工作流程

十三、工具集与速查

本节做两件事:① 把第七、第八两节散落的工具函数汇总成一个完整可拷贝的工具集;② 给出 13 条核心 API 的单行速查,配合工具集即可覆盖日常 Frida Java 层逆向 80% 的场景。

13.1 工具集索引

函数
一句话说明
出处
bytesToHex(bytes)
byte[] → 十六进制字符串
7.2 节
bytesToUtf8(bytes)
byte[] → UTF-8 字符串
7.2 节
bytesToBase64(bytes)
byte[] → Base64(NO_WRAP)
7.2 节
hexToBytes(hex)
十六进制字符串 → Java byte[]
7.2 节
utf8ToBytes(s)
UTF-8 字符串 → Java byte[]
7.2 节
base64ToBytes(b64)
Base64 → Java byte[]
7.2 节
smartPrintBytes(bytes, maxLen)
自动判断文本/二进制后打印
7.3 节
getJavaStack()
完整 Java 调用栈字符串
8.2 节
getAppStack(maxLines, pkg)
过滤后的业务调用栈
8.5 节
printList(list)
 / printMap(map) / printSet(set) / printBundle(b)
集合遍历
7.4 节

13.2 完整代码(拷贝即用)

把下面这段直接粘到你 Frida 脚本的最前面,所有工具函数立即可用。后续加密专题、JNI 追踪、签名校验相关代码块都假定脚本里已包含它。

// ============================================================
// frida-java-utils  ·  Frida Java 层逆向通用工具集
// 用法:把整段拷到脚本最前面即可
// ============================================================

// ====== byte[] / 字符串相互转换 ======

functionbytesToHex(bytes{
if (bytes === null || bytes === undefinedreturn"null";
var hex = [];
for (var i = 0; i < bytes.length; i++) {
// & 0xff 将有符号 byte 转为无符号
var b = (bytes[i] & 0xff).toString(16);
        hex.push(b.length === 1 ? "0" + b : b);
    }
return hex.join("");
}

functionbytesToUtf8(bytes{
if (bytes === null || bytes === undefinedreturn"null";
return Java.use("java.lang.String").$new(bytes, "UTF-8");
}

functionbytesToBase64(bytes{
if (bytes === null || bytes === undefinedreturn"null";
// Base64.NO_WRAP = 2,不添加换行符
return Java.use("android.util.Base64").encodeToString(bytes, 2);
}

functionhexToBytes(hexStr{
var bytes = [];
for (var i = 0; i < hexStr.length; i += 2) {
        bytes.push(parseInt(hexStr.substr(i, 2), 16));
    }
return Java.array("byte", bytes);
}

functionutf8ToBytes(str{
return Java.use("java.lang.String").$new(str).getBytes("UTF-8");
}

functionbase64ToBytes(b64Str{
return Java.use("android.util.Base64").decode(b64Str, 0);
}

// ====== 智能打印 byte[]:自动判断文本/二进制 ======

functionsmartPrintBytes(bytes, maxLen{
if (bytes === null || bytes === undefinedreturn"null";
if (bytes.length === 0return"(empty)";
    maxLen = maxLen || 256;

var printableCount = 0;
var checkLen = Math.min(bytes.length, maxLen);

for (var i = 0; i < checkLen; i++) {
var b = bytes[i] & 0xff;
// 可打印 ASCII:0x20-0x7E;常见控制字符:\t \n \r
if ((b >= 0x20 && b <= 0x7e) || b === 0x09 || b === 0x0a || b === 0x0d) {
            printableCount++;
        }
    }

var ratio = printableCount / checkLen;
var truncated = bytes.length > maxLen;
var suffix = truncated ? "...(" + bytes.length + " bytes total)" : "";

if (ratio > 0.85) {
try {
var text = Java.use("java.lang.String").$new(bytes, "UTF-8").toString();
if (truncated) text = text.substring(0, maxLen);
return'"' + text + '"' + suffix + " | (UTF8)";
        } catch (e) { /* fallback to hex */ }
    }
var hex = bytesToHex(bytes).substring(0, maxLen * 2);
return hex + suffix + " | (HEX)";
}

// ====== Java 调用栈打印 ======

functiongetJavaStack() {
var Exception = Java.use("java.lang.Exception");
var Log = Java.use("android.util.Log");
return Log.getStackTraceString(Exception.$new());
}

// 过滤后的 App 业务调用栈:跳过反射/dalvik/internal/frida 帧
functiongetAppStack(maxLines, packageFilter{
    maxLines = maxLines || 8;
    packageFilter = packageFilter || null;

try {
var Exception = Java.use("java.lang.Exception");
var Log = Java.use("android.util.Log");
var stack = Log.getStackTraceString(Exception.$new());
var lines = stack.split("\n");
var filtered = [];

for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line.startsWith("at ")) continue;
if (line.indexOf("frida") !== -1continue;
if (line.indexOf("java.lang.reflect") !== -1continue;
if (line.indexOf("dalvik.system") !== -1continue;
if (line.indexOf("com.android.internal") !== -1continue;
if (packageFilter && line.indexOf(packageFilter) === -1continue;

            filtered.push("  " + line);
if (filtered.length >= maxLines) break;
        }
return filtered.join("\n");
    } catch (e) {
return"(堆栈获取失败: " + e.message + ")";
    }
}

// ====== Java 集合遍历 ======

functionprintList(list{
if (list === null) { console.log("null"); return; }
console.log("List (size=" + list.size() + "):");
for (var i = 0; i < list.size(); i++) {
console.log("  [" + i + "] " + list.get(i));
    }
}

functionprintMap(map{
if (map === null) { console.log("null"); return; }
console.log("Map (size=" + map.size() + "):");
var MapEntry = Java.use("java.util.Map$Entry");
var iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
var entry = Java.cast(iterator.next(), MapEntry);
console.log("  " + entry.getKey() + " => " + entry.getValue());
    }
}

functionprintSet(set{
if (set === null) { console.log("null"); return; }
console.log("Set (size=" + set.size() + "):");
    var iterator = set.iterator();
    while (iterator.hasNext()) {
console.log("  " + iterator.next());
    }
}

functionprintBundle(bundle{
if (bundle === null) { console.log("null"); return; }
console.log("Bundle:");
var iterator = bundle.keySet().iterator();
while (iterator.hasNext()) {
var key = iterator.next().toString();
var value = bundle.get(key);
console.log("  " + key + " => " + (value !== null ? value.toString() : "null"));
    }
}

13.3 核心 API 单行速查(13 条)

// 1. 环境检查
console.log(Java.available, Java.androidVersion);

// 2. 获取应用 Context
var ctx = Java.use("android.app.ActivityThread")
    .currentApplication().getApplicationContext();

// 3. 主动调用静态方法
Java.use("com.example.Utils").encrypt("test");

// 4. 搜索堆上实例(早终止:return 'stop')
Java.choose("com.example.TokenManager", {
onMatchfunction(instconsole.log(inst.getToken()); return'stop'; },
onCompletefunction() {}
});

// 5. 创建 Java 对象
Java.use("java.io.File").$new("/sdcard/test.txt");

// 6. 创建 Java 数组
Java.array("byte", [0x010x020x03]);

// 7. 类型转换
var jsonObj = Java.cast(obj, Java.use("org.json.JSONObject"));

// 8. 切换 ClassLoader(详见 3.3 节模板)
Java.classFactory.loader = targetLoader;

// 9. 搜索类名
Java.enumerateLoadedClassesSync()
    .filter(function(creturn c.indexOf("Encrypt") !== -1; });

// 10. 打印调用栈(完整版见 13.2 节的 getAppStack)
console.log(getAppStack(5"com.example"));

// 11. 主线程执行
Java.scheduleOnMainThread(function() /* UI 操作放这里 */ });

// 12. Hook 不触发时的最后手段
Java.deoptimizeBootImage();   // 只对 framework 类;优于 deoptimizeEverything
// Java.deoptimizeEverything();  // 全局降速 3-5×,万不得已才用

// 13. 保存 Hook 回调中的引用,防止 stale local reference
var saved = Java.retain(this);   // 详见 2.3 节

总结

**写脚本前先想清楚"被动等还是主动取"**。同一个 App 数据,被动 Hook 等触发可能要等半小时,主动调用 Java.choose + 实例方法两秒就拿到。新手写脚本经常默认走 Hook 路线,结果跑十分钟才发现没人触发——选错了路。

80% 的"类找不到"都是 ClassLoader 问题,不是 API 用错。看到 ClassNotFoundException 不要先怀疑包名,直接套 3.5 节的 hookWithClassLoader 模板搜一遍。加固/插件化 App 几乎必中,常规 App 走默认分支也无副作用。

Java.cast 是接口/抽象类参数的开锁工具。形参声明类型只代表"承诺什么",实际类型才是"是什么"——前者决定能调什么方法,后者决定能拿到什么数据。加密分析里几乎每次必用,记住 Key → SecretKeySpecAlgorithmParameterSpec → IvParameterSpec/GCMParameterSpec 这两组就够。

registerClass 是接口替换的唯一干净解法。当目标是个匿名内部类回调、且实现类名混淆到无法稳定定位时,与其反复定位,不如自己注册一个代理插进去。

工具函数集中维护,别在每篇脚本里重写。本篇把 bytesToHexsmartPrintBytesgetAppStack 等十余个高频函数汇成一个可拷贝的工具集,后续直接引用即可。


最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-05-13 09:25:04 HTTP/2.0 GET : https://67808.cn/a/487723.html
  2. 运行时间 : 0.125329s [ 吞吐率:7.98req/s ] 内存消耗:4,844.29kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=65bdd592245682449e46b5a8e635b2d3
  1. /yingpanguazai/ssd/ssd1/www/no.67808.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/no.67808.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/no.67808.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/no.67808.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/no.67808.cn/runtime/temp/6df755f970a38e704c5414acbc6e8bcd.php ( 12.06 KB )
  140. /yingpanguazai/ssd/ssd1/www/no.67808.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000482s ] mysql:host=127.0.0.1;port=3306;dbname=no_67808;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000620s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000259s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000284s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000560s ]
  6. SELECT * FROM `set` [ RunTime:0.013078s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000779s ]
  8. SELECT * FROM `article` WHERE `id` = 487723 LIMIT 1 [ RunTime:0.010054s ]
  9. UPDATE `article` SET `lasttime` = 1778635504 WHERE `id` = 487723 [ RunTime:0.008610s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 65 LIMIT 1 [ RunTime:0.003900s ]
  11. SELECT * FROM `article` WHERE `id` < 487723 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001373s ]
  12. SELECT * FROM `article` WHERE `id` > 487723 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.006103s ]
  13. SELECT * FROM `article` WHERE `id` < 487723 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.000947s ]
  14. SELECT * FROM `article` WHERE `id` < 487723 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.001701s ]
  15. SELECT * FROM `article` WHERE `id` < 487723 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.008425s ]
0.126971s