当前位置:首页 > 逆向工程 > 正文

Frida 动态注入入门:hook 一个函数、改一个返回值

约 5825 字 · 预计阅读 15 分钟
一句话总结:调试器适合「停下来看」,Frida 适合「一直在旁边盯着」。这篇从 attach 一个本地进程开始,讲清 Interceptor 的三种用法(改返回、读参数、调内部函数)、JavaScript 与 Python 两侧怎么通信,以及版本差异和反 Frida 检测这几个坑。

前一篇讲 PE 是静态看结构,x64dbg 那篇是停下来单步。但有一类需求这两样都不顺手:我需要知道这个函数每次被调用时参数是什么,连续观察几百次。

在调试器里下断点,每次都要手动 F5,几十次之后就烦了。Frida 解决的就是这个:写一段脚本挂上去,它在后台记录,你该干嘛干嘛。

前置

pip install frida frida-tools
frida --veRSIon

Windows 本地进程直接 attach,不需要额外组件。Android 要往设备里推 frida-server(需 root,或使用可调试应用的重打包方案),这部分后面单独说。

注意一点:Frida 的位数要和目标一致。用 64 位 Python 装的 Frida 只能 attach 64 位进程,32 位目标得装 32 位 Python 环境。

最小可跑:二十行

import frida, sys

TARGET = "target.exe"

JS = """
const addr = Module.getExportByName(null, "IsDebuggerPresent");
console.log("[*] IsDebuggerPresent @ " + addr);

Interceptor.attach(addr, {
    onEnter(args) {
        this.caller = this.returnAddress;
        console.log("[+] 被调用,返回地址 " + this.caller);
    },
    onLeave(retval) {
        console.log("    原返回值 " + retval);
        retval.replace(0);          // 强制返回「没有调试器」
    }
});
"""

def on_message(msg, data):
    if msg["type"] == "send":
        print("[脚本]", msg["payload"])
    else:
        print("[", msg["type"], "]", msg.get("description") or msg.get("stack"))

session = frida.attach(TARGET)
script = session.create_script(JS)
script.on("message", on_message)
script.load()
print("[*] 已挂载,CtRL+C 退出")
sys.stdin.read()

跑起来之后,只要目标调用了 IsDebuggerPresent,你就能在终端看到一行日志,而且它的返回值被改成了 0。

这就是 Frida 的核心价值:不用停止进程,就能观察和改写行为。

Interceptor 的三种用法

改返回值。 上面那个例子。retval.replace(0) 能把检查函数的返回值改掉,绕过反调试最省力的一招。

注意 retval 是个 NativePointer,replace 要传同宽度的值。64 位下返回 bool 时用 retval.replace(0) 就行,传字符串会静默失败。

读参数。 onEnter(args) 里 args[0]、args[1] 是对应寄存器的值。如果参数是指针,得自己读内存:

Interceptor.attach(Module.getExportByName(null, "CreateFileW"), {
    onEnter(args) {
        const path = Memory.readUtf16String(args[0]);   // 宽字符
        console.log("[打开文件] " + path);
    }
});

这里有个常见错误:CreateFileW 的 W 表示宽字符(UTF-16),要用 readUtf16String;CreateFileA 才是 readAnsiString。混用会读出乱码或者直接崩。

调内部函数。 想把某个没导出的函数当工具用,用 NativeFunction 包一层:

const base = Module.getBaseAddress("target.exe");
const fn = new NativeFunction(base.add(0x1A2C0), "int", ["pointer", "int"]);
const result = fn(ptr(0x1000), 42);

这一步在分析加密、解包这类活时特别有用:你不用重写算法,直接调它自己的实现。

图 1:Python 主控与目标进程内的 JS Agent 双层结构,以及三种典型用法

两侧怎么通信

Frida 是双层结构:Python 侧负责 attach、加载脚本、收消息;注入到目标进程里的 JS agent 负责实际 hook。

JS 里用 send() 往外发结构化数据:

send({ event: "call", API: "IsDebuggerPresent", ret: 0 });

Python 侧收:

def on_message(msg, data):
    if msg["type"] == "send":
        payload = msg["payload"]
        if payload.get("event") == "call":
            print(payload["api"], payload["ret"])

console.log 的输出也会通过 on_message 送过来,类型是 log。这意味着一件事:别在 JS 里用 send 传巨大的对象,每次跨进程都有序列化成本,高频调用时会明显拖慢目标。

