这次发布让我格外兴奋。多年来我一直想简化 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 -wREPL
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!
oleavr