云计算百科
云计算领域专业知识百科平台

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 主要覆盖两类场景:

  • Java Native 方法:Java 代码中声明为 native 的方法,既可以从 Java 层 Hook,也可以从 so 层 Hook。
  • 纯 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) 返回签名字符串,需要深入分析算法的输入输出。

    分析思路:

  • 先通过 Java 层 Hook 确认 native 方法的调用时机与入参;
  • 用 IDA 分析 libsign.so,从导出的 JNI 函数向下追踪,找到内部核心加密函数(未导出,偏移 0x256C);
  • 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 特征,常用对抗手段:

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

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

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

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

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Frida入门到实战:hook so层与Java层的姿势总结
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!