是时候彻底改造 spawn() API,并修正 spawn gating 和 child gating API 中一些不够完善的地方了。

启动进程(spawn())

假设你正在使用 Frida 的 Python 绑定,目前会这样做:

pid = device.spawn(["/bin/cat", "/etc/passwd"])

或者这样启动一个 iOS 应用:

pid = device.spawn(["com.apple.mobilesafari"])

嗯,这个 API 真正能做的差不多也就这些了……只有一项例外,而 Python 和 Node.js 绑定还没有公开它。稍后我们会讲到。在此之前,先看看 frida-core 中的底层 API,各语言绑定正是将它公开给不同语言:

namespace Frida {
	…
	public class Device : GLib.Object {
		…
		public async uint spawn (string path,
			string[] argv, string[] envp)
			throws Frida.Error;
		public uint spawn_sync (string path,
			string[] argv, string[] envp)
			throws Frida.Error;
	}
	…
}

顺便说一下,这是 Vala 代码,frida-core 正是用这种语言编写的。它是一种类似 C#、可以编译为 C 的语言,而且非常棒。不过说远了。第一个方法 spawn() 是异步的,允许调用线程在调用进行期间处理其他事情;而 spawn_sync() 会阻塞,直到操作完成。

这两个方法会编译为下面三个 C 函数:

void frida_device_spawn (FridaDevice * self,
    const gchar * path,
    gchar ** argv, int argv_length,
    gchar ** envp, int envp_length,
    GAsyncReadyCallback callback, gpointer user_data);
guint frida_device_spawn_finish (FridaDevice * self,
    GAsyncResult * result, GError ** error);
guint frida_device_spawn_sync (FridaDevice * self,
    const gchar * path,
    gchar ** argv, int argv_length,
    gchar ** envp, int envp_length,
    GError ** error);

前两个函数共同构成 spawn():你先调用第一个函数并传入回调;回调被调用后,再调用第二个函数 spawn_finish(),并传入回调收到的 GAsyncResult。返回值是 PID;如果操作失败,则由 error 输出参数说明出了什么问题。如果你好奇,这就是 GIO 的异步模式。

至于第三个函数 spawn_sync(),Frida 的 Python 绑定使用的就是它。我们的 Node.js 绑定实际使用前两个函数,因为这些绑定完全异步。将来,如果能通过集成 Python 3.5 引入的 async/await 支持,也把 Python 绑定迁移为完全异步,那会很不错。

言归正传,回到上面的示例。我提到过,有一项功能尚未公开。仔细查看 frida-core API,你会注意到其中有一个 envp 字符串数组。查看绑定的内部实现便会发现,我们确实没有公开它,实际上做的是:

  envp = g_get_environ ();
  envp_length = g_strv_length (envp);

这意味着,我们只是把 Python 进程碰巧拥有的环境原样传了过去。如果实际的启动操作发生在完全不同的系统上,例如连接的 iOS 或 Android 设备上,这显然很不妥。这个问题之所以没有那么严重,是因为启动 iOS 和 Android 应用时会忽略 envp,只有启动普通程序时才使用它。

旧 API 的另一个问题是,声明 string[] envp 意味着它不可为空;如果声明为 string[]? envp,它才可以为空。因此,我们无法区分“不提供任何环境”(直觉上意味着“使用默认值”)和“使用空环境”这两种意图。

在着手修复 API 的这一点时,我意识到,是时候一并解决它长期存在的其他几个问题了,例如允许:

  • 在默认环境的基础上仅提供少数额外环境变量
  • 设置工作目录
  • 自定义 stdio 重定向
  • 传递特定于平台的选项

此前,我们一直将 stdio 重定向到自己的管道,并通过 Device 上的 output 信号传输所有输出。另外还有 Device.input(),用于写入 stdin。这些 API 保持不变,唯一的区别是我们不再默认执行这种重定向。不过,你们中的大多数人大概并未深受其扰,因为我们此前没有为 iOS 和 Android 应用实现这种重定向。从这个版本开始,我们终于为 iOS 应用实现了它。

现在,你大概已经想知道新 API 长什么样了。来看看:

namespace Frida {
	…
	public class Device : GLib.Object {
		…
		public async uint spawn (string program,
			Frida.SpawnOptions? options = null)
			throws Frida.Error;
		public uint spawn_sync (string program,
			Frida.SpawnOptions? options = null)
			throws Frida.Error;
	}
	…
	public class SpawnOptions : GLib.Object {
		public string[]? argv { get; set; }
		public string[]? envp { get; set; }
		public string[]? env { get; set; }
		public string? cwd { get; set; }
		public Frida.Stdio stdio { get; set; }
		public GLib.VariantDict aux { get; }

		public SpawnOptions ();
	}
	…
}

回到开头的 Python 示例,它们仍然可以原样工作。不过,除了:

device.spawn(["com.apple.mobilesafari"])

现在也可以这样做:

