// COMPSCI 702 · Security for Smart-devices · Android 攻防

Frida Android
动态插桩实战

从零搭建 Frida 环境,掌握 Java/Native Hook、内存读写、SSL Pinning 绕过等核心技术,覆盖攻防两端完整链路。

frida 16.x android 12+ javascript api jadx 联动 smali / native ssl pinning root detection anti-frida
01

Frida 是什么?

Frida 是一款开源的动态二进制插桩(Dynamic Binary Instrumentation,DBI)框架,支持 Android、iOS、Windows、Linux、macOS。它允许你在运行时注入 JavaScript 代码到目标进程,实时 Hook 函数、修改参数和返回值、读写内存、追踪执行流,而无需修改 APK 或重新编译。

🔬
动态分析
无需静态反编译,直接在运行时观测 App 行为、参数传递和返回值。
🎣
方法 Hook
Hook Java 方法、Native 函数,修改逻辑行为,绕过加密/检测。
📡
网络分析
Hook SSL/TLS 层,绕过证书固定,抓取明文通信内容。
💉
内存操作
读写进程内存,扫描特征字节,追踪函数调用栈和分支。
工具方式优点缺点
Frida动态插桩 / 运行时无需修改 APK、实时交互、脚本灵活需要 root 或重打包;容易被检测
JADX / APKTool静态反编译离线分析、无需设备混淆难读,无法分析运行时状态
Xposed框架级 Hook持久化、系统级需要 Xposed 框架支持,重启生效
objectionFrida 封装一键操作,适合快速渗透测试功能较固定,定制性不如纯 Frida
02

架构与原理

Frida 采用 C/S 架构,由运行在 PC 上的客户端(frida-tools)和运行在 Android 设备上的 Server(frida-server)组成。

ARCHITECTURE
┌──────────────────────────────────┐     ADB / USB / TCP
│          PC (Host)               │◄─────────────────────────────►│
│                                  │                               │
│  frida (Python CLI)              │                         ┌─────┴──────────────────────────┐
│  frida-tools / objection         │                         │    Android Device              │
│  自定义 Python 脚本              │                         │                                │
│                                  │                         │  frida-server (root)           │
│  ┌──────────────────────┐        │                         │         │                      │
│  │  JavaScript Engine   │        │                         │         ▼                      │
│  │  (QuickJS / V8)      │◄── 双向RPC ──────────────────►  │  ptrace → 注入 frida-agent     │
│  └──────────────────────┘        │                         │         │                      │
└──────────────────────────────────┘                         │         ▼                      │
                                                             │  目标进程 (App)                │
                                                             │  ├─ Java Runtime (ART)         │
                                                             │  ├─ .so Native Libs            │
                                                             │  └─ Hook 生效 ✓                │
                                                             └────────────────────────────────┘

注入原理

Frida Server 通过 ptrace 系统调用附加到目标进程,在进程内存中注入 frida-agent 动态库。Agent 内嵌 QuickJS(或 V8)引擎,执行你传入的 JavaScript 脚本。对于 Java 层,它通过 ART 内部 API(JNI / jvmti)实现 Hook;对于 Native 层,它通过修改 PLT/GOT 表或 inline hook(覆盖函数头部指令)来重定向调用。

ℹ️
Gadget 模式(无 Root) 若设备无 root,可将 libfrida-gadget.so 打包进 APK 并在 AndroidManifest.xml 的 Application 中添加 extractNativeLibs=true,再重新签名安装。Gadget 会在 App 启动时自动初始化,监听本地端口等待连接。
03

环境搭建

1. 安装 frida-tools(PC 端)

BASH · macOS / Linux
# 推荐用 pipx 或 venv 隔离环境
pip3 install frida-tools   # 包含 frida-ps、frida-trace 等命令行工具
pip3 install frida         # Python 绑定(用于写脚本)

# 验证安装
frida --version            # → 16.x.x

2. 下载并推送 frida-server(Android 端)

BASH
# 查看设备 CPU 架构
adb shell getprop ro.product.cpu.abi
# 常见值: arm64-v8a / armeabi-v7a / x86_64

# 前往 https://github.com/frida/frida/releases 下载对应版本
# 例: frida-server-16.x.x-android-arm64.xz

xz -d frida-server-16.x.x-android-arm64.xz

