Frida 基础指南

最后更新:2026-03-29 相关文档:js逆向调试技巧(/js-ni-xiang-diao-shi-ji-qiao/) 补环境(/bu-huan-jing/) 签名算法还原(/qian-ming-suan-fa-huan-yuan/) Frida 是一款动态插桩(Dynamic Instrumentation)框架,可以在运行时向 Native 程序(Android/iOS App、PC 程序)注入 JavaScript 代码,实现: 直接在 Java.perform 外调用 Java API 会报错。 方法有多个重载时,不指定 overload 会

分享

官方文档:https://frida.re/docs/
适用版本:Frida 16.x+(2026-05-08 核实)

最后更新:2026-03-29

相关文档:js逆向调试技巧 补环境 签名算法还原


1. 基础概念

Frida 是什么

Frida 是一款动态插桩(Dynamic Instrumentation)框架,可以在运行时向 Native 程序(Android/iOS App、PC 程序)注入 JavaScript 代码,实现:

  • Hook 任意函数(Java 层 / Native 层 / OC/Swift 层)
  • 读写内存、修改参数和返回值
  • 追踪函数调用链
  • 绕过 SSL Pinning、Root 检测
  • 辅助 App 逆向、接口分析

架构

PC(Frida Tools / Python)
  ↕ USB / TCP
手机(frida-server 运行在 Root 权限下)
  ↕ 进程注入
目标 App 进程(frida-agent 注入后运行 JS 脚本)

安装

# 安装 Python 工具
pip install frida-tools

# 验证安装
frida --version

# 下载对应版本的 frida-server(手机端,需 Root)
# 地址:https://github.com/frida/frida/releases
# 选择 frida-server-xx.xx.x-android-arm64.xz

frida-server 部署(Android)

# 解压 frida-server
unxz frida-server-*.xz

# 推送到手机
adb push frida-server /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/frida-server"

# 以 Root 权限启动
adb shell "su -c '/data/local/tmp/frida-server &'"

# 验证连接
frida-ps -U          # 列出手机上所有进程
frida-ps -Ua         # 只列出已安装的 App

2. 基础使用

常用命令行工具

# 列出设备上的进程
frida-ps -U

# 附加到进程(进程名或 PID)
frida -U -n "com.example.app" -l script.js

# 附加到应用(包名)并在启动时注入(用于初始化阶段的 Hook)
frida -U -f "com.example.app" -l script.js --no-pause

# 交互式 REPL(调试用)
frida -U -n "com.example.app"

# 指定设备
frida -D <device-id> -n "com.example.app" -l script.js

脚本结构

// script.js

// 等待 Java 虚拟机初始化完成
Java.perform(function () {
    console.log("[*] Frida 脚本已加载");

    // 在这里写 Hook 代码
});

3. Java 层 Hook

Hook 实例方法

Java.perform(function () {
    // 获取目标类
    var TargetClass = Java.use("com.example.app.crypto.SignUtil");

    // Hook 方法(根据方法签名区分重载)
    TargetClass.sign.implementation = function (data) {
        // this:当前对象实例
        console.log("[sign] 被调用,入参: " + data);

        // 调用原始方法,获取原始返回值
        var result = this.sign(data);

        console.log("[sign] 返回值: " + result);

        // 修改返回值(可选)
        // return "fake_signature";

        return result;
    };
});

Hook 重载方法

Java.perform(function () {
    var Crypto = Java.use("com.example.Crypto");

    // 方法有多个重载时,用 overload 指定参数类型
    Crypto.encrypt.overload("java.lang.String").implementation = function (data) {
        console.log("[encrypt(String)] data: " + data);
        return this.encrypt(data);
    };

    Crypto.encrypt.overload("java.lang.String", "java.lang.String").implementation = function (data, key) {
        console.log("[encrypt(String, String)] data: " + data + ", key: " + key);
        return this.encrypt(data, key);
    };
});

Hook 静态方法

Java.perform(function () {
    var Utils = Java.use("com.example.Utils");

    // 静态方法也是一样的写法,用 this 调用原始方法(静态方法的 this 是类本身)
    Utils.md5.implementation = function (input) {
        var result = this.md5(input);
        console.log("[md5] " + input + " => " + result);
        return result;
    };
});

