这次发布让我格外兴奋。多年来我一直想简化 Frida 的 JavaScript 开发体验。作为开发者,我可能从一个非常简单的 agent 起步,但随着它不断增长,痛苦也逐渐显现。

早期可能会想把 agent 拆分为多个文件,也可能想使用 npm 上的现成软件包,例如 frida-remote-stream。之后又会需要代码补全、内联文档、类型检查等,于是把 agent 迁移到 TypeScript 并启动 VS Code。

由于我们一直借助现有的优秀 Web 前端工具,拼图的各个部分其实都已具备。可以使用 Rollup 等打包器将源文件合并为单个 .js,可以使用 @frida/rollup-plugin-node-polyfills 与 npm 软件包互操作,也可以接入 @rollup/plugin-typescript 来支持 TypeScript。

不过,每次都重新搭建这么多基础设施很麻烦,所以我最终创建了 frida-compile:一个替你完成这些接线工作的简单工具,其默认配置针对 Frida 场景进行了优化。但它仍需要 package.json、tsconfig.json 等样板文件。

为解决这个问题,我发布了 frida-agent-example,这个仓库可以克隆后作为起点。但这仍有些麻烦,因此后来 frida-tools 又加入了名为 frida-create 的 CLI 工具。即便如此,我们仍要求用户安装 Node.js、处理 npm,并可能面对那些摆在那里的 .json 文件而感到困惑。

这时我突然想到:如果能用 frida-compile 把 frida-compile 本身编译成一个自包含的 .js,并在 Frida 的系统会话中运行,会怎么样?系统会话是一个不太为人熟知的功能,可以在托管 frida-core 的进程中加载脚本。例如使用 Python 绑定时,该进程就是 Python 解释器。

一旦能在 GumJS 内运行这个 frida-compile agent,就可以与它通信并将其转化为 API。随后可通过语言绑定公开该 API,frida-tools 也能使用它,为用户提供无需安装 Node.js/npm 的 frida-compile CLI 工具。当用户要求加载扩展名为 .ts 的脚本时,REPL 等工具也可以无缝使用该 API。

而这一切正是我们已经完成的工作!🥳

build()

在 Python 中使用它非常简单:

import frida

compiler = frida.Compiler()
bundle = compiler.build("agent.ts")

bundle 变量是一个字符串,可以传给 create_script(),也可以写入文件。

运行该示例时,可能会看到类似下面的内容:

Traceback (most recent call last):
  File "/home/oleavr/src/explore.py", line 4, in <module>
    bundle = compiler.build("agent.ts")
  File "/home/oleavr/.local/lib/python3.10/site-packages/frida/core.py", line 76, in wrapper
    return f(*args, **kwargs)
  File "/home/oleavr/.local/lib/python3.10/site-packages/frida/core.py", line 1150, in build
    return self._impl.build(entrypoint, **kwargs)
frida.NotSupportedError: compilation failed

这会让我们想知道它为什么失败,因此为 diagnostics 信号添加一个处理程序:

import frida

def on_diagnostics(diag):
    print("on_diagnostics:", diag)

compiler = frida.Compiler()
compiler.on("diagnostics", on_diagnostics)
bundle = compiler.build("agent.ts")

于是问题突然变得一目了然:

on_diagnostics: [{'category': 'error', 'code': 6053,
    'text': "File '/home/oleavr/src/agent.ts' not "
            "found.\n  The file is in the program "
            "because:\n    Root file specified for"
             " compilation"}]
…

我们忘记真正创建文件了!好,先创建 agent.ts:

console.log("Hello from Frida:", Frida.version);

再把该脚本写入文件:

import frida

def on_diagnostics(diag):
    print("on_diagnostics:", diag)

compiler = frida.Compiler()
compiler.on("diagnostics", on_diagnostics)
bundle = compiler.build("agent.ts")
with open("_agent.js", "w", newline="\n") as f:
    f.write(bundle)

现在运行它,就会得到一个可直接使用的 _agent.js:

