Lazy loaded image
【看雪转载】Frida 整体启动逻辑
Words Read Time  min
2026-9-8
2026-9-8
type
Post
status
Published
date
Sep 8, 2026
slug
summary
《凡人修仙传之 - Android 逆向开发》· 03 动态插桩功法 · 技能 F01 前置:01 炼气境 · 第 05 课《Android 应用启动过程》、第 07 课《Android ptrace 原理》 后续:F02 脚本加载时机;F04 server IPC;F05 Android SELinux;F09 自定义 linker 版本锚点:上游源码统一锚定 GitHub frida/frida-core tag 17.9.1(2026-03-27,对应 frida 主仓库中 frida-core submodule commit a62376a3);frida-tools 取同期版本 14.8.0(2026-03-26)。正文行号与 #L 链接均实测自 17.9.1。凡标“当前工程/我们”的改动只描述设计思路与取舍,不附本工程源码。Android 14/API 34 实验记录。
tags
frida
看雪
category
转载
icon
password
网址
上次编辑时间
Sep 8, 2026 06:14 AM
comment
Hide
AI 总结

F01 · Frida 整体启动、进程注入与 Zygote 门控

《凡人修仙传之 - Android 逆向开发》· 03 动态插桩功法 · 技能 F01 前置:01 炼气境 · 第 05 课《Android 应用启动过程》、第 07 课《Android ptrace 原理》 后续:F02 脚本加载时机;F04 server IPC;F05 Android SELinux;F09 自定义 linker 版本锚点:上游源码统一锚定 GitHub [frida/frida-core tag 17.9.1](https://bbs.kanxue.com/elink@e2bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8
3!0J5k6g2\)9J5c8Y4c8J5k6h3g2Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4) (2026-03-27,对应 frida 主仓库中 frida-core submodule commit a62376a3 );frida-tools 取同期版本 14.8.09J5c8X3k6J5K9h3c8S2i4K6u0V1N6r3!0G2L8s2y4Q4x3V1k6@1M7X3g2W2i4K6u0r3x3e0c8Q4x3X3f1^5i4K6u0W2x3l9%60.%60.) (2026-03-26)。正文行号与 #L 链接均实测自 17.9.1。凡标“当前工程/我们”的改动只描述设计思路与取舍,不附本工程源码。Android 14/API 34 实验记录。

开课词库:先分清三组角色

下表按三个主题收齐本课正文会出现的主要名词。这张表不是预习作业——正文讲到每个词时都会重新解释,听课时遇到陌生词回来查即可;带 ★ 的 12 个是贯穿全课的主线词,先记住它们,并分清四个角色:谁发起启动请求、App 进程由谁复制、Frida 在哪里门控、脚本最终由谁执行。

A. Android 创建进程:从桌面到 App

术语
通俗解释
正文位置
桌面(Launcher)
主屏幕本身也是一个 App。点图标只是发出“我要启动谁”的请求,并不亲自创建进程
§0.1
Intent
描述“启动哪个组件、带什么参数”的消息对象
§0.1
Binder
Android 进程间通信的主要通道,桌面用它把启动请求交给系统
§0.1
★ system_server
系统服务集中所在的进程:解析启动请求、决定是否新建进程,但自己不执行 fork
§0.2
ATMS / ActivityStarter
管理页面启动与任务栈的系统服务,以及具体处理这一次启动的对象
§0.1
AMS / ProcessList
管理应用进程的服务;目标进程不存在时,由它准备新进程的身份、进程名等创建参数
§0.2
★ Zygote
所有 App 进程的共同父进程,新 App 进程由它复制而来
§0.3
Zygote socket
system_server 向 Zygote 下发创建命令的本地 socket 通道
§0.2
★ fork()
把当前进程复制成两份的系统调用;返回值区分父进程和子进程
§0.3
★ specialize
fork 之后给子进程设置具体 App 身份(uid、SELinux 域、进程名)的阶段
§0.3
★ USAP
Zygote 预先 fork 好、尚未绑定应用身份的备用进程;App 也可能从它出生
§0.5
ActivityThread
子进程进入 Java 世界的入口类;Application 和 Activity 在它之后才开始加载
§0.2、§0.3

B. Frida 控制与注入:从命令行到目标进程

本课主线是 spawn,词表顺序也按 spawn 的时间线排。
术语
通俗解释
正文位置
frida-tools / client
电脑上的 Frida 命令行或程序,负责向手机发送控制请求
§1
★ frida-server
运行在手机上的 Frida 服务端,接收电脑命令,指挥门控与注入
§3
★ spawn
让系统启动一个新 App,并在它刚出生时就接管;-f 的主线
§1、§4–§10
★ attach
接管一个进程;spawn 装载段对新生子进程做的就是一次 attach
§7、§11
RoboLauncher
frida-server 中负责 spawn 的模块:请求系统启动 App、等新进程上报 PID;门控注入也由它的 preload/ensure_loaded 执行
§4、§6.1
★ zymbiote
frida-server 启动(preload)即注入 Zygote/USAP 的小型门控载荷;新进程出生时上报 PID 并暂停等待放行
§4、§5、§6.2
★ ptrace
Linux 的进程调试接口:暂停目标、读写寄存器和内存,是注入的入口能力
§7.3
Linjector / Linux helper
frida-server 侧执行注入的模块:ptrace 目标、远程分配内存、送入引导代码
§7.2、§7.3
bootstrapper
最先进入目标进程的一小段引导代码,负责搭好内存和通信环境
§7.3
loader
接手装载的小程序:创建工作线程,把 frida-agent 装进目标并启动
§7.4、§8
★ frida-agent
最终进入目标 App 的完整 Frida 载荷;Gum 和脚本都在它里面运行
§7、§8、§9

C. 门控与会话:什么时候停,什么时候放

术语
通俗解释
正文位置
payload
送进目标进程的一小段代码或数据;zymbiote 和 frida-agent 都是 payload
§1、§5
RX / RWX
内存页权限:RX 可读可执行,RWX 再加可写;zymbiote 平时只保留 RX
§5、§6.2
尾页(padding)
ELF 映射最后一页里 segment 内容结束后的剩余填充字节;上游 17.9.1 的 zymbiote 就借 libstagefright.so 这里藏身
§5.2
setcontext / setArgV0
specialize 阶段先后执行的两个函数;zymbiote 通过改写指向它们的函数指针接入门控
§4、§5、§6.2
ACK
zymbiote 等待的 1 字节放行确认;非目标 App 由 server 立即回复,目标 App 要等客户端 resume()
§6.2、§6.3、§10
★ resume
放行操作:还原子进程的指针改写、发出 ACK,让 App 继续启动
§10
AgentSession
server 与目标进程内 agent 建立的一次控制会话,脚本挂在它上面
§9
Gum / GumJS
agent 里的插桩引擎和它的 JS 绑定,Frida 脚本的能力来源
§9
custom mapper
当前工程自己的 agent 装载器,替代 dlopen() 把 agent 匿名映射进目标
§8.2
最容易混淆的三个名字先单独钉住:

0. 录课导学:从桌面点击到 Zygote fork

本课不从开机讲起。先把 Zygote 当作一个已经运行、正在等待创建请求的父进程,只看一个最常见的现场:
目标 App 当前没有进程。用户在桌面点击图标以后,究竟是谁找到 Zygote,又是谁执行了 fork()
容易产生两个误判:
  1. Launcher 点击图标后直接创建了目标进程;
  1. system_server 收到启动请求后亲自 fork() 出 App。
