首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Android安全
发新帖
14
21
[原创]Frida 整体启动逻辑
发表于: 2026-8-31 03:32
22924
[原创]Frida 整体启动逻辑
mb_peeqldfc
1
2026-8-31 03:32
22924
# F01 · Frida 整体启动、进程注入与 Zygote 门控 > 《凡人修仙传之 - Android 逆向开发》· 03 动态插桩功法 · 技能 F01 > > 前置:01 炼气境 · 第 05 课《Android 应用启动过程》、第 07 课《Android ptrace 原理》 > > 后续:F02 脚本加载时机;F04 server IPC;F05 Android SELinux;F09 自定义 linker > > 版本锚点:上游源码统一锚定 GitHub <a href="elink@27dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8Y4c8J5k6h3g2Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4">`frida/frida-core` tag **17.9.1**</a>(2026-03-27,对应 frida 主仓库中 frida-core submodule commit `a62376a3`);frida-tools 取同期版本 <a href="elink@eb5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1N6r3!0G2L8s2y4Q4x3V1k6@1M7X3g2W2i4K6u0r3x3e0c8Q4x3X3f1^5i4K6u0W2x3l9`.`.">14.8.0</a>(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 | 最容易混淆的三个名字先单独钉住: ```text Zygote = Android 的应用进程父体 zymbiote = Frida 放进父体的小型门控载荷 frida-agent = 最终进入目标 App、真正运行 Gum 与脚本的完整载荷 ``` ## 0. 录课导学:从桌面点击到 Zygote fork 本课不从开机讲起。先把 Zygote 当作一个已经运行、正在等待创建请求的父进程,只看一个最常见的现场: > 目标 App 当前没有进程。用户在桌面点击图标以后,究竟是谁找到 Zygote,又是谁执行了 `fork()`? 容易产生两个误判: 1. Launcher 点击图标后直接创建了目标进程; 2. `system_server` 收到启动请求后亲自 `fork()` 出 App。 两者都不准确。Launcher 只发起 Activity 启动请求;`system_server` 负责解析目标组件、检查启动条件并准备进程参数;真正复制进程地址空间的是 Zygote。先建立这条 Android 正常基线,后面才能看懂 Frida `-f` 为什么要提前处理 Zygote 和 USAP。 本节源码导航统一使用 <a href="elink@f0bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1j5`.">Android Code Search</a> 的 AOSP `main` 链接 [16]–[22];课程实验仍以 Android 14/API 34 为基线。`main` 后续增加的快速路径会单独标出,不把当前实现误写成所有版本唯一实现。 ### 0.1 Launcher:点击图标只是发起启动请求 > 源码直达:<a href="elink@f8bK9s2c8@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@1S2S2L8X3c8D9k6i4u0Q4x3X3g2B7j5i4k6S2">Launcher3 · ItemClickHandler.java</a> · <a href="elink@6b6K9s2c8@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$3j5b7`.`.">ActivityTaskManagerService.java</a> · <a href="elink@7adK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3M7$3g2J5N6X3W2U0k6i4y4Q4x3V1k6U0L8%4u0W2i4K6u0r3K9X3q4$3j5g2)9J5c8X3y4G2L8g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6K6k6i4u0$3k6i4u0Q4x3V1k6%4L8g2)9J5c8V1q4U0N6r3W2$3K9i4c8&6f1%4c8S2M7Y4c8W2M7W2)9J5k6h3A6S2N6X3p5`.">ActivityStarter.java</a> AOSP Launcher3 的参考实现把桌面图标点击交给 `ItemClickHandler.onClick()`;厂商桌面的类名和动画实现可能不同,但最终仍要发起 framework 的 Activity 启动请求。普通应用图标依次进入: ```text ItemClickHandler.onClick() → onClickAppShortcut() → startAppShortcutOrInfoActivity() → launcher.startActivitySafely(...) ``` 源码入口见 `ItemClickHandler.java` [16]。这一步持有的是描述目标 Activity 的 `Intent`,没有 `fork()`,也没有加载目标 APK。请求继续经过 Android 的 Activity 启动接口,以 Binder 调用进入 `system_server` 中的 `ActivityTaskManagerService`(ATMS);ATMS 与 `ActivityStarter` 负责解析目标 Activity、任务栈、用户和启动限制 [17]。 第一段链路可先记成: ```text 桌面图标 → Launcher3 → startActivity → Binder → system_server:ATMS / ActivityStarter ``` ### 0.2 system_server:决定是否需要新进程 > 源码直达:<a href="elink@548K9s2c8@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$3j5b7`.`.">ProcessList.java</a> · <a href="elink@b9aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3L8%4y4Q4x3V1k6b7M7X3!0U0k6i4y4K6i4K6u0W2K9X3q4$3j5b7`.`.">Process.java</a> · <a href="elink@261K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3L8%4y4Q4x3V1k6K9P5h3N6G2N6r3g2b7M7X3!0U0k6i4y4K6i4K6u0W2K9X3q4$3j5b7`.`.">ZygoteProcess.java</a> 如果目标进程已经存在,系统可以直接向现有进程下发 Activity 生命周期事务,不需要再次请求 Zygote。 如果目标进程不存在,启动链会进入 AMS 的进程管理逻辑,最终由 `ProcessList.startProcessLocked()` 准备创建参数。这里有一个关键值 [18]: ```java 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 命令: ```text 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: ```text ProcessList → Process.start() → ZygoteProcess.startViaZygote() → 写入 Zygote socket → 等待 pid / usingWrapper ``` ### 0.3 Zygote:收到命令后才真正 fork > 源码直达:<a href="elink@371K9s2c8@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)9J5k6h3A6S2N6X3p5`.">ZygoteServer.java</a> · <a href="elink@cb1K9s2c8@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&6Q4x3X3g2B7j5i4k6S2">ZygoteConnection.java</a> · <a href="elink@5cfK9s2c8@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)9J5k6h3A6S2N6X3p5`.">Zygote.java</a> · <a href="elink@e37K9s2c8@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%4m8H3">Zygote native</a> Zygote 已经在 `ZygoteServer.runSelectLoop()` 中监听命令。socket 到来后,server 创建 `ZygoteConnection` 并调用 `processCommand()` [20]。 连接建立时,`ZygoteConnection` 会读取对端凭据并限制调用方: ```java if (peer.getUid() != Process.SYSTEM_UID) { throw new ZygoteSecurityException( "Only system UID is allowed to connect to Zygote."); } ``` 因此普通 App 不能绕过 `system_server`,直接要求 Zygote 按任意 uid 创建进程。命令通过参数与权限检查后,常规路径进入: ```java 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 | 子进程随后经: ```text handleChildProc() → ZygoteInit.zygoteInit() → RuntimeInit.applicationInit() → findStaticMain("android.app.ActivityThread") → ActivityThread.main() ``` 对应源码入口见 `ZygoteInit.java`、`RuntimeInit.java` 与 `ActivityThread.java` [22]。到 `ActivityThread.main()` 时,新 App 进程已经出生,但应用自己的 `Application.onCreate()` 和 Activity 生命周期仍要等待后续绑定与事务分派。 > 子进程入口直达:<a href="elink@d55K9s2c8@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@1k6f1W2F1K9i4c8Q4x3X3g2B7j5i4k6S2">ZygoteInit.java</a> · <a href="elink@ba3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3K9h3&6@1k6i4u0F1j5h3I4Q4x3V1k6G2M7#2)9J5c8W2u0#2L8Y4c8A6L8h3g2u0L8X3W2@1i4K6u0W2K9X3q4$3j5b7`.`.">RuntimeInit.java</a> · <a href="elink@52bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3j5i4m8H3i4K6u0r3b7h3y4@1K9i4k6A6N6s2W2f1K9s2u0W2j5h3c8Q4x3X3g2B7j5i4k6S2">ActivityThread.java</a> ### 0.4 一张图看懂桌面点击到 App 子进程 ```text 用户点击桌面图标 → 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”: ```text 常规路径:请求 → 主 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 时: ```text 父进程已有 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 执行: ```bash frida -U -f TARGET_PACKAGE -l probe.js ``` 做的主事是 **spawn**:让系统启动一个全新的 App 进程,并赶在它的业务代码运行之前接管。它不是“把一段 JavaScript 发给目标进程”这么简单。Android 上至少发生两次性质不同的注入: 1. **zygote 门控注入**:把一个很小的 `zymbiote` 载荷写入 zygote、USAP 或 Chrome zygote,并 hook 两个函数指针;默认在 frida-server 启动时(preload)就完成,先于任何客户端命令,spawn 到来时只是幂等兜底; 2. **目标进程 agent 注入(spawn 与 attach 共用)**:得到子进程 PID 后,再通过 ptrace/bootstrap/loader 把完整 `frida-agent` 装入这个子进程。 最重要的结论是: > Frida 为了实现 `-f`,会处理 zygote;但它不会把完整 `frida-agent` 常驻到 zygote。zygote 中驻留的是负责出生门控的 `zymbiote` 小载荷,完整 agent 最终进入目标 App 子进程。 从 frida-server 启动到 `-f` 跑完的完整时间线如下,也是本课第 3–10 节的展开顺序: ```text [设备] 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` 如何接住客户端请求 > 源码直达:<a href="elink@0b2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7$3g2J5N6X3g2J5i4K6u0r3M7$3g2J5N6X3g2J5i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3I4z5e0W2Q4x3X3c8x3x3U0V1&6">server/server.vala @17.9.1</a> · <a href="elink@0d6K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3j5$3!0F1N6s2u0G2L8q4)9J5k6s2y4W2M7Y4k6A6j5$3g2Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6V1I4y4g2)9J5k6p5H3&6x3K6b7`.">src/control-service.vala @17.9.1</a> `server/server.vala:199-220` 的 `run_application()` 创建 `Application`。`Application.start()` 在 `server/server.vala:294-296` 中构造并启动 `ControlService`: ```vala 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 实测行号,与快照一致): ```vala 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 确定之后才注入: ```text 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 的前置) > 源码直达:<a href="elink@39fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3I4x3K6j5H3i4K6u0V1e0o6p5@1x3U0p5`.">src/linux/linux-host-session.vala @17.9.1 · RoboLauncher</a> 门控不属于 spawn 流程:默认情况下,它在 frida-server 启动时经 §3 的 preload 链就已经完成,先于任何客户端命令。注入与卸载的时机只有这几处: ```text 注入(幂等,已注入的父进程直接跳过): 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 注入对象:五个父进程 > 源码直达:<a href="elink@c01K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3I4y4e0p5%4i4K6u0V1e0o6p5$3x3o6b7`.">ensure_loaded() @17.9.1</a> `RoboLauncher.ensure_loaded()` 在 `linux-host-session.vala:1517-1603` 建立随机名称的抽象 Unix socket(socket 名见 `:1531`,形如 `/frida-zymbiote-<uuid>`),然后枚举: ```text zygote zygote64 usap32 usap64 com.android.chrome_zygote ``` 对应的选择逻辑是: ```vala 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 怎么装进父进程、装在哪里 > 源码直达:<a href="elink@562K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3I4y4U0l9#2i4K6u0V1e0o6p5^5z5e0V1`.">inject_zymbiote() / do_prepare_zymbiote_injection() @17.9.1</a> · <a href="elink@0c2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3P5Y4W2E0j5X3W2G2N6r3g2Q4x3X3g2U0">helpers/zymbiote.c @17.9.1</a> `inject_zymbiote()` 不走 `frida_agent_main`,也不建立 GumJS。5.1 是注入流程,5.2 是载荷位置——位置直接决定 zygote 的 maps 里会多出什么特征。 ### 5.1 注入流程:选位置、写入、改指针 上游 17.9.1 的 `inject_zymbiote()`(`linux-host-session.vala:1605-1648`)先经 `do_prepare_zymbiote_injection()` 完成全部定位,再一次性写入: 1. 扫描父进程 maps,挑出 `libstagefright.so` 的可执行映射,取**最后一页**作为载荷位置(取舍见 5.2); 2. 同时定位 `libc.so`、`libselinux.so` 与 `libandroid_runtime.so`,解析导出/导入表找到两个接入点,并在 boot heap / 匿名 rw 映射里搜索 `setArgV0` 的指针槽;若发现该父进程已被注入过(`already_patched`),改按 `file_offset` 从磁盘上的 so 文件读回原始字节,补全还原账本; 3. `STOP` + `wait_until_stopped` 暂停父进程; 4. `patches.apply()` 把 payload 写入尾页,再把两个指针槽改到 payload 的 replacement; 5. `finally CONT` 恢复父进程。 核心写入顺序(上游 17.9.1 摘录): ```vala 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` 还原。生命周期只有三步(逻辑示意,非关键实现): ```text 注入: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 <a href="elink@e33K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3I4z5e0p5I4i4K6u0V1e0o6t1H3x3o6l9`.">`linux-host-session.vala:1911-2000`</a>)只截取 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 <a href="elink@ce3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3I4y4K6p5J5i4K6u0V1e0o6p5%4x3U0p5`.">`linux-host-session.vala:1712-1721`</a>)扫父进程 maps,找**已存在的可执行文件映射**,路径以 `/libstagefright.so` 结尾(17.9.1 只认这一个候选,不挑 `libmedia.so`): ```text 可执行 && 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): ```text 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 的两条写入路径,各占一半: 1. **父进程不变**:server 经 `/proc/<pid>/mem` 写尾页是页表级 COW,不触碰 VMA 树;两个 replacement 又只在 fork 出的子进程里执行(specialize 是子进程路径),zygote 自身从不执行那次 `mprotect`。所以父进程永远一条 r-x,与实测一致。 2. **子进程切口**:fork 继承 payload 与指针改写后,setcontext replacement 在保存 `package_name` 时执行 `mprotect (payload_base, payload_size, RWX)`(上游 17.9.1 <a href="elink@034K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3P5Y4W2E0j5X3W2G2N6r3g2Q4x3X3g2U0i4K6t1K6e0o6M7H3">`zymbiote.c:70`</a> 的权限窗口)。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()`:执行顺序与启动请求 > 源码直达:<a href="elink@3a4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3I4y4o6b7J5i4K6u0V1e0o6p5#2x3o6t1`.">RoboLauncher.spawn() @17.9.1</a> spawn 请求在 §3 的 `HostSession.spawn()` 落地。目标参数是 Android 包名,包名不是普通可执行文件路径,`linux-host-session.vala:338` 因此进入: ```vala return yield robo_launcher.spawn (program, options, cancellable); ``` `RoboLauncher.spawn()`(`linux-host-session.vala:1442-1502`)的执行顺序: ```text 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 已经拆过的正常链,直接引用: ```text start_package → system_server:ATMS / AMS / ProcessList.startProcessLocked → Zygote socket(或 USAP 池) → fork child → specialize ``` fork 出的子进程继承门控,在 specialize 阶段撞上 zymbiote(6.2)。 ### 6.2 子进程如何被识别并卡在正确时机 > 源码直达:<a href="elink@e89K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3P5Y4W2E0j5X3W2G2N6r3g2Q4x3X3g2U0i4K6t1K6e0o6j5H3i4K6u0V1e0o6V1^5">helpers/zymbiote.c @17.9.1 · 两个 replacement</a> zygote fork 后,匿名 payload 和被改写的指针槽由子进程继承。`helpers/zymbiote.c` 中有两个 replacement: ```text 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()`,随后: ```text 连接 server 的 abstract Unix socket → 发送 pid + ppid + process-name → 阻塞等待 1 字节 ACK ``` 对应代码骨架是: ```c 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()` 发送固定头和进程名: ```c 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 后:目标等待,非目标立即放行 > 源码直达:<a href="elink@89fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3J5x3o6f1J5i4K6u0V1e0o6t1H3z5o6R3`.">handle_zymbiote_connection() @17.9.1</a> server 在 `linux-host-session.vala:2052-2088` 的 `handle_zymbiote_connection()` 里收到 hello 后,不会立刻给每个子进程注入 agent,而是先判断它属于哪一条分支: ```vala 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()`,但不会一直卡住。当前工程对它立即执行下面这条清理链: ```text 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 > 源码直达:<a href="elink@39eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1N6r3!0G2L8s2y4Q4x3V1k6T1L8r3!0T1i4K6u0r3x3e0c8Q4x3X3f1^5i4K6u0W2x3q4)9J5c8X3k6J5K9h3c8S2i4K6g2X3N6r3!0G2L8s2y4Q4x3V1k6S2M7s2m8D9K9h3y4S2N6r3W2G2L8W2)9J5k6i4m8&6i4K6t1K6e0o6j5I4y4W2)9J5k6p5H3$3x3U0j5`.">frida_tools/application.py @14.8.0</a> · <a href="elink@c62K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1N6r3!0G2L8s2y4Q4x3V1k6T1L8r3!0T1i4K6u0r3x3e0c8Q4x3X3f1^5i4K6u0W2x3q4)9J5c8X3k6J5K9h3c8S2i4K6g2X3N6r3!0G2L8s2y4Q4x3V1k6J5k6i4m8D9i4K6u0W2M7s2W2Q4x3U0y4x3x3U0b7%4i4K6u0V1e0o6x3H3y4R3`.`.">frida_tools/repl.py @14.8.0</a> · <a href="elink@9c1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3k6Y4u0A6k6r3q4Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6p5I4x3K6S2Q4x3X3c8x3x3e0p5$3x3H3`.`.">src/frida.vala @17.9.1</a> · <a href="elink@ed4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3K9r3!0K6N6q4)9J5k6s2y4W2M7%4y4A6L8$3&6Q4x3X3c8K6k6i4u0$3K9h3y4W2i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3#2y4K6q4Q4x3X3c8x3y4U0p5I4">src/host-session-service.vala @17.9.1</a> `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: ```text Device.attach(child_pid) → 本节 7.2–7.4 的 ptrace/bootstrap/loader 路径 → 完整 frida-agent 进入 App 子进程 → 创建并加载脚本 → Device.resume(child_pid)(第 10 节) ``` 所以 zygote 门控与 agent 注入不是二选一,而是前后相接: ```text zymbiote 负责“发现并卡住新生 App” frida-agent 负责“在 App 内运行 Gum 与脚本” ``` 这条装载链的 API 侧入口如下。`src/frida.vala:1138-1163` 的 `Device.attach()` 先调用远端 `host_session.attach()`,再把返回的 `AgentSessionId` 链接成本地 `Session`: ```vala 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`): ```text establish(pid) → perform_attach_to(pid) → 等待 agent control stream → 建立 DBusConnection → 获取 AgentSessionProvider proxy → provider.open(session_id) ``` 这里的 `attach()` 不是单一系统调用,而是一条从控制面到目标进程运行时的状态机。 ### 7.2 Linux 后端选择 agent > 源码直达:<a href="elink@d2aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3K6z5o6N6Q4x3X3c8x3y4o6l9#2">perform_attach_to() @17.9.1</a> · <a href="elink@a6cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6B7k6h3y4@1L8%4u0Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6R3@1i4K6u0V1e0o6p5I4y4l9`.`.">src/linux/linjector.vala @17.9.1</a> `src/linux/linux-host-session.vala:387-405` 明确指定 agent 入口: ```vala 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()`。 ```text 目标 pid → 判断 ABI → 取得匹配的 frida-agent → fd/path 交给 Linux helper → 等待 control channel ``` ### 7.3 ptrace 只是入口,真正任务是建立远程执行环境 > 源码直达:<a href="elink@49fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6X3M7X3W2V1j5g2)9J5k6r3S2W2L8s2m8W2M7W2)9J5k6r3u0S2j5$3E0W2L8X3c8Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6x3H3x3q4)9J5k6p5H3K6x3K6l9`.">frida-helper-backend.vala @17.9.1 · InjectTask</a> 上游 17.9.1 的 `InjectTask.run()`(`frida-helper-backend.vala:308-330`)经 `InjectSession.open()`(`:868`)创建 `InjectSession`。其基类 `SeizeSession`(`:1805-2110`;`PTRACE_SEIZE` 在 `:1888`,`GETREGSET` 在 `:2084-2105`)完成: ```text PTRACE_SEIZE(旧内核回退 PTRACE_ATTACH) → PTRACE_INTERRUPT / 等待 stop → GETREGSET 保存寄存器 → 获得目标线程的受控执行点 ``` 进入 ptrace 停止态后,Frida 没有直接把 PC 改到 `frida_agent_main`。目标地址空间此时还没有 loader、agent、远程栈和控制通道。`InjectSession.bootstrap()`(`frida-helper-backend.vala:1054` 起)先建立这些条件: 1. 从目标 maps 定位 libc 和 Android linker; 2. 计算目标进程中的 `mmap`、`munmap` 等函数地址; 3. 远程调用 `mmap` 分配 bootstrapper、loader 数据和 64 KiB 工作栈; 4. 若不能直接使用目标 libc 的 `mmap`,临时借用目标现有代码页执行 bootstrapper,分配完成后还原原字节; 5. 在目标内执行 bootstrapper,解析 libc/linker API,并建立 socketpair 或抽象 Unix socket 回退通道(这条通道上 fd 的跨进程传递方式见 §7.4 的 SCM_RIGHTS 小节); 6. 把 loader 代码、入口名、agent 参数和函数表写入目标内存。 这一步的输出不是 agent,而是一个可以在目标内部继续装载 agent 的最小运行环境。 ### 7.4 loader 从被劫持线程切到自己的工作线程 > 源码直达:<a href="elink@8b8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6X3M7X3W2V1j5g2)9J5k6r3S2W2L8s2m8W2M7W2)9J5k6r3u0S2j5$3E0W2L8X3c8Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6V1&6x3#2)9J5k6p5H3I4x3o6f1H3">launch_loader() / RemoteAgent @17.9.1</a> · <a href="elink@777K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3L8r3!0S2k6r3g2J5i4K6u0W2j5#2)9J5x3@1H3$3x3g2)9J5k6p5H3I4z5e0l9`.">helpers/loader.c @17.9.1</a> `frida-helper-backend.vala:993-1050` 将目标寄存器中的 PC 指向 loader 基址、SP 指向远程栈,再通过 `RemoteCall` 执行 loader 入口。`helpers/loader.c:61-63` 的 `frida_load()` 立即创建工作线程: ```c 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`): ```text 向 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()` 内): ```vala 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 表里取: ```c 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 <a href="elink@19bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3L8r3!0S2k6r3g2J5i4K6u0W2j5#2)9J5x3@1H3I4x3e0m8Q4x3X3c8x3x3e0x3I4">`loader.c:110-131`</a>): ```text 接收 agent fd → 拼出 /proc/self/fd/<fd> → bionic dlopen()(把 libc 的 close 伪装成调用者地址传入) → dlsym("frida_agent_main") → 调用入口 ``` 这会让 bionic linker 参与装载,并建立 `soinfo`、namespace、`link_map`、模块计数和 CFI 等账本。 ### 8.2 当前工程路径:完全脱离 linker 的自定义装载 > 源码直达(上游对照):<a href="elink@83cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3L8r3!0S2k6r3g2J5i4K6u0W2j5#2)9J5x3@1H3I4x3e0m8Q4x3X3c8x3x3e0x3I4">helpers/loader.c @17.9.1 · dlopen 路径</a>。本工程的 agent-mapper 为私有实现,不附源码,这里只讲设计。 我们删掉了 loader 里的 `dlopen()` / `dlsym()`,换成自己的最小 ELF 装载器。设计目标只有一个:**让 agent 与 bionic linker 彻底无关**——不调用 linker、不注册 `soinfo`、不进 `link_map`、不触发命名空间合并与 `DT_NEEDED` 递归装载、不产生任何 linker 侧账本与回调。上游靠 `dlopen` 换来的“省事”,代价是把 agent 整个写进系统账本(§8.1);自定义 mapper 把这一整类 frida 特征从装载环节移出,而不是事后清理。装载流程本身只有七步: ```text 读取 ELF header / program header → 匿名 mmap 整个 load span → 复制 PT_LOAD → 处理 RELA / JMPREL → 设置 segment 权限 → 执行 init array → 从动态符号表找到 frida_agent_main ``` 支撑这个设计的两个硬性取舍: 1. **只做 agent 需要的最小子集。** mapper 只实现当前 agent 实际用到的装载语义:AArch64 与 x86_64,拒绝 `PT_TLS`、`DT_RELR` 和未实现的 relocation 类型;不处理 namespace、`DT_NEEDED` 递归依赖、符号版本。换来的是实现小、行为可穷举——装载器自身不引入新的系统交互面。 2. **失败不回退 `dlopen()`。** 上游 `dlopen` 失败会向 helper 报 `ERROR_DLOPEN` 并走异常路径(`loader.c:150-158`);我们的 mapper 失败直接退出装载,宁可这次注入失败,也不让 agent 重新出现在 linker 账本里——回退等于把刚消掉的特征又请回来。 与 §5.2 的门控是同一套设计签名:**匿名 mmap 进来、不碰任何已有 so、不进任何账本**——门控在 ACK 后 `munmap` 自毁,agent 则随进程生命周期驻留。落到装载位置上,我们与上游的差别压缩成代码就是一对一的替换(逻辑示意,非关键实现): ```c /* 上游 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` > 源码直达:<a href="elink@076K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3L8r3W2T1i4K6u0r3j5X3q4K6k6g2)9J5c8Y4y4W2M7%4y4A6L8$3&6Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6p5H3x3U0t1`.">lib/base/session.vala @17.9.1 · LinuxInjectorState</a> · <a href="elink@4f7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3L8r3W2T1i4K6u0r3j5h3N6W2L8Y4c8Q4x3V1k6S2k6$3g2F1N6q4)9J5k6i4k6S2L8r3q4Q4x3U0y4x3x3e0p5$3i4K6u0V1e0o6p5^5y4l9`.`.">lib/agent/agent.vala @17.9.1 · create_and_run</a> 匿名 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` 之后发生什么 > 源码直达:<a href="elink@965K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3L8r3W2T1i4K6u0r3j5h3N6W2L8Y4c8Q4x3V1k6S2k6$3g2F1N6q4)9J5k6i4k6S2L8r3q4Q4x3U0y4x3x3g2)9J5k6p5H3%4">lib/agent/agent.vala @17.9.1 · 入口</a> `lib/agent/agent.vala:1-7` 的入口只负责创建或恢复 `Runner`: ```vala 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`)依次完成(前三行是上游行为,标注处为本工程扩展): ```text 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`: ```vala 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 继续启动 > 源码直达:<a href="elink@1a5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3J5x3e0M7#2i4K6u0V1e0o6t1I4z5e0b7`.">ZymbioteConnection.resume() @17.9.1</a> · <a href="elink@90bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3P5Y4W2E0j5X3W2G2N6r3g2Q4x3X3g2U0i4K6t1K6e0o6p5&6x3g2)9J5k6p5H3J5y4e0x3`.">zymbiote.c @17.9.1 · TAILCALL_TO_RAISE_SIGSTOP</a> > **放行时的分工,先钉死:两个版本里动手写槽位的永远是 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`): ```vala 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`): ```text 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 开还原窗口”是同一次跳转完成的。 当前工程把顺序改为“先还原、再放行、后确认”: ```text 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 的是: ```c 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): ```text 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。如果不需要“早于业务代码”,也可以省掉整个门控段,直接对已运行进程执行: ```bash frida -U -n TARGET_NAME # 按进程名;或 -p PID ``` 此时走的就是第 7 节拆过的那条装载链,一个环节都不少: ```text attach(pid) → HostSession.attach → Linjector → LinuxHelperBackend → ptrace → bootstrapper → loader → frida-agent → AgentSession → create_script / load_script ``` 与 spawn 只有两点不同: 1. 前面没有 zymbiote 门控段,也就没有第 4–6 节的出生拦截; 2. 时机晚:目标进程早已跑完 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 内部的: ```text 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` 主线) ```text 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 已运行进程(对照) ```text 目标已运行 → 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 的思路对照,不在上游源码中): ```bash git clone --depth 1 --branch 17.9.1 https://github.com/frida/frida-core 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 ``` 复核结果应能组成四条连续证据链(第三条左半是上游路径,右半是本工程思路): ```text spawn API → RoboLauncher → zymbiote hello → child pid attach API → Linjector → InjectSession → loader loader → dlopen/dlsym(上游)· custom mapper(本工程)→ frida_agent_main agent → AgentSessionProvider → AgentSession ``` ## 16. 必须能回答的问题 1. **Frida 是否把完整 agent 注入 zygote?** 否。zygote 中是 `zymbiote` 门控载荷;完整 agent 在目标子进程 PID 确定后通过普通 attach 注入。zymbiote 默认在 frida-server 启动时经 preload 注入父进程(§3、§4),不是等 spawn 才做。 2. **`-f` 为什么能早于 App 业务代码?** 子进程在 specialize/进程命名阶段通过 zymbiote 上报并阻塞,server 在放行前完成 agent 注入与脚本加载。 3. **spawn 返回 PID 之后、resume 之前发生了什么?** 客户端立刻对这个 PID 做一次 attach:ptrace → bootstrapper → loader → mapper 装载完整 agent → 建立 AgentSession → 创建并加载脚本。 4. **当前 zymbiote 修改解决了什么?** App 子进程收到 ACK 后还原继承的函数槽并自卸载临时 payload;注入前的 stale cleanup 负责异常残留。 5. **独立的 attach 和 spawn 是什么关系?** 同一条装载链的单独使用:没有门控段、时机晚于 App 早期初始化,也没有 ACK 放行点。 6. **普通 attach 为什么需要 ptrace?** ptrace 提供暂停、寄存器读写和受控远程执行入口;真正装载还依赖 bootstrapper、远程内存、loader、fd 传递与控制通道。 7. **当前工程改掉 `dlopen()` 后,为什么 agent 还能运行?** 自定义 mapper(agent-mapper,本工程私有)自行处理当前 agent 所需的 ELF segment、relocation、权限、constructor 和入口查找。 8. **为什么必须传 `agent_base/agent_size`?** 自定义 mapper 不登记 `soinfo`,maps 又可能被过滤;agent 需要一条独立、确定的自身范围来源。 9. **不是本次目标的 App 也会进入 zymbiote 的 `recv()` 吗?** 会。只要它从已安装门控的 Zygote/USAP 出生并执行到 `setArgV0`,就会发送 hello 并短暂等待。server 发现没有匹配的 spawn 请求且未开启全局 spawn gating 后,会立即还原子进程继承的相应函数指针槽、发送 ACK,并让 payload 自卸载;不会对它 attach,也不会注入完整 agent。 ## 参考文献 1. Frida 上游 17.9.1 — `server/server.vala`(`run_application` :199、ControlService 构造 :294-296). <<mark class="encrypted">696K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7$3g2J5N6X3g2J5i4K6u0r3M7$3g2J5N6X3g2J5i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3I4z5e0W2Q4x3X3c8x3x3U0V1&6</mark>> 2. Frida 上游 17.9.1 — `src/control-service.vala`(spawn/attach 转发 :915-934). <<mark class="encrypted">347K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3j5$3!0F1N6s2u0G2L8q4)9J5k6s2y4W2M7Y4k6A6j5$3g2Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6V1I4y4g2)9J5k6p5H3&6x3K6b7`.</mark>> 3. Frida 上游 17.9.1 — `src/frida.vala`(`Device.spawn` :994、`Device.attach` :1138). <<mark class="encrypted">e12K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3k6Y4u0A6k6r3q4Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6p5I4x3K6S2Q4x3X3c8x3x3e0p5$3x3H3`.`.</mark>> 4. Frida 上游 17.9.1 — `src/host-session-service.vala`(`attach` :571、`establish` :605). <<mark class="encrypted">33aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3K9r3!0K6N6q4)9J5k6s2y4W2M7%4y4A6L8$3&6Q4x3X3c8K6k6i4u0$3K9h3y4W2i4K6u0W2N6X3q4D9j5g2)9J5x3@1H3#2y4K6q4Q4x3X3c8x3y4U0p5I4</mark>> 5. 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). <<mark class="encrypted">135K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6#2P5q4)9J5k6r3S2G2M7%4c8Q4x3X3c8K6k6i4y4K6K9h3!0F1i4K6u0W2N6X3q4D9j5b7`.`.</mark>> 6. Frida 上游 17.9.1 — `src/linux/linjector.vala`(inject_library_resource :84、memfd :94、request_control_channel :112). <<mark class="encrypted">1c6K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6D9K9h3&6B7k6h3y4@1L8%4u0Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6R3@1i4K6u0V1e0o6p5I4y4l9`.`.</mark>> 7. Frida 上游 17.9.1 — `src/linux/frida-helper-backend.vala`(InjectTask :308-330、bootstrap :1054、launch_loader :993、RemoteAgent :1452-1560、SeizeSession :1805-2110). <<mark class="encrypted">388K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6X3M7X3W2V1j5g2)9J5k6r3S2W2L8s2m8W2M7W2)9J5k6r3u0S2j5$3E0W2L8X3c8Q4x3X3g2$3j5h3I4S2</mark>> 8. Frida 上游 17.9.1 — `src/linux/helpers/loader.c`(frida_load :61-63、dlopen :110-131、receive_fd :332、收尾 :168-188). <<mark class="encrypted">5d5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3L8r3!0S2k6r3g2J5i4K6u0W2j5#2)9J5x3@1H3$3x3g2)9J5k6p5H3I4z5e0l9`.</mark>> 9. Frida 上游 17.9.1 — `src/linux/helpers/loader.c` 的 dlopen 路径(:110-131),作为本工程自定义 mapper 的对照;agent-mapper 为本工程私有实现,不公开. <<mark class="encrypted">e27K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3L8r3!0S2k6r3g2J5i4K6u0W2j5#2)9J5x3@1H3I4x3e0m8Q4x3X3c8x3x3e0x3I4</mark>> 10. 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). <<mark class="encrypted">90bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3L8r3W2F1N6i4S2Q4x3V1k6Z5k6h3I4H3k6i4u0K6i4K6u0r3P5Y4W2E0j5X3W2G2N6r3g2Q4x3X3g2U0</mark>> 11. Frida 上游 17.9.1 — `lib/base/session.vala`(LinuxInjectorState :1022;`agent_base/agent_size` 为本工程在同名结构体上的扩展). <<mark class="encrypted">22eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3L8r3W2T1i4K6u0r3j5X3q4K6k6g2)9J5c8Y4y4W2M7%4y4A6L8$3&6Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6p5H3x3U0t1`.</mark>> 12. Frida 上游 17.9.1 — `lib/agent/agent.vala`(入口 :1-7、create_and_run :116-184、run :267-277、provider 注册 :1175-1187). <<mark class="encrypted">4c8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3L8r3W2T1i4K6u0r3j5h3N6W2L8Y4c8Q4x3V1k6S2k6$3g2F1N6q4)9J5k6i4k6S2L8r3p5`.</mark>> 13. 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`. <<mark class="encrypted">898K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1N6r3!0G2L8s2y4Q4x3V1k6T1L8r3!0T1i4K6u0r3x3e0c8Q4x3X3f1^5i4K6u0W2x3q4)9J5c8X3k6J5K9h3c8S2i4K6g2X3N6r3!0G2L8s2y4Q4x3V1k6S2M7s2m8D9K9h3y4S2N6r3W2G2L8W2)9J5k6i4m8&6i4K6t1K6e0o6j5I4y4W2)9J5k6p5H3$3x3U0j5`.</mark>>;<<mark class="encrypted">b94K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1N6r3!0G2L8s2y4Q4x3V1k6T1L8r3!0T1i4K6u0r3x3e0c8Q4x3X3f1^5i4K6u0W2x3q4)9J5c8X3k6J5K9h3c8S2i4K6g2X3N6r3!0G2L8s2y4Q4x3V1k6J5k6i4m8D9i4K6u0W2M7s2W2Q4x3U0y4x3x3U0b7%4i4K6u0V1e0o6x3H3y4R3`.`.</mark>> 14. Frida 上游源码 — `frida-core/src/linux/linux-host-session.vala`, `frida-helper-backend.vala`, `helpers/loader.c`, `helpers/zymbiote.c`. <<mark class="encrypted">097K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6b7`.`.</mark>> 15. Frida 官方文档 — Modes of Operation. <<mark class="encrypted">923K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6X3M7X3W2V1j5g2)9J5k6i4u0W2i4K6u0r3k6r3!0U0M7#2)9J5c8X3#2G2k6r3g2K6i4K6u0r3</mark>> 16. Android Code Search — Launcher3 `ItemClickHandler.java`(桌面图标点击进入 `startActivitySafely`). <<mark class="encrypted">a05K9s2c8@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@1S2S2L8X3c8D9k6i4u0Q4x3X3g2B7j5i4k6S2</mark>> 17. Android Code Search — `ActivityTaskManagerService.java` 与 `ActivityStarter.java`(Activity 启动请求在 `system_server` 中的解析与执行). <<mark class="encrypted">6e2K9s2c8@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$3j5b7`.`.</mark>>;<<mark class="encrypted">702K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3M7$3g2J5N6X3W2U0k6i4y4Q4x3V1k6U0L8%4u0W2i4K6u0r3K9X3q4$3j5g2)9J5c8X3y4G2L8g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6K6k6i4u0$3k6i4u0Q4x3V1k6%4L8g2)9J5c8V1q4U0N6r3W2$3K9i4c8&6f1%4c8S2M7Y4c8W2M7W2)9J5k6h3A6S2N6X3p5`.</mark>> 18. Android Code Search — `ProcessList.java`(`startProcessLocked()`、`entryPoint = "android.app.ActivityThread"` 与进程创建参数). <<mark class="encrypted">7e2K9s2c8@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$3j5b7`.`.</mark>> 19. Android Code Search — `Process.java` 与 `ZygoteProcess.java`(`Process.start()`、`startViaZygote()`、Zygote socket 协议与 USAP 路径). <<mark class="encrypted">c75K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3L8%4y4Q4x3V1k6b7M7X3!0U0k6i4y4K6i4K6u0W2K9X3q4$3j5b7`.`.</mark>>;<<mark class="encrypted">0f6K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3L8%4y4Q4x3V1k6K9P5h3N6G2N6r3g2b7M7X3!0U0k6i4y4K6i4K6u0W2K9X3q4$3j5b7`.`.</mark>> 20. Android Code Search — `ZygoteServer.java` 与 `ZygoteConnection.java`(`runSelectLoop()`、对端身份检查、`processCommand()` 与父子分流). <<mark class="encrypted">d59K9s2c8@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)9J5k6h3A6S2N6X3p5`.</mark>>;<<mark class="encrypted">906K9s2c8@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&6Q4x3X3g2B7j5i4k6S2</mark>> 21. Android Code Search — `Zygote.java` 与 `com_android_internal_os_Zygote.cpp`(`forkAndSpecialize()`、`nativeForkAndSpecialize()`、`ForkCommon()` 与 `SpecializeCommon()`). <<mark class="encrypted">2e8K9s2c8@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)9J5k6h3A6S2N6X3p5`.</mark>>;<<mark class="encrypted">946K9s2c8@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%4m8H3</mark>> 22. Android Code Search — `ZygoteInit.java`、`RuntimeInit.java` 与 `ActivityThread.java`(子进程从 `handleChildProc()` 进入 `ActivityThread.main()`). <<mark class="encrypted">5ccK9s2c8@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@1k6f1W2F1K9i4c8Q4x3X3g2B7j5i4k6S2</mark>>;<<mark class="encrypted">952K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3K9h3&6@1k6i4u0F1j5h3I4Q4x3V1k6G2M7#2)9J5c8W2u0#2L8Y4c8A6L8h3g2u0L8X3W2@1i4K6u0W2K9X3q4$3j5b7`.`.</mark>>;<<mark class="encrypted">653K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6E0j5h3W2F1i4K6u0r3i4K6u0n7i4K6u0r3L8h3q4A6L8W2)9K6b7h3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3j5$3!0J5k6g2)9J5c8X3A6S2N6X3q4Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3j5i4m8H3i4K6u0r3b7h3y4@1K9i4k6A6N6s2W2f1K9s2u0W2j5h3c8Q4x3X3g2B7j5i4k6S2</mark>> 23. 看雪论坛 · 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> 24. Frida 17.17.0 上游源码 — `RoboLauncher.handle_zymbiote_connection()`、`ZymbioteConnection.resume()` 与 `zymbiote.c` 的非目标立即放行、`raise(SIGSTOP)`、server 还原和 `SIGCONT` 收尾. <<mark class="encrypted">c29K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0p5%4i4K6u0W2x3q4)9J5c8Y4y4J5j5#2)9J5c8X3I4A6L8Y4g2^5i4K6u0r3L8r3W2F1N6i4S2Q4x3X3c8Z5L8%4y4@1i4K6u0V1M7$3g2K6M7$3W2G2L8W2)9J5k6i4k6S2L8r3p5`.</mark>>;<<mark class="encrypted">d7aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0p5%4i4K6u0W2x3q4)9J5c8Y4y4J5j5#2)9J5c8X3I4A6L8Y4g2^5i4K6u0r3K9r3g2D9M7r3g2J5M7#2)9J5c8Y4A6&6L8h3u0A6L8%4c8W2i4K6u0W2j5H3`.`.</mark>>
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于
2026-8-31 06:20 被mb_peeqldfc编辑 ,原因: 上传错了,被ai改错了
#HOOK注入
#系统相关
收藏
・
14
点赞
・
21
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
git_99604alipayhihonor
你的帖子非常有用,感谢分享!
2026-9-14 10:36
zhczf
+1
感谢你分享这么好的资源!
2026-9-11 21:31
Bileton
期待更多优质内容的分享,论坛有你更精彩!
2026-9-11 16:23
unxeer
非常支持你的观点!
2026-9-7 10:08
sinker_
感谢你的积极参与,期待更多精彩内容!
2026-9-4 23:48
陈留
+1
谢谢你的细致分析,受益匪浅!
2026-9-3 16:44
mb_rttzgpaj
谢谢你的细致分析,受益匪浅!
2026-9-3 14:45
andy张刘
期待更多优质内容的分享,论坛有你更精彩!
2026-9-3 11:33
mb_gpgkjjvd
感谢你分享这么好的资源!
2026-9-2 09:25
bluegatar
感谢你的积极参与,期待更多精彩内容!
2026-9-2 02:10
孤独的街
这个讨论对我很有帮助,谢谢!
2026-9-1 20:39
cr_lgdx
谢谢你的细致分析,受益匪浅!
2026-9-1 14:37
qqizai
期待更多优质内容的分享,论坛有你更精彩!
2026-9-1 10:18
王叔叔
感谢你的贡献,论坛因你而更加精彩!
2026-9-1 00:06
focaccia
为你点赞!
2026-8-31 16:27
asm_zhang
+1
为你点赞!
2026-8-31 13:45
烬奇小云
感谢你的贡献,论坛因你而更加精彩!
2026-8-31 10:30
我不是卷王
+1
谢谢你的细致分析,受益匪浅!
2026-8-31 10:22
东方玻璃
感谢你分享这么好的资源!
2026-8-31 10:20
fyrlove
感谢你的积极参与,期待更多精彩内容!
2026-8-31 09:18
pexillove
你的分享对大家帮助很大,非常感谢!
2026-8-31 09:12
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
8
)
mb_peeqldfc
雪 币:
1219
活跃值:
(275)
能力值:
( LV4,RANK:50 )
在线值:
发帖
3
回帖
13
粉丝
29
关注
私信
mb_peeqldfc
1
2
楼
视频讲解可以看:
f45K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2T1K9h3I4A6j5X3W2D9K9g2)9J5k6h3y4G2L8g2)9J5c8Y4k6A6k6r3g2G2i4K6u0r3b7W2j5I4k6f1I4@1P5o6k6w2c8i4S2&6i4K6u0r3i4K6y4r3N6X3c8Q4y4h3k6K6L8%4g2J5j5$3g2Q4x3@1c8V1j5K6M7^5z5e0x3I4y4$3g2X3z5o6m8W2x3r3f1I4x3r3p5J5y4X3p5K6y4o6q4U0j5$3p5J5j5e0W2U0x3b7`.`.
2026-8-31 04:11
0
墨穹呢
雪 币:
4770
活跃值:
(8232)
能力值:
( LV3,RANK:20 )
在线值:
发帖
2
回帖
231
粉丝
18
关注
私信
墨穹呢
3
楼
感谢分享
2026-8-31 08:11
0
九黎
雪 币:
51
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
93
粉丝
0
关注
私信
九黎
4
楼
太强了
2026-8-31 08:27
0
Imxz
雪 币:
110
活跃值:
(9571)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
775
粉丝
9
关注
私信
Imxz
5
楼
tql
2026-8-31 09:10
0
Migc
雪 币:
20
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
39
粉丝
0
关注
私信
Migc
6
楼
太强了
2026-8-31 10:24
0
mb_qrlsimls
雪 币:
40
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
5
粉丝
0
关注
私信
mb_qrlsimls
7
楼
太强了
2026-8-31 15:22
0
龙飞雪
雪 币:
720
活跃值:
(7137)
能力值:
( LV3,RANK:30 )
在线值:
发帖
1
回帖
267
粉丝
3
关注
私信
龙飞雪
8
楼
666
2026-8-31 15:52
0
focaccia
雪 币:
347
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
8
粉丝
0
关注
私信
focaccia
9
楼
666
2026-8-31 16:32
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
mb_peeqldfc
1
3
发帖
13
回帖
50
RANK
关注
私信
他的文章
[原创]最好用过VPN检测
1538
[原创]Frida 脚本运行时机与Java.perform的原理
21433
[原创]Frida 整体启动逻辑
22924
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部