Frida 脚本入门:Hook Java 方法的 10 个常用模板

为什么需要「模板」

Frida 的 API 不算多,但每次写脚本都要重新想「怎么 hook 构造函数」「怎么改返回值」「怎么处理重载」,效率很低。

更麻烦的是,网上抄来的脚本经常跑不通——不是语法错,而是方法签名不对、类加载器找不到类、或者目标 App 用了 Kotlin 的伴生对象。

本文把最常见的 10 种 Hook 场景做成可直接改用的模板。所有模板都遵循同一个骨架,先记住这个:

Java.perform(function () {
  var Cls = Java.use("com.example.app.Target");
  Cls.method.implementation = function (arg1) {
    console.log("called:", arg1);
    var ret = this.method(arg1);   // 调原方法
    console.log("return:", ret);
    return ret;                    // 原样返回
  };
});

三件事:Java.perform 保证在 Java 运行时里执行、Java.use 拿到类、implementation 替换方法体。在替换体里一定要调用原方法(除非你就是要改行为),否则会破坏 App 逻辑。

环境准备(frida-server 部署、版本对齐)见《Frida 抓包实战》,本文不重复。

模板 1:打印参数与返回值(最常用)

Java.perform(function () {
  var Cls = Java.use("com.example.app.LoginActivity");
  Cls.checkPassword.implementation = function (user, pwd) {
    console.log("[+] checkPassword(", user, ",", pwd, ")");
    var ret = this.checkPassword(user, pwd);
    console.log("[+] return:", ret);
    return ret;
  };
});

用途:快速搞清楚一个方法在什么时候被调用、传了什么、回了什么。这是所有逆向工作的起点。

模板 2:Hook 重载方法(最容易报错)

同名方法有多个签名时,必须用 overload 指定:

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

  // 签名写法:基本类型用缩写,对象用全限定名
  Cls.request.overload("java.lang.String", "int").implementation = function (url, timeout) {
    console.log("[+] request:", url, timeout);
    return this.request(url, timeout);
  };
});

签名怎么查?两种办法:

// 在 Frida REPL 里直接打印
Java.use("com.example.app.Net").request.overloads.forEach(function (o) {
  console.log(o.argumentTypes.join(", "));
});

或用 jadx 打开 APK,右键方法「Copy as frida snippet」,直接得到签名。

常见错误:overload not found。八成是基本类型写错——int 要写 "int",boolean 要写 "boolean",数组要写 "[B" 这种 JVM 描述符。

模板 3:Hook 构造函数

构造函数在 Frida 里叫 $init:

Java.perform(function () {
  var Cls = Java.use("com.example.app.Session");
  Cls.$init.overload("java.lang.String").implementation = function (token) {
    console.log("[+] new Session token =", token);
    return this.$init(token);
  };
});

用途:抓对象创建时机,尤其是密钥、token、配置在构造时被赋值的场景。

模板 4:改返回值(绕过校验)

Java.perform(function () {
  var Cls = Java.use("com.example.app.RootCheck");
  Cls.isRooted.implementation = function () {
    console.log("[+] isRooted() -> forced false");
    return false;                 // 不调用原方法,直接返回
  };
});

注意这里是故意不调原方法。改返回值的场景包括:root 检测、签名校验、VIP 判断、试用期判断。

提示:很多 App 会多处校验(Java 层 + native 层 + 服务端),改一处往往不够。

模板 5:读写字段

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

  // 读静态字段
  console.log("[+] static field:", Cls.sToken.value);

  // 改实例字段:先找到实例
  Java.choose("com.example.app.User", {
    onMatch: function (obj) {
      console.log("[+] before:", obj.balance.value);
      obj.balance.value = 99999;
      console.log("[+] after:", obj.balance.value);
    },
    onComplete: function () { console.log("[+] done"); }
  });
});

模板 6:遍历所有实例(Java.choose)

Java.perform(function () {
  Java.choose("com.example.app.Order", {
    onMatch: function (instance) {
      console.log("[+] order id:", instance.getId());
    },
    onComplete: function () { console.log("[+] total scanned"); }
  });
});

警告:Java.choose 会扫描堆,对象多时非常慢甚至卡死。只在必要时用,能通过 Hook 拿到的就别扫堆。

模板 7:Hook 接口 / 抽象方法

Java.perform(function () {
  var Iface = Java.use("com.example.app.Callback");
  Iface.onResult.implementation = function (code, data) {
    console.log("[+] onResult:", code, data);
    return this.onResult(code, data);
  };
});

