第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.4frida-java-bridge 7.0.3:新增 Android 16 支持
17.2.12运行时检测 ART class spec 偏移量,不再依赖 SDK 版本启发式推断
17.2.14Art::GetOsThreadStat 伪装支持,应对 Zygote 单线程检查
17.4.1静态 trampoline 修复;GC 后 ArtMethod 类字段同步
17.6.0轻量级 Zygote 注入:通过 setArgV0Native() 补丁替代完整 bridge 加载
17.6.1BTI(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.0frida-java-bridge 解耦为独立 ESM 模块;废弃回调风格枚举 API
17.1.4frida-java-bridge 7.0.3,新增 Android 16 支持
17.2.12ART class spec 偏移量运行时检测,告别 SDK 硬编码
17.2.14Zygote 单线程检查伪装
17.4.1ArtMethod 字段 GC 后同步修复
17.6.0Zymbiote 轻量 Zygote 注入(920 字节 arm64)
17.6.1arm64 BTI 兼容修复
17.8.0进程内 ART VM 加载,消除 dex helper 边界问题
17.9.10Android 自定义进程名匹配改进