Frida入门到实战:hook so层与Java层的姿势总结

Frida 是一款跨平台的动态二进制插桩工具,凭借 JavaScript 脚本化、轻量灵活、全平台覆盖的特性,成为移动端逆向分析、安全测试领域的标配工具。无论是 Java 层的业务逻辑 Hook,还是 so 原生层的算法分析,Frida 都能提供统一的操作范式。本文将从基础环境搭建出发,系统总结 Java 层与 so 层的常用 Hook 姿势,结合实战案例梳理完整的分析链路,帮助读者快速掌握 Frida 的核心用法。

一、Frida 基础入门

1.1 什么是 Frida

Frida 本质是一个注入式调试框架,它向目标进程注入 frida-gum 引擎,允许开发者通过 JavaScript 脚本直接操作进程内存、Hook 函数、调用原生方法。它支持 Android、iOS、Linux、Windows、macOS 等主流平台,尤其在 Android 逆向场景中,Frida 可以无缝工作在 Java 虚拟机层与 Native 原生层,是分析 APP 逻辑、破解验证、逆向算法的核心工具。

1.2 环境搭建

  • 电脑端 :安装 Python 环境后,执行 pip install frida frida-tools 即可安装客户端工具与依赖库。
  • 手机端 :下载与手机架构(arm64/armv7)、电脑端版本完全一致的 frida-server,推送到手机 /data/local/tmp 目录,赋予执行权限并后台运行。
  • 环境验证 :手机通过 USB 连接电脑并开启调试,执行 frida-ps -U 可列出手机端进程,即为环境搭建成功。

1.3 核心概念与基础脚本结构

Frida 有两种核心运行模式,对应不同分析场景:

  • Attach 模式:附加到已经运行的进程,适合调试后台运行的应用、页面交互逻辑,无需重启 APP。
  • Spawn 模式:应用启动时直接注入,能 Hook 应用初始化、Application 生命周期、so 加载阶段的逻辑,适合分析启动流程。

基础脚本的通用结构如下:

复制代码
// Java 层操作必须包裹在 Java.perform 中,确保在 Java 线程执行
Java.perform(function () {
  // Java 层 Hook 逻辑写在这里
});

// Native 层操作可直接全局执行
Interceptor.attach(目标函数地址, {
  onEnter: function(args) {
    // 函数进入时触发,用于读取参数
  },
  onLeave: function(retval) {
    // 函数返回时触发,用于读取/修改返回值
  }
});

运行脚本的基础命令:

复制代码
# Attach 模式:附加到运行中的进程
frida -U 应用包名 -l script.js

# Spawn 模式:冷启动应用并注入脚本
frida -U -f 应用包名 -l script.js

二、Java 层 Hook 常用姿势

Java 层 Hook 是 Frida 最常用的场景,核心围绕 Java.use() 获取类引用,通过重写方法实现拦截。

2.1 基础方法 Hook

最常规的场景是拦截指定类的普通方法,读取参数、修改返回值或直接替换逻辑。

示例:Hook 登录校验方法,打印输入密码与返回结果

复制代码
Java.perform(function () {
  // 通过完整类名获取目标类引用
  var LoginManager = Java.use("com.example.app.LoginManager");

  // 重写方法实现
  LoginManager.verifyPassword.implementation = function (inputPwd) {
    // 打印入参
    console.log("[Java Hook] 输入密码:" + inputPwd);
    
    // 调用原方法,保留原有逻辑
    var result = this.verifyPassword(inputPwd);
    console.log("[Java Hook] 校验结果:" + result);
    
    // 也可直接返回 true 绕过校验
    // return true;
    return result;
  };
});

2.2 构造方法与重载方法

  • 构造方法 Hook :使用 $init 代表类的构造函数,用于监听对象创建过程。
  • 重载方法 Hook :同一个方法名存在多个参数版本时,必须用 overload() 精确指定参数类型。

示例:Hook 重载方法与构造函数

复制代码
var UserInfo = Java.use("com.example.app.UserInfo");

// Hook 带两个参数的构造方法
UserInfo.$init.overload("java.lang.String", "int").implementation = function (name, age) {
  console.log("[构造] 创建用户:" + name + ",年龄:" + age);
  this.$init(name, age); // 必须调用原构造,否则对象无法初始化
};

// Hook 重载的 getSign 方法(String + int 参数版本)
UserInfo.getSign.overload("java.lang.String", "int").implementation = function (data, type) {
  console.log("[重载] data=" + data + ",type=" + type);
  return this.getSign(data, type);
};