要传二进制(比如内存 dump 的片段),用 send(payload, dataArrayBuffer) 的第二个参数,走单独的二进制通道。

Windows 本地与 Android 的差别

本地 Windows 进程:frida.attach("进程名或PID") 就完事,权限够就行(同用户、非保护进程)。

Android 麻烦得多:

  1. 需要 frida-server 在设备上以 root 运行(adb push + chmod +x + 后台启动)
  2. 或者用「重打包 + 注入 gadget」的方案,不需要 root,但要改 APK 签名
  3. 现在很多应用有 Frida 检测,跑起来直接被拒绝

第三种情况最常见。检测手段包括:扫描 /proc/self/maps 里的 frida 字样、检测默认端口 27042、检查线程名 gum-js-loop、检测 frida-agent 的内存特征。绕这些需要改端口、改名、用定制编译的 gadget,属于另一个话题了。

坑清单

坑一:32/64 位不匹配。 报错通常是 unable to attach 或者进程直接崩。先确认 Python 和目标的位数一致。

坑二:API 名字在不同版本里不一样。 Module.findExportByName 在旧版本用得多,新版本改成了 Module.getExportByName。写脚本前用 frida --version 确认,报 undefined is not a function 基本就是这个原因。

坑三:hook 不到被内联的函数。 编译器的内联优化会把小函数直接展开,导出表里还有符号,但实际执行根本不走那条指令。表现在 hook 成功挂上、永远不触发。换更上层的函数,或者用 Interceptor.replace 整个替换。

坑四:字符串编码搞错。 W 结尾用 UTF-16、A 结尾用 ANSI,中文路径在后者下要处理代码页。乱码先查这个。

坑五:JS 里做重活拖慢目标。 在 onEnter 里跑循环、拼大字符串、频繁 send,目标进程会明显变卡,甚至触发超时检测。原则是只采必要数据,加工放到 Python 侧。

边界

Frida 是分析工具,用在你有权分析的目标上:自己的程序、靶场、明确授权的样本、自己的移动应用。

它的正经用途很广:给没有源码的第三方库加日志、观测加密前后的数据、验证补丁是否生效、做自动化回归测试(hook API 断言调用顺序)。

需要提醒的是:对在线游戏或他人应用使用 Frida,检测和封禁只是最轻的后果。技术本身中性,用在哪由人决定。

FAQ

Frida 和 x64dbg 该怎么选?

要看「一次性定位」还是「持续观测」。断点找根因用调试器,批量记录、长时间观察、自动改行为用 Frida。两者可以同时开。

需要 root 吗?

Windows 本地进程不需要。Android 通常需要 root 跑 frida-server,或者用 gadget 重打包方案绕过。

能不能 hook 系统 API?

能。Module.getExportByName(null, "名字") 的第一个参数传 null 就是全局搜索,会找到系统 DLL 里的导出。

hook 之后程序崩了怎么办?

先看 on_message 里的异常信息(Frida 会把 JS 异常和 native crash 分开报)。最常见的原因是参数类型读错(把指针当整数用了),另一类就是 replace 的宽度不对。

有没有办法检测自己被 hook 了?

有,读函数开头的字节看有没有被改成跳转。这也是很多反作弊的做法,属于攻防常规动作。

结语

Frida 的学习曲线在于「不知道有哪些 API」,而不在于概念难。真正要记的就三件事:怎么拿到函数地址、怎么在前后插逻辑、两侧怎么通信。

把这三件练熟,剩下的都是查文档的活。

参考资料

目录导航
  • 前置
  • 最小可跑:二十行
  • Interceptor 的三种用法
  • 两侧怎么通信
  • Windows 本地与 Android 的差别
  • 坑清单
  • 边界
  • FAQ
  • 结语
    • 参考资料
  • 相关文章:

  • PE 文件结构入门:导入表、节表,和「这个 EXE 到底链接了什么」2026-10-27 14:20:56
  • 反调试手段识别:调试点运行就退出,到底是谁在拦你2026-10-27 14:20:56
  • x64dbg 入门:断点、调用栈,以及「谁在改这个地址」2026-10-27 14:20:56
  • 用 ReClass 还原游戏结构体:从内存碎片到可直接用的偏移表2026-10-27 14:20:56
  • Cheat Engine 入门实战:从扫描到指针链,自己动手定位游戏数据2026-10-27 14:20:56
  • 发表评论

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。