Hook 构造函数

Java.perform(function () {
    var OkHttpClient = Java.use("okhttp3.OkHttpClient$Builder");

    OkHttpClient.$init.implementation = function () {
        console.log("[OkHttpClient.Builder] 构造函数被调用");
        console.log(Java.use("android.util.Log").getStackTraceString(
            Java.use("java.lang.Exception").$new()
        ));
        return this.$init();
    };
});

4. 常用 Hook 场景

打印函数调用栈

Java.perform(function () {
    var TargetClass = Java.use("com.example.Sign");

    TargetClass.getSign.implementation = function () {
        var result = this.getSign();

        // 打印 Java 调用栈
        var stack = Java.use("android.util.Log")
            .getStackTraceString(Java.use("java.lang.Exception").$new());
        console.log("[getSign] 调用栈:\n" + stack);
        console.log("[getSign] 结果: " + result);

        return result;
    };
});

Hook 所有加载的类的某个方法

Java.perform(function () {
    Java.enumerateLoadedClasses({
        onMatch: function (className) {
            if (className.includes("Encrypt")) {
                try {
                    var clazz = Java.use(className);
                    var methods = clazz.class.getDeclaredMethods();
                    methods.forEach(function (method) {
                        console.log("[类] " + className + "." + method.getName());
                    });
                } catch (e) {}
            }
        },
        onComplete: function () {},
    });
});

主动调用方法

Java.perform(function () {
    // 主动调用静态方法
    var CryptoUtil = Java.use("com.example.CryptoUtil");
    var result = CryptoUtil.md5("hello world");
    console.log("MD5 结果: " + result);

    // 枚举所有实例,主动调用实例方法
    Java.choose("com.example.TokenManager", {
        onMatch: function (instance) {
            console.log("token: " + instance.getToken());
        },
        onComplete: function () {},
    });
});

5. 绕过常见检测

绕过 Root 检测

Java.perform(function () {
    // Hook RootBeer 库
    try {
        var RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
        RootBeer.isRooted.implementation = function () {
            console.log("[RootBeer] isRooted → false");
            return false;
        };
    } catch (e) {}

    // Hook 常见的文件检测
    var File = Java.use("java.io.File");
    File.exists.implementation = function () {
        var path = this.getAbsolutePath();
        if (path.includes("su") || path.includes("superuser")) {
            return false;
        }
        return this.exists();
    };
});

绕过 SSL Pinning