# 推送到设备
adb push frida-server-16.x.x-android-arm64 /data/local/tmp/frida-server
adb shell chmod +x /data/local/tmp/frida-server

# 启动(需要 root)
adb shell su -c "/data/local/tmp/frida-server &"

# PC 端验证:列出所有进程
frida-ps -U           # -U = USB 设备
frida-ps -U -a        # -a = 仅列出 App

3. ADB 端口转发(WiFi 调试)

BASH
adb forward tcp:27042 tcp:27042   # Frida 默认端口
frida-ps -H 127.0.0.1             # 通过 TCP 连接
⚠️
注意 Android 14+ 对 SELinux 管控更严,若 frida-server 无法启动,尝试:adb shell setenforce 0(临时关闭 SELinux)。在模拟器(如 Android Studio AVD、Genymotion)上调试时更方便,推荐使用无 Google Play 的镜像,root 权限开箱即用。

4. 常用命令速查

命令说明
frida-ps -Ua列出 USB 设备上运行的 App 进程
frida -U -f com.pkg.name -l script.jsspawn 模式:启动 App 并注入脚本
frida -U -n "App Name" -l script.jsattach 模式:附加到已运行的进程
frida -U -p 1234 -l script.js按 PID 附加
frida-trace -U -i "recv*" -f com.pkg自动 trace 匹配函数
objection -g com.pkg explore进入 objection 交互 Shell
04

Hook Java 方法

所有 Java Hook 操作必须包裹在 Java.perform() 回调内,确保在 ART 完全初始化后执行。

基础 Hook 模板

JAVASCRIPT · Frida Script
Java.perform(function () {

    // 1. 获取目标类
    var TargetClass = Java.use('com.example.app.TargetClass');

    // 2. Hook 指定方法(注意重载)
    TargetClass.checkPassword.implementation = function (pwd) {
        // this = Java 对象实例
        console.log('[*] checkPassword called, arg: ' + pwd);

        // 调用原始方法
        var result = this.checkPassword(pwd);
        console.log('[*] original result: ' + result);

        // 强制返回 true(绕过验证)
        return true;
    };

});

处理方法重载(Overload)

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

    // 通过 overload() 指定参数类型签名来选择具体重载
    Utils.encrypt.overload('java.lang.String').implementation = function (data) {
        console.log('encrypt(String): ' + data);
        return this.encrypt(data);
    };

    Utils.encrypt.overload('java.lang.String', '[B').implementation = function (data, key) {
        // '[B' = byte[] 类型签名
        console.log('encrypt(String, byte[]): data=' + data + ' key=' + key);
        return this.encrypt(data, key);
    };
});

Hook 构造函数

JAVASCRIPT
Java.perform(function () {
    var MyClass = Java.use('com.example.MyClass');

    // $init 代表构造函数
    MyClass.$init.overload('java.lang.String', 'int').implementation = function (name, age) {
        console.log('[ctor] name=' + name + ' age=' + age);
        // 打印调用栈
        console.log(Java.use('android.util.Log').getStackTraceString(
            Java.use('java.lang.Exception').$new()
        ));
        this.$init(name, age);  // 调用原始构造
    };
});

主动调用 Java 方法(无需等待 Hook 触发)

JAVASCRIPT
Java.perform(function () {
    var Crypto = Java.use('com.example.CryptoHelper');

    // 调用静态方法
    var result = Crypto.md5('hello_world');
    console.log('md5 result: ' + result);

    // 实例化对象并调用实例方法
    var obj = Crypto.$new('AES');
    console.log('instance method: ' + obj.getAlgorithm());

    // 修改私有字段(反射方式)
    var field = Crypto.class.getDeclaredField('secretKey');
    field.setAccessible(true);
    field.set(obj, 'my_custom_key');
});

枚举所有实例(Instance Enumeration)