2.3 字段读写与内部类 Hook

  • 修改成员字段 :通过 .value 读写对象的成员变量,静态字段可直接通过类访问。
  • 内部类 Hook :Java 内部类在虚拟机中以 外部类$内部类 的形式命名,按此规则引用即可。

示例:修改字段与 Hook 匿名内部类

复制代码
var MainActivity = Java.use("com.example.app.MainActivity");

// Hook 按钮点击的匿名内部类
var ClickListener = Java.use("com.example.app.MainActivity$1");
ClickListener.onClick.implementation = function (v) {
  console.log("[内部类] 登录按钮被点击");
  this.onClick(v);
};

// 修改静态字段,强制开启调试模式
MainActivity.IS_DEBUG.value = true;
console.log("[字段] 调试模式状态:" + MainActivity.IS_DEBUG.value);

2.4 主动调用与批量 Hook

Frida 不仅能被动拦截,还可以主动调用类的静态 / 实例方法;批量 Hook 则适合快速定位目标函数。

示例:主动调用静态加密方法

复制代码
Java.perform(function () {
  var SignUtils = Java.use("com.example.app.utils.SignUtils");
  // 主动调用 md5 加密方法
  var sign = SignUtils.md5Encrypt("test123456");
  console.log("[主动调用] 加密结果:" + sign);
});

批量 Hook 目标类的所有方法,用于快速定位关键逻辑:

复制代码
var targetClass = Java.use("com.example.app.utils.SecurityUtils");
var methods = targetClass.class.getDeclaredMethods();

methods.forEach(function (method) {
  var methodName = method.getName();
  targetClass[methodName].implementation = function () {
    console.log("[批量Hook] 调用方法:" + methodName);
    return this[methodName].apply(this, arguments);
  };
});

三、Native(so 层)Hook 核心姿势

so 层 Hook 针对 Native 代码,核心依赖 Interceptor 拦截模块与 Module 内存定位模块。

3.1 Native 层 Hook 的两种思路

so 层 Hook 主要覆盖两类场景:

  1. Java Native 方法 :Java 代码中声明为 native 的方法,既可以从 Java 层 Hook,也可以从 so 层 Hook。
  2. 纯 Native 函数 :so 内部的普通函数(导出或未导出),比如系统 libc 的 strcmp、自定义 so 的内部算法函数,只能通过 Native 层 Hook。

3.2 导出函数 Hook

导出函数是 so 对外暴露的接口,可以直接通过 Module.findExportByName() 定位内存地址。

示例 1:Hook Java Native 方法(so 层视角)

复制代码
// 确保目标 so 已加载完成
Module.ensureInitialized("libsecurity.so");

// 按 JNI 命名规则查找导出函数
var nativeSign = Module.findExportByName("libsecurity.so", 
  "Java_com_example_app_utils_SignUtils_nativeSign");

Interceptor.attach(nativeSign, {
  onEnter: function (args) {
    // JNI 函数前两个参数固定为 JNIEnv* 和 jobject
    // args[2] 才对应 Java 层传入的第一个参数
    var input = Memory.readCString(args[2]);
    console.log("[so Hook] 签名输入:" + input);
  },
  onLeave: function (retval) {
    var result = Memory.readCString(retval);
    console.log("[so Hook] 签名结果:" + result);
  }
});

示例 2:Hook 系统 libc 的 strcmp 函数

复制代码
// 常用于绕过字符串比较校验
var strcmp = Module.findExportByName("libc.so", "strcmp");
Interceptor.attach(strcmp, {
  onEnter: function (args) {
    var str1 = Memory.readCString(args[0]);
    var str2 = Memory.readCString(args[1]);
    console.log("[strcmp] 比较:" + str1 + " vs " + str2);
  },
  onLeave: function (retval) {
    // 修改返回值为 0,强制判定字符串相等
    retval.replace(0);
  }
});

3.3 未导出函数 Hook(基址 + 偏移)

核心算法函数通常不会导出,需要先通过 IDA Pro 等静态分析工具得到函数在 so 文件中的偏移,再加上内存加载基址,计算出运行时地址。

示例:通过基址 + 偏移 Hook 未导出算法函数

复制代码
// 获取 so 在内存中的加载基址
var baseAddr = Module.findBaseAddress("libnative.so");
if (!baseAddr) {
  console.log("[-] libnative.so 尚未加载");
  return;
}