两者都不准确。Launcher 只发起 Activity 启动请求; system_server 负责解析目标组件、检查启动条件并准备进程参数;真正复制进程地址空间的是 Zygote。先建立这条 Android 正常基线,后面才能看懂 Frida -f 为什么要提前处理 Zygote 和 USAP。
本节源码导航统一使用 Android Code Search9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1j5%60.) 的 AOSP main 链接 \[16\]–\[22\];课程实验仍以 Android 14/API 34 为基线。 main 后续增加的快速路径会单独标出,不把当前实现误写成所有版本唯一实现。

0.1 Launcher:点击图标只是发起启动请求

AOSP Launcher3 的参考实现把桌面图标点击交给 ItemClickHandler.onClick() ;厂商桌面的类名和动画实现可能不同,但最终仍要发起 framework 的 Activity 启动请求。普通应用图标依次进入:
源码入口见 ItemClickHandler.java \[16\]。这一步持有的是描述目标 Activity 的 Intent ,没有 fork() ,也没有加载目标 APK。请求继续经过 Android 的 Activity 启动接口,以 Binder 调用进入 system_server 中的 ActivityTaskManagerService (ATMS);ATMS 与 ActivityStarter 负责解析目标 Activity、任务栈、用户和启动限制 \[17\]。
第一段链路可先记成:

0.2 system_server:决定是否需要新进程

如果目标进程已经存在,系统可以直接向现有进程下发 Activity 生命周期事务,不需要再次请求 Zygote。
如果目标进程不存在,启动链会进入 AMS 的进程管理逻辑,最终由 ProcessList.startProcessLocked() 准备创建参数。这里有一个关键值 \[18\]:
这个值说明 Zygote 即将创建的不是“直接执行 APK main() 的进程”,而是以框架类 ActivityThread 为 Java 入口的新进程。目标 APK、 Application 和 Activity 要等新进程向 AMS 完成 attachbindApplication 后才开始加载。
ProcessList 随后通过 Process.start() 进入 ZygoteProcess.start() / startViaZygote() \[19\]。这一段会把下面这些参数编码成 Zygote 命令:
system_server 与 Zygote 之间使用本地 socket,而不是再走 Binder。 ZygoteProcess 把参数写入 Zygote socket,并同步等待子进程 PID:

0.3 Zygote:收到命令后才真正 fork

Zygote 已经在 ZygoteServer.runSelectLoop() 中监听命令。socket 到来后,server 创建 ZygoteConnection 并调用 processCommand() \[20\]。
连接建立时, ZygoteConnection 会读取对端凭据并限制调用方:
因此普通 App 不能绕过 system_server ,直接要求 Zygote 按任意 uid 创建进程。命令通过参数与权限检查后,常规路径进入:
Java 层的 forkAndSpecialize() 继续进入 native nativeForkAndSpecialize()com_android_internal_os_Zygote.cppForkCommon() 才是实际调用 fork() 的位置 \[21\]。
AOSP mainprocessCommand() 还会让满足条件的简单请求进入 Zygote.forkSimpleApps() 批处理快路。它改变的是命令处理与批量 fork 的组织方式,不改变“由 Zygote 家族创建进程、native 层执行 fork、父子进程从返回值处分流”这三个结论。本课先沿 forkAndSpecialize() 主线建立模型,再在 USAP 小节补充分支。
fork 返回后,同一段代码出现两条命运:
| 返回位置 | pid | 后续动作 | | --- | --- | --- | | 父进程 Zygote | > 0 | 把子进程 PID 回写给 system_server ,继续等待下一条命令 | | 新生 App 子进程 | 0 | 关闭不应继承的 socket/fd,按目标 uid、gid、capability、SELinux 域完成 specialize |
子进程随后经:
对应源码入口见 ZygoteInit.javaRuntimeInit.javaActivityThread.java \[22\]。到 ActivityThread.main() 时,新 App 进程已经出生,但应用自己的 Application.onCreate() 和 Activity 生命周期仍要等待后续绑定与事务分派。
子进程入口直达: ZygoteInit.java · RuntimeInit.java · ActivityThread.java

0.4 一张图看懂桌面点击到 App 子进程

0.5 USAP 是这条主线的重要分支

上图描述的是主 Zygote 收到命令后直接 forkAndSpecialize() 的常规路径。启用 USAP(Unspecialized App Process)池时,系统可能预先从 Zygote fork 出若干“尚未绑定具体应用身份”的进程;创建请求到来后, ZygoteProcess 优先尝试把参数交给一个 USAP,让它完成 specialize,而不是此刻才从主 Zygote 重新 fork \[19\]。
因此不能把所有设备上的实际路径都写成“点击后一定由主 Zygote 当场 fork”:
这正是 Frida 不能只处理 zygote / zygote64 的原因。只要目标 App 可能从 USAP 池出生,Frida 就必须把 usap32 / usap64 也纳入门控。

0.6 这条正常链与 Frida -f 在哪里接上

手点桌面图标与执行 frida -U -f TARGET_PACKAGE 的请求发起者不同:前者由 Launcher 发起,后者由 Frida 的 RoboLauncher 请求 Android 启动目标包。但进入 framework 的进程创建阶段后,两条路径都会汇入 Zygote / USAP 创建 App 进程的机制。
frida-server 启动时默认经 preload 链把小型 zymbiote 载荷装入可能产生目标进程的 Zygote、USAP 和 Chrome Zygote(spawn 里的 ensure_loaded() 只是幂等兜底,见 §3–§4)。之后发生 fork 时:
所以 Zygote 基线不是额外背景知识,而是理解 Frida spawn 的必要前提:
Frida 没有替 Android 创造另一套 App 启动机制。它先进入 Android 已有的进程父体,再利用 fork 的地址空间继承,把一个短暂门控点带进新生子进程。
后文第 4–10 节按 spawn 的五个步骤,继续拆解 RoboLauncherinject_zymbiote()setcontext / setArgV0 replacement、agent 注入与 ACK 放行的源码。

1. 问题边界:-f 的主线是 spawn

执行:
做的主事是 spawn :让系统启动一个全新的 App 进程,并赶在它的业务代码运行之前接管。它不是“把一段 JavaScript 发给目标进程”这么简单。Android 上至少发生两次性质不同的注入:
  1. zygote 门控注入 :把一个很小的 zymbiote 载荷写入 zygote、USAP 或 Chrome zygote,并 hook 两个函数指针;默认在 frida-server 启动时(preload)就完成,先于任何客户端命令,spawn 到来时只是幂等兜底;
  1. 目标进程 agent 注入(spawn 与 attach 共用) :得到子进程 PID 后,再通过 ptrace/bootstrap/loader 把完整 frida-agent 装入这个子进程。
最重要的结论是:
Frida 为了实现 -f ,会处理 zygote;但它不会把完整 frida-agent 常驻到 zygote。zygote 中驻留的是负责出生门控的 zymbiote 小载荷,完整 agent 最终进入目标 App 子进程。
从 frida-server 启动到 -f 跑完的完整时间线如下,也是本课第 3–10 节的展开顺序:
对照:不带 -f 、直接对已运行进程执行的独立 attach,只有上面中间“装载段”那一条链——没有门控段,也抓不到 App 最早期。本课主线沿 spawn 把这条链讲透,独立 attach 在第 11 节收尾时单独说。
本课只讨论“控制权如何进入 zygote 和目标进程”。JS 创建、加载与首行代码的执行边界在 F02;Java VM 与 App ClassLoader 的可用时机在 F03。