JAVASCRIPT
Java.perform(function () {
    // 枚举堆内存中所有 Activity 实例(可用于获取 Context)
    Java.choose('android.app.Activity', {
        onMatch: function (instance) {
            console.log('Found activity: ' + instance.getClass().getName());
        },
        onComplete: function () {
            console.log('Enumeration complete');
        }
    });
});
💡
技巧:自动 Hook 类的所有方法 配合 JADX 分析出类名后,可用以下代码一键 Hook 目标类的所有方法,快速找到感兴趣的调用点。
JAVASCRIPT · Hook 所有方法
function hookAllMethods(className) {
    Java.perform(function () {
        var clazz = Java.use(className);
        var methods = clazz.class.getDeclaredMethods();
        methods.forEach(function (method) {
            var name = method.getName();
            try {
                clazz[name].overloads.forEach(function (overload) {
                    overload.implementation = function () {
                        var args = Array.from(arguments).map(a => JSON.stringify(a)).join(', ');
                        console.log(`[HOOK] ${className}.${name}(${args})`);
                        return overload.apply(this, arguments);
                    };
                });
            } catch (e) { /* 忽略无法 Hook 的方法 */ }
        });
        console.log(`[*] Hooked all methods of ${className}`);
    });
}

hookAllMethods('com.example.app.PaymentManager');
05

Hook Native(.so 函数)

许多 App 将加密算法、授权验证、反作弊逻辑写在 Native 层(C/C++ 编译的 .so 文件)。Frida 提供 InterceptorNativeFunction API 处理 Native Hook。

按导出名称 Hook

JAVASCRIPT
// Hook libc 的 open 系统调用(可用于追踪文件访问)
var openPtr = Module.getExportByName('libc.so', 'open');

Interceptor.attach(openPtr, {
    onEnter: function (args) {
        // args[0] = const char* pathname
        this.path = args[0].readUtf8String();
        console.log('open() path: ' + this.path);
    },
    onLeave: function (retval) {
        // retval = 文件描述符,-1 表示失败
        console.log('open() fd = ' + retval.toInt32());
    }
});

按内存地址 Hook(配合 JADX / Ghidra 分析)

JAVASCRIPT
// 先获取 .so 在内存中的基地址(ASLR 每次运行会变化)
var baseAddr = Module.findBaseAddress('libapp.so');
console.log('libapp.so base: ' + baseAddr);

// Ghidra 中分析出偏移量 0x12A40(函数 validate_license 的 RVA)
var funcAddr = baseAddr.add(0x12A40);

Interceptor.attach(funcAddr, {
    onEnter: function (args) {
        console.log('validate_license called, arg0=' + args[0]);
        // ARM64: args[0..7] → 寄存器 x0..x7
    },
    onLeave: function (retval) {
        console.log('original retval: ' + retval.toInt32());
        retval.replace(1);  // 强制返回 1(成功)
    }
});

主动调用 Native 函数

JAVASCRIPT
// 声明 Native 函数签名:返回类型 + 参数类型数组
var strlen = new NativeFunction(
    Module.getExportByName('libc.so', 'strlen'),
    'size_t',      // 返回类型
    ['pointer']    // 参数类型
);

var str = Memory.allocUtf8String('hello frida');
console.log('strlen = ' + strlen(str));  // → 11

枚举所有已加载模块与导出

JAVASCRIPT
// 列出所有已加载 .so
Process.enumerateModules().forEach(function (mod) {
    console.log(mod.name + ' @ ' + mod.base + ' size=' + mod.size);
});

// 列出指定 .so 的所有导出函数
Module.enumerateExports('libssl.so').forEach(function (exp) {
    if (exp.name.includes('SSL_CTX')) {
        console.log(exp.name + ' @ ' + exp.address);
    }
});
06

内存操作

读写内存

JAVASCRIPT
var ptr = ptr('0x7f1234abcd');   // 从地址字符串构造指针

// 读操作
ptr.readU8();           // 读 1 字节无符号整数
ptr.readU32();          // 读 4 字节
ptr.readU64();          // 读 8 字节
ptr.readFloat();        // 读浮点数
ptr.readPointer();      // 读指针(平台位宽)
ptr.readUtf8String();   // 读 C 字符串
ptr.readByteArray(16);  // 读 16 字节原始数据(返回 ArrayBuffer)

// 写操作
Memory.protect(ptr, 0x1000, 'rwx');  // 先确保内存可写
ptr.writeU32(0xDEADBEEF);
ptr.writeByteArray([0x90, 0x90, 0x90]);  // NOP patch
ptr.writeUtf8String('patched!');

// 分配内存(Frida 管理生命周期)
var buf = Memory.alloc(256);                 // 分配 256 字节
var strBuf = Memory.allocUtf8String('test');  // 分配字符串

内存扫描(搜索特征字节)