// IDA 中分析得到的函数文件偏移(示例值)
var funcOffset = 0x3F2C;
// 计算运行时的函数地址
var targetFunc = baseAddr.add(funcOffset);

Interceptor.attach(targetFunc, {
  onEnter: function (args) {
    console.log("[未导出函数] 进入目标函数,参数1:" + args[0]);
    this.arg1 = args[0]; // 保存上下文,onLeave 中使用
  },
  onLeave: function (retval) {
    console.log("[未导出函数] 返回值:" + retval.toInt32());
  }
});

3.4 内存操作与参数解析

Native 层参数多为指针类型,需要通过 Memory 模块读写内存内容,常用 API 包括:

  • Memory.readCString(addr):读取 C 风格字符串
  • Memory.readInt(addr) / Memory.readLong(addr):读取整型数值
  • Memory.writeInt(addr, value):向指定内存写入数值
  • hexdump(addr, {length: len}):以十六进制格式打印内存

示例:解析结构体类型参数

复制代码
Interceptor.attach(targetFunc, {
  onEnter: function (args) {
    // args[1] 为结构体指针,偏移 0x8 处是 int 类型的长度字段
    var dataPtr = Memory.readPointer(args[1]);
    var dataLen = Memory.readInt(args[1].add(8));
    var data = Memory.readByteArray(dataPtr, dataLen);
    
    console.log("[结构体] 数据长度:" + dataLen);
    console.log(hexdump(data, { length: dataLen }));
  }
});

四、实战案例:从 Java 到 so 的全链路 Hook

4.1 案例一:Java 层登录明文抓取

场景:某 APP 登录密码在 Java 层做了加密后传输,需要获取用户输入的原始明文密码。

分析思路 :反编译 APK 定位登录按钮点击事件,追踪到密码处理类 LoginHelper 的 encryptPassword 方法,Hook 该方法的入参即可得到明文。

完整脚本:

复制代码
Java.perform(function () {
  var LoginHelper = Java.use("com.example.app.login.LoginHelper");
  
  LoginHelper.encryptPassword.overload("java.lang.String").implementation = function (pwd) {
    console.log("========== 登录明文抓取 ==========");
    console.log("原始明文密码:" + pwd);
    var encrypted = this.encryptPassword(pwd);
    console.log("加密后密码:" + encrypted);
    console.log("==================================");
    return encrypted;
  };
});

执行命令 frida -U -f com.example.app -l login_hook.js,启动 APP 输入密码后,控制台即可输出原始明文。

4.2 案例二:so 层加密函数 Hook 与参数还原

场景 :某 APP 的核心签名算法放在 so 层,Java 层调用 nativeGetSign(String data) 返回签名字符串,需要深入分析算法的输入输出。

分析思路:

  1. 先通过 Java 层 Hook 确认 native 方法的调用时机与入参;
  2. 用 IDA 分析 libsign.so,从导出的 JNI 函数向下追踪,找到内部核心加密函数(未导出,偏移 0x256C);
  3. Hook 核心函数,打印输入缓冲区与输出结果。

完整脚本:

复制代码
// 1. Java 层:确认 native 方法调用时机
Java.perform(function () {
  var SignUtils = Java.use("com.example.app.utils.SignUtils");
  SignUtils.nativeGetSign.implementation = function (data) {
    console.log("[Java] 调用 nativeGetSign,入参:" + data);
    var result = this.nativeGetSign(data);
    console.log("[Java] 返回结果:" + result);
    return result;
  };
});

// 2. so 层:监听 so 加载并 Hook 核心函数
setImmediate(function () {
  var base = Module.findBaseAddress("libsign.so");
  if (!base) {
    // so 未加载时,Hook dlopen 监听加载事件
    var dlopen = Module.findExportByName(null, "dlopen");
    Interceptor.attach(dlopen, {
      onEnter: function (args) {
        this.soPath = Memory.readCString(args[0]);
      },
      onLeave: function () {
        if (this.soPath && this.soPath.indexOf("libsign.so") !== -1) {
          console.log("[so] libsign.so 加载完成,开始Hook核心函数");
          hookCoreFunc();
        }
      }
    });
  } else {
    hookCoreFunc();
  }
});

