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-tracefrida命令行的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到一个关键参数。这时候用sendon('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基础操作的知识脉络。你可以把它当作一个速查地图:

Frida基础操作知识体系 Frida 核心 环境搭建 • PC端: pip install frida-tools • 手机端: frida-server • 版本必须匹配 • 验证: frida-ps -U JS/Python 绑定 • Python: 调度/控制 • JavaScript: 注入逻辑 • attach vs spawn • send/on('message')通信 Hook 操作 • 参数: 读取/修改 • 返回值: 篡改 • 调用栈: 定位来源 • 综合: 加密方法分析 核心原则 不破坏原逻辑 · 版本匹配 · 善用调用栈

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上手,接下来你就可以去实战中折腾了。


公众号:蓝海资料掘金营,微信deep3321