2. 进程与组件

| 组件 | 所在位置 | 作用 | | --- | --- | --- | | frida-tools / binding | PC | 调用 spawnattachresume 、脚本 API | | frida-server | Android 设备 | 接收控制请求,持有 HostSession | | RoboLauncher | server 进程 | Android App 启动、zygote/USAP 门控与 PID 匹配 | | zymbiote | zygote 及其子进程 | 在进程命名/SELinux specialize 节点向 server 报告新进程并等待放行 | | LinuxHelperBackend | server/helper 侧 | ptrace 目标、远程分配内存、执行 bootstrapper 与 loader | | loader | 目标进程 | 建立工作线程,接收 agent fd,装载 agent 并调用入口 | | frida-agent | 目标进程 | 初始化 Gum、DBus provider、脚本引擎和运行时能力 | | AgentSession | server 与 agent 两侧 | 承载某一次 attach 的脚本与消息会话 |
frida-server 在线只说明控制服务已经启动; zymbiote 已进入 zygote 只说明出生门控已建立; frida-agent 映射成功也只说明 native 载荷进入目标。三者不能互相替代。

3. frida-server 如何接住客户端请求

源码直达: server/server.vala @17.9.19J5c8X3k6J5K9h3c8S2i4K6u0V1j5
3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7
3g2J5N6X3g2J5i4K6u0W2N6X3q4D9j5g2\)9J5x3@1H3I4z5e0W2Q4x3X3c8x3x3U0V1&6) · src/control-service.vala @17.9.19J5c8X3k6J5K9h3c8S2i4K6u0V1j5
3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3j5
3g2Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6V1I4y4g2\)9J5k6p5H3&6x3K6b7%60.)
server/server.vala:199-220run_application() 创建 ApplicationApplication.start()server/server.vala:294-296 中构造并启动 ControlService
ControlService 内部持有设备侧 HostSession 。客户端连接后, control-service.vala:915-934spawn()attach() 转交给 host_session (17.9.1 实测行号,与快照一致):
启动 frida-server 的时候,它注入了什么、hook 了什么?
默认配置下的答案是:把 zymbiote 门控注入 zygote/USAP,并 hook 两个函数指针。 ControlServiceOptions.enable_preload 默认为 true( server.vala:17 ;命令行 -P / --disable-preload 可关),因此 ControlService.start() 建立监听后立即走 LinuxHostSession.preload()linux-host-session.vala:91-95 )→ RoboLauncher.preload()linux-host-session.vala:1382-1383 )→ ensure_loaded() ,把门控装进 zygote / zygote64 / usap32 / usap64 / Chrome zygote,并改写 setcontext / setArgV0 两个函数指针。这一切先于任何客户端命令;之后 spawn()enable_spawn_gating() 开头的 ensure_loaded() 只是幂等兜底;server 退出时 close() 还原指针并释放 payload( linux-host-session.vala:1386-1412 )。
注意被 preload 注入的只有门控载荷:完整 frida-agent 不会在启动时装进任何进程,它仍然要等客户端 spawn/attach、目标子进程 PID 确定之后才注入:
本课接下来沿 spawn 主线展开:第 4–5 节是门控(默认 server 启动时就位),第 6 节是启动与出生拦截,第 7–9 节是装载段,第 10 节是放行段。

4. zymbiote 门控:注入 zygote 并 hook 两个函数(spawn 的前置)

源码直达: src/linux/linux-host-session.vala @17.9.1 · RoboLauncher9J5c8X3k6J5K9h3c8S2i4K6u0V1j5
3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4\)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2\)9J5x3@1H3I4x3K6j5H3i4K6u0V1e0o6p5@1x3U0p5%60.)
门控不属于 spawn 流程:默认情况下,它在 frida-server 启动时经 §3 的 preload 链就已经完成,先于任何客户端命令。注入与卸载的时机只有这几处:
对应源码(17.9.1):preload 链 linux-host-session.vala:91-95, 1382-1383 ;spawn 兜底 1462 ;gating 兜底 1416-1417 ;close 卸载 1386-1412
hook 的对象是两个函数指针槽:specialize 阶段先后执行的 selinux_android_setcontext()android_os_Process_setArgV0() 。改写的是指针指向(指向 zymbiote 的 replacement 函数),不改函数机器码;槽位定位与写入流程在 §5.1。

4.1 注入对象:五个父进程

源码直达: ensure\_loaded() @17.9.19J5c8X3k6J5K9h3c8S2i4K6u0V1j5
3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4\)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2\)9J5x3@1H3I4y4e0p5%4i4K6u0V1e0o6p5$3x3o6b7%60.)
RoboLauncher.ensure_loaded()linux-host-session.vala:1517-1603 建立随机名称的抽象 Unix socket(socket 名见 :1531 ,形如 /frida-zymbiote-<uuid> ),然后枚举:
对应的选择逻辑是:
每个尚未处理的 PID 都进入 inject_zymbiote()
zygote 与 USAP 都是 App 的潜在父进程。只处理 zygote、不处理 USAP,会在启用 USAP 池的系统上漏掉从预热池出生的应用。Chrome/WebView 还可能再创建自己的子 zygote,因此需要单独延续门控状态。

5. 注入细节:payload 怎么装进父进程、装在哪里

源码直达: inject\_zymbiote() / do\_prepare\_zymbiote\_injection() @17.9.19J5c8X3k6J5K9h3c8S2i4K6u0V1j5
3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4\)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2\)9J5x3@1H3I4y4U0l9#2i4K6u0V1e0o6p5^5z5e0V1%60.) · helpers/zymbiote.c @17.9.19J5c8X3k6J5K9h3c8S2i4K6u0V1j5
3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3P5Y4W2E0j5X3W2G2N6r3g2Q4x3X3g2U0)
inject_zymbiote() 不走 frida_agent_main ,也不建立 GumJS。5.1 是注入流程,5.2 是载荷位置——位置直接决定 zygote 的 maps 里会多出什么特征。

5.1 注入流程:选位置、写入、改指针

上游 17.9.1 的 inject_zymbiote()linux-host-session.vala:1605-1648 )先经 do_prepare_zymbiote_injection() 完成全部定位,再一次性写入:
  1. 扫描父进程 maps,挑出 libstagefright.so 的可执行映射,取 最后一页 作为载荷位置(取舍见 5.2);
  1. 同时定位 libc.solibselinux.solibandroid_runtime.so ,解析导出/导入表找到两个接入点,并在 boot heap / 匿名 rw 映射里搜索 setArgV0 的指针槽;若发现该父进程已被注入过( already_patched ),改按 file_offset 从磁盘上的 so 文件读回原始字节,补全还原账本;
  1. STOP + wait_until_stopped 暂停父进程;
  1. patches.apply() 把 payload 写入尾页,再把两个指针槽改到 payload 的 replacement;
  1. finally CONT 恢复父进程。
核心写入顺序(上游 17.9.1 摘录):
两个接入点的定位来自 do_prepare_zymbiote_injection()linux-host-session.vala:1692-1899 ):
  • libandroid_runtime.so 导出表定位 android_os_Process_setArgV0():1814 );
  • libselinux.so 导出表与其在 libandroid_runtime.so 导入表中的 slot 定位 selinux_android_setcontext():1796, :1822 );
  • 在 zygote 的 boot heap 与匿名 rw 映射中搜索保存 setArgV0 原函数地址的指针槽(:1857-1875 )。
