准备迎接一次重大升级。这次我们在一个版本中解决了三个存在时间最久的限制。
限制一:fork()
简单回顾一下:这个传统 UNIX API 会克隆整个进程,向父进程返回子进程 ID,并向子进程 返回零。子进程获得父进程地址空间的独立副本;由于采用写时复制,开销通常很小。
涉及多线程后,处理起来就很棘手。子进程中只有调用 fork() 的线程会存活,因此如果其他 线程恰好持有锁,这些锁在子进程中仍处于持有状态,却再也没有线程会释放它们。
这意味着,同时使用 fork 和多线程的应用必须经过非常谨慎的设计。虽然大多数会 fork 的 应用是单线程的,但 Frida 注入 agent 后实际上会把它们变成多线程。文件描述符又是共享的, 同样需要小心管理。
我非常高兴地宣布,我们终于能检测到 fork() 即将发生,临时停止自身线程、暂停通信 通道,并在之后重新启动。这样就能在让子进程继续运行前,按需对它应用插桩。
限制二:execve()、posix_spawn()、CreateProcess() 等
通俗地说,就是程序启动其他程序:要么像 execve() 那样完全替换自身,要么像不带 POSIX_SPAWN_SETEXEC 的 posix_spawn() 那样启动子进程。
与 fork() 后的情况一样,现在可以在子进程执行第一条指令前应用插桩,并控制它何时开始运行。
限制三:处理进程突然终止
过去一个令人困惑的问题是:把数据交给 Frida 的 send() API 后,如果进程即将终止, 这些数据可能实际上无法到达另一端。
此前推荐的解决方案一直是挂钩 exit()、abort() 等函数,通过 send() 加 recv().wait() 的往返过程刷新仍在传输的数据。现在看来这并不理想,因为很难在多个 平台上都正确实现。如今我们有了更好的方案。
下面介绍新的 API 和功能。
子进程门控
这是解决前两个限制的方法。提供 create_script() 的 Session 对象现在也提供 enable_child_gating() 和 disable_child_gating()。Frida 默认行为仍与以前相同; 必须调用 enable_child_gating() 才会启用新行为。
从那以后,所有子进程都会处于挂起状态,你需要使用其 PID 调用 resume()。Device 对象还新增名为 delivered 的信号,应为其连接回调,以便在出现新子进程时收到通知。 此时应先按需应用插桩,再调用 resume()。Device 还新增 enumerate_pending_children() 方法,可获取全部待处理子进程。进程会一直保持挂起并 留在列表中,直到被你恢复或最终被终止。
理论就是这样。下面通过 Frida 的 Python 绑定查看一个实际示例:
import frida
from frida.application import Reactor
import threading
class Application(object):
def __init__(self):
self._stop_requested = threading.Event()
self._reactor = Reactor(run_until_return=lambda _:
self._stop_requested.wait())
self._device = frida.get_local_device()
self._sessions = set()
self._device.on("delivered", lambda child:
self._reactor.schedule(
lambda: self._on_delivered(child)))
def run(self):
self._reactor.schedule(lambda: self._start())
self._reactor.run()
def _start(self):
argv = ["/bin/sh", "-c", "cat /etc/hosts"]
print("✔ spawn(argv={})".format(argv))
pid = self._device.spawn(argv)
self._instrument(pid)
def _stop_if_idle(self):
if len(self._sessions) == 0:
self._stop_requested.set()
def _instrument(self, pid):
print("✔ attach(pid={})".format(pid))
session = self._device.attach(pid)
session.on("detached", lambda reason:
self._reactor.schedule(lambda:
self._on_detached(pid, session, reason)))
print("✔ enable_child_gating()")
session.enable_child_gating()
print("✔ create_script()")
script = session.create_script("""
Interceptor.attach(Module.getExportByName(null, 'open'), {
onEnter(args) {
send({
type: 'open',
path: Memory.readUtf8String(args[0])
});
}
});
""")
script.on("message", lambda message, data:
self._reactor.schedule(
lambda: self._on_message(pid, message)))
print("✔ load()")
script.load()
print("✔ resume(pid={})".format(pid))
self._device.resume(pid)
self._sessions.add(session)
def _on_delivered(self, child):
print("⚡ delivered: {}".format(child))
self._instrument(child.pid)
def _on_detached(self, pid, session, reason):
print("⚡ detached: pid={}, reason='{}'"
.format(pid, reason))
self._sessions.remove(session)
self._reactor.schedule(self._stop_if_idle, delay=0.5)
def _on_message(self, pid, message):
print("⚡ message: pid={}, payload={}"
.format(pid, message["payload"]))
app = Application()
app.run()运行效果如下:
$ python3 example.py
✔ spawn(argv=['/bin/sh', '-c', 'cat /etc/hosts'])
✔ attach(pid=42401)
✔ enable_child_gating()
✔ create_script()
✔ load()
✔ resume(pid=42401)
⚡ message: pid=42401,
↪payload={'type': 'open', 'path': '/dev/tty'}
⚡ detached: pid=42401, reason='process-replaced'
⚡ delivered: Child(pid=42401, parent_pid=42401,
↪path="/bin/cat", argv=['cat', '/etc/hosts'],
↪envp=['SHELL=/bin/bash', 'TERM=xterm-256color', …],
↪origin=exec)
✔ attach(pid=42401)
✔ enable_child_gating()
✔ create_script()
✔ load()
✔ resume(pid=42401)
⚡ message: pid=42401,
↪payload={'type': 'open', 'path': '/etc/hosts'}
⚡ detached: pid=42401, reason='process-terminated'
$退出前刷新
对于第三个限制,即处理进程突然终止,Frida 现在会拦截最常见的进程终止 API,并代为 刷新所有待处理数据。
不过,有些高级 agent 会缓冲数据并仅定期执行 send() 来优化吞吐量;现在它们可以在 进程终止或脚本卸载时运行自定义代码。只需定义一个名为 dispose 的 RPC 导出,例如:
rpc.exports = {
dispose() {
send(bufferedData);
}
};结语
基于 Frida 全新的 fork() 处理,我们还彻底重写了 Android 应用启动实现。 frida-loader-{32,64}.so 辅助 agent 已被移除,幕后的 Zygote 插桩现在利用全新的 子进程门控完成所有繁重工作。这意味着你也可以按自己的需要对 Zygote 插桩。只需记得 调用 enable_child_gating(),并对不关心的子进程调用 resume()。
本次发布大致就是这些。尽情享用吧!
oleavr