$ cat _agent.js
📦
175 /explore.js.map
39 /explore.js
✄
{"version":3,"file":"explore.js","sourceRoot":"/home/oleavr/src/","sources":["explore.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC"}
✄
console.log(`Hello ${Frida.version}!`);

这种看起来很奇怪的格式,是 GumJS 让我们选择使用新 ECMAScript Module(ESM)格式的方式。在这种格式中,代码被限制在其所属模块内,而不会在全局作用域中求值。这也意味着可以加载多个导入/导出值的模块。.map 文件是可选的,可以省略;如果保留,GumJS 就能在堆栈跟踪中将生成的 JavaScript 行号映射回 TypeScript。

总之,来试运行一下 _agent.js:

$ frida -p 0 -l _agent.js
     ____
    / _  |   Frida 15.2.0 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to Local System (id=local)
Attaching...
Hello 15.2.0!
[Local::SystemSession ]->

成功了!现在重构一下,把代码拆分为两个文件:

agent.ts

import { log } from "./log.js";

log("Hello from Frida:", Frida.version);

log.ts

export function log(...args: any[]) {
    console.log(...args);
}

现在再次运行示例编译器脚本,它应生成一个看起来更有意思的 _agent.js:

📦
204 /agent.js.map
72 /agent.js
199 /log.js.map
58 /log.js
✄
{"version":3,"file":"agent.js","sourceRoot":"/home/oleavr/src/","sources":["agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,GAAG,CAAC,mBAAmB,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC"}
✄
import { log } from "./log.js";
log("Hello from Frida:", Frida.version);
✄
{"version":3,"file":"log.js","sourceRoot":"/home/oleavr/src/","sources":["log.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,CAAC,GAAG,IAAW;IAC9B,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;AACzB,CAAC"}
✄
export function log(...args) {
    console.log(...args);
}

把它加载到 REPL 中,应得到与之前完全相同的结果。

watch()

把这个玩具编译器变成一个工具:它加载编译后的脚本,并在磁盘上的源文件发生变化时重新编译:

import frida
import sys

session = frida.attach(0)
script = None

def on_output(bundle):
    global script
    if script is not None:
        print("Unloading old bundle...")
        script.unload()
        script = None
    print("Loading bundle...")
    script = session.create_script(bundle)
    script.on("message", on_message)
    script.load()

def on_diagnostics(diag):
    print("on_diagnostics:", diag)

def on_message(message, data):
    print("on_message:", message)

compiler = frida.Compiler()
compiler.on("output", on_output)
compiler.on("diagnostics", on_diagnostics)
compiler.watch("agent.ts")

sys.stdin.read()

开始运行:

$ python3 explore.py
Loading bundle...
Hello from Frida: 15.2.0

让它持续运行,再编辑磁盘上的源代码,应看到一些新输出:

Unloading old bundle...
Loading bundle...
Hello from Frida version: 15.2.0

太棒了!

frida-compile

还可以使用 frida-tools 新增的 frida-compile CLI 工具:

$ frida-compile agent.ts -o _agent.js

它也支持监视模式:

$ frida-compile agent.ts -o _agent.js -w

REPL

REPL 也由新的 frida.Compiler 提供支持:

$ frida -p 0 -l agent.ts
     ____
    / _  |   Frida 15.2.0 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to Local System (id=local)
Compiled agent.ts (1428 ms)
Hello from Frida version: 15.2.0
[Local::SystemSession ]->

致谢

感谢 @hsorbo!我们一起开发 frida.Compiler 的结对编程过程既有趣又高效!🙌

EOF

此版本还有不少其他精彩改进,请务必查看下面的变更日志。

祝使用愉快!

变更日志

  • core:添加 Compiler API。目前只通过 Python 绑定公开,但可从 C/Vala 使用。
  • interceptor:改进 replace(),支持返回原始实现。感谢 @aviramha!
  • gumjs:修复 writer 选项中 pc 的类型。
  • gumjs:修复存在循环依赖时 V8 ESM 崩溃的问题。
  • gumjs:处理每个模块具有多个别名的 ESM bundle。
  • gumjs:收紧 Checksum 数据参数的解析。
  • android:修复崩溃传递中的空指针解引用。感谢 @muhzii!
  • fruity:使用环境变量查找 usbmuxd。感谢 @0x3c3e!
  • ios:提高 Substrate 检测逻辑的韧性。感谢 @lemon4ex!
  • meson:仅在 V8 可用时才尝试使用。感谢 @muhzii!
  • windows:增加无 V8 构建支持。
  • devkit:修复 Windows 上的库依赖提示。感谢 @nblog!