最终改写的是“函数指针指向哪里”,不是直接改写两个函数的机器码。
我们的差异集中在载荷位置:不借尾页,改由 helper 在父进程远程 mmap 一块全新的匿名 RX 页写入(helper 侧为本工程扩展,上游无此路径),中途失败即 unmap 还原,注入前还会校验并回收可能遗留的旧 payload(§12);暂停、写入、改槽、恢复的顺序与上游一致。

5.2 载荷位置:上游借媒体库尾页,当前工程换独立匿名映射

**我们的核心取舍,先钉在前面:不像上游那样把 payload 写进任何 so,而是独立 mmap 一块匿名页;门控销毁自己靠的就是对这页的 munmap ——自毁与释放是同一个动作、同一个时刻。整套“匿名映射进、 munmap 出、全程不碰已有 so”的装载方式,等同于把门控改成了 ZygiskNext 一类的加载路子:载荷生命周期完全自管理,用完即整体消失。**
当前工程:一块全新的匿名 RX 页。 我们让 helper 在父进程远程 mmap 一块全新匿名页,权限直接给 RX;中途任何一步失败都会把映射 unmap 还原。生命周期只有三步(逻辑示意,非关键实现):
写入的 payload 本体是预编译 zymbiote 小 ELF 的 可执行段make_zymbiote_payload() (上游 17.9.1 [linux-host-session.vala:1911-2000](https://bbs.kanxue.com/elink@f8aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8
3!0J5k6g2\)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4\)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2\)9J5x3@1H3I4z5e0p5I4i4K6u0V1e0o6t1H3x3o6l9%60.) )只截取 text 段,并在段内数据区依次写入 server socket 名、payload 自身的 base/size/原始权限、两个原函数地址,以及 12 个 libc 函数地址。这张 API 表不是我们的发明——上游 17.9.1 写入的就是 mprotect/strdup/free/socket/connect/__errno/getpid/getppid/sendmsg/recv/close/raise ,保存 package_name 靠的正是其中的 mprotect + strdup (也是 §5.2.1 切口的成因);我们的差异是 **去掉 raise 、补上 munmap** ,把收尾从 raise(SIGSTOP) 等 server 还原,改为 ACK 后 payload 自行 munmap (§10、§12)。整块载荷只有一两页,也不进入任何 so 装载账本。
上游:不新建映射,借媒体库最后一页。 上游 do_prepare_zymbiote_injection() (17.9.1 [linux-host-session.vala:1712-1721](https://bbs.kanxue.com/elink@9c9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8
3!0J5k6g2\)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4\)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2\)9J5x3@1H3I4y4K6p5J5i4K6u0V1e0o6p5%4x3U0p5%60.) )扫父进程 maps,找 已存在的可执行文件映射 ,路径以 /libstagefright.so 结尾(17.9.1 只认这一个候选,不挑 libmedia.so ):
媒体库最后一个可执行 segment 的内容通常不满一整页,页尾剩余字节是无人使用的填充;payload 就直接 pwrite 进这一页,权限沿用该页原有的 RX,maps 里不发生任何变化。也因为这一页是文件映射,上游把“恢复原状”实现为按 file_offset 从磁盘上的 so 文件读回原始字节。
这段 payload 没有经过任何 linker 装载,“重定位”由 server 手工完成:把上面那 12 个 libc 函数地址填进 payload 自带的 API 结构体(参考文献 \[23\] 对 Frida 17.6.0 的逐行分析与此一致)。17.9.1 的两个指针槽 不自还原 ——replacement 触发后照常透传原函数,改写一直保留,直到放行时由 server 统一还原(“replacement 第一行就还原槽位”的一次性 hook 是 17.6.0 时期的旧实现 \[23\])。上游 API 表里的 raise 用在子进程上报并收到 ACK 之后:payload raise(SIGSTOP) 把自己挂起,给外部留出稳定的注入窗口,gadget 由 server 在这个窗口里以 ptrace 注入;我们的 API 表去掉 raise 、补上 munmap ,ACK 后 payload tail-call munmap 自卸载,不再需要 SIGSTOP/SIGCONT 往返,指针槽由 server 在 resume 时统一还原(§10、§12)。
两种位置各暴露一个面:
| | 上游:借媒体库尾页 | 当前工程:独立匿名映射 | | --- | --- | --- | | maps 变化 | 父进程 一个条目都不多 ,payload 藏在合法媒体库映射内部;但门控执行过的子进程会留下 §5.2.1 的 VMA 切口 | zygote/USAP 的 maps 多出一块 匿名可执行映射 (整块翻权限,不产生切口) | | 文件一致性 | 尾页字节被改写,与磁盘上的 so 文件 不一致 | 不改任何文件映射,所有 so 页与磁盘一致 | | 恢复方式 | 从磁盘 so 按 file_offset 读回原字节 | 还原指针槽 + payload 自卸载 munmap | | 生命周期 | payload“消失”要靠 server 把尾页字节写回去,so 始终被动过 | munmap 即自毁,与门控销毁同时刻,无需还原任何 so 字节 |
一句话:上游藏住“maps 条目数”,留下“文件内容差”;当前工程消掉“文件内容差”,换来“匿名可执行页”。检测与反检测都会用到这两个面,具体观察手段在 F06 起的课程展开。

5.2.1 第三个观察面:门控子进程的 VMA 切口(真机实测)

2026-08-30 真机记录(TB321FU):zygote64(pid 1392)中 libstagefright.so 的可执行段是一条 r-x;在经过门控的 App 子进程(父进程同为 1392)里,同一可执行段被切成 两条相邻 r-x ,切口正好落在最后一页——即上游 payload 位置 m.end - page_size (file offset 0x1f9000):
成因对应 §5.1–§5.2 的两条写入路径,各占一半:
  1. 父进程不变 :server 经 /proc/<pid>/mem 写尾页是页表级 COW,不触碰 VMA 树;两个 replacement 又只在 fork 出的子进程里执行(specialize 是子进程路径),zygote 自身从不执行那次 mprotect 。所以父进程永远一条 r-x,与实测一致。
  1. 子进程切口 :fork 继承 payload 与指针改写后,setcontext replacement 在保存 package_name 时执行 mprotect (payload_base, payload_size, RWX) (上游 17.9.1 [zymbiote.c:70](https://bbs.kanxue.com/elink@e01K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8
  1. 3!0J5k6g2\)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3P5Y4W2E0j5X3W2G2N6r3g2Q4x3X3g2U0i4K6t1K6e0o6M7H3) 的权限窗口)。mprotect 是 VMA 级操作,内核把 r-x 段切成 r-x | rwx | r-x 三段;窗口结束恢复 RX 后两侧权限一致,但 VMA 边界不愈合 ——实测采样时权限已恢复而切口仍在,说明这是持久痕迹而非瞬态。
检测侧含义:同一.so 出现 页粒度的相邻同权限条目 ,是 mprotect 权限循环的指纹,与“匿名可执行页”“文件内容差”并列为第三个观察面。
边界与对账:该切口只出现在“payload 位于文件映射内部”的布局。2026-08-30 补记:TB321FU 为课程演示机,运行的是 上游原版 frida-server (尾页布局),实测切口与其行为一致;课程主线展示上游行为,本工程的修改版只以思路对照(§12),不附源码——独立匿名映射布局整块翻权限、不产生切口,且 ACK 后 munmap 整体消失。

