7、Frida框架基础:Frida环境搭建、JavaScript与Python绑定、基本Hook操作
Frida,说实话,是我这些年做逆向用得最顺手的动态插桩工具。没有之一。
你想想看,以前我们要分析一个App的运行时行为,要么靠猜,要么靠改smali重打包。重打包那套流程,签名校验、反调试、资源混淆……光踩坑就能踩一天。而Frida不一样,它直接注入到进程里,动态修改逻辑,不用动原始APK。说白了,就是“运行时手术刀”。
这一章,我会带你从零搭好Frida环境,然后搞定JavaScript和Python的绑定,最后上手几个最常用的Hook操作。嗯,都是我在项目里反复用到的套路。
7.1 环境搭建:别小看这一步
Frida的环境分两部分:PC端(你的电脑)和手机端(目标设备)。两边都得装对版本,否则连不上。
7.1.1 PC端安装
我个人习惯用pip直接装,简单粗暴:
pip install frida-tools
这会同时装上frida核心库和frida命令行工具。装完后验证一下:
frida --version
# 比如输出 16.0.1
我曾经因为PC端装了16.x,手机端还是14.x,折腾了一下午才发现是版本不匹配。记住:PC端和手机端的frida版本号必须一致。
7.1.2 手机端安装
手机端需要运行一个frida-server。你得先确认CPU架构:
adb shell getprop ro.product.cpu.abi
# 常见输出:arm64-v8a / armeabi-v7a / x86
然后去GitHub Releases下载对应架构的frida-server,解压后push到手机:
adb push frida-server-16.0.1-android-arm64 /data/local/tmp/
adb shell chmod 755 /data/local/tmp/frida-server-16.0.1-android-arm64
启动它:
adb shell su -c /data/local/tmp/frida-server-16.0.1-android-arm64 &
手机必须root,否则frida-server没有权限注入其他进程。如果你用的是模拟器,x86架构的模拟器可以直接跑frida-server,但很多App在模拟器里会检测环境,建议用真机。
7.1.3 验证连通性
PC端执行:
frida-ps -U
如果能看到手机上的进程列表,恭喜你,环境通了。
7.2 JavaScript与Python绑定:双剑合璧
Frida的工作模式是这样的:Python负责控制逻辑(启动、附加、收数据),JavaScript负责注入逻辑(Hook、改参数、读内存)。
说白了,Python是调度员,JS是执行者。
7.2.1 基础绑定写法
一个最简单的Python脚本,附加到目标进程并执行JS代码:
import frida
import sys
# 附加到进程
session = frida.attach("com.example.app")
# 要注入的JS代码
js_code = """
console.log("Hello from Frida!");
"""
# 创建脚本并加载
script = session.create_script(js_code)
script.load()
# 保持进程运行
sys.stdin.read()
运行后,你会在手机端的log里看到输出。嗯,这里要注意:console.log在Frida里默认输出到frida-trace或frida命令行的stdout,不是Android的logcat。
7.2.2 进程附加的两种方式
| 方式 | 代码 | 适用场景 |
|---|---|---|
| 按进程名 | frida.attach("com.example.app") |
App已经在运行 |
| 按PID | frida.attach(12345) |
多个同名进程时 |
| 启动并附加 | frida.spawn(["com.example.app"]) |
App还没启动,需要从入口开始Hook |
我个人最常用的是spawn,因为很多App在启动阶段就有反调试,用attach会错过关键逻辑。
7.2.3 Python与JS的通信
有时候你需要从JS往Python回传数据,比如Hook到一个关键参数。这时候用send和on('message'):
// JS端
send({ "tag": "login_param", "data": password });
# Python端
def on_message(message, data):
if message['type'] == 'send':
print(f"收到: {message['payload']}")
script.on('message', on_message)
这个模式我在分析登录协议时用过无数次,直接把密码参数捞出来,省得去抠算法。
7.3 基本Hook操作:参数、返回值、调用栈
好了,环境搭好了,绑定也懂了。现在来点真格的。
7.3.1 Hook方法参数
假设目标App有一个方法:
public void login(String username, String password)
我们想看看用户输入的密码是什么。JS代码这样写:
Java.perform(function() {
var TargetClass = Java.use("com.example.app.LoginActivity");
TargetClass.login.implementation = function(username, password) {
console.log("用户名: " + username);
console.log("密码: " + password);
// 调用原方法,不破坏逻辑
return this.login(username, password);
};
});
这里有个坑:一定要调用原方法,否则App会崩溃。除非你明确想让它崩溃来测试容错。
7.3.2 Hook返回值
有时候我们想篡改返回值。比如一个校验方法:
public boolean checkVip()
让它永远返回true:
Java.perform(function() {
var TargetClass = Java.use("com.example.app.VipManager");
TargetClass.checkVip.implementation = function() {
console.log("checkVip 被调用,直接返回 true");
return true;
};
});
嗯,这个技巧在破解VIP功能时特别实用。但要注意,有些App会二次校验返回值,光Hook这一个方法可能不够。
7.3.3 打印调用栈
这是我最常用的调试手段。当你不知道某个方法是谁调的时候,打印调用栈一清二楚:
Java.perform(function() {
var TargetClass = Java.use("com.example.app.SensitiveApi");
TargetClass.doSomething.implementation = function() {
console.log("doSomething 被调用");
console.log(Java.use("android.util.Log").getStackTraceString(
Java.use("java.lang.Exception").$new()
));
return this.doSomething();
};
});
输出会显示完整的Java调用链,从系统框架到你的目标方法。我曾经靠这个定位到一个隐蔽的第三方SDK调用,那个SDK偷偷上传了用户数据。
7.3.4 综合示例:Hook一个加密方法
把上面三个技巧合起来,实战一下。假设有个加密方法:
public byte[] encrypt(byte[] data, String key)
我们想拿到明文、密钥、密文,以及谁调了它:
Java.perform(function() {
var CryptoClass = Java.use("com.example.app.CryptoUtils");
var StringClass = Java.use("java.lang.String");
CryptoClass.encrypt.implementation = function(data, key) {
// 打印参数
var plaintext = StringClass.$new(data);
console.log("明文: " + plaintext);
console.log("密钥: " + key);
// 打印调用栈
console.log("调用栈: " +
Java.use("android.util.Log").getStackTraceString(
Java.use("java.lang.Exception").$new()
)
);
// 调用原方法,拿到返回值
var result = this.encrypt(data, key);
var ciphertext = StringClass.$new(result);
console.log("密文: " + ciphertext);
return result;
};
});
这个脚本我几乎每周都会用。不管是分析加密协议,还是定位敏感数据泄露,一套组合拳下来,目标基本裸奔。
7.4 核心知识体系
下面这张图,是我梳理的Frida基础操作的知识脉络。你可以把它当作一个速查地图:
7.5 避坑指南
最后,分享几个我踩过的坑:
- 版本不匹配:PC端和手机端的frida版本必须一致,否则连不上。我曾经因为升级了PC端忘了升级手机端,排查了半小时。
- 忘记调用原方法:Hook时如果不调用
this.xxx(),原方法不会执行,App大概率崩溃。除非你故意想让它崩。 - Java.perform包裹:所有Java相关的Hook代码必须放在
Java.perform(function(){...})里,否则会报错。这是Frida的线程模型决定的。 - spawn vs attach:如果App有反调试,用
spawn在启动时注入,比attach更稳。我遇到过App在onCreate里检测调试器,用spawn完美绕过。
好了,这一章的内容就到这里。环境搭好、绑定搞懂、Hook上手,接下来你就可以去实战中折腾了。