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 加载前附加调试器。只需使用调试器启动应用即可,无需一直保持附加;放宽后的代码签名状态一经设置就会持续有效。

支持的交互类型

  1. Listen
  2. Connect
  3. Script
  4. ScriptDirectory

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 配置相同。