Gadget
当 Injected 运行模式不适用时,Frida 的 Gadget 是一个用于由待插桩程序加载的共享库。
可以通过多种方式做到这一点,例如:
- 修改程序源代码
- 修补程序或其某个库,例如使用 insert_dylib 之类的工具
- 使用 LD_PRELOAD 或 DYLD_INSERT_LIBRARIES 等动态链接器功能
动态链接器执行 Gadget 的构造函数后,Gadget 会立即启动。
它根据使用场景支持四种不同的交互方式,默认为 Listen 交互。可以添加配置文件来覆盖默认设置。该文件的名称应与 Gadget 二进制文件完全相同,但扩展名改为 .config。例如,如果二进制文件名为 FridaGadget.dylib,配置文件就应命名为 FridaGadget.config。
请注意,Gadget 二进制文件可以任意命名。这有助于避开某些反 Frida 检测方案,因为它们会查找名称中含有“Frida”的已加载库。
还应注意,使用 Xcode 向 iOS 应用添加 .config 时,Xcode 可能倾向于把 FridaGadget.dylib 放在名为“Frameworks”的子目录中,而把“.config”放在其上一级目录,也就是应用可执行文件和各种资源文件所在的位置。因此,在这种情况下,Gadget 也会到父目录中查找 .config,但仅限 Gadget 位于名为“Frameworks”的目录时。
在 Android 上,对于不可调试的应用,软件包管理器只会从其 /lib 目录复制名称符合以下条件的文件:
- 以
lib前缀开头 - 以
.so后缀结尾 - 文件名为
gdbserver
Frida 已充分考虑这一限制,也会接受按这些规则更名的配置文件。例如:
lib
└── arm64-v8a
├── libgadget.config.so
├── libgadget.so
更多信息请参阅这篇文章。
配置文件应为 UTF-8 编码的文本文件,其根节点是 JSON 对象。根级别支持四个不同的键:
-
interaction:描述所用交互方式的对象。默认为 Listen 交互。 -
teardown:值为minimal或full的字符串,指定卸载库时执行多少清理工作。默认为minimal,即不会关闭内部线程,也不会释放已分配的内存和操作系统资源。如果 Gadget 的生命周期与程序本身绑定,这样做没有问题。如果打算在某个时刻卸载 Gadget,请指定full。 -
runtime:值为default、qjs或v8的字符串,用于覆盖所使用的默认 JavaScript 运行时。 -
code_signing:值为optional或required的字符串。将其设为required,即可在没有附加调试器的受限 iOS 设备上运行。默认为optional,即 Frida 会假定可以修改内存中的现有代码并运行未签名代码,且这两种操作都不会导致内核终止进程。设为required也意味着 Interceptor API 不可用。因此,在受限 iOS 设备上使用 Interceptor API 的唯一方式,是在 Gadget 加载前附加调试器。只需使用调试器启动应用即可,无需一直保持附加;放宽后的代码签名状态一经设置就会持续有效。
支持的交互类型
Listen
这是默认交互方式。Gadget 会公开一个与 frida-server 兼容的接口,默认监听 localhost:27042。唯一的区别是,运行中进程列表和已安装应用列表都只包含一个条目,也就是程序自身。进程名称始终为 Gadget,已安装应用的标识符始终为 re.frida.Gadget。
为了实现早期插桩,Gadget 的构造函数会阻塞,直到你对进程执行 attach(),或者完成常规的 spawn() -> attach() -> …应用插桩… 步骤后调用 resume()。这意味着 frida-trace 等现有 CLI 工具可以继续按熟悉的方式使用。
如果不希望出现这种阻塞行为,而是让程序立即启动,或希望监听其他接口或端口,可以通过配置文件进行自定义。
默认配置如下:
{
"interaction": {
"type": "listen",
"address": "127.0.0.1",
"port": 27042,
"on_port_conflict": "fail",
"on_load": "wait"
}
}支持的配置键如下:
-
address:指定监听接口的字符串。支持 IPv4 和 IPv6。默认为127.0.0.1。指定0.0.0.0可监听所有 IPv4 接口,指定::可监听所有 IPv6 接口。 -
port:指定监听 TCP 端口的数字。默认为27042。 -
certificate:指定此项以启用 TLS。它必须是 PEM 编码的公钥和私钥,可以是包含多行 PEM 数据的字符串,也可以是指定加载路径的单行文件系统路径字符串。服务器会接受客户端提供的任何证书。 -
token:指定此项以启用身份验证。它必须是字符串,指定入站客户端应提供的机密令牌。 -
on_port_conflict:值为fail或pick-next的字符串,指定监听端口已被占用时应采取的操作。默认为fail,即 Gadget 启动失败。如果希望依次尝试后续端口,直到找到可用端口,请指定pick-next。 -
on_load:值为resume或wait的字符串,指定 Gadget 加载后应采取的操作。默认为wait,即等待你连接并通知它恢复执行。如果希望允许程序立即启动,请指定resume;如果只想稍后能够附加,这很有用。 -
origin:指定此项可防止网页浏览器未经授权的跨源使用,要求“Origin”标头与这里指定的值匹配。 -
asset_root:指定此项可通过 HTTP/HTTPS 提供静态文件,指定目录中任何可访问的文件都会公开。默认不提供任何文件。
Connect
这是“Listen”交互的反向形式:Gadget 不监听 TCP,而是连接到运行中的 frida-portal,并成为其进程集群中的一个节点。它所监听的是所谓的 cluster 接口。Portal 通常还会公开一个 control 接口,它使用与 frida-server 相同的协议。这样,任何已连接的控制器都可以对这些进程执行 enumerate_processes() 和 attach(),就像它们位于运行 Portal 的本机一样。
为了实现早期插桩,Gadget 的构造函数会阻塞,直到控制器请求 resume(),但只有启用了 spawn-gating 时才会如此。(通过 Device.enable_spawn_gating() 启用。)这意味着在简单配置中,Gadget 只会阻塞到连接 Portal 并加入其集群为止,以便询问 Portal 是否启用了 spawn-gating。
默认配置如下:
{
"interaction": {
"type": "connect",
"address": "127.0.0.1",
"port": 27052
}
}支持的配置键如下:
-
address:指定要连接的主机的字符串,Portal 的 cluster 接口公开在该主机上。支持 IPv4 和 IPv6。默认为127.0.0.1。 -
port:指定要连接的 TCP 端口的数字,该端口位于公开 Portal cluster 接口的主机上。默认为27052。 -
certificate:如果 Portal 启用了 TLS,则必须指定此项。它包含 PEM 编码的公钥,可以是包含多行 PEM 数据的字符串,也可以是指定加载路径的单行文件系统路径字符串。这是受信任 CA 的公钥,服务器证书必须与其匹配或由其派生。 -
token:如果 Portal 的 cluster 接口启用了身份验证,则必须指定此项。它是一个字符串,指定要提供给 Portal 的令牌。该字符串的实际解释取决于 Portal 实现:对于 frida-portal,它可以是固定机密;如果 Portal 通过 API 实例化并接入自定义身份验证服务,则可以是任何内容,例如 OAuth 访问令牌。 -
acl:字符串数组,指定访问控制列表,用于限制哪些控制器能够发现此进程并与之交互。例如,对于["team-a", "team-b"],来自“team-a”或“team-b”的任何控制器都会获得访问权限。只有 Portal 通过 API 实例化时才应设置此键,因为需要自定义应用代码为获准访问的控制器连接添加 tag,通常依据某种自定义身份验证方案。
高级用户
如果需要更强的控制能力,例如自定义身份验证、每节点 ACL 和应用特定的协议消息,也可以实例化 PortalService 对象,而不是运行 frida-portal CLI 程序。
Script
有时,只需在程序入口点执行前从文件系统加载脚本,就能以完全自主的方式应用插桩,这会很有用。
所需的最小配置如下:
{
"interaction": {
"type": "script",
"path": "/home/oleavr/explore.js"
}
}其中 explore.js 包含以下框架:
rpc.exports = {
init(stage, parameters) {
console.log('[init]', stage, JSON.stringify(parameters));
Interceptor.attach(Module.getGlobalExportByName('open'), {
onEnter(args) {
const path = args[0].readUtf8String();
console.log('open("' + path + '")');
}
});
},
dispose() {
console.log('[dispose]');
}
};使用语言桥接
从 Frida 17.0.0 开始,语言桥接不再随运行时捆绑。使用 Java.perform() 的脚本因此必须 import Java from 'frida-java-bridge'(frida-objc-bridge 和 frida-swift-bridge 同理),然后由 frida-compile 等打包工具处理。
rpc.exports 部分实际上是可选的,当脚本需要感知自身生命周期时很有用。
Gadget 会调用 init() 方法,并等待它返回后再允许程序执行入口点。这意味着,如果需要执行异步操作(例如 Socket.connect()),可以返回 Promise,从而保证不会错过任何早期调用。
第一个参数 stage 是值为 early 或 late 的字符串,用于判断 Gadget 是刚刚加载,还是脚本正在重新加载。后文会进一步介绍后一种情况。
第二个参数 parameters 是配置文件中可选指定的对象;未指定时则为空对象。它可用于为脚本提供参数。
如果脚本卸载时需要执行一些显式清理,也可以公开 dispose() 方法。通常会在进程退出、Gadget 被卸载,或从磁盘加载新版本前卸载当前脚本时发生这种情况。
调试时可以使用 console.log()、console.warn() 和 console.error(),它们会输出到 stdout/stderr。
支持的配置键如下:
-
path:指定要加载脚本的文件系统路径的字符串。也可以是相对于 Gadget 二进制文件所在位置的路径。在 iOS 上指定相对路径时,会先相对于应用的 Documents 目录查找脚本。这意味着可以使用 iTunes 文件共享上传脚本的新版本,也可以通过 AFC 提供整个容器来更新脚本;可调试应用允许这样做。与"on_change": "reload"一起使用时尤其方便。 此键没有默认值,必须提供。 -
parameters:包含任意配置数据的对象,这些数据将传递给init()RPC 方法。默认为空对象。 -
on_change:值为ignore或reload的字符串。ignore表示脚本只加载一次,reload表示 Gadget 会监控文件,并在文件每次变化时重新加载脚本。默认为ignore,但强烈建议在开发期间使用reload。
ScriptDirectory
某些情况下,你可能希望修改全系统的程序和库;但不是在脚本逻辑中识别程序,而是进行少量筛选,并根据 Gadget 当前所在的程序加载不同脚本。也可能完全不需要筛选,只是觉得把每个脚本作为独立插件更方便。在 GNU/Linux 系统上,这些脚本甚至可以由软件包提供,从而轻松为现有应用安装调整项。
所需的最小配置如下:
{
"interaction": {
"type": "script-directory",
"path": "/usr/local/frida/scripts"
}
}支持的配置键如下:
-
path:指定包含待加载脚本目录的文件系统路径的字符串。也可以是相对于 Gadget 二进制文件所在位置的路径。此键没有默认值,必须提供。脚本应使用 .js 作为扩展名,每个脚本旁边还可以有一个 .config 文件提供配置数据。这意味着 twitter.js 可以在名为 twitter.config 的文件中指定配置。 -
on_change:值为ignore或rescan的字符串。ignore表示目录只扫描一次,rescan表示 Gadget 会监控目录,并在每次发生变化时重新扫描。默认为ignore,但强烈建议在开发期间使用rescan。
每个脚本的可选配置文件可以包含以下键:
-
filter:包含脚本加载条件的对象。只需其中一项匹配即可,因此如有需要,复杂筛选应在脚本自身实现。支持以下用于指定匹配目标的键:-
executables:指定可执行文件名称的字符串数组 -
bundles:指定 bundle 标识符的字符串数组 -
objc_classes:指定 Objective-C 类名的字符串数组
-
-
parameters:包含任意配置数据的对象,这些数据将传递给init()RPC 方法。默认为空对象。 -
on_change:值为ignore或reload的字符串。ignore表示脚本只加载一次,reload表示 Gadget 会监控文件,并在每次变化时重新加载脚本。默认为ignore,但强烈建议在开发期间使用reload。
假设要为 Twitter 的 macOS 应用编写调整项,可以在 /usr/local/frida/scripts 中创建名为 twitter.js 的文件,内容如下:
const { TMTheme } = ObjC.classes;
rpc.exports = {
init(stage, parameters) {
console.log('[init]', stage, JSON.stringify(parameters));
ObjC.schedule(ObjC.mainQueue, () => {
TMTheme.switchToTheme_(TMTheme.darkTheme());
});
},
dispose() {
console.log('[dispose]');
ObjC.schedule(ObjC.mainQueue, () => {
TMTheme.switchToTheme_(TMTheme.lightTheme());
});
}
};然后,为确保该脚本只加载到这个特定应用中,应再创建名为 twitter.config 的文件,内容如下:
{
"filter": {
"executables": ["Twitter"],
"bundles": ["com.twitter.twitter-mac"],
"objc_classes": ["Twitter"]
}
}此示例表示,只要满足以下任一条件,就加载该脚本:
- 可执行文件名称为
Twitter,或 - bundle 标识符为
com.twitter.twitter-mac,或 - 已加载名为
Twitter的 Objective-C 类。
对于这个具体示例,可能只按 bundle ID 筛选,因为它是最稳定的标识符;如有需要,再在代码中进行兼容性检查。
除 filter 键外,还可以指定 parameters 和 on_change,与上面的 Script 配置相同。