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-coretag 17.9.1](https://bbs.kanxue.com/elink@e2bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8
3!0J5k6g2\)9J5c8Y4c8J5k6h3g2Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4) (2026-03-27,对应 frida 主仓库中 frida-core submodule commita62376a3);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()?
容易产生两个误判:
- Launcher 点击图标后直接创建了目标进程;
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 完成 attach 与 bindApplication 后才开始加载。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.cpp 的 ForkCommon() 才是实际调用 fork() 的位置 \[21\]。AOSP
main 的 processCommand() 还会让满足条件的简单请求进入 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.java 、 RuntimeInit.java 与 ActivityThread.java \[22\]。到 ActivityThread.main() 时,新 App 进程已经出生,但应用自己的 Application.onCreate() 和 Activity 生命周期仍要等待后续绑定与事务分派。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 的五个步骤,继续拆解
RoboLauncher 、 inject_zymbiote() 、 setcontext / setArgV0 replacement、agent 注入与 ACK 放行的源码。1. 问题边界:-f 的主线是 spawn
执行:
做的主事是 spawn :让系统启动一个全新的 App 进程,并赶在它的业务代码运行之前接管。它不是“把一段 JavaScript 发给目标进程”这么简单。Android 上至少发生两次性质不同的注入:
- zygote 门控注入 :把一个很小的
zymbiote载荷写入 zygote、USAP 或 Chrome zygote,并 hook 两个函数指针;默认在 frida-server 启动时(preload)就完成,先于任何客户端命令,spawn 到来时只是幂等兜底;
- 目标进程 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 | 调用 spawn 、 attach 、 resume 、脚本 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 如何接住客户端请求
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-220 的 run_application() 创建 Application 。 Application.start() 在 server/server.vala:294-296 中构造并启动 ControlService :ControlService 内部持有设备侧 HostSession 。客户端连接后, control-service.vala:915-934 将 spawn() 与 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 的前置)
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 注入对象:五个父进程
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 怎么装进父进程、装在哪里
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() 完成全部定位,再一次性写入:- 扫描父进程 maps,挑出
libstagefright.so的可执行映射,取 最后一页 作为载荷位置(取舍见 5.2);
- 同时定位
libc.so、libselinux.so与libandroid_runtime.so,解析导出/导入表找到两个接入点,并在 boot heap / 匿名 rw 映射里搜索setArgV0的指针槽;若发现该父进程已被注入过(already_patched),改按file_offset从磁盘上的 so 文件读回原始字节,补全还原账本;
STOP+wait_until_stopped暂停父进程;
patches.apply()把 payload 写入尾页,再把两个指针槽改到 payload 的 replacement;
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@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L83!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@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L83!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 的两条写入路径,各占一半:
- 父进程不变 :server 经
/proc/<pid>/mem写尾页是页表级 COW,不触碰 VMA 树;两个 replacement 又只在 fork 出的子进程里执行(specialize 是子进程路径),zygote 自身从不执行那次mprotect。所以父进程永远一条 r-x,与实测一致。
- 子进程切口 :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
- 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():执行顺序与启动请求
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 子进程如何被识别并卡在正确时机
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 后:目标等待,非目标立即放行
3u0Q4x3V1j5I4y4#2\)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4\)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2\)9J5x3@1H3J5x3o6f1J5i4K6u0V1e0o6t1H3z5o6R3%60.)
server 在
linux-host-session.vala:2052-2088 的 handle_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 只是一两页的信标加闸门:它总共只带
mprotect 、 munmap 、 socket 、 connect 等 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
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-1163 的 Device.attach() 先调用远端 host_session.attach() ,再把返回的 AgentSessionId 链接成本地 Session :设备侧
host-session-service.vala:571-611 继续执行( attach() 在 :571 , establish() 在 :605 ):这里的
attach() 不是单一系统调用,而是一条从控制面到目标进程运行时的状态机。11. 收尾一提:独立的 attach 已运行进程
本课主线是 spawn。如果不需要“早于业务代码”,也可以省掉整个门控段,直接对已运行进程执行:
此时走的就是第 7 节拆过的那条装载链,一个环节都不少:
与 spawn 只有两点不同:
- 前面没有 zymbiote 门控段,也就没有第 4–6 节的出生拦截;
- 时机晚:目标进程早已跑完 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. 必须能回答的问题
- Frida 是否把完整 agent 注入 zygote?
否。zygote 中是
zymbiote 门控载荷;完整 agent 在目标子进程 PID 确定后通过普通 attach 注入。zymbiote 默认在 frida-server 启动时经 preload 注入父进程(§3、§4),不是等 spawn 才做。- **
-f为什么能早于 App 业务代码?**
子进程在 specialize/进程命名阶段通过 zymbiote 上报并阻塞,server 在放行前完成 agent 注入与脚本加载。
- spawn 返回 PID 之后、resume 之前发生了什么?
客户端立刻对这个 PID 做一次 attach:ptrace → bootstrapper → loader → mapper 装载完整 agent → 建立 AgentSession → 创建并加载脚本。
- 当前 zymbiote 修改解决了什么?
App 子进程收到 ACK 后还原继承的函数槽并自卸载临时 payload;注入前的 stale cleanup 负责异常残留。
- 独立的 attach 和 spawn 是什么关系?
同一条装载链的单独使用:没有门控段、时机晚于 App 早期初始化,也没有 ACK 放行点。
- 普通 attach 为什么需要 ptrace?
ptrace 提供暂停、寄存器读写和受控远程执行入口;真正装载还依赖 bootstrapper、远程内存、loader、fd 传递与控制通道。
- **当前工程改掉
dlopen()后,为什么 agent 还能运行?**
自定义 mapper(agent-mapper,本工程私有)自行处理当前 agent 所需的 ELF segment、relocation、权限、constructor 和入口查找。
- **为什么必须传
agent_base/agent_size?**
自定义 mapper 不登记
soinfo ,maps 又可能被过滤;agent 需要一条独立、确定的自身范围来源。- **不是本次目标的 App 也会进入 zymbiote 的
recv()吗?**
会。只要它从已安装门控的 Zygote/USAP 出生并执行到
setArgV0 ,就会发送 hello 并短暂等待。server 发现没有匹配的 spawn 请求且未开启全局 spawn gating 后,会立即还原子进程继承的相应函数指针槽、发送 ACK,并让 payload 自卸载;不会对它 attach,也不会注入完整 agent。参考文献
- Frida 上游 17.9.1 —
server/server.vala(run_application:199、ControlService 构造:294-296). <https://github.com/frida/frida-core/blob/17.9.1/server/server.vala#L199-L299>
- 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>
- Frida 上游 17.9.1 —
src/frida.vala(Device.spawn:994、Device.attach:1138). <https://github.com/frida/frida-core/blob/17.9.1/src/frida.vala#L1138-L1163>
- Frida 上游 17.9.1 —
src/host-session-service.vala(attach:571、establish:605). <https://github.com/frida/frida-core/blob/17.9.1/src/host-session-service.vala#L571-L611>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- 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>
- frida-tools 14.8.0(2026-03-26,与 frida 17.9.1 同期)—
frida_tools/application.py:616-626、frida_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>
- 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>
- Frida 官方文档 — Modes of Operation. <https://frida.re/docs/modes/>
- 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>
- Android Code Search —
ActivityTaskManagerService.java与ActivityStarter.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>
- Android Code Search —
ProcessList.java(startProcessLocked()、entryPoint = "android.app.ActivityThread"与进程创建参数). <https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/am/ProcessList.java>
- Android Code Search —
Process.java与ZygoteProcess.java(Process.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>
- Android Code Search —
ZygoteServer.java与ZygoteConnection.java(runSelectLoop()、对端身份检查、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>
- Android Code Search —
Zygote.java与com_android_internal_os_Zygote.cpp(forkAndSpecialize()、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>
- Android Code Search —
ZygoteInit.java、RuntimeInit.java与ActivityThread.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>
- 看雪论坛 · 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>
- 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>
- Author:24th
- URL:https://24th.top/article/3d5e5b08-46db-8032-8790-f82671c8d366
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!




.png?table=block&id=3a4e5b08-46db-807e-b0f5-c267678a1d3d&t=3a4e5b08-46db-807e-b0f5-c267678a1d3d)