Frida 可以 Hook 接口声明的方法,实际拦截的是实现类的该方法。如果实现类是匿名内部类,类名带 $1,可以用 Java.enumerateLoadedClasses 找出来。

模板 8:Hook 静态方法

Java.perform(function () {
  var Cls = Java.use("com.example.app.Utils");
  Cls.md5.implementation = function (input) {
    console.log("[+] md5(", input, ")");
    var ret = this.md5(input);
    console.log("[+] ->", ret);
    return ret;
  };
});

静态方法的 this 是类包装对象,写法与实例方法一致,不需要特殊处理。

模板 9:Hook native 函数(.so)

Java.perform(function () {
  // 方式一:按导出符号找
  var addr = Module.findExportByName("libnative.so", "Java_com_example_Native_check");
  if (addr === null) { console.log("[-] symbol not found"); return; }

  Interceptor.attach(addr, {
    onEnter: function (args) {
      console.log("[+] arg0:", args[0]);
      console.log("[+] arg1:", args[1].toInt32());
    },
    onLeave: function (retval) {
      console.log("[+] ret:", retval.toInt32());
      retval.replace(ptr(1));      // 强制返回 1
    }
  });
});

要点:

  • JNI 函数的第一个参数永远是 JNIEnv*,第二个是 jobject/jclass,业务参数从 args[2] 开始。
  • 符号被 strip 掉时,findExportByName 会返回 null,只能按偏移或特征码定位。
  • retval.replace() 对指针/整数有效,对浮点要用 retval.replace(1.0) 的对应写法。

模板 10:打印调用栈(定位谁调了它)

Java.perform(function () {
  var Cls = Java.use("com.example.app.Crypto");
  Cls.encrypt.implementation = function (data) {
    console.log("[+] encrypt called from:\n" +
      Thread.backtrace(this.context, Backtracer.ACCURATE)
        .map(DebugSymbol.fromAddress).join("\n"));
    return this.encrypt(data);
  };
});

用途:搞清楚「这个加密函数是谁触发的」,比盲目 Hook 一百个方法高效得多。

补充:类找不到怎么办

java.lang.ClassNotFoundException 是最高频的报错。原因是类在 App 自己的 ClassLoader 里,Frida 默认用系统 ClassLoader 找不到。

Java.perform(function () {
  Java.enumerateClassLoaders({
    onMatch: function (loader) {
      try {
        var factory = Java.ClassFactory.get(loader);
        var Cls = factory.use("com.example.app.Hidden");
        Cls.secret.implementation = function () {
          console.log("[+] secret:", this.secret());
          return this.secret();
        };
        console.log("[+] hooked with loader: " + loader);
      } catch (e) { /* 换下一个 loader */ }
    },
    onComplete: function () { console.log("[+] scan done"); }
  });
});

十种模板速查

# 场景 关键写法
1 看参数返回值 Cls.m.implementation = function(...)
2 重载方法 .overload("java.lang.String","int")
3 构造函数 Cls.$init
4 改返回值 直接 return false,不调原方法
5 读写字段 obj.field.value
6 遍历实例 Java.choose
7 接口方法 Hook 接口名,拦截实现
8 静态方法 写法同实例方法
9 native 函数 Interceptor.attach
10 调用栈 Thread.backtrace

常见错误对照

报错 原因 处理
ClassNotFoundException 类在 App 的 ClassLoader 用模板里的 Java.enumerateClassLoaders
overload not found 签名写错 打印 overloads 或从 jadx 复制
TypeError: not a function 方法名写错,或不是 Java 方法 确认是 .method 而非字段
expected a pointer native 参数类型用错 args[1].toInt32() 或 ptr()
Hook 成功但 App 崩溃 原方法没调用,破坏了状态 检查是否漏了 this.method(...)
脚本没生效 注入时机太晚 用 frida -U -f 包名 -l script.js 从启动注入

小结

  1. 骨架就一个:Java.perform → Java.use → implementation。
  2. 先打印,再修改。搞清楚调用关系之前不要急着改返回值。
  3. 签名是最大障碍,用 jadx 复制比手写靠谱。
  4. 类找不到就换 ClassLoader,这是 Android 逆向的必修课。

延伸阅读:


免责声明:本文技术内容仅用于自有 App 的调试、授权范围内的安全测试与逆向学习。对他人 App 进行破解、篡改或数据窃取可能违反《网络安全法》与《著作权法》,后果自负。

© 版权声明
THE END
喜欢就支持一下吧
点赞6赞赏 分享
相关推荐
评论 共2条

请登录后发表评论

    暂无评论内容