JAVASCRIPT
// 在 libapp.so 内存范围内搜索特定字节序列(?? = 通配符)
var mod = Process.getModuleByName('libapp.so');
Memory.scan(mod.base, mod.size, '48 65 6c 6c ?? ?? 57 6f 72 6c 64', {
    onMatch: function (address, size) {
        console.log('Pattern found @ ' + address);
        console.log(hexdump(address, { length: 32, ansi: true }));
    },
    onComplete: function () {
        console.log('Scan complete');
    }
});

hexdump 工具

JAVASCRIPT
// hexdump 是 Frida 全局函数,可直接使用
console.log(hexdump(ptr, {
    offset: 0,
    length: 64,
    header: true,
    ansi: true
}));
// 输出类似:
// 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
// 48 65 6c 6c 6f 00 57 6f 72 6c 64 00 00 00 00 00  Hello.World.....
07

Interceptor API 详解

Interceptor 是 Frida 用于 Native Hook 的核心 API,支持 inline hook(替换函数头部指令)和 call replacement(完全替换函数实现)。

Interceptor.replace(完全替换)

JAVASCRIPT
var isRootedAddr = Module.getExportByName('libsec.so', 'isRooted');

Interceptor.replace(isRootedAddr, new NativeCallback(function () {
    console.log('[*] isRooted() hooked → returning false');
    return 0;  // 0 = false,非 root 设备
}, 'int', []));  // 返回类型 int,无参数

追踪 JNI 调用

JAVASCRIPT · 追踪 FindClass / CallMethod
// 追踪 Native 代码对 Java 层的所有 JNI 调用
var jni_env = Java.vm.getEnv();
var findClass = jni_env.handle.readPointer().add(6 * Process.pointerSize).readPointer();

Interceptor.attach(findClass, {
    onEnter: function(args) {
        console.log('FindClass: ' + args[1].readUtf8String());
    }
});
08

绕过 SSL Pinning

SSL Pinning(证书固定)是 App 在 TLS 握手时校验服务器证书与内置证书是否匹配,防止中间人攻击。对于安全研究,我们需要绕过它,让 Burp Suite / Charles 等代理工具能解密 HTTPS 流量。

⚠️
免责声明 以下技术仅用于授权的安全研究与渗透测试,不得用于非法用途。

方法一:Hook OkHttp3 CertificatePinner

JAVASCRIPT · OkHttp3
Java.perform(function () {

    // OkHttp3 证书固定核心类
    var CertificatePinner = Java.use('okhttp3.CertificatePinner');

    // check(String hostname, List peerCertificates) — 原始校验逻辑
    CertificatePinner.check.overload('java.lang.String', 'java.util.List')
        .implementation = function (hostname, certs) {
            console.log('[SSL] CertificatePinner.check bypassed for: ' + hostname);
            // 直接返回,不抛出 SSLPeerUnverifiedException
        };

    // check(String hostname, Certificate... peerCertificates)(旧版重载)
    CertificatePinner.check.overload('java.lang.String', '[Ljava.security.cert.Certificate;')
        .implementation = function (hostname, certs) {
            console.log('[SSL] CertificatePinner.check (varargs) bypassed for: ' + hostname);
        };

});

方法二:Hook TrustManager(通用方案)

JAVASCRIPT · X509TrustManager
Java.perform(function () {

    // 替换 TrustManager:让所有证书都被信任
    var TrustManager = Java.registerClass({
        name: 'com.frida.bypass.TrustManager',
        implements: [Java.use('javax.net.ssl.X509TrustManager')],
        methods: {
            checkClientTrusted: function (chain, authType) {},
            checkServerTrusted: function (chain, authType) {},  // 不抛异常 = 信任所有
            getAcceptedIssuers: function () { return []; }
        }
    });

    var SSLContext = Java.use('javax.net.ssl.SSLContext');
    var ctx = SSLContext.getInstance('TLS');
    ctx.init(null, [TrustManager.$new()], null);
    SSLContext.setDefault(ctx);

    console.log('[*] TrustManager replaced successfully');
});

方法三:使用 objection 一键绕过

BASH · objection
# 启动 App 并注入
objection -g com.example.app explore

# 在 objection shell 内执行
android sslpinning disable

# 同时可以:
# android root disable    ← 禁用 Root 检测
# android hooking list classes
# android hooking watch class com.example.PaymentService

常见 SSL Pinning 实现 & 对应 Hook 点