6. spawn 执行:请求启动 App,子进程出生上报

6.1 RoboLauncher.spawn():执行顺序与启动请求

源码直达: RoboLauncher.spawn() @17.9.19J5c8X3k6J5K9h3c8S2i4K6u0V1j5
3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4\)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2\)9J5x3@1H3I4y4o6b7J5i4K6u0V1e0o6p5#2x3o6t1%60.)
spawn 请求在 §3 的 HostSession.spawn() 落地。目标参数是 Android 包名,包名不是普通可执行文件路径, linux-host-session.vala:338 因此进入:
RoboLauncher.spawn()linux-host-session.vala:1442-1502 )的执行顺序:
stop_package() / start_package() 位于 linux-host-session.vala:1476-1477 ;请求进入 framework 之后,走的就是 §0.2–§0.3 已经拆过的正常链,直接引用:
fork 出的子进程继承门控,在 specialize 阶段撞上 zymbiote(6.2)。

6.2 子进程如何被识别并卡在正确时机

源码直达: helpers/zymbiote.c @17.9.1 · 两个 replacement9J5c8X3k6J5K9h3c8S2i4K6u0V1j5
3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3P5Y4W2E0j5X3W2G2N6r3g2Q4x3X3g2U0i4K6t1K6e0o6j5H3i4K6u0V1e0o6V1^5)
zygote fork 后,匿名 payload 和被改写的指针槽由子进程继承。 helpers/zymbiote.c 中有两个 replacement:
setcontext replacement 先调用原始 selinux_android_setcontext() ,再保存 specialize 阶段得到的进程名。它不阻止 SELinux 域切换。两个 replacement 分工不同: setcontext 负责取名字,setargv0 负责卡时机
为什么取名要靠 setcontext?hello 报文要带 process-name,server 靠它匹配 spawn 请求(6.3)。native 侧进程名最早以 C 字符串形式出现在 selinux_android_setcontext(uid, is_system_server, seinfo, name)name 参数里,specialize 前段就能 strdup 一份(上游 17.9.1 zymbiote.c:64-71 );而 setArgV0 拿到的是 jstring ,要变成本地字符串必须走 JNI。payload 首选用 setcontext 存下的副本, GetStringUTFChars 只是兜底( zymbiote.c:88-90 )。相应地,setcontext 的调用槽从 libandroid_runtime.so 的导入表(GOT)就能定位,比堆扫描稳;代码里找得到才打补丁( setcontext_slot == 0 则跳过),与 setArgV0 的堆扫描槽互为保险。
为什么卡点选在 setArgV0?setcontext 处在 specialize 前段,SELinux 域切换之后还有 uid/gid 设置、capability 清理、fd 关闭等一串步骤;setArgV0 在 specialize 尾声,进程身份已全部就位、App 代码一行未跑。在 setcontext 阻塞会把注入窗口开在一个“身份做了一半”的进程上;在 setArgV0 上报才是既早又完整的窗口。所以 setcontext 只抄名字不打断,setargv0 才连接 server、上报并等待 ACK。
setargv0 replacement 先调用原始 android_os_Process_setArgV0() ,随后:
对应代码骨架是:
payload 默认是 RX;保存和清空 package_name 指针时会短暂切为 RWX,完成后恢复 payload_original_protection (上游 17.9.1 zymbiote.c:70, :98 )。这段权限窗口也是后续验证必须覆盖的瞬态状态。尾页布局下,权限恢复后子进程 maps 里的 VMA 切口并不随之愈合——瞬态权限操作留下了持久痕迹(真机实测与成因见 §5.2.1)。至于 ACK 到达之后门控做什么——上游停稳自己给 server 开还原窗口、我们 munmap 自毁——“谁动手写槽位”的分工在 §10 开头单独钉死。
frida_wait_for_permission_to_resume() 发送固定头和进程名:
这里必须注意: recv() 不是“只有目标 App 才走”的代码。只要普通子进程从已经安装 zymbiote 的 Zygote/USAP 出生、继承了两个函数指针改写,并正常执行到 setArgV0 ,它就会连接 server、发送 hello,然后在这里等待 server 作出决定。目标与非目标的分流发生在 server 收到 hello 之后 ,见下一节。
“所有 App 都会经过”仍有边界:已经在门控安装前出生的进程不会倒回这里;未被 ensure_loaded() 覆盖的其他 AppZygote 也不在当前枚举范围内;如果 abstract socket 连接失败,payload 会直接返回而不会停在 recv()

6.3 server 收到 hello 后:目标等待,非目标立即放行

源码直达: handle\_zymbiote\_connection() @17.9.19J5c8X3k6J5K9h3c8S2i4K6u0V1j5
3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4\)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2\)9J5x3@1H3J5x3o6f1J5i4K6u0V1e0o6t1H3z5o6R3%60.)
server 在 linux-host-session.vala:2052-2088handle_zymbiote_connection() 里收到 hello 后,不会立刻给每个子进程注入 agent,而是先判断它属于哪一条分支:
四条分支如下:
| 子进程 | server 的判断 | 接下来的动作 | | --- | --- | --- | | 本次 spawn(package) 的目标 | spawn_requests.unset() 精确命中(上游按 spawn 时预解析的 process-name 登记请求;我们先精确匹配进程名,带 : 时再回退到包名) | 保存连接,返回目标 PID;继续等待客户端 attach、加载脚本和 resume() | | 开启全局 spawn gating 后捕获的 App | 没有单次 spawn 请求,但 spawn_gating_enabled=true | 加入 pending_spawn 并上报 spawn_added ;等待客户端决定何时放行 | | 普通非目标 App | 两项都不命中 | 立即调用 connection.resume() ,不注入完整 frida-agent | | Chrome 子 zygote | package_name == com.android.chrome_zygote | 不发送 ACK,把父级 patch 记录交给它,继续门控后续 renderer |
所以非目标 App 也会短暂走到 recv() ,但不会一直卡住。当前工程对它立即执行下面这条清理链:
这一分支没有 Device.attach(child_pid) ,不会装入完整 agent,也不会创建脚本。它付出的只是一次 hello/ACK 往返和门控清理时间;父 Zygote 中的门控仍保留,用于观察下一次 fork。
最新版上游的分流判断与 17.9.1 相同:非目标 App 也立即进入 connection.resume() (17.17.0 亦然 \[24\])。差别只在清理协议:上游先发 ACK,让 payload tail-call raise(SIGSTOP) ,server 等子进程停稳后还原继承的改写并发送 SIGCONT (17.9.1 linux-host-session.vala:2175-2194 );当前工程则先还原相应指针槽、再发 ACK,由 payload 自行 munmap() 。两者都不会向非目标 App 注入完整 agent。
目标 App 则不同:上游 17.9.1 在 spawn 时就经 helper.get_process_name() 预解析出真实 process-name 并登记请求(:1466-1472 ),hello 到达后按 package_name 精确 unset ,没有冒号回退;当前实现在此之上先尝试精确匹配进程名,若进程名带 :,再用冒号前的包名匹配原始 spawn 请求。匹配成功后, Device.spawn() 返回 PID,但连接仍保存在 zymbiote_connections ,ACK 要等客户端完成第 7–9 节并调用 resume() 才发送。
到这里,App 子进程仍停在 zymbiote 的等待点上,业务代码一行未跑;返回的 PID 就是下一步注入完整 agent 的目标。

7. 装载段:向刚出生的子进程注入完整 frida-agent

spawn 返回 PID 的那一刻,子进程正阻塞在 setArgV0 之后的 zymbiote 等待点, Application 还没加载,Java 入口没跑。客户端此刻要做的,是在放行之前把完整 frida-agent 装进这个子进程。这一步用的正是一条标准 attach 装载链;独立 attach 与它的差别在第 11 节对照。
子进程已经被 zymbiote 卡住了,装 agent 为什么还要 ptrace?因为 zymbiote 只是一两页的信标加闸门:它总共只带 mprotectmunmapsocketconnect 等 12 个 libc 函数(§5.2),没有 mmap 、没有 pthread_create ,也没有任何 ELF 装载能力,装不下、也运不了几百 KB 的 agent。子进程阻塞在 recv() 上只说明业务代码还没跑,不说明它受控——server 对它依旧没有写内存和执行代码的权限。把 agent 装进去并启动,仍然要 seize 线程、保存寄存器、远程 mmap 、改 PC/SP 执行 bootstrapper 与 loader、恢复寄存器后 detach,这正是 7.3 的 SeizeSession 。ACK 之后 zymbiote 还会自卸载(§10),它从设计上就不承担装载。上游也是同一分工:zymbiote 只取“时机”,gadget 由 server 在外部以 ptrace 注入(参考文献 \[23\])。

7.1 spawn 返回 PID 后,立刻对这个 PID 做一次 attach

源码直达: frida\_tools/application.py @14.8.09J5c8X3k6J5K9h3c8S2i4K6u0V1N6r3!0G2L8s2y4Q4x3V1k6T1L8r3!0T1i4K6u0r3x3e0c8Q4x3X3f1^5i4K6u0W2x3q4\)9J5c8X3k6J5K9h3c8S2i4K6g2X3N6r3!0G2L8s2y4Q4x3V1k6S2M7s2m8D9K9h3y4S2N6r3W2G2L8W2\)9J5k6i4m8&6i4K6t1K6e0o6j5I4y4W2\)9J5k6p5H3
3#2Q4x3V1k6X3M7X3W2V1j5g2\)9J5c8X3k6J5K9h3c8S2i4K6u0V1N6r3!0G2L8s2y4Q4x3V1k6T1L8r3!0T1i4K6u0r3x3e0c8Q4x3X3f1^5i4K6u0W2x3q4\)9J5c8X3k6J5K9h3c8S2i4K6g2X3N6r3!0G2L8s2y4Q4x3V1k6J5k6i4m8D9i4K6u0W2M7s2W2Q4x3U0y4x3x3U0b7%4i4K6u0V1e0o6x3H3y4R3%60.%60.) · src/frida.vala @17.9.19J5c8X3k6J5K9h3c8S2i4K6u0V1j5
3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3k6Y4u0A6k6r3q4Q4x3X3g2
3x3H3%60.%60.) · src/host-session-service.vala @17.9.19J5c8X3k6J5K9h3c8S2i4K6u0V1j5
3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3K9r3!0K6N6q4\)9J5k6s2y4W2M7%4y4A6L8
3K9h3y4W2i4K6u0W2N6X3q4D9j5g2\)9J5x3@1H3#2y4K6q4Q4x3X3c8x3y4U0p5I4)
frida_tools/application.py:616-626 (14.8.0,与 frida 17.9.1 同期)直接给出了命令行工具在 spawn 返回后的下一步: device.spawn() 返回的 PID 被赋给 attach_target ,随后立即调用 _attach(attach_target)repl.py:294-306 再创建并加载脚本,是否立即 resume 由 :247-252 的选项决定。
也就是说,spawn 的后半段就是对新生子进程的一次标准 attach:
所以 zygote 门控与 agent 注入不是二选一,而是前后相接:
这条装载链的 API 侧入口如下。 src/frida.vala:1138-1163Device.attach() 先调用远端 host_session.attach() ,再把返回的 AgentSessionId 链接成本地 Session
设备侧 host-session-service.vala:571-611 继续执行( attach():571establish():605 ):
这里的 attach() 不是单一系统调用,而是一条从控制面到目标进程运行时的状态机。

11. 收尾一提:独立的 attach 已运行进程

本课主线是 spawn。如果不需要“早于业务代码”,也可以省掉整个门控段,直接对已运行进程执行:
此时走的就是第 7 节拆过的那条装载链,一个环节都不少:
与 spawn 只有两点不同:
  1. 前面没有 zymbiote 门控段,也就没有第 4–6 节的出生拦截;
  1. 时机晚:目标进程早已跑完 specialize、 Application.onCreate 等早期初始化,脚本只能从 attach 时刻开始观察;也没有第 10 节的 ACK 放行点,注入完成即继续运行。
所以独立 attach 是第 7 节那条装载链的单独使用;要 hook Application.onCreate 、早期 native 初始化或反调试逻辑,必须用 -f 走完整 spawn。

12. 当前工程相对上游改变了什么

以下只列与“启动和注入主链”直接相关、且能由上游 17.9.1 源码对照确认的变化。普通版本演进造成的接口差异不计入本表;线程名、默认 Hook、Stalker、pthread 字段和 maps 内核协作分别在 F06-F11 展开。
| 改动面 | 上游主线 | 当前工程 | 直接结果 | 边界 | | --- | --- | --- | --- | --- | | agent 装载 | /proc/self/fd/<fd> + bionic dlopen/dlsym | agent-mapper 匿名映射并自行 relocation | agent 不进入 bionic 常规装载账本 | mapper 只支持受限 ELF;失败不自动回退 | | agent 自定位 | agent 从运行时视图恢复自身范围 | loader 传 agent_base/agent_size | maps 视图变化后仍能确定 agent span | C/Vala ABI 必须完全一致 | | Gum program 模块发现 | 沿原有模块发现路径 | agent 启动时开启 Android fast path | 减少 maps 过滤对内部功能的反噬 | 不会让匿名 agent 自动进入 linker 账本 | | zymbiote 载荷位置(§5.2) | 借用 libstagefright.so 尾页 | 独立匿名 RX 映射:mmap 进、munmap 毁,不写任何 so | 不再改写媒体库文件映射的尾页 | 新增匿名可执行映射窗口 | | child 放行 | ACK 后 tail-call raise(SIGSTOP) ,server 等 stop、还原再 SIGCONT | ACK 前先还原指针;ACK 后 payload tail-call munmap | 子进程稳定态不保留 zymbiote payload | ACK 丢失或异常退出仍需清理 | | 遗留处理 | 再注入时按 already_patched 从磁盘 so 读回原字节补全还原账本(:1614-1641, :1856-1884 ) | 注入前扫描并校验旧 payload,恢复指针后 unmap | server 异常留下的旧状态可在下一次启动前收口 | 扫描必须严格校验地址、大小和 ABI | | 多进程名匹配 | spawn 时经 helper 预解析 process-name,hello 精确 unset:1466-1472, :2069 ) | 先精确匹配,再对 :remote 回退包名 | 同时支持指定进程名和包级 spawn | 同包多进程仍需明确目标语义 |

12.1 自定义 mapper 改的是 agent 装载,不是 ptrace 原语