function hookCoreFunc() {
  var baseAddr = Module.findBaseAddress("libsign.so");
  var coreFunc = baseAddr.add(0x256C); // IDA 分析得到的函数偏移
  
  Interceptor.attach(coreFunc, {
    onEnter: function (args) {
      this.inputBuf = args[0];
      this.inputLen = args[1].toInt32();
      console.log("[so核心] 输入数据长度:" + this.inputLen);
      console.log(hexdump(this.inputBuf, { length: this.inputLen }));
    },
    onLeave: function (retval) {
      console.log("[so核心] 输出签名:" + Memory.readCString(retval));
    }
  });
}

五、进阶技巧与避坑指南

5.1 Spawn 与 Attach 模式选型

  • Spawn 模式:适合分析应用启动逻辑、Application 初始化、so 加载阶段的校验,缺点是会重启应用。
  • Attach 模式:适合分析页面交互、后台任务,无需重启 APP;缺点是无法拦截启动阶段已经执行过的代码。

5.2 常见时序问题

  • 多 ClassLoader 问题 :插件化、热修复 APP 存在多个类加载器,直接 Java.use 可能找不到类。需要遍历 ClassLoader 定位目标类所在的加载器。
  • so 加载时机问题 :so 是动态加载的,脚本执行时 so 可能尚未加载。标准解决方案是 Hook dlopen / android_dlopen_ext 监听加载事件,加载完成后再执行 Hook。

5.3 反检测与对抗技巧

很多 APP 会检测 Frida 特征,常用对抗手段:

  1. 基础隐藏:重命名 frida-server 二进制文件,修改默认监听端口,使用非默认端口连接。
  2. 特征屏蔽 :Hook strcmp、strstr 等函数,拦截对 "frida"、"gum-js" 等特征字符串的检测。
  3. 注入方式对抗:使用 frida-gadget 代替 frida-server,通过重打包 APK 注入 so,绕过 ptrace 检测。

5.4 高频坑点汇总

  1. JNI 参数偏移:Native 函数前两个参数是 JNIEnv* 和 jobject,Java 层第一个参数对应 Native 层的 args 2。
  2. 位数差异:64 位系统指针长度为 8 字节,32 位为 4 字节,计算结构体偏移时必须区分。
  3. Java 对象打印 :直接打印 Java 对象会显示 [object Object],需要调用 toString() 或手动拼接字段。
  4. 线程安全 :Java.perform 必须在有 Java 环境的线程执行,Native 线程中调用需使用 Java.scheduleOnMainThread。

六、总结

Frida 的核心价值在于打通了 Java 层与 Native 层的分析壁垒:Java 层 Hook 适合快速定位业务逻辑、锁定关键函数入口;so 层 Hook 则深入底层,用于还原加密算法、绕过 native 校验。

实际逆向分析中,通常遵循 "先 Java 后 so,先导出后未导出" 的思路:从上层业务逻辑逐步追踪到底层算法,结合静态分析与动态调试,形成完整的分析链路。掌握本文的基础姿势与实战技巧,足以应对绝大多数 APP 的逆向分析场景。在此基础上,还可以进一步学习 Frida RPC、Stalker 代码追踪、内存漫游等进阶功能,向更深度的逆向分析方向进阶。

相关推荐
小静AI工程实验室5 小时前
robots.txt 不是门禁:RFC 9309 规则、缓存与 Python 爬虫的 10 个边界
爬虫·python·缓存
27669582925 小时前
麦当劳APP authorization算法分析
app·frida·麦当劳·麦当劳app·app逆向实战·app数据采集·frida app逆向
远航计算机6 小时前
AI 爬虫分三种,你 robots.txt 里挡的是哪一种?
人工智能·爬虫·aigc
深蓝电商API1 天前
大厂验证码对抗升级:从“破解”到“养环境”的思路转变
爬虫·验证码
Getgit1 天前
亮数据 | Browser API 实战:Booking.com 动态页面抓取对比
爬虫·亮数据·broowser api
深蓝电商API2 天前
reCAPTCHA/hCaptcha/Turnstile三大国际验证码对比研究
爬虫·recaptcha·hcaptcha·turnstile
深蓝电商API2 天前
从零训练自己的验证码识别模型:数据生成到部署
爬虫·验证码
wuyk5553 天前
Python 网络爬虫入门到实战 第 04 章:请求头、UA 伪装、超时、异常处理、基础反爬绕过
开发语言·爬虫·python
weixin_440401693 天前
质朴的爬虫+数据处理
爬虫·python·数据分析·pandas