框架 / 实现关键类 / 方法Hook 策略
OkHttp3okhttp3.CertificatePinner#check覆盖 implementation 为空函数
Android 原生javax.net.ssl.X509TrustManager#checkServerTrusted替换 TrustManager 实例
Retrofit内部使用 OkHttp,同上同 OkHttp3
Conscrypt / Cronetorg.conscrypt.TrustManagerImplHook verifyChain / checkTrusted
FlutterNative ssl_crypto_device_session_newHook libflutter.so 内 SSL_CTX_set_custom_verify
React Native底层 OkHttp / NSURLSession同 OkHttp3
09

绕过 Root 检测

App 常通过以下方式检测 Root:检查 su 路径是否存在、读取系统属性、调用 RootBeer 等第三方库、检测 Magisk 模块挂载点等。以下脚本覆盖主要场景。

JAVASCRIPT · 综合 Root 检测绕过
Java.perform(function () {

    // ──────────────────────────────────────────────────
    // 1. 文件存在性检查:su、busybox、Magisk 路径
    // ──────────────────────────────────────────────────
    var File = Java.use('java.io.File');
    var suPaths = [
        '/system/bin/su', '/system/xbin/su', '/sbin/su',
        '/system/app/Superuser.apk', '/data/data/com.noshufou.android.su',
        '/data/adb/magisk', '/sbin/.magisk'
    ];

    File.exists.implementation = function () {
        var path = this.getAbsolutePath();
        if (suPaths.some(p => path.includes(p))) {
            console.log('[Root] File.exists() bypassed for: ' + path);
            return false;
        }
        return this.exists();
    };

    // ──────────────────────────────────────────────────
    // 2. 系统属性检查(ro.debuggable、ro.secure 等)
    // ──────────────────────────────────────────────────
    var SystemProperties = Java.use('android.os.SystemProperties');
    SystemProperties.get.overload('java.lang.String').implementation = function (key) {
        var val = this.get(key);
        if (key === 'ro.debuggable') { return '0'; }
        if (key === 'ro.secure')     { return '1'; }
        return val;
    };

    // ──────────────────────────────────────────────────
    // 3. Runtime.exec() 执行 "which su"
    // ──────────────────────────────────────────────────
    var Runtime = Java.use('java.lang.Runtime');
    Runtime.exec.overload('java.lang.String').implementation = function (cmd) {
        if (cmd.includes('su') || cmd.includes('which')) {
            console.log('[Root] Runtime.exec() blocked: ' + cmd);
            throw Java.use('java.io.IOException').$new();
        }
        return this.exec(cmd);
    };

    // ──────────────────────────────────────────────────
    // 4. RootBeer 库
    // ──────────────────────────────────────────────────
    try {
        var RootBeer = Java.use('com.scottyab.rootbeer.RootBeer');
        RootBeer.isRooted.implementation     = function () { return false; };
        RootBeer.isRootedWithBusyBox.implementation = function () { return false; };
        console.log('[Root] RootBeer bypassed');
    } catch(e) { console.log('[Root] RootBeer not found, skipping'); }

});
10

完整性校验绕过

签名校验绕过

JAVASCRIPT · APK 签名绕过
Java.perform(function () {

    // 先获取真实签名(用于白名单填充)
    Java.choose('android.app.ActivityThread', {
        onMatch: function (thread) {
            var ctx = thread.getApplication().getApplicationContext();
            var pm  = ctx.getPackageManager();
            var pkg = ctx.getPackageName();
            var PackageManager = Java.use('android.content.pm.PackageManager');
            var sigs = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES.value).signatures.value;
            console.log('[Sig] Real signature: ' + sigs[0].toCharsString());
        },
        onComplete: function () {}
    });

    // Hook PackageManager.getPackageInfo,返回真实签名(即使 APK 被重新签名)
    var PackageInfo = Java.use('android.content.pm.PackageManager');
    var realSig = null;  // 在首次调用时捕获真实签名并缓存

    // 实际上最简单的方案是让 App 重打包时保留原始签名的哈希值
    // 并在这里 Hook checksum 比较函数直接返回 true

});

Google Play Integrity API 绕过

JAVASCRIPT · Play Integrity
Java.perform(function () {

    // Hook IntegrityManager — requestIntegrityToken 返回成功的 Task
    try {
        var StandardIntegrityManager = Java.use(
            'com.google.android.play.core.integrity.StandardIntegrityManager'
        );
        // 具体实现取决于 Play Core 版本,通常需要 Hook Task 的回调
        console.log('[Integrity] StandardIntegrityManager hooked');
    } catch(e) {
        console.log('[Integrity] Play Integrity API not found');
    }

    // 更通用:直接 Hook App 内对 integrity verdict 的处理逻辑
    // (需要先用 JADX 定位具体类名和方法名)

});
11

Anti-Frida 检测与对抗

高安全级别的 App(银行、游戏反作弊)会主动检测 Frida 的存在。了解这些技术才能有效对抗(防守方也需了解以加固 App)。

检测方式原理对抗方案
端口扫描扫描 27042 等 Frida 默认端口启动时加 --listen-backlog,或修改 frida-server 端口
进程名检测读取 /proc 目录查找 frida-server重命名 frida-server 二进制文件
/proc/maps 扫描查找 frida-agent、gadget 内存映射使用自定义名称编译 frida-gadget
D-Bus 检测frida-server 使用 D-Bus 通信,特征明显使用 Gadget 嵌入模式,绑定到 unix socket
ptrace 检测进程自 ptrace 防止外部附加Hook ptrace 系统调用返回 0
完整性内存检测比较函数头部字节检测 inline hook使用 Frida 的 Stalker 而非 Interceptor
线程枚举检查是否有 Frida 注入线程修改 frida-agent 线程名

Hook ptrace 绕过自 ptrace 防附加

JAVASCRIPT
// Anti-debug 技术:App 对自身调用 ptrace(PTRACE_TRACEME)
// 使得其他调试器无法附加(一个进程只能有一个 tracer)
// Frida 在 spawn 模式下已自动处理,但某些情况下仍需手动处理

var ptracePtr = Module.getExportByName('libc.so', 'ptrace');
Interceptor.attach(ptracePtr, {
    onEnter: function (args) {
        var request = args[0].toInt32();
        var PTRACE_TRACEME = 0;
        if (request === PTRACE_TRACEME) {
            console.log('[Anti-Debug] ptrace(PTRACE_TRACEME) blocked');
            args[0] = ptr('-1');  // 将请求号改为无效值
        }
    }
});

绕过 /proc/maps 内存检测

JAVASCRIPT
// Hook open/fopen,当打开 /proc/maps 时返回空内容
var fopenPtr = Module.getExportByName('libc.so', 'fopen');
Interceptor.attach(fopenPtr, {
    onEnter: function (args) {
        this.path = args[0].readUtf8String();
    },
    onLeave: function (retval) {
        if (this.path && this.path.includes('/proc/self/maps')) {
            console.log('[Anti-Frida] /proc/self/maps access intercepted');
            // 可以返回空 FILE* 或自定义内容
        }
    }
});
12

Stalker:代码追踪引擎

Stalker 是 Frida 的代码追踪引擎,能追踪线程执行的每一条指令(或每次函数调用/基本块),功能强大但性能开销较高。

追踪指定线程的所有调用

JAVASCRIPT · Stalker
var targetThreadId = Process.getCurrentThreadId();

Stalker.follow(targetThreadId, {
    events: {
        call: true,   // 追踪 CALL 指令
        ret:  false,  // 是否追踪 RET
        exec: false,  // 追踪每条指令(极慢,慎用)
        block: false, // 追踪基本块
        compile: false
    },
    onReceive: function (events) {
        var parsed = Stalker.parse(events, {
            annotate: true,
            stringify: true
        });
        parsed.forEach(function (event) {
            console.log(event);
        });
    },
    transform: function (iterator) {
        var instruction = iterator.next();
        while (instruction !== null) {
            // 可在此插入自定义代码(类似 DBI callout)
            iterator.keep();
            instruction = iterator.next();
        }
    }
});

// 停止追踪
// Stalker.unfollow(targetThreadId);
💡
Stalker 实战用途 Stalker 适合在已知某个功能触发后,追踪其执行路径,帮助定位 Native 层的关键函数(比如:点击"购买"按钮后追踪主线程,找到 license 校验函数的地址)。
13

RPC 双向通信

Frida 支持 JavaScript 脚本与 Python Host 之间的双向 RPC 调用,可以构建复杂的自动化分析工具。

JavaScript 端(暴露 RPC 函数)

JAVASCRIPT · script.js
// 通过 rpc.exports 暴露可被 Python 调用的函数
rpc.exports = {
    callEncrypt: function (data) {
        var result = null;
        Java.perform(function () {
            var Crypto = Java.use('com.example.Crypto');
            result = Crypto.encrypt(data);
        });
        return result;
    },
    getSharedPrefs: function (key) {
        var value = null;
        Java.perform(function () {
            Java.choose('android.app.Activity', {
                onMatch: function(act) {
                    var prefs = act.getPreferences(0);
                    value = prefs.getString(key, 'NOT_FOUND');
                },
                onComplete: function(){}
            });
        });
        return value;
    }
};

// send() 主动向 Python 发消息
send({ type: 'init', message: 'Script loaded' });

Python 端(驱动脚本)

PYTHON · host.py
import frida
import sys

def on_message(message, data):
    if message['type'] == 'send':
        print(f"[JS→PY] {message['payload']}")
    elif message['type'] == 'error':
        print(f"[ERROR] {message['stack']}")

# 连接设备并 spawn 目标 App
device = frida.get_usb_device()
pid    = device.spawn(['com.example.app'])
session = device.attach(pid)

with open('script.js', 'r') as f:
    script = session.create_script(f.read())

script.on('message', on_message)
script.load()
device.resume(pid)

# 调用 JS 端暴露的 RPC 函数
encrypted = script.exports.call_encrypt('secret_data')
print(f"Encrypted: {encrypted}")

pref_value = script.exports.get_shared_prefs('auth_token')
print(f"auth_token: {pref_value}")

sys.stdin.read()  # 保持进程存活
14

最佳实践 & 调试技巧

调试技巧

  1. 先静态分析,再动态插桩

    用 JADX 分析出类名、方法名和混淆映射后再写 Frida 脚本,效率远高于盲目 Hook。对于你正在做的 Tomato App 逆向,先在 JADX 中找到 Premium 检测类,再用 Frida 动态验证。

  2. 使用 spawn 而非 attach 模式

    frida -U -f com.pkg -l script.js --no-pause,spawn 模式在 App 启动最早阶段注入,能 Hook 到初始化过程中的安全检测,attach 则可能错过早期调用。

  3. 捕获并打印调用栈

    遇到未知触发点时,在 Hook 内打印 Java 调用栈定位上层调用链:Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Exception').$new())

  4. 处理混淆类名

    混淆后类名形如 a.b.c,可用 Java.enumerateLoadedClasses() 配合关键字过滤,或在 JADX 中通过字符串常量、特征方法逆向找到真实类名。

  5. 脚本热重载

    配合 frida -U -f com.pkg -l script.js 和 IDE 保存自动触发,或用 Python 脚本监听文件变化并 script.unload() / create_script() 实现热重载,避免频繁重启 App。

  6. 使用 frida-compile 模块化

    复杂项目用 frida-compile(TypeScript 支持)将多个 .ts 文件编译打包成单个 JS,获得类型提示、代码补全等工程化能力,提升脚本可维护性。

常见错误与解决

错误信息原因解决方案
java.lang.ClassNotFoundException类名拼写错误或 ProGuard 混淆Java.enumerateLoadedClasses() 搜索
No such method方法被混淆,或参数类型签名错误clazz.class.getDeclaredMethods() 枚举
Process terminated脚本语法错误或 Hook 导致 App 崩溃加 try/catch,先 log 再修改逻辑
unable to find export.so 文件未加载或导出名被 stripModule.enumerateExports() 确认名称
Script destroyedSession 中断(App 崩溃 / 重启)监听 script.destroyed 事件自动重连
frida-server 启动失败SELinux 拦截adb shell setenforce 0

推荐工具链

🔍
JADX + APKLab
静态反编译,在 VS Code 中直接查看 smali 和 Java,配合 Frida 动态验证。
🤖
objection
Frida 封装,交互式 shell,适合快速渗透测试,一键绕过 SSL / Root 检测。
👻
Ghidra
NSA 开源,分析 Native .so,定位偏移量,配合 Frida 做 Native Hook。
🌐
Burp Suite
SSL Pinning 绕过后,配合 Burp 抓取 HTTPS 明文,分析 API 通信协议。
📚
延伸学习 官方文档 frida.re/docs、CodeShare codeshare.frida.re(社区 Hook 脚本库)、以及 GitHub 上的 frida-android-helperfridump(内存 dump 工具)都是非常好的学习资源。