-
-
[原创]Frida 整体启动逻辑
-
发表于: 9小时前 415
-
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(2026-03-27,对应 frida 主仓库中 frida-core submodule commita62376a3);frida-tools 取同期版本 14.8.0(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 |
最容易混淆的三个名字先单独钉住:
Zygote = Android 的应用进程父体
zymbiote = Frida 放进父体的小型门控载荷
frida-agent = 最终进入目标 App、真正运行 Gum 与脚本的完整载荷
0. 录课导学:从桌面点击到 Zygote fork
本课不从开机讲起。先把 Zygote 当作一个已经运行、正在等待创建请求的父进程,只看一个最常见的现场:
目标 App 当前没有进程。用户在桌面点击图标以后,究竟是谁找到 Zygote,又是谁执行了
fork()?
容易产生两个误判:
- Launcher 点击图标后直接创建了目标进程;
system_server收到启动请求后亲自fork()出 App。
两者都不准确。Launcher 只发起 Activity 启动请求;system_server 负责解析目标组件、检查启动条件并准备进程参数;真正复制进程地址空间的是 Zygote。先建立这条 Android 正常基线,后面才能看懂 Frida -f 为什么要提前处理 Zygote 和 USAP。
本节源码导航统一使用 Android Code Search 的 AOSP main 链接 [16]–[22];课程实验仍以 Android 14/API 34 为基线。main 后续增加的快速路径会单独标出,不把当前实现误写成所有版本唯一实现。
0.1 Launcher:点击图标只是发起启动请求
源码直达:Launcher3 · ItemClickHandler.java · ActivityTaskManagerService.java · ActivityStarter.java
AOSP Launcher3 的参考实现把桌面图标点击交给 ItemClickHandler.onClick();厂商桌面的类名和动画实现可能不同,但最终仍要发起 framework 的 Activity 启动请求。普通应用图标依次进入:
ItemClickHandler.onClick()
→ onClickAppShortcut()
→ startAppShortcutOrInfoActivity()
→ launcher.startActivitySafely(...)
源码入口见 ItemClickHandler.java [16]。这一步持有的是描述目标 Activity 的 Intent,没有 fork(),也没有加载目标 APK。请求继续经过 Android 的 Activity 启动接口,以 Binder 调用进入 system_server 中的 ActivityTaskManagerService(ATMS);ATMS 与 ActivityStarter 负责解析目标 Activity、任务栈、用户和启动限制 [17]。
第一段链路可先记成:
桌面图标
→ Launcher3
→ startActivity
→ Binder
→ system_server:ATMS / ActivityStarter
0.2 system_server:决定是否需要新进程
如果目标进程已经存在,系统可以直接向现有进程下发 Activity 生命周期事务,不需要再次请求 Zygote。
如果目标进程不存在,启动链会进入 AMS 的进程管理逻辑,最终由 ProcessList.startProcessLocked() 准备创建参数。这里有一个关键值 [18]:
final String entryPoint = "android.app.ActivityThread";
这个值说明 Zygote 即将创建的不是“直接执行 APK main() 的进程”,而是以框架类 ActivityThread 为 Java 入口的新进程。目标 APK、Application 和 Activity 要等新进程向 AMS 完成 attach 与 bindApplication 后才开始加载。
ProcessList 随后通过 Process.start() 进入 ZygoteProcess.start() / startViaZygote() [19]。这一段会把下面这些参数编码成 Zygote 命令:
uid / gid / supplementary groups
runtime flags
targetSdkVersion
seInfo
niceName / processName
ABI / instruction set
app data directory
entryPoint = android.app.ActivityThread
system_server 与 Zygote 之间使用本地 socket,而不是再走 Binder。ZygoteProcess 把参数写入 Zygote socket,并同步等待子进程 PID:
ProcessList
→ Process.start()
→ ZygoteProcess.startViaZygote()
→ 写入 Zygote socket
→ 等待 pid / usingWrapper
0.3 Zygote:收到命令后才真正 fork
源码直达:ZygoteServer.java · ZygoteConnection.java · Zygote.java · Zygote native
Zygote 已经在 ZygoteServer.runSelectLoop() 中监听命令。socket 到来后,server 创建 ZygoteConnection 并调用 processCommand() [20]。
连接建立时,ZygoteConnection 会读取对端凭据并限制调用方:
if (peer.getUid() != Process.SYSTEM_UID) {
throw new ZygoteSecurityException(
"Only system UID is allowed to connect to Zygote.");
}
因此普通 App 不能绕过 system_server,直接要求 Zygote 按任意 uid 创建进程。命令通过参数与权限检查后,常规路径进入:
pid = Zygote.forkAndSpecialize(...);
if (pid == 0) {
// child
return handleChildProc(...);
} else {
// parent zygote
handleParentProc(pid, ...);
return null;
}
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 |
子进程随后经:
handleChildProc()
→ ZygoteInit.zygoteInit()
→ RuntimeInit.applicationInit()
→ findStaticMain("android.app.ActivityThread")
→ ActivityThread.main()
对应源码入口见 ZygoteInit.java、RuntimeInit.java 与 ActivityThread.java [22]。到 ActivityThread.main() 时,新 App 进程已经出生,但应用自己的 Application.onCreate() 和 Activity 生命周期仍要等待后续绑定与事务分派。
子进程入口直达:ZygoteInit.java · RuntimeInit.java · ActivityThread.java
0.4 一张图看懂桌面点击到 App 子进程
用户点击桌面图标
→ Launcher3.startActivitySafely(Intent)
→ Binder
→ system_server:ATMS / ActivityStarter
→ 目标进程不存在
→ AMS / ProcessList.startProcessLocked
→ Process.start
→ ZygoteProcess.startViaZygote
→ Zygote socket
→ ZygoteServer.runSelectLoop
→ ZygoteConnection.processCommand
→ Zygote.forkAndSpecialize
→ native ForkCommon → fork()
├─ 父 Zygote:回写 child pid,继续接单
└─ 子进程:specialize → ActivityThread.main
0.5 USAP 是这条主线的重要分支
上图描述的是主 Zygote 收到命令后直接 forkAndSpecialize() 的常规路径。启用 USAP(Unspecialized App Process)池时,系统可能预先从 Zygote fork 出若干“尚未绑定具体应用身份”的进程;创建请求到来后,ZygoteProcess 优先尝试把参数交给一个 USAP,让它完成 specialize,而不是此刻才从主 Zygote 重新 fork [19]。
因此不能把所有设备上的实际路径都写成“点击后一定由主 Zygote 当场 fork”:
常规路径:请求 → 主 Zygote → fork → specialize
USAP 路径:主 Zygote 预先 fork → 请求 → 某个 USAP specialize
这正是 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 时:
父进程已有 zymbiote
→ fork 后子进程继承 payload 与函数指针改写
→ child specialize / setcontext
→ child setArgV0
→ zymbiote 上报 pid、ppid、process-name 并等待
→ Frida 对 child pid 注入完整 frida-agent
→ 加载脚本
→ resume 放行
所以 Zygote 基线不是额外背景知识,而是理解 Frida spawn 的必要前提:
Frida 没有替 Android 创造另一套 App 启动机制。它先进入 Android 已有的进程父体,再利用 fork 的地址空间继承,把一个短暂门控点带进新生子进程。
后文第 4–10 节按 spawn 的五个步骤,继续拆解 RoboLauncher、inject_zymbiote()、setcontext / setArgV0 replacement、agent 注入与 ACK 放行的源码。
1. 问题边界:-f 的主线是 spawn
执行:
frida -U -f TARGET_PACKAGE -l probe.js
做的主事是 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 节的展开顺序:
[设备] frida-server start(默认 enable_preload)
→ 建立 ControlService / HostSession
→ preload:zymbiote 注入 zygote/USAP + hook 两个函数指针 ← 门控就位(§3–§5)
[PC] frida-tools / Python API
├─ Device.spawn(package) ← 启动与拦截(第 6 节)
│ └─ RoboLauncher:启动 App(ensure_loaded 幂等兜底)
│ └─ 子进程上报 pid/ppid/process-name 后等待
│
├─ Device.attach(pid) ← 装载段(第 7–9 节)
│ └─ HostSession → Linjector → LinuxHelperBackend
│ └─ ptrace → bootstrapper → loader → frida-agent
│
├─ Session.create_script() / Script.load()
│
└─ Device.resume(pid) ← 放行段(第 10 节)
└─ 恢复子进程中的门控点,App 继续启动
对照:不带 -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 如何接住客户端请求
源码直达:server/server.vala @17.9.1 · src/control-service.vala @17.9.1
server/server.vala:199-220 的 run_application() 创建 Application。Application.start() 在 server/server.vala:294-296 中构造并启动 ControlService:
service = new ControlService (endpoint_params, options);
yield service.start (io_cancellable);
ControlService 内部持有设备侧 HostSession。客户端连接后,control-service.vala:915-934 将 spawn() 与 attach() 转交给 host_session(17.9.1 实测行号,与快照一致):
public async uint spawn (...) {
return yield parent.host_session.spawn (program, options, cancellable);
}
public async AgentSessionId attach (...) {
return yield parent.attach (pid, options, this, cancellable);
}
启动 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 确定之后才注入:
server start(默认 enable_preload)
→ 建立 ControlService / HostSession
→ preload:zymbiote 注入 zygote/USAP + hook 两个函数指针(§4、§5)
客户端命令到达(spawn / attach)
→ spawn:请求启动 App、等子进程上报、注入 agent(§6–§9)
→ attach:ptrace 目标、注入 agent(§7、§11)
本课接下来沿 spawn 主线展开:第 4–5 节是门控(默认 server 启动时就位),第 6 节是启动与出生拦截,第 7–9 节是装载段,第 10 节是放行段。
4. zymbiote 门控:注入 zygote 并 hook 两个函数(spawn 的前置)
源码直达:src/linux/linux-host-session.vala @17.9.1 · RoboLauncher
门控不属于 spawn 流程:默认情况下,它在 frida-server 启动时经 §3 的 preload 链就已经完成,先于任何客户端命令。注入与卸载的时机只有这几处:
注入(幂等,已注入的父进程直接跳过):
server 启动 → preload() → ensure_loaded() 默认路径
spawn 请求 → RoboLauncher.spawn() 开头兜底
开启 gating → enable_spawn_gating() 开头兜底
卸载:
server 退出 → close() 暂停父进程、还原指针、释放 payload
对应源码(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 注入对象:五个父进程
RoboLauncher.ensure_loaded() 在 linux-host-session.vala:1517-1603 建立随机名称的抽象 Unix socket(socket 名见 :1531,形如 /frida-zymbiote-<uuid>),然后枚举:
zygote
zygote64
usap32
usap64
com.android.chrome_zygote
对应的选择逻辑是:
foreach (HostProcessInfo info in System.enumerate_processes (...)) {
var name = info.name;
if (name == "zygote" || name == "zygote64" ||
name == "usap32" || name == "usap64" ||
name == CHROME_ZYGOTE_PACKAGE_NAME) {
if (!zymbiote_patches.has_key (info.pid))
do_inject_zygote_agent.begin (info.pid, name, ...);
}
}
每个尚未处理的 PID 都进入 inject_zymbiote()。
zygote 与 USAP 都是 App 的潜在父进程。只处理 zygote、不处理 USAP,会在启用 USAP 池的系统上漏掉从预热池出生的应用。Chrome/WebView 还可能再创建自己的子 zygote,因此需要单独延续门控状态。
5. 注入细节:payload 怎么装进父进程、装在哪里
源码直达:inject_zymbiote() / do_prepare_zymbiote_injection() @17.9.1 · helpers/zymbiote.c @17.9.1
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 摘录):
Posix.kill ((Posix.pid_t) pid, Posix.Signal.STOP);
yield wait_until_stopped (pid, cancellable);
patches.apply (payload, prep.process_memory, prep.payload_base);
patches.apply (prep.replaced_setargv0_ptr, prep.process_memory,
prep.setargv0_slot);
if (prep.setcontext_slot != 0)
patches.apply (prep.replaced_setcontext_ptr, prep.process_memory,
prep.setcontext_slot);
zymbiote_patches[pid] = patches;
Posix.kill ((Posix.pid_t) pid, Posix.Signal.CONT);
两个接入点的定位来自 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 还原。生命周期只有三步(逻辑示意,非关键实现):
注入:mmap 独立匿名 RX 页
→ 写入 payload(只写自己的页,不碰任何 so)
→ 改两个函数指针槽
存活:fork 继承;子进程在 setcontext / setargv0 replacement 里照常运行
自毁:ACK 到达 → must-tail munmap(payload_base, payload_size)
← 门控销毁自己与 munmap 是同一时刻,不需要任何“还原 so 字节”的步骤
写入的 payload 本体是预编译 zymbiote 小 ELF 的可执行段:make_zymbiote_payload()(上游 17.9.1 linux-host-session.vala:1911-2000)只截取 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)扫父进程 maps,找已存在的可执行文件映射,路径以 /libstagefright.so 结尾(17.9.1 只认这一个候选,不挑 libmedia.so):
可执行 && path 以 /libstagefright.so 结尾
→ payload_base = m.end - page_size # 该映射的最后一页
→ payload_file_offset = m.file_offset # 记录页在 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):
zygote64(payload 写入方):
7ef5b05000-7ef5c63000 r-xp 0009c000 … /system/lib64/libstagefright.so
App 子进程(payload 执行方,采样时 App 已正常运行、权限早已恢复):
7ef5b05000-7ef5c62000 r-xp 0009c000 … /system/lib64/libstagefright.so
7ef5c62000-7ef5c63000 r-xp 001f9000 … /system/lib64/libstagefright.so ← 尾页被切出
成因对应 §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.1zymbiote.c:70的权限窗口)。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():执行顺序与启动请求
spawn 请求在 §3 的 HostSession.spawn() 落地。目标参数是 Android 包名,包名不是普通可执行文件路径,linux-host-session.vala:338 因此进入:
return yield robo_launcher.spawn (program, options, cancellable);
RoboLauncher.spawn()(linux-host-session.vala:1442-1502)的执行顺序:
ensure_loaded()
→ 幂等兜底;门控默认已在 server 启动时就位(§4、§5)
按 process-name 登记一次 spawn 请求
→ 记下 Promise<pid>,等 zymbiote hello 到来时兑现(6.3)
stop_package()
→ 清掉目标包可能存留的进程
start_package()
→ 发起启动,请求汇入 §0.2 的 ProcessList → Zygote 链
等待 zymbiote hello 上报新生子进程 PID(20 秒超时)
→ spawn() 返回 pid
stop_package() / start_package() 位于 linux-host-session.vala:1476-1477;请求进入 framework 之后,走的就是 §0.2–§0.3 已经拆过的正常链,直接引用:
start_package
→ system_server:ATMS / AMS / ProcessList.startProcessLocked
→ Zygote socket(或 USAP 池)
→ fork child → specialize
fork 出的子进程继承门控,在 specialize 阶段撞上 zymbiote(6.2)。
6.2 子进程如何被识别并卡在正确时机
zygote fork 后,匿名 payload 和被改写的指针槽由子进程继承。helpers/zymbiote.c 中有两个 replacement:
frida_zymbiote_replacement_setcontext()
frida_zymbiote_replacement_setargv0()
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(),随后:
连接 server 的 abstract Unix socket
→ 发送 pid + ppid + process-name
→ 阻塞等待 1 字节 ACK
对应代码骨架是:
res = zymbiote.original_setcontext (uid, is_system_server, seinfo, name);
if (res == -1)
return -1;
if (zymbiote.package_name == NULL)
{
zymbiote.mprotect (zymbiote.payload_base, zymbiote.payload_size,
PROT_READ | PROT_WRITE | PROT_EXEC);
zymbiote.package_name = zymbiote.strdup (name);
}
zymbiote.original_set_argv0 (env, clazz, name);
frida_wait_for_permission_to_resume (name_utf8, &revert_now);
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() 发送固定头和进程名:
header.pid = zymbiote.getpid ();
header.ppid = zymbiote.getppid ();
header.package_name_len = ...;
frida_sendmsg_all (fd, iov, 2, MSG_NOSIGNAL);
frida_recv (fd, &rx, 1, 0);
这里必须注意:recv() 不是“只有目标 App 才走”的代码。只要普通子进程从已经安装 zymbiote 的 Zygote/USAP 出生、继承了两个函数指针改写,并正常执行到 setArgV0,它就会连接 server、发送 hello,然后在这里等待 server 作出决定。目标与非目标的分流发生在 server 收到 hello 之后,见下一节。
“所有 App 都会经过”仍有边界:已经在门控安装前出生的进程不会倒回这里;未被 ensure_loaded() 覆盖的其他 AppZygote 也不在当前枚举范围内;如果 abstract socket 连接失败,payload 会直接返回而不会停在 recv()。
6.3 server 收到 hello 后:目标等待,非目标立即放行
server 在 linux-host-session.vala:2052-2088 的 handle_zymbiote_connection() 里收到 hello 后,不会立刻给每个子进程注入 agent,而是先判断它属于哪一条分支:
Promise<uint> spawn_request;
if (spawn_requests.unset (hello.package_name, out spawn_request)) {
spawn_request.resolve (hello.pid);
needs_resume = true;
} else if (spawn_gating_enabled) {
var spawn_info = HostSpawnInfo (hello.pid, hello.package_name);
pending_spawn[hello.pid] = spawn_info;
spawn_added (spawn_info);
needs_resume = true;
}
if (needs_resume)
zymbiote_connections[hello.pid] = connection;
else
connection.resume.begin (io_cancellable);
四条分支如下:
| 子进程 | 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(),但不会一直卡住。当前工程对它立即执行下面这条清理链:
server 发现“不匹配且未开启全局门控”
→ 立即还原该子进程继承的 setArgV0 和可选 setcontext 指针槽
→ 立即发送 ACK
→ recv 返回
→ zymbiote payload must-tail 调用 munmap() 自卸载
→ App 回到 setArgV0 的 Java 调用点继续启动
这一分支没有 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.0 · frida_tools/repl.py @14.8.0 · src/frida.vala @17.9.1 · src/host-session-service.vala @17.9.1
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:
Device.attach(child_pid)
→ 本节 7.2–7.4 的 ptrace/bootstrap/loader 路径
→ 完整 frida-agent 进入 App 子进程
→ 创建并加载脚本
→ Device.resume(child_pid)(第 10 节)
所以 zygote 门控与 agent 注入不是二选一,而是前后相接:
zymbiote 负责“发现并卡住新生 App”
frida-agent 负责“在 App 内运行 Gum 与脚本”
这条装载链的 API 侧入口如下。src/frida.vala:1138-1163 的 Device.attach() 先调用远端 host_session.attach(),再把返回的 AgentSessionId 链接成本地 Session:
id = yield host_session.attach (pid, raw_options, cancellable);
session = new Session (this, pid, id, opts);
session.active_session =
yield provider.link_agent_session (host_session, id, session, cancellable);
设备侧 host-session-service.vala:571-611 继续执行(attach() 在 :571,establish() 在 :605):
establish(pid)
→ perform_attach_to(pid)
→ 等待 agent control stream
→ 建立 DBusConnection
→ 获取 AgentSessionProvider proxy
→ provider.open(session_id)
这里的 attach() 不是单一系统调用,而是一条从控制面到目标进程运行时的状态机。
7.2 Linux 后端选择 agent
源码直达:perform_attach_to() @17.9.1 · src/linux/linjector.vala @17.9.1
src/linux/linux-host-session.vala:387-405 明确指定 agent 入口:
string entrypoint = "frida_agent_main";
string parameters = make_agent_parameters (pid, "", options);
AgentFeatures features = CONTROL_CHANNEL;
id = yield linjector.inject_library_resource (
pid, agent, entrypoint, parameters, features, cancellable);
IOStream stream =
yield linjector.request_control_channel (id, cancellable);
linjector.vala:84-96 根据目标 ABI 选择 32/64 位 agent。支持 memfd 时直接取得 agent resource 的 fd(:94),否则使用临时文件路径;随后统一调用 helper.inject_library()。
目标 pid
→ 判断 ABI
→ 取得匹配的 frida-agent
→ fd/path 交给 Linux helper
→ 等待 control channel
7.3 ptrace 只是入口,真正任务是建立远程执行环境
上游 17.9.1 的 InjectTask.run()(frida-helper-backend.vala:308-330)经 InjectSession.open()(:868)创建 InjectSession。其基类 SeizeSession(:1805-2110;PTRACE_SEIZE 在 :1888,GETREGSET 在 :2084-2105)完成:
PTRACE_SEIZE(旧内核回退 PTRACE_ATTACH)
→ PTRACE_INTERRUPT / 等待 stop
→ GETREGSET 保存寄存器
→ 获得目标线程的受控执行点
进入 ptrace 停止态后,Frida 没有直接把 PC 改到 frida_agent_main。目标地址空间此时还没有 loader、agent、远程栈和控制通道。InjectSession.bootstrap()(frida-helper-backend.vala:1054 起)先建立这些条件:
- 从目标 maps 定位 libc 和 Android linker;
- 计算目标进程中的
mmap、munmap等函数地址; - 远程调用
mmap分配 bootstrapper、loader 数据和 64 KiB 工作栈; - 若不能直接使用目标 libc 的
mmap,临时借用目标现有代码页执行 bootstrapper,分配完成后还原原字节; - 在目标内执行 bootstrapper,解析 libc/linker API,并建立 socketpair 或抽象 Unix socket 回退通道(这条通道上 fd 的跨进程传递方式见 §7.4 的 SCM_RIGHTS 小节);
- 把 loader 代码、入口名、agent 参数和函数表写入目标内存。
这一步的输出不是 agent,而是一个可以在目标内部继续装载 agent 的最小运行环境。
7.4 loader 从被劫持线程切到自己的工作线程
源码直达:launch_loader() / RemoteAgent @17.9.1 · helpers/loader.c @17.9.1
frida-helper-backend.vala:993-1050 将目标寄存器中的 PC 指向 loader 基址、SP 指向远程栈,再通过 RemoteCall 执行 loader 入口。helpers/loader.c:61-63 的 frida_load() 立即创建工作线程:
void
frida_load (FridaLoaderContext * ctx)
{
ctx->libc->pthread_create (&ctx->worker, NULL, frida_main, ctx);
}
这样,受 ptrace 控制的原线程只负责启动 loader——frida_load() 的全部工作就是一个 pthread_create,几条指令就返回。loader 一返回,InjectSession 立即恢复保存的寄存器并 detach:ptrace 的使命到此结束。主线程回到被打断的那一刻——spawn 场景下就是 replacement 里阻塞的 recv()(被打断的系统调用由内核自动重启);注意此刻两个指针槽仍处于改写状态,要等到 resume 时才由 server 还原(§10)。此后 agent 的装载与通信全部由工作线程和 helper 之间的 socketpair 承担,不再有 ptrace。
loader 工作线程随后完成(上游 17.9.1 loader.c:96-137):
向 helper 发送 HELLO
→ 接收 agent fd
→ 装载 agent
→ 接收 agent control fd
→ 发送 READY
→ 等待 ACK
→ 调用 frida_agent_main
helper 侧对应状态位于 frida-helper-backend.vala:1452-1560(RemoteAgent)。RemoteAgent.start() 通过 Unix socket 发送 agent fd 和 control fd;收到 loader 的 READY 后,helper 才认为注入已同步完成。
fd 怎么跨进程:AF_UNIX 套接字 + SCM_RIGHTS
“通过 Unix socket 发送 fd”需要拆开讲,否则容易误解成“把 fd 号当数据发过去”。通道与机制分两层:
通道:一条 AF_UNIX + SOCK_STREAM 套接字。主路在 bootstrap 阶段建立——helper 用 RemoteCall 在目标进程内远程执行 socketpair(),fd 号写进 loader 的 context(frida-helper-backend.vala:929);兜底是 loader 主动连接 context 里预埋的抽象地址(loader.c:97 的 frida_connect (ctx->fallback_address, ...),与 zymbiote hello 连 server 是同款手法,方向相反:这里是目标主动连 helper)。另外 control 通道的 socketpair 在 helper 侧创建(frida-helper-backend.vala:1530),一端自留、另一端发给 loader。
机制:fd 号只是本进程 fd 表的下标,跨进程毫无意义。真正的跨进程传递靠 SCM_RIGHTS——sendmsg() 的辅助控制消息:内核把发送方 fd 指向的打开文件描述复制进接收方的 fd 表,接收方 recvmsg() 返回后拿到的是自己进程里一个新的 fd 号,指向同一个 memfd。
helper 侧发送(frida-helper-backend.vala:1556-1559,RemoteAgent.start() 内):
frida_ctrl.send_fd (spec.library_so.get_fd (), cancellable); // agent 的 memfd
if (agent.agent_ctrlfd_for_peer != null)
frida_ctrl.send_fd (agent.agent_ctrlfd_for_peer.handle, ...); // control 通道另一端
send_fd 是 GLib UnixConnection 的标准方法,底层实现就是 sendmsg + SCM_RIGHTS。
loader 侧接收(loader.c:332-365,frida_receive_fd())——因为 loader 不链接任何库,recvmsg 也从手工 API 表里取:
msg.msg_control = &control; /* FridaControlMessage:CMSG_SPACE(sizeof(int)) 的缓冲 */
msg.msg_controllen = sizeof (control);
res = libc->recvmsg (sockfd, &msg, 0);
if (res == -1 || res == 0 || msg.msg_controllen == 0)
return -1;
return *((int *) CMSG_DATA (CMSG_FIRSTHDR (&msg)));
文件开头的 union _FridaControlMessage 就是这块控制缓冲的类型定义。调用点:loader.c:110 收 agent fd、:131 收 control fd。
一句话收拢:通道是 AF_UNIX 套接字,fd 过河靠 SCM_RIGHTS 让内核复制文件描述;两端一个用 GLib 封装、一个手写 recvmsg + CMSG 解析,因为 loader 里连 libc 都是靠指针表借来的。
“就绪”有两级,不要混作一个信号
loader 的 READY 是第一级——fd 交接与装载同步完成,此刻 agent 入口还没调用;第二级在 §9——agent 在 control fd 上注册 AgentSessionProvider 成功,server 侧挂着的 attach() 才由此返回。两级用的都是注入时传进来的 fd,agent 不另建连接。
8. agent 装载:上游 dlopen 与当前工程的 custom mapper
第 7 节停在 loader 工作线程接管 agent fd。agent 装进目标的方式,上游与当前工程并不相同,这里单独拆开。
8.1 上游路径
上游 loader 的核心是(17.9.1 loader.c:110-131):
接收 agent fd
→ 拼出 /proc/self/fd/<fd>
→ bionic dlopen()(把 libc 的 close 伪装成调用者地址传入)
→ dlsym("frida_agent_main")
→ 调用入口
这会让 bionic linker 参与装载,并建立 soinfo、namespace、link_map、模块计数和 CFI 等账本。
8.2 当前工程路径:完全脱离 linker 的自定义装载
源码直达(上游对照):helpers/loader.c @17.9.1 · dlopen 路径。本工程的 agent-mapper 为私有实现,不附源码,这里只讲设计。
我们删掉了 loader 里的 dlopen() / dlsym(),换成自己的最小 ELF 装载器。设计目标只有一个:让 agent 与 bionic linker 彻底无关——不调用 linker、不注册 soinfo、不进 link_map、不触发命名空间合并与 DT_NEEDED 递归装载、不产生任何 linker 侧账本与回调。上游靠 dlopen 换来的“省事”,代价是把 agent 整个写进系统账本(§8.1);自定义 mapper 把这一整类 frida 特征从装载环节移出,而不是事后清理。装载流程本身只有七步:
读取 ELF header / program header
→ 匿名 mmap 整个 load span
→ 复制 PT_LOAD
→ 处理 RELA / JMPREL
→ 设置 segment 权限
→ 执行 init array
→ 从动态符号表找到 frida_agent_main
支撑这个设计的两个硬性取舍:
- 只做 agent 需要的最小子集。 mapper 只实现当前 agent 实际用到的装载语义:AArch64 与 x86_64,拒绝
PT_TLS、DT_RELR和未实现的 relocation 类型;不处理 namespace、DT_NEEDED递归依赖、符号版本。换来的是实现小、行为可穷举——装载器自身不引入新的系统交互面。 - 失败不回退
dlopen()。 上游dlopen失败会向 helper 报ERROR_DLOPEN并走异常路径(loader.c:150-158);我们的 mapper 失败直接退出装载,宁可这次注入失败,也不让 agent 重新出现在 linker 账本里——回退等于把刚消掉的特征又请回来。
与 §5.2 的门控是同一套设计签名:匿名 mmap 进来、不碰任何已有 so、不进任何账本——门控在 ACK 后 munmap 自毁,agent 则随进程生命周期驻留。落到装载位置上,我们与上游的差别压缩成代码就是一对一的替换(逻辑示意,非关键实现):
/* 上游 17.9.1:经 bionic dlopen,agent 进系统账本 */
ctx->agent_handle = libc->dlopen ("/proc/self/fd/N", ...);
ctx->agent_entrypoint_impl = libc->dlsym (ctx->agent_handle, "frida_agent_main", ...);
/* 我们:匿名映射 + 手工重定位,无 so、无账本、整块自管理 */
base = anon_mmap (elf_load_span);
map_and_relocate_segments (elf, base);
entry = lookup_dynamic_symbol (elf, "frida_agent_main");
agent 装进目标后的自身定位不再依赖任何运行时视图,改由 loader 显式传入(§8.3)。
8.3 为什么增加 agent_base/agent_size
源码直达:lib/base/session.vala @17.9.1 · LinuxInjectorState · lib/agent/agent.vala @17.9.1 · create_and_run
匿名 mapper 不向 bionic linker 登记 agent。与此同时,当前工程的内核侧 maps 处理可能让 agent 无法通过 /proc/self/maps 找到自己。
先澄清归属:LinuxInjectorState 不是我们发明的——上游 17.9.1 就用这条通道把 control fd 从 loader 带进 agent(session.vala:1022 定义,agent.vala:141-152 读取)。而且上游在 Darwin(iOS/macOS)注入链里本来就传自身范围(agent.vala:123-127 的 DARWIN 分支读 DarwinInjectorState.mapped_range)——我们等于把这个平台上的既有做法复用到了 Linux:扩展既有传递参数最省事,也最不容易破坏 ABI。我们只是沿着这条既有通道扩展了两个字段:
| 位置 | 修改 |
|---|---|
| loader 的注入器状态(C 侧,本工程) | 在既有结构体中增加 agent_base/agent_size |
Vala 侧 LinuxInjectorState(上游 session.vala:1022) |
本工程扩展同名结构体,字段顺序与位宽和 C 侧完全一致 |
agent 启动路径(上游 agent.vala:116-184) |
本工程让 agent 优先使用 loader 传入范围,不再依赖 maps 反查 |
这三个位置必须一起变化。任一端字段顺序或位宽不一致,都会把 control fd、地址和长度解释错位。
9. frida_agent_main 之后发生什么
lib/agent/agent.vala:1-7 的入口只负责创建或恢复 Runner:
if (Runner.shared_instance == null)
Runner.create_and_run (...);
else
Runner.resume_after_transition (...);
上游 17.9.1 的入口会“久留”:Runner.create_and_run()(agent.vala:116-184)在loader 工作线程上直接 run() → main_loop.run()(agent.vala:267-277),入口在 agent 生命周期内不返回,loader 的 pthread_detach、frida_send_bye 收尾(loader.c:168-188)要等 agent 停止才执行——上游的 loader 工作线程最终就变成 agent 的主循环线程。当前工程把主循环移到 agent 自己新建的线程上,入口立即返回,loader 工作线程随即收尾退出;承载 agent 的线程从注入完成起就与 App 线程并行。
首次进入时,create_and_run()(agent.vala:116-184)依次完成(前三行是上游行为,标注处为本工程扩展):
Environment._init()
→ 确定并 Cloak agent 范围(上游 detect_own_range;我们改用传入的 agent_base/agent_size)
→ 处理 control fd(上游 :141-152)
→ 创建 Runner
→ 建立主循环和 DBus connection
〔本工程〕开启 Android program-module fast path(§12)
agent.vala:1175-1187(setup_connection_with_stream)在 control stream 上注册 AgentSessionProvider:
registration_id = connection.register_object (
ObjectPath.AGENT_SESSION_PROVIDER, provider);
controller = yield connection.get_proxy (
null, ObjectPath.AGENT_CONTROLLER, ...);
connection.start_message_processing ();
server 取得 provider 后,再调用 open() 创建具体 AgentSession。
状态边界如下:
| 状态 | 已经完成 | 尚未完成 |
|---|---|---|
| helper 获得 inject id | 注入任务已登记 | agent 初始化 |
loader 发出 READY |
loader 与 agent fd 交接完成 | AgentSessionProvider 注册 |
| provider proxy 可用 | agent 控制面成立 | 具体 session |
provider.open() 返回 |
AgentSession 成立 |
JS 顶层代码 |
create_script() 返回 |
脚本对象和引擎实例已创建 | 脚本执行 |
load_script() 返回 |
顶层 JS 已投递并执行 | Java App ClassLoader 必然可用 |
因此“agent 已在 maps 中”“frida_agent_main 已调用”和“脚本第一行已执行”是三个不同事实。
10. resume 放行:归还控制权,App 继续启动
源码直达:ZymbioteConnection.resume() @17.9.1 · zymbiote.c @17.9.1 · TAILCALL_TO_RAISE_SIGSTOP
放行时的分工,先钉死:两个版本里动手写槽位的永远是 server——server 经
/proc/<pid>/mem(linux-host-session.vala:2627的open_process_memory())把两个指针槽写回原始地址。门控(payload)从头到尾不写任何槽位,它只负责处置自己:上游靠门控raise(SIGSTOP)把整个子进程停稳,给 server 开一个稳定还原窗口;我们靠门控munmap自毁,连窗口都不需要。这不是实现巧合,而是能力边界:payload 的 API 表里本来就没有任何写进程内存的函数(§5.2),它想还原也做不到。
先分清 ACK 的两种触发方式:
- 匹配目标或被全局门控的 App:ACK 不会因为 agent 就绪而自动发出。子进程一直卡在
recv(),直到客户端调用Device.resume(pid);frida CLI 默认在脚本加载完成后调用,用 Python API 时忘了调用,目标 App 就会一直停在等待点上。 - 普通非目标 App:§6.3 的 server 分流会立即调用同一个
ZymbioteConnection.resume(),直接完成 ACK 与清理,不等待客户端。
两条分支最终共用同一个放行函数。目标分支由 perform_resume() 触发。上游 17.9.1 的 ZymbioteConnection.resume()(linux-host-session.vala:2175-2194):
uint8 ack[1] = { 0x42 };
yield connection.get_output_stream ().write_async (ack, ...);
yield input.read_async (bye, ...);
yield wait_until_stopped (hello.pid, cancellable);
if (patches_to_revert != null)
patches_to_revert.revert (open_process_memory (hello.pid));
Posix.kill ((Posix.pid_t) hello.pid, Posix.Signal.CONT);
对应顺序:发 ACK → 读对端 bye → 等子进程 tail-call raise(SIGSTOP) 后停稳 → 还原两个指针槽 → SIGCONT 放行。
这个 raise(SIGSTOP) 是门控自己实现的(就在 zymbiote.c 的 replacement 里,靠 API 表里的 raise 完成),时机在 setArgV0 replacement 内部的尾声——不是 setArgV0 返回 Java 之后。子进程侧的完整顺序(上游 zymbiote.c:80-107):
original setArgV0() 先透传执行
→ 连 server、发 hello
→ 阻塞 recv 等 1 字节 ACK ← App 主线程就停在这里
ACK 到达(recv 返回)
→ 清理 package_name、恢复页权限 RX
→ if (revert_now) → must-tail raise(SIGSTOP) ← 门控把整个子进程停稳
→ server 在停稳窗口里经 /proc/<pid>/mem 写回两个槽位
→ SIGCONT
→ raise 的返回地址就是 Java 侧 setArgV0 的调用点,App 从这里继续启动
注意 replacement 自己从不正常返回:must-tail 让 raise 直接顶替 replacement 的栈帧,所以“回到 Java 调用点”与“停稳给 server 开还原窗口”是同一次跳转完成的。
当前工程把顺序改为“先还原、再放行、后确认”:
1. 在该子进程中还原继承来的 setArgV0 指针槽,以及存在时的 setcontext 指针槽
2. 向子进程发送 ACK
3. 等待 socket 对端关闭
4. 确认 zymbiote payload 映射已经消失(payload 自行 munmap)
没有 SIGSTOP/SIGCONT 往返,子进程也不会短暂多出一次“被外部停稳”的状态。
子进程收到 ACK 后的收尾是同款 must-tail 手法:上游 17.9.1 的 payload 在 revert_now(zymbiote.c:105-109)置位后,用四个架构宏 tail-call raise(SIGSTOP)(:191-253);当前工程同一位置 tail-call 的是:
munmap(payload_base, payload_size)
调用代码本身位于即将被释放的 payload 中,所以不能先普通调用 munmap() 再从已释放页面返回。这里必须让函数尾调用直接跳到 libc munmap,由 munmap 按原调用者的返回地址返回——上游的 raise 不释放页面,但同样靠 must-tail 把返回地址直接留给 Java 调用点。
放行之后,控制流从哪里回到 App?recv 拿到 ACK 后,replacement 释放进程名、恢复页权限,然后以 setArgV0 的身份向调用者返回——must-tail munmap 的返回地址就是 Java 侧 Process.setArgV0 的调用点。控制流由此接回 Android 启动链(§0.3):
ACK → recv 返回
→ replacement 清理,以 setArgV0 身份 return(munmap 直接返回到 Java 调用点)
→ handleChildProc 里的 Process.setArgV0 调用点
→ ZygoteInit.zygoteInit → RuntimeInit.applicationInit
→ findStaticMain("android.app.ActivityThread")
→ ActivityThread.main() → Application / Activity 生命周期
另一边,agent 不需要“回到”App 流程:frida_agent_main 运行在 loader 创建的工作线程上,进入自己的主循环(§9),从此与 App 线程并行。App 主线程在注入期间只是被 ptrace 借去启动了 loader,寄存器恢复、detach 后仍回到阻塞的 recv(§7.4);脚本 hook 的 App 函数,要等 App 真正跑到那里,才被 Interceptor 的 inline hook 接住。
对目标 App 而言:zymbiote 只借一个等待点,ptrace 只借主线程几毫秒,agent 活在自己的线程里,最后由客户端 resume() 发出的 1 字节 ACK 把 App 放回 Android 启动链。对普通非目标 App,server 在匹配失败后立即走同一放行函数,只做指针还原、ACK 和 payload 自卸载,不进入 ptrace/agent 链。
父 zygote 中的 payload 和指针改写继续保留,用于门控下一次 fork;关闭 RoboLauncher 时,server 才暂停父进程、还原指针并释放 payload。
Chrome 子 zygote 是特例:它本身还要继续 fork renderer。server 将父级 patch 记录转交给 Chrome zygote,不发送 ACK;连接关闭后 payload 不执行自卸载,门控能力继续由这个二级 zygote 继承。
最后把整条链完整走一遍:App 子进程在 setArgV0 的 replacement 里阻塞等 ACK 时,server 已经用 ptrace 借它的主线程跑完了 bootstrapper 和 loader——loader 入口只做一件事,pthread_create 出工作线程后立即返回,于是寄存器恢复、ptrace detach,主线程回到阻塞的 recv() 继续等;工作线程收下 agent fd、用 custom mapper 把完整 agent 装进目标、向 helper 发 READY,再调用 frida_agent_main——agent 入口把主循环放到自己新建的线程上随即返回,loader 工作线程收尾退出(上游入口则在 loader 工作线程上直接运行主循环,见 §9);agent 在注入时传进来的 control fd 上与 server 建立 DBus 连接并注册 AgentSessionProvider,server 侧挂着的 attach() 由此返回,客户端随后创建并加载脚本;最后客户端调用 resume(),server 还原子进程的两个指针槽、向那个还在等的 recv() 发出 1 字节 ACK——recv 返回,payload 自卸载并以 setArgV0 的身份返回 Java 调用点,App 从这里继续走 ActivityThread.main(),agent 与它并行运行。
11. 收尾一提:独立的 attach 已运行进程
本课主线是 spawn。如果不需要“早于业务代码”,也可以省掉整个门控段,直接对已运行进程执行:
frida -U -n TARGET_NAME # 按进程名;或 -p PID
此时走的就是第 7 节拆过的那条装载链,一个环节都不少:
attach(pid)
→ HostSession.attach → Linjector → LinuxHelperBackend
→ ptrace → bootstrapper → loader → frida-agent
→ AgentSession → create_script / load_script
与 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 内部的:
dlopen + dlsym
而不是把整条注入链改成另一种技术。若 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 主线)
spawn(package)
→ ensure_loaded() 幂等兜底(门控默认已在 server 启动时安装)
→ Android framework 请求启动 package
→ zygote/USAP fork child
→ child specialize / setcontext
→ child setArgV0
→ zymbiote 上报 pid/ppid/process-name 并等待 ACK
→ spawn 返回 child pid
→ attach(child pid)
→ ptrace/bootstrap/loader 注入完整 agent
→ create/load script
→ resume(child pid)
→ 还原 child 指针槽
→ ACK
→ child 自卸载 zymbiote payload,以 setArgV0 身份返回
→ ActivityThread.main → Application / Activity 生命周期
放行段最后三步是当前工程协议;上游为 ACK → tail-call raise(SIGSTOP) → server 等停、还原 → SIGCONT(§10)。
attach 已运行进程(对照)
目标已运行
→ attach(pid)
→ ptrace seize/interruption
→ 远程 mmap bootstrap 区和栈
→ 执行 bootstrapper
→ 写入并启动 loader
→ loader 创建工作线程
→ mapper 装载 frida-agent(本工程;上游为 dlopen)
→ frida_agent_main
→ AgentSessionProvider / AgentSession
→ 脚本 create/load
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 的思路对照,不在上游源码中):
git clone --depth 1 --branch 17.9.1 601K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6b7`.`. frida-core-17.9.1
cd frida-core-17.9.1
rg -n "public async uint spawn|public async Session attach" src/frida.vala
rg -n "perform_attach_to|inject_library_resource|request_control_channel" \
src/linux/linux-host-session.vala \
src/linux/linjector.vala
rg -n "class InjectTask|class InjectSession|bootstrap \(|launch_loader|class SeizeSession" \
src/linux/frida-helper-backend.vala
rg -n "ensure_loaded|inject_zymbiote|prepare_zymbiote_injection|handle_zymbiote_connection" \
src/linux/linux-host-session.vala
rg -n "replacement_setcontext|replacement_setargv0|TAILCALL_TO_RAISE_SIGSTOP" \
src/linux/helpers/zymbiote.c
rg -n "dlopen|dlsym|frida_agent_main|LinuxInjectorState" \
src/linux/helpers/loader.c \
lib/base/session.vala \
lib/agent/agent.vala
复核结果应能组成四条连续证据链(第三条左半是上游路径,右半是本工程思路):
spawn API → RoboLauncher → zymbiote hello → child pid
attach API → Linjector → InjectSession → loader
loader → dlopen/dlsym(上游)· custom mapper(本工程)→ frida_agent_main
agent → AgentSessionProvider → AgentSession
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). <8e5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7$3g2J5N6X3g2J5i4K6u0r3M7$3g2J5N6X3g2J5i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3I4z5e0W2Q4x3X3c8x3x3U0V1&6i4K6t1$3k6%4c8Q4x3@1t1`. - Frida 上游 17.9.1 —
src/control-service.vala(spawn/attach 转发 :915-934). <bb1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3j5$3!0F1N6s2u0G2L8q4)9J5k6s2y4W2M7Y4k6A6j5$3g2Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6V1I4y4g2)9J5k6p5H3&6x3K6c8Q4x3U0k6Y4N6q4)9K6b7R3`.`. - Frida 上游 17.9.1 —
src/frida.vala(Device.spawn:994、Device.attach:1138). <e36K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3k6Y4u0A6k6r3q4Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6p5I4x3K6S2Q4x3X3c8x3x3e0p5$3x3#2)9J5y4X3N6@1i4K6y4n7 - Frida 上游 17.9.1 —
src/host-session-service.vala(attach:571、establish:605). <4bcK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3K9r3!0K6N6q4)9J5k6s2y4W2M7%4y4A6L8$3&6Q4x3X3c8K6k6i4u0$3K9h3y4W2i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3#2y4K6q4Q4x3X3c8x3y4U0p5I4i4K6t1$3k6%4c8Q4x3@1t1`. - 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). <9b6K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2)9J5y4X3N6@1i4K6y4n7 - Frida 上游 17.9.1 —
src/linux/linjector.vala(inject_library_resource :84、memfd :94、request_control_channel :112). <fb9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6B7k6h3y4@1L8%4u0Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6R3@1i4K6u0V1e0o6p5I4y4q4)9J5y4X3N6@1i4K6y4n7 - Frida 上游 17.9.1 —
src/linux/frida-helper-backend.vala(InjectTask :308-330、bootstrap :1054、launch_loader :993、RemoteAgent :1452-1560、SeizeSession :1805-2110). <6c0K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6X3M7X3W2V1j5g2)9J5k6r3S2W2L8s2m8W2M7W2)9J5k6r3u0S2j5$3E0W2L8X3c8Q4x3X3g2$3j5h3I4S2i4K6t1$3k6%4c8Q4x3@1t1`. - Frida 上游 17.9.1 —
src/linux/helpers/loader.c(frida_load :61-63、dlopen :110-131、receive_fd :332、收尾 :168-188). <253K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3L8r3!0S2k6r3g2J5i4K6u0W2j5#2)9J5x3@1H3$3x3g2)9J5k6p5H3I4z5e0m8Q4x3U0k6Y4N6q4)9K6b7R3`.`. - Frida 上游 17.9.1 —
src/linux/helpers/loader.c的 dlopen 路径(:110-131),作为本工程自定义 mapper 的对照;agent-mapper 为本工程私有实现,不公开. <5cfK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3L8r3!0S2k6r3g2J5i4K6u0W2j5#2)9J5x3@1H3I4x3e0m8Q4x3X3c8x3x3e0x3I4i4K6t1$3k6%4c8Q4x3@1t1`. - 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). <9a7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3P5Y4W2E0j5X3W2G2N6r3g2Q4x3X3g2U0i4K6t1$3k6%4c8Q4x3@1t1`. - Frida 上游 17.9.1 —
lib/base/session.vala(LinuxInjectorState :1022;agent_base/agent_size为本工程在同名结构体上的扩展). <e99K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3L8r3W2T1i4K6u0r3j5X3q4K6k6g2)9J5c8Y4y4W2M7%4y4A6L8$3&6Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6p5H3x3U0u0Q4x3U0k6Y4N6q4)9K6b7R3`.`. - Frida 上游 17.9.1 —
lib/agent/agent.vala(入口 :1-7、create_and_run :116-184、run :267-277、provider 注册 :1175-1187). <120K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3L8r3W2T1i4K6u0r3j5h3N6W2L8Y4c8Q4x3V1k6S2k6$3g2F1N6q4)9J5k6i4k6S2L8r3q4Q4x3U0k6Y4N6q4)9K6b7R3`.`. - 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. <f4aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1N6r3!0G2L8s2y4Q4x3V1k6T1L8r3!0T1i4K6u0r3x3e0c8Q4x3X3f1^5i4K6u0W2x3q4)9J5c8X3k6J5K9h3c8S2i4K6g2X3N6r3!0G2L8s2y4Q4x3V1k6S2M7s2m8D9K9h3y4S2N6r3W2G2L8W2)9J5k6i4m8&6i4K6t1K6e0o6j5I4y4W2)9J5k6p5H3$3x3U0k6Q4x3U0k6Y4N6q4)9K6b7W2!0q4c8W2!0n7b7#2)9&6b7W2)9J5y4X3I4@1i4K6y4n7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1N6r3!0G2L8s2y4Q4x3V1k6T1L8r3!0T1i4K6u0r3x3e0c8Q4x3X3f1^5i4K6u0W2x3q4)9J5c8X3k6J5K9h3c8S2i4K6g2X3N6r3!0G2L8s2y4Q4x3V1k6J5k6i4m8D9i4K6u0W2M7s2W2Q4x3U0y4x3x3U0b7%4i4K6u0V1e0o6x3H3y4W2)9J5y4X3N6@1i4K6y4n7 - Frida 上游源码 —
frida-core/src/linux/linux-host-session.vala,frida-helper-backend.vala,helpers/loader.c,helpers/zymbiote.c. <323K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5y4X3N6@1i4K6y4n7 - Frida 官方文档 — Modes of Operation. <426K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6X3M7X3W2V1j5g2)9J5k6i4u0W2i4K6u0r3k6r3!0U0M7#2)9J5c8X3#2G2k6r3g2K6i4K6u0r3i4K6t1$3k6%4c8Q4x3@1t1`.
- Android Code Search — Launcher3
ItemClickHandler.java(桌面图标点击进入startActivitySafely). <298K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7i4m8S2j5$3E0S2k6$3g2K6i4K6u0r3j5i4m8H3M7#2)9J5c8V1I4S2N6h3&6U0K9r3g2J5x3#2)9J5c8Y4y4J5j5#2)9J5c8X3y4G2L8g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6D9j5i4g2F1j5$3S2W2M7U0y4Q4x3V1k6@1L8%4g2U0K9q4)9J5c8V1W2@1k6h3#2o6L8r3W2U0K9@1S2S2L8X3c8D9k6i4u0Q4x3X3g2B7j5i4k6S2i4K6t1$3k6%4c8Q4x3@1t1`. - Android Code Search —
ActivityTaskManagerService.java与ActivityStarter.java(Activity 启动请求在system_server中的解析与执行). <ad9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3M7$3g2J5N6X3W2U0k6i4y4Q4x3V1k6U0L8%4u0W2i4K6u0r3K9X3q4$3j5g2)9J5c8X3y4G2L8g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6K6k6i4u0$3k6i4u0Q4x3V1k6%4L8g2)9J5c8V1q4U0N6r3W2$3K9i4c8&6g2r3q4K6K9@1#2S2L8X3q4Y4k6i4u0e0k6i4u0$3K9h3y4W2i4K6u0W2K9X3q4$3j5g2)9J5y4X3N6@1i4K6y4n7i4@1g2r3i4@1u0o6i4K6W2n7i4K6t1$3L8s2c8Q4x3@1u0Z5N6s2c8H3M7#2)9K6b7g2)9J5c8W2)9J5c8X3y4K6i4K6u0W2j5h3&6V1M7X3!0A6k6q4)9J5k6h3y4G2L8g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6H3L8r3q4@1k6X3!0J5L8g2)9J5c8Y4y4#2M7r3g2J5M7s2u0G2K9X3g2U0N6q4)9J5c8X3#2S2K9h3&6Q4x3V1k6Q4x3V1u0Q4x3V1k6E0j5h3W2F1i4K6y4m8k6Y4u0S2L8h3g2%4L8%4u0C8M7#2)9J5c8X3u0S2M7$3g2Q4x3V1k6K6k6i4u0$3K9h3y4W2M7#2)9J5c8X3y4G2M7X3g2Q4x3V1k6B7j5i4k6S2i4K6u0r3j5$3!0E0i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5c8Y4y4W2M7Y4k6W2M7W2)9J5c8Y4N6E0i4K6u0r3b7h3y4@1K9i4k6A6N6s2W2e0N6r3q4J5N6r3g2J5i4K6u0W2K9X3q4$3j5g2)9J5y4X3N6@1i4K6y4n7 - Android Code Search —
ProcessList.java(startProcessLocked()、entryPoint = "android.app.ActivityThread"与进程创建参数). <425K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3M7$3g2J5N6X3W2U0k6i4y4Q4x3V1k6U0L8%4u0W2i4K6u0r3K9X3q4$3j5g2)9J5c8X3y4G2L8g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6K6k6i4u0$3k6i4u0Q4x3V1k6S2L8g2)9J5c8W2m8J5L8$3y4W2M7%4y4x3K9i4y4@1i4K6u0W2K9X3q4$3j5g2)9J5y4X3N6@1i4K6y4n7 - Android Code Search —
Process.java与ZygoteProcess.java(Process.start()、startViaZygote()、Zygote socket 协议与 USAP 路径). <9b1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3L8%4y4Q4x3V1k6b7M7X3!0U0k6i4y4K6i4K6u0W2K9X3q4$3j5g2)9J5y4X3N6@1i4K6y4n7i4@1g2r3i4@1u0o6i4K6W2n7i4K6t1$3L8s2c8Q4x3@1u0Z5N6s2c8H3M7#2)9K6b7g2)9J5c8W2)9J5c8X3y4K6i4K6u0W2j5h3&6V1M7X3!0A6k6q4)9J5k6h3y4G2L8g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6H3L8r3q4@1k6X3!0J5L8g2)9J5c8Y4y4#2M7r3g2J5M7s2u0G2K9X3g2U0N6q4)9J5c8X3#2S2K9h3&6Q4x3V1k6Q4x3V1u0Q4x3V1k6E0j5h3W2F1i4K6y4m8k6Y4u0S2L8h3g2%4L8%4u0C8M7#2)9J5c8X3u0S2M7$3g2Q4x3V1k6U0L8%4u0W2i4K6u0r3K9X3q4$3j5g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6G2M7#2)9J5c8W2A6&6k6$3!0@1k6g2m8J5L8$3y4W2M7%4y4Q4x3X3g2B7j5i4k6S2i4K6t1$3k6%4c8Q4x3@1t1`. - Android Code Search —
ZygoteServer.java与ZygoteConnection.java(runSelectLoop()、对端身份检查、processCommand()与父子分流). <c80K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3K9h3&6@1k6i4u0F1j5h3I4Q4x3V1k6G2M7#2)9J5c8W2A6&6k6$3!0@1k6g2y4W2M7Y4k6W2M7W2)9J5k6h3A6S2N6X3q4Q4x3U0k6Y4N6q4)9K6b7W2!0q4c8W2!0n7b7#2)9&6b7W2)9J5y4X3I4@1i4K6y4n7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3K9h3&6@1k6i4u0F1j5h3I4Q4x3V1k6G2M7#2)9J5c8W2A6&6k6$3!0@1k6f1y4G2L8X3&6W2j5%4c8A6L8$3&6Q4x3X3g2B7j5i4k6S2i4K6t1$3k6%4c8Q4x3@1t1`. - Android Code Search —
Zygote.java与com_android_internal_os_Zygote.cpp(forkAndSpecialize()、nativeForkAndSpecialize()、ForkCommon()与SpecializeCommon()). <94aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3K9h3&6@1k6i4u0F1j5h3I4Q4x3V1k6G2M7#2)9J5c8W2A6&6k6$3!0@1k6g2)9J5k6h3A6S2N6X3q4Q4x3U0k6Y4N6q4)9K6b7W2!0q4c8W2!0n7b7#2)9&6b7W2)9J5y4X3I4@1i4K6y4n7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6F1K9g2)9J5c8X3y4G2L8g2)9#2k6X3q4F1k6s2u0G2K9h3c8Q4y4h3k6A6L8Y4c8W2M7X3&6S2L8q4)9#2k6X3!0K6i4K6g2X3h3Y4W2Y4L8%4c8W2i4K6u0W2j5%4m8H3i4K6t1$3k6%4c8Q4x3@1t1`. - Android Code Search —
ZygoteInit.java、RuntimeInit.java与ActivityThread.java(子进程从handleChildProc()进入ActivityThread.main()). <45dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3K9h3&6@1k6i4u0F1j5h3I4Q4x3V1k6G2M7#2)9J5c8W2A6&6k6$3!0@1k6f1W2F1K9i4c8Q4x3X3g2B7j5i4k6S2i4K6t1$3k6%4c8Q4x3@1u0Q4c8f1k6Q4b7V1y4Q4z5f1u0Q4x3U0k6D9N6q4)9K6b7X3S2@1N6s2m8K6i4K6y4m8i4K6u0r3i4K6u0r3j5%4y4Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2j5$3!0E0i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5c8Y4m8D9j5i4c8X3L8%4u0E0i4K6u0r3M7%4g2H3k6i4u0H3M7X3!0B7k6h3y4@1i4K6u0r3L8h3q4A6L8W2)9J5c8W2)9J5b7W2)9J5c8X3#2S2K9h3&6Q4x3@1q4X3M7X3q4E0k6i4N6G2M7X3E0K6i4K6u0r3j5X3q4K6k6g2)9J5c8X3y4G2M7X3g2Q4x3V1k6B7j5i4k6S2i4K6u0r3j5$3!0E0i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5c8X3W2F1N6r3g2J5L8X3q4D9i4K6u0r3L8%4y4Q4x3V1k6d9N6h3&6@1K9h3#2W2d9h3&6A6N6q4)9J5k6h3A6S2N6X3q4Q4x3U0k6Y4N6q4)9K6b7W2!0q4c8W2!0n7b7#2)9&6b7W2)9J5y4X3I4@1i4K6y4n7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3j5i4m8H3i4K6u0r3b7h3y4@1K9i4k6A6N6s2W2f1K9s2u0W2j5h3c8Q4x3X3g2B7j5i4k6S2i4K6t1$3k6%4c8Q4x3@1t1`. - 看雪论坛 · 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收尾. <576K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0p5%4i4K6u0W2x3q4)9J5c8Y4y4J5j5#2)9J5c8X3I4A6L8Y4g2^5i4K6u0r3L8r3W2F1N6i4S2Q4x3X3c8Z5L8%4y4@1i4K6u0V1M7$3g2K6M7$3W2G2L8W2)9J5k6i4k6S2L8r3q4Q4x3U0k6Y4N6q4)9K6b7W2!0q4c8W2!0n7b7#2)9&6b7W2)9J5y4X3I4@1i4K6y4n7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0p5%4i4K6u0W2x3q4)9J5c8Y4y4J5j5#2)9J5c8X3I4A6L8Y4g2^5i4K6u0r3K9r3g2D9M7r3g2J5M7#2)9J5c8Y4A6&6L8h3u0A6L8%4c8W2i4K6u0W2j5#2)9J5y4X3N6@1i4K6y4n7
赞赏
- [原创]Frida 整体启动逻辑 367