device.spawn("com.apple.mobilesafari")

因为第一个参数是要启动的 program。你仍然可以在这里传入 argv,它将用于设置 argv 选项,也就是说,argv[0] 会被用作 program 参数。还可以这样做:

device.spawn("/bin/busybox", argv=["/bin/cat", "/etc/passwd"])

如果想替换整个环境,而不是使用默认值:

device.spawn("/bin/ls", envp={ "CLICOLOR": "1" })

不过,在大多数情况下,你可能只想增加或覆盖几个环境变量,现在这也可以做到:

device.spawn("/bin/ls", env={ "CLICOLOR": "1" })

你可能还想使用不同的工作目录:

device.spawn("/bin/ls", cwd="/etc")

或者,你或许想重定向 stdio:

device.spawn("/bin/ls", stdio="pipe")

正如之前提到的,stdio 的默认值是 inherit。

现在,我们已经介绍了所有 SpawnOptions,只剩最后一项:aux。这是一个存放平台特定选项的字典。使用 Python 绑定设置这些选项非常简单:任何无法识别的关键字参数最终都会进入这个字典。

例如,启动 Safari 并让它打开指定 URL:

device.spawn("com.apple.mobilesafari", url="https://frida.re")

或者,你可能想在禁用 ASLR 的情况下启动 i/macOS 程序:

device.spawn("/bin/ls", aslr="disable")

另一个例子是使用指定 activity 启动 Android 应用:

spawn("com.android.settings", activity=".SecuritySettings")

以上其实就是我们目前支持的全部 aux 选项。好处在于,我们可以增加新选项,而不必更新绑定。

不过,在继续之前,先快速看看使用 Node.js 绑定时这个新 API 会是什么样子:

const pid = await device.spawn('/bin/sh', {
  argv: ['/bin/sh', '-c', 'ls /'],
  env: {
    'BADGER': 'badger-badger-badger',
    'SNAKE': true,
    'MUSHROOM': 42,
  },
  cwd: '/usr',
  stdio: 'pipe',
  aslr: 'auto'
});

如你所见,第二个参数是选项对象,无法识别的选项会进入 aux 字典。

11.0.0 中的其余变化

简单总结一下其余变化,先从 Device 类开始:

  • 为了符合语法,enumerate_pending_spawns() 现已改名为 enumerate_pending_spawn()。
  • spawned 信号已重命名为 spawn-added,并且现在还新增了 spawn-removed。
  • delivered 信号已重命名为 child-added,并且现在还新增了 child-removed。

最后一项变化是,Child 类的 path、argv 和 envp 属性现在都可以为空。这样就能区分例如“未提供 envp”和“提供了空 envp”。

11.0.1 中的变化

  • core:修复 32 位 ARM 进程中 agent 线程的栈对齐
  • core:修复脆弱的 SELinux 规则补丁逻辑

11.0.2 中的变化

  • core:修复 i/macOS 上 spawn() 逻辑中的 Mach 端口泄漏(长期存在的问题)

11.0.3 中的变化

  • core:修复 arm64 上 tls_base 计算错误导致 iPhone 8 和 X 崩溃的问题

11.0.4 中的变化

  • python:更新元数据并提升依赖版本

11.0.5 中的变化

  • core:修复与 Electra 的 Tweak Injector 的兼容性问题
  • core:修复 iOS 上 enumerate_processes() 中进程名被截断的问题
  • java:Java.registerClass() 现在支持重载
  • packaging:此后每个新版本都会提供 Fedora 和 Ubuntu 软件包

11.0.6 中的变化

  • python:修复依赖项规范,使从 PyPI 安装后 REPL 能够再次工作

11.0.7 中的变化

  • core:更完善地覆盖 Windows 上的 child gating API
  • python:从 PyPI 安装时,Linux 的 2.7 二进制文件不再损坏

11.0.8 中的变化

  • core:修复某些平台上设置系统会话时因竞态条件而导致崩溃的问题
  • python:修复解释器关闭时的死锁

11.0.9 中的变化

  • java:修复早期插桩期间无法调用 Java.registerClass() 的问题

11.0.10 中的变化

  • core:修复枚举具有空 DT_GNU_HASH 的 ELF 模块导入/导出时发生的崩溃

11.0.11 中的变化

  • java:修复类型兼容性检查;该问题通常会导致调用错误的重载,继而使虚拟机 abort()

11.0.12 中的变化

  • core:修复与最新版 macOS Mojave 和 iOS 12 测试版的兼容性问题
  • core:改进系统会话(即 PID 0)中可用的 iOS Kernel API
  • frida-trace:修复跟踪器脚本通过 send() 发送任意数据时发生的崩溃

11.0.13 中的变化

  • core:修复拆卸从未 load() 过的脚本时发生的卡死

结语(EOF)

差不多就是这些了。如果你还没有读过上周发布的 Frida 10.8 版本介绍,请务必前往这里阅读。

用得开心!