ptrace、远程 mmap 、bootstrapper、loader 启动和 control channel 仍然存在。当前工程替换的是 loader 内部的:
而不是把整条注入链改成另一种技术。若 ptrace 失败,自定义 mapper 根本没有运行机会。

12.2 zymbiote 自卸载改的是门控收尾

zygote 父进程仍需要保留 payload 与两个接入点,否则无法观察后续 fork。自卸载发生在普通 App 子进程收到 ACK 以后,目标是清除 子进程继承来的临时门控状态 ,不是让父 zygote 从未被修改。

12.3 Gum.Cloak 不是系统级隐藏

agent 获得自己的 base/size 后调用 Gum.Cloak.add_range() ,只会影响 Frida 自己提供的部分枚举结果。Linux 内核仍然持有 VMA;系统 /proc/<pid>/maps 是否过滤由 xiaojia-hide 的私有 prctl ABI 与 procfs 实现决定,详见 F11 和 04 内核工程 M02。

13. 两条路径的时序对照

spawn Android App(-f 主线)

放行段最后三步是当前工程协议;上游为 ACK → tail-call raise(SIGSTOP) → server 等停、还原 → SIGCONT (§10)。

attach 已运行进程(对照)

14. 故障定位

按 spawn 主线从前往后排查,装载段的问题最后查。
| 表象 | 最先检查的状态 | 对应源码(上游 17.9.1;本工程差异另注) | | --- | --- | --- | | -f 等不到 PID | zygote/USAP 是否处理、函数槽定位、hello socket | linux-host-session.vala:1517-1899 | | server 重启后 zygote 注入失败 | stale payload 与旧指针槽是否清理 | 上游 already_patched 路径 linux-host-session.vala:1614-1641, 1856-1884 ;本工程清理链见 §12 | | Chrome renderer 未门控 | Chrome zygote 的二级继承状态 | linux-host-session.vala:2055-2066 | | 子进程拿到 PID 后不继续 | spawn 是否 attach/load/resume、ACK 是否发出 | linux-host-session.vala:2052-2194 | | 子进程仍有 zymbiote 映射 | 上游看 ACK 与 raise(SIGSTOP) ;本工程看 ACK、自卸载与映射确认 | 上游 zymbiote.c:105-109, 191-253 ;本工程 munmap 自卸载见 §10 | | 能枚举进程,attach 失败 | ptrace 权限、目标 ABI、 InjectSession.open() | frida-helper-backend.vala:300-330, 868, 1805-2110 | | attach 卡在 loader | bootstrap、remote mmap、loader HELLO/READY | frida-helper-backend.vala:868-1050 | | mapper 返回空 | ELF 类型、PHDR、TLS、relocation、入口导出 | 本工程 agent-mapper(私有);上游对照 dlopen 失败路径 loader.c:150-158 | | agent 已映射但 session 失败 | control fd、DBus、provider 注册 | agent.vala:1175-1187 |

15. 源码复核

对上游 17.9.1 可直接执行(本工程改动只以 §12 的思路对照,不在上游源码中):
复核结果应能组成四条连续证据链(第三条左半是上游路径,右半是本工程思路):

16. 必须能回答的问题

  1. Frida 是否把完整 agent 注入 zygote?
否。zygote 中是 zymbiote 门控载荷;完整 agent 在目标子进程 PID 确定后通过普通 attach 注入。zymbiote 默认在 frida-server 启动时经 preload 注入父进程(§3、§4),不是等 spawn 才做。
  1. **-f 为什么能早于 App 业务代码?**
子进程在 specialize/进程命名阶段通过 zymbiote 上报并阻塞,server 在放行前完成 agent 注入与脚本加载。
  1. spawn 返回 PID 之后、resume 之前发生了什么?
客户端立刻对这个 PID 做一次 attach:ptrace → bootstrapper → loader → mapper 装载完整 agent → 建立 AgentSession → 创建并加载脚本。
  1. 当前 zymbiote 修改解决了什么?
App 子进程收到 ACK 后还原继承的函数槽并自卸载临时 payload;注入前的 stale cleanup 负责异常残留。
  1. 独立的 attach 和 spawn 是什么关系?
同一条装载链的单独使用:没有门控段、时机晚于 App 早期初始化,也没有 ACK 放行点。
  1. 普通 attach 为什么需要 ptrace?
ptrace 提供暂停、寄存器读写和受控远程执行入口;真正装载还依赖 bootstrapper、远程内存、loader、fd 传递与控制通道。
  1. **当前工程改掉 dlopen() 后,为什么 agent 还能运行?**
自定义 mapper(agent-mapper,本工程私有)自行处理当前 agent 所需的 ELF segment、relocation、权限、constructor 和入口查找。
  1. **为什么必须传 agent_base/agent_size ?**
自定义 mapper 不登记 soinfo ,maps 又可能被过滤;agent 需要一条独立、确定的自身范围来源。
  1. **不是本次目标的 App 也会进入 zymbiote 的 recv() 吗?**
会。只要它从已安装门控的 Zygote/USAP 出生并执行到 setArgV0 ,就会发送 hello 并短暂等待。server 发现没有匹配的 spawn 请求且未开启全局 spawn gating 后,会立即还原子进程继承的相应函数指针槽、发送 ACK,并让 payload 自卸载;不会对它 attach,也不会注入完整 agent。