Java.perform(function () {
    // 方法 1:Hook OkHttp3 CertificatePinner
    try {
        var CertificatePinner = Java.use("okhttp3.CertificatePinner");
        CertificatePinner.check.overload("java.lang.String", "java.util.List").implementation = function (host, certs) {
            console.log("[SSL Pinning] 已绕过,host: " + host);
            return;  // 不调用原始方法,直接通过
        };
    } catch (e) {}

    // 方法 2:Hook TrustManager(信任所有证书)
    var TrustManagerImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl");
    TrustManagerImpl.verifyChain.implementation = function (untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {
        return untrustedChain;
    };
});

6. Native 层 Hook

// Hook so 库中的函数
var libso = Module.findBaseAddress("libcrypto.so");

if (libso) {
    // 按导出函数名 Hook
    var target = Module.findExportByName("libssl.so", "SSL_write");
    if (target) {
        Interceptor.attach(target, {
            onEnter: function (args) {
                console.log("[SSL_write] 调用");
                // 打印参数(buf)
                var buf = args[1];
                var num = parseInt(args[2]);
                console.log("[SSL_write] 数据: " + buf.readUtf8String(num));
            },
            onLeave: function (retval) {
                console.log("[SSL_write] 返回: " + retval);
            },
        });
    }
}

7. Python API(脚本化)

import frida
import sys
import json

PACKAGE_NAME = "com.example.app"

SCRIPT = """
Java.perform(function () {
    var Sign = Java.use("com.example.Sign");
    Sign.getSign.implementation = function(data) {
        var result = this.getSign(data);
        send({type: "sign", data: data, result: result});
        return result;
    };
});
"""

def on_message(message, data):
    if message["type"] == "send":
        payload = message["payload"]
        print(f"[+] 签名: {payload['data']} → {payload['result']}")
    elif message["type"] == "error":
        print(f"[!] 错误: {message['stack']}")

def main():
    device = frida.get_usb_device()
    session = device.attach(PACKAGE_NAME)
    script = session.create_script(SCRIPT)
    script.on("message", on_message)
    script.load()

    print(f"[*] 已附加到 {PACKAGE_NAME},按 Ctrl+C 退出")
    sys.stdin.read()

if __name__ == "__main__":
    main()

8. 常用代码段速查

打印 Byte 数组

function bytes2hex(arr) {
    return Array.from(arr).map(b => ('0' + (b & 0xFF).toString(16)).slice(-2)).join('');
}

function bytes2str(arr) {
    return Java.use("java.lang.String").$new(arr, "UTF-8");
}

主动调用加密函数

Java.perform(function () {
    var context = Java.use("android.app.ActivityThread")
        .currentApplication().getApplicationContext();

    var SharedPreferences = context.getSharedPreferences("config", 0);
    var token = SharedPreferences.getString("token", "");
    console.log("token: " + token);
});

9. 踩坑与注意事项

Java.perform 必须包裹所有 Java 操作

直接在 Java.perform 外调用 Java API 会报错。

Hook 时机:frida -f 与 frida -n 的区别

  • -f:在 App 启动前注入(适合 Hook 初始化阶段的加密,如 so 加载)
  • -n:附加到已运行的进程(适合 Hook 业务逻辑)

重载选择错误

方法有多个重载时,不指定 overload 会只 Hook 第一个重载。若 Hook 后未触发,检查是否选对了重载。

frida-server 版本必须与 frida-tools 版本一致

版本不一致会导致连接失败或奇怪的报错,更新时两端都要同步更新。


最佳实践

attach 前确认进程名:用 frida-ps -U 列出设备所有进程,确认目标 App 的进程名(或包名),避免 attach 到错误进程;对于同名进程,用 -H-D 加设备序列号指定设备。

Hook Java 方法时用 overload 明确指定参数类型:同一方法名可能有多个重载,Java.use('cls').method.overload('java.lang.String', 'int').implementation = ... 精确匹配,避免意外 Hook 错重载。

Interceptor.attachonLeave 查看返回值:不仅要在 onEnter 看输入参数,在 onLeave(retval) 中打印 retval.toString()hexdump(retval) 可以看到函数输出,对追踪加密结果尤其有用。

脚本调试用 console.log + frida -U -l script.js:通过文件加载脚本比 -e 内联脚本更易维护;修改脚本后直接 Ctrl+C 重启,不需要重新 attach(脚本支持 --no-pause 直接运行)。

Memory.scan 配合 frida-dump 提取内存中的明文密钥:加密密钥有时只在内存中出现(从接口动态获取并解密),用 Memory.scan 在堆内存中搜索特征字节序列,找到密钥地址后读出。


常见陷阱

陷阱:frida-server 在设备重启后失效

现象: 重启 Android 设备后 Frida 连接失败,需要重新 push frida-server。
原因: frida-server 放在 /data/local/tmp/ 的可执行文件在重启后不自动运行,需要手动启动或配置自启。
解决: 每次连接前先 adb shell /data/local/tmp/frida-server &;或使用 Magisk 模块自动启动 frida-server。

陷阱:Hook 后 App 直接崩溃

现象: 注入 Frida 脚本后 App 立即 crash,没有输出任何日志。
原因: App 有反 Frida 检测(检测 frida-server 端口 27042、/proc/self/maps 中的 frida 库、ptrace 检测等),或 Hook 脚本语法错误导致异常。
解决: 先用 frida --no-pause 只 attach 不注入脚本,确认 App 能运行;再用 objection explore 绕过常见反检测;或用 frida-gadget 模式替代 frida-server,减少检测特征。

陷阱:Java.use 的类在运行时找不到

现象: Java.use('com.example.Crypto')ClassNotFoundException
原因: 目标类可能在非主线程或特定 ClassLoader 中加载(如插件 APK、热修复框架)。
解决:Java.enumerateClassLoaders 遍历所有 ClassLoader,找到包含目标类的 ClassLoader 后用 Java.classFactory.loader = targetLoader 切换。


参见

mitmproxy完全指南
unidbg基础
js逆向调试技巧

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog