Frida高级实战:从脚本到武器

说实话,Frida 这东西,刚接触时觉得就是个 hook 工具。用久了才发现,它简直是个移动安全领域的瑞士军刀。今天我要讲的这几个高级特性——内存搜索、类实例化、动态调用、RPC、Stalker,还有反调试绕过——都是我这些年实战中反复打磨出来的硬核技能。

嗯,咱们一个一个来。先说说内存搜索。

内存搜索:不只是找字符串

很多人以为内存搜索就是找字符串。其实不然。我在分析一个恶意 SDK 时,它把关键配置用 XOR 加密后藏在堆内存里。这时候就需要 Frida 的 Memory.scan 了。

// 搜索十六进制模式
var pattern = "48 83 EC 28 48 8B 05 ?? ?? ?? ??";
Memory.scan(ptr("0x40000000"), 0x100000, pattern, {
    onMatch: function(address, size) {
        console.log("找到模式在: " + address);
        // 可以在这里读取或修改内存
    },
    onComplete: function() {
        console.log("搜索完成");
    }
});

我个人习惯用 Memory.scanSync 做批量搜索,性能更好。但要注意,搜索范围别太大,否则会卡死。我曾经在 64 位应用上扫了 500MB 堆内存,结果手机直接重启了……

小技巧:搜索前先用 Process.enumerateRanges('rw-') 看看哪些内存段是可读写的,缩小范围。

类实例化与动态调用

这个功能太强了。你可以直接 new 一个 Java 对象,然后调用它的方法。我在分析某社交应用的加密协议时,就是靠这个绕过了整个加密流程。

// 获取类引用
var StringCls = Java.use("java.lang.String");
var StringBuilderCls = Java.use("java.lang.StringBuilder");

// 实例化对象
var str = StringCls.$new("Hello Frida");
var sb = StringBuilderCls.$new();

// 动态调用方法
sb.append(str);
sb.append(" - 动态调用示例");
console.log(sb.toString());

// 调用静态方法
var SystemCls = Java.use("java.lang.System");
SystemCls.currentTimeMillis();

这里有个坑:$new 是 Frida 的语法,不是 Java 原生的。我第一次用的时候还纳闷为什么 new 关键字不行。另外,调用重载方法时要指定参数类型:

// 重载方法调用
var cls = Java.use("com.example.MyClass");
cls.method.overload('int', 'java.lang.String').call(cls.$new(), 42, "test");
注意:动态创建的对象要小心内存泄漏。Frida 不会自动回收你 new 出来的 Java 对象,建议用完就置 null。

Frida RPC:让 Python 和 Java 对话

RPC 是我最常用的功能之一。说白了,就是让 Python 脚本调用 Java 层的方法,或者反过来。我在做自动化测试时,经常用 RPC 来触发业务逻辑。

先看 JavaScript 端怎么暴露接口:

// JS 端注册 RPC 接口
rpc.exports = {
    encrypt: function(data) {
        var result = "";
        Java.perform(function() {
            var CryptoCls = Java.use("com.example.Crypto");
            var instance = CryptoCls.$new();
            result = instance.encrypt(data);
        });
        return result;
    },
    decrypt: function(data) {
        // 类似实现
    }
};

然后 Python 端调用:

import frida

device = frida.get_usb_device()
session = device.attach("com.example.app")

script = session.create_script(js_code)
script.load()

# 调用 RPC 接口
encrypted = script.exports.encrypt("hello")
print("加密结果:", encrypted)

我曾经用这个方案,在 10 分钟内完成了某金融应用的自动化登录流程。你想想看,不用逆向整个加密算法,直接调它的原生方法,多省事。

Frida Stalker:代码级跟踪器

Stalker 是 Frida 的代码跟踪引擎。它能让你看到每条指令的执行情况。说实话,这玩意儿性能开销不小,但关键时刻真能救命。

我在分析一个混淆严重的 native 库时,就是用 Stalker 一步步跟踪,才找到了真正的加密入口。

// 启动 Stalker 跟踪
var targetAddr = Module.findExportByName("libnative.so", "Java_com_example_Native_method");
Stalker.follow({
    events: {
        call: true,   // 跟踪函数调用
        ret: true,    // 跟踪函数返回
        exec: false   // 跟踪每条指令(性能开销大)
    },
    onReceive: function(events) {
        console.log("收到事件:", JSON.stringify(events));
    },
    transform: function(iterator) {
        var instruction = iterator.next();
        while (instruction) {
            // 可以在这里修改指令
            iterator.keep(); // 保留原指令
            instruction = iterator.next();
        }
    }
});
核心要点:Stalker 的 transform 回调可以修改指令流。这意味着你可以动态 patch 代码,甚至插入新的逻辑。但要注意,ARM64 指令是 4 字节对齐的,别改出非法指令。

绕过常见反调试

反调试是每个逆向工程师的必修课。我见过最狠的应用,同时用了 ptrace、TracerPid、时间差检测、文件完整性校验……嗯,咱们一个个破。

ptrace 检测绕过

ptrace 检测的原理很简单:一个进程只能被一个调试器 ptrace。应用自己 ptrace 自己,如果失败就说明已经被调试了。

// 方案一:hook ptrace 函数
var ptracePtr = Module.findExportByName("libc.so", "ptrace");
Interceptor.attach(ptracePtr, {
    onEnter: function(args) {
        var request = args[0].toInt32();
        if (request === 0) { // PTRACE_TRACEME
            console.log("拦截 ptrace(PTRACE_TRACEME)");
            args[0] = ptr(-1); // 返回错误
        }
    },
    onLeave: function(retval) {
        // 让 ptrace 返回 0,表示成功
        retval.replace(ptr(0));
    }
});

我遇到过更狡猾的——它用 ptrace(PTRACE_ATTACH, pid) 来检测。这时候需要 hook 得更精细:

// 方案二:让 ptrace 始终返回成功
Interceptor.replace(ptracePtr, new NativeCallback(function(request, pid, addr, data) {
    console.log("ptrace 被调用: request=" + request + ", pid=" + pid);
    return 0; // 永远返回成功
}, 'int', ['int', 'int', 'pointer', 'pointer']));

TracerPid 检测绕过

TracerPid 是 Linux 内核提供的一个调试检测机制。当进程被调试时,/proc/self/status 里的 TracerPid 会变成调试器的 PID。

// hook 文件读取操作
var fopenPtr = Module.findExportByName("libc.so", "fopen");
Interceptor.attach(fopenPtr, {
    onEnter: function(args) {
        this.path = args[0].readCString();
        if (this.path && this.path.indexOf("status") !== -1) {
            console.log("检测到读取 status 文件:", this.path);
        }
    }
});

// 或者直接 hook 字符串比较
var strstrPtr = Module.findExportByName("libc.so", "strstr");
Interceptor.attach(strstrPtr, {
    onEnter: function(args) {
        var haystack = args[0].readCString();
        var needle = args[1].readCString();
        if (needle === "TracerPid") {
            console.log("拦截 TracerPid 检测");
            // 可以修改返回值
        }
    }
});
避坑指南:我曾经遇到一个应用,它不光读 TracerPid,还检查 /proc/self/status 的文件大小。如果文件被修改过,大小不对也会触发反调试。所以,更稳妥的做法是 hook read 系统调用,在读取到 TracerPid 行时直接替换内容。

知识体系总览

下面这张图是我整理的 Frida 高级实战知识体系。你可以看到,这些技术不是孤立的,它们互相配合才能发挥最大威力。

Frida 高级实战知识体系 Frida 高级实战 核心能力矩阵 内存搜索 Memory.scan 模式匹配/堆分析 类实例化 $new / 动态调用 重载方法处理 Frida RPC Python ↔ Java 远程过程调用 Stalker 代码跟踪引擎 指令级分析 反调试绕过 ptrace / TracerPid 五大核心模块 · 实战驱动 · 层层递进

你看,这五个模块其实是层层递进的。内存搜索是基础,类实例化让你能操控 Java 对象,RPC 打通了 Python 和 Java 的桥梁,Stalker 让你看到最底层的指令执行,而反调试绕过则是所有操作的前提——过不了反调试,后面都是白搭。

说实话,这些技术单独拿出来都不难。难的是在实战中灵活组合。我见过太多人,工具用得溜,但遇到真实应用就抓瞎。为什么?因为真实场景永远是混合的——既有反调试,又有混淆,还有各种保护。

嗯,今天就先聊这么多。记住一点:Frida 是武器,但真正的战斗力来自你对系统底层的理解。工具可以学,但思维方式需要慢慢磨。


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