参考文献

  1. Frida 上游 17.9.1 — server/server.valarun_application:199、ControlService 构造:294-296). <https://github.com/frida/frida-core/blob/17.9.1/server/server.vala#L199-L299>
  1. Frida 上游 17.9.1 — src/control-service.vala (spawn/attach 转发:915-934). <https://github.com/frida/frida-core/blob/17.9.1/src/control-service.vala#L915-L934>
  1. Frida 上游 17.9.1 — src/frida.valaDevice.spawn:994、 Device.attach:1138). <https://github.com/frida/frida-core/blob/17.9.1/src/frida.vala#L1138-L1163>
  1. Frida 上游 17.9.1 — src/host-session-service.valaattach:571、 establish:605). <https://github.com/frida/frida-core/blob/17.9.1/src/host-session-service.vala#L571-L611>
  1. Frida 上游 17.9.1 — src/linux/linux-host-session.vala (本课主锚点:preload:91-95、RoboLauncher.close:1386-1412、spawn:1442-1502、ensure\_loaded:1517-1603、inject\_zymbiote:1605-1648、do\_prepare\_zymbiote\_injection:1692-1899、make\_zymbiote\_payload:1911-2000、handle\_zymbiote\_connection:2052-2088、resume:2175-2194). <https://github.com/frida/frida-core/blob/17.9.1/src/linux/linux-host-session.vala>
  1. Frida 上游 17.9.1 — src/linux/linjector.vala (inject\_library\_resource:84、memfd:94、request\_control\_channel:112). <https://github.com/frida/frida-core/blob/17.9.1/src/linux/linjector.vala#L84-L114>
  1. Frida 上游 17.9.1 — src/linux/frida-helper-backend.vala (InjectTask:308-330、bootstrap:1054、launch\_loader:993、RemoteAgent:1452-1560、SeizeSession:1805-2110). <https://github.com/frida/frida-core/blob/17.9.1/src/linux/frida-helper-backend.vala>
  1. Frida 上游 17.9.1 — src/linux/helpers/loader.c (frida\_load:61-63、dlopen:110-131、receive\_fd:332、收尾:168-188). <https://github.com/frida/frida-core/blob/17.9.1/src/linux/helpers/loader.c#L61-L190>
  1. Frida 上游 17.9.1 — src/linux/helpers/loader.c 的 dlopen 路径(:110-131),作为本工程自定义 mapper 的对照;agent-mapper 为本工程私有实现,不公开. <https://github.com/frida/frida-core/blob/17.9.1/src/linux/helpers/loader.c#L110-L131>
  1. Frida 上游 17.9.1 — src/linux/helpers/zymbiote.c (replacement\_setcontext:60-71、replacement\_setargv0:80-98、wait\_for\_permission:115-189、TAILCALL\_TO\_RAISE\_SIGSTOP:191-253). <https://github.com/frida/frida-core/blob/17.9.1/src/linux/helpers/zymbiote.c>
  1. Frida 上游 17.9.1 — lib/base/session.vala (LinuxInjectorState:1022; agent_base/agent_size 为本工程在同名结构体上的扩展). <https://github.com/frida/frida-core/blob/17.9.1/lib/base/session.vala#L1022>
  1. Frida 上游 17.9.1 — lib/agent/agent.vala (入口:1-7、create\_and\_run:116-184、run:267-277、provider 注册:1175-1187). <https://github.com/frida/frida-core/blob/17.9.1/lib/agent/agent.vala>
  1. frida-tools 14.8.0(2026-03-26,与 frida 17.9.1 同期)— frida_tools/application.py:616-626frida_tools/repl.py:247-252, 294-306. <https://github.com/frida/frida-tools/blob/14.8.0/frida\_tools/application.py#L616-L626>;<https://github.com/frida/frida-tools/blob/14.8.0/frida\_tools/repl.py#L247-L306>
  1. Frida 上游源码 — frida-core/src/linux/linux-host-session.vala, frida-helper-backend.vala, helpers/loader.c, helpers/zymbiote.c. <https://github.com/frida/frida-core>
  1. Frida 官方文档 — Modes of Operation. <https://frida.re/docs/modes/>
  1. Android Code Search — Launcher3 ItemClickHandler.java (桌面图标点击进入 startActivitySafely ). <https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Launcher3/src/com/android/launcher3/touch/ItemClickHandler.java>
  1. Android Code Search — ActivityTaskManagerService.javaActivityStarter.java (Activity 启动请求在 system_server 中的解析与执行). <https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java>;<https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java>
  1. Android Code Search — ProcessList.javastartProcessLocked()entryPoint = "android.app.ActivityThread" 与进程创建参数). <https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/am/ProcessList.java>
  1. Android Code Search — Process.javaZygoteProcess.javaProcess.start()startViaZygote() 、Zygote socket 协议与 USAP 路径). <https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/Process.java>;<https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/ZygoteProcess.java>
  1. Android Code Search — ZygoteServer.javaZygoteConnection.javarunSelectLoop() 、对端身份检查、 processCommand() 与父子分流). <https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/com/android/internal/os/ZygoteServer.java>;<https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/com/android/internal/os/ZygoteConnection.java>
  1. Android Code Search — Zygote.javacom_android_internal_os_Zygote.cppforkAndSpecialize()nativeForkAndSpecialize()ForkCommon()SpecializeCommon() ). <https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/com/android/internal/os/Zygote.java>;<https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/jni/com\_android\_internal\_os\_Zygote.cpp>
  1. Android Code Search — ZygoteInit.javaRuntimeInit.javaActivityThread.java (子进程从 handleChildProc() 进入 ActivityThread.main() ). <https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java>;<https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/com/android/internal/os/RuntimeInit.java>;<https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/ActivityThread.java>
  1. 看雪论坛 · Yangser —《新版 Frida Zymbiote 注入机制解析》(Frida 17.6.0:zygote 侧 /proc/<pid>/mem 远程读写、 libstagefright.so 尾页载荷、server 手工重定位 API 表、一次性 entry\_point hook、子进程上报后 raise(SIGSTOP) 自挂起、gadget 由外部 ptrace 注入). <https://bbs.kanxue.com/thread-289866.htm>
  1. Frida 17.17.0 上游源码 — RoboLauncher.handle_zymbiote_connection()ZymbioteConnection.resume()zymbiote.c 的非目标立即放行、 raise(SIGSTOP) 、server 还原和 SIGCONT 收尾. <https://github.com/frida/frida-core/blob/17.17.0/src/linux/linux-host-session.vala>;<https://github.com/frida/frida-core/blob/17.17.0/src/linux/helpers/zymbiote.c>
  1. F01 · Frida 整体启动、进程注入与 Zygote 门控
  1. 开课词库:先分清三组角色
  1. A. Android 创建进程:从桌面到 App
  1. B. Frida 控制与注入:从命令行到目标进程
  1. C. 门控与会话:什么时候停,什么时候放
  1. 0\. 录课导学:从桌面点击到 Zygote fork
  1. 0.1 Launcher:点击图标只是发起启动请求
  1. 0.2 system\_server:决定是否需要新进程
  1. 0.3 Zygote:收到命令后才真正 fork
  1. 0.4 一张图看懂桌面点击到 App 子进程
  1. 0.5 USAP 是这条主线的重要分支
  1. 0.6 这条正常链与 Frida -f 在哪里接上
  1. 1\. 问题边界:-f 的主线是 spawn
  1. 2\. 进程与组件
  1. 3\. frida-server 如何接住客户端请求
  1. 4\. zymbiote 门控:注入 zygote 并 hook 两个函数(spawn 的前置)
  1. 4.1 注入对象:五个父进程
  1. 5\. 注入细节:payload 怎么装进父进程、装在哪里
  1. 5.1 注入流程:选位置、写入、改指针
  1. 5.2 载荷位置:上游借媒体库尾页,当前工程换独立匿名映射
  1. 5.2.1 第三个观察面:门控子进程的 VMA 切口(真机实测)
  1. 6\. spawn 执行:请求启动 App,子进程出生上报
  1. 6.1 RoboLauncher.spawn():执行顺序与启动请求
  1. 6.2 子进程如何被识别并卡在正确时机
  1. 6.3 server 收到 hello 后:目标等待,非目标立即放行
  1. 7\. 装载段:向刚出生的子进程注入完整 frida-agent
  1. 7.1 spawn 返回 PID 后,立刻对这个 PID 做一次 attach
  1. 7.2 Linux 后端选择 agent
  1. 7.3 ptrace 只是入口,真正任务是建立远程执行环境
  1. 7.4 loader 从被劫持线程切到自己的工作线程
  1. 8\. agent 装载:上游 dlopen 与当前工程的 custom mapper
  1. 8.1 上游路径
  1. 8.2 当前工程路径:完全脱离 linker 的自定义装载
  1. 8.3 为什么增加 agent\_base/agent\_size
  1. 9\. frida\_agent\_main 之后发生什么
  1. 10\. resume 放行:归还控制权,App 继续启动
  1. 11\. 收尾一提:独立的 attach 已运行进程
  1. 12\. 当前工程相对上游改变了什么
  1. 12.1 自定义 mapper 改的是 agent 装载,不是 ptrace 原语
  1. 12.2 zymbiote 自卸载改的是门控收尾
  1. 12.3 Gum.Cloak 不是系统级隐藏
  1. 13\. 两条路径的时序对照
  1. spawn Android App(-f 主线)
  1. attach 已运行进程(对照)
  1. 14\. 故障定位
  1. 15\. 源码复核
  1. 16\. 必须能回答的问题
  1. 参考文献

Comments
Catalog