Frida学习-Frida spwn模式启动APP流程学习

两只羊 Lv3

test

App启动流程

总所周知呢,我们在使用frida hook的时候,要么是通过attach,要么是通过spawn模式启动

attach好理解,通过pid去ptrace attach,然后注入so就好了,但问题就在这个spawn模式

他到底是如何通过这样一条指令,去完整启动app的工作的呢

1
frida -U -f "com.test.example" -l hook.js

Launcher -> AMS

让我们先来学习一下APP的启动流程

点击了桌面上的应用图标后,会Launcher程序便启动了,通过binder向系统服务system_server发送请求

然后是ActivityManagerService (AMS),收到请求后检查目标应用是否已经运行,然后开始准备创建新进程

AMS -> Zygote

AMS向Zygote发送一个创建进程的请求,Zygote即为安卓应用的孵化程序,它在系统启动时就加载了Java运行库,还有一些核心类和资源

Zygote收到请求后,会调用fork创建新进程,可以说所有APP都是通过Zygote孵化过来的。

Zygote -> App Process

通过fork创建出app的新进程后,会初始化Android Runtime(ART),然后开始启动AcitvityThread。

AppProcess <-> AMS

进程已经创建了,但还需要通过binder告知AMS已经启动,通过调用attachApplication,已经bindApplication,发送相关配置信息

最后进程依次通过attachBaseContext()启动,Application.onCreate进行全局初始化,再通过Acivity.onCreate创建UI线程,进行界面初始化之类的。

frida spawn启动分析

那么frida的spawn模式到底是如何完成从启动应用到注入的过程呢,接下来的分析以frida-16.2.1为例

当通过普通的linux进程启动并注入时,其实原理就是exec,我之前写的调试器已经把原理略知一二了,但问题是我们注入的是一个包名,它到底是如何完成前面那么多的APP启动流程的

我们直接定位到spawn的起点处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public async uint spawn (string program, HostSpawnOptions options, Cancellable? cancellable) throws Error, IOError {
string package = program;

if (options.has_argv)
throw new Error.NOT_SUPPORTED ("The 'argv' option is not supported when spawning Android apps");

if (options.has_envp)
throw new Error.NOT_SUPPORTED ("The 'envp' option is not supported when spawning Android apps");

if (options.has_env)
throw new Error.NOT_SUPPORTED ("The 'env' option is not supported when spawning Android apps");

if (options.cwd.length > 0)
throw new Error.NOT_SUPPORTED ("The 'cwd' option is not supported when spawning Android apps");

if (options.stdio != INHERIT)
throw new Error.NOT_SUPPORTED ("Redirected stdio is not supported when spawning Android apps");

var entrypoint = PackageEntrypoint.parse (package, options);

yield ensure_loaded (cancellable);

var system_server_agent = host_session.system_server_agent;

var process_name = yield system_server_agent.get_process_name (package, entrypoint.uid, cancellable);

if (spawn_requests.has_key (process_name))
throw new Error.INVALID_OPERATION ("Spawn already in progress for the specified package name");

var request = new Promise<uint> ();
spawn_requests[process_name] = request;

uint pid = 0;
try {
yield system_server_agent.stop_package (package, entrypoint.uid, cancellable);
yield system_server_agent.start_package (package, entrypoint, cancellable);

var timeout = new TimeoutSource.seconds (20);
timeout.set_callback (() => {
request.reject (new Error.TIMED_OUT ("Unexpectedly timed out while waiting for app to launch"));
return false;
});
timeout.attach (MainContext.get_thread_default ());
try {
pid = yield request.future.wait_async (cancellable);
} finally {
timeout.destroy ();
}
} catch (GLib.Error e) {
if (!spawn_requests.unset (process_name)) {
var pending_pid = request.future.value;
if (pending_pid != 0)
host_session.resume.begin (pending_pid, io_cancellable);
}

throw_api_error (e);
}

return pid;
}

这里关键的就是ensure_loaded这个函数,可以看到它枚举了zygote64等信息,那么可以大概猜测,它可能通过劫持了zygote,进行了一系列的工作。

在找到对应的进程后,通过do_inject_zygote_agent进行了注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
private async void ensure_loaded (Cancellable? cancellable) throws Error, IOError {
while (ensure_request != null) {
try {
yield ensure_request.future.wait_async (cancellable);
return;
} catch (Error e) {
throw e;
} catch (IOError e) {
cancellable.set_error_if_cancelled ();
}
}
ensure_request = new Promise<bool> ();

uint pending = 1;
GLib.Error? first_error = null;

CompletionNotify on_complete = error => {
pending--;
if (error != null && first_error == null)
first_error = error;

if (pending == 0) {
var source = new IdleSource ();
source.set_callback (ensure_loaded.callback);
source.attach (MainContext.get_thread_default ());
}
};

foreach (HostProcessInfo info in System.enumerate_processes (new ProcessQueryOptions ())) {
var name = info.name;
if (name == "zygote" || name == "zygote64" || name == "usap32" || name == "usap64") {
uint pid = info.pid;
if (zygote_agents.has_key (pid))
continue;

pending++;
do_inject_zygote_agent.begin (pid, name, cancellable, on_complete);
}
}

on_complete (null);

yield;

on_complete = null;

if (first_error == null) {
ensure_request.resolve (true);
} else {
ensure_request.reject (first_error);
ensure_request = null;

throw_api_error (first_error);
}
}

那么这个ZygoteAgent是什么呢,再来看看它的定义

这里通过session.enable_child_gating,启动了一个child_gating

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
private class ZygoteAgent : InternalAgent {
public uint pid {
get;
construct;
}

public string name {
get;
construct;
}

public bool child_gating_only_used_by_us {
get;
set;
}

public ZygoteAgent (LinuxHostSession host_session, uint pid, string name) {
Object (
host_session: host_session,
pid: pid,
name: name
);
}

public async void load (Cancellable? cancellable) throws Error, IOError {
#if ARM || ARM64
LinuxHelper helper = ((LinuxHostSession) host_session).helper;
yield helper.await_syscall (pid, POLL_LIKE, cancellable);
try {
#endif
yield ensure_loaded (cancellable);

try {
yield session.enable_child_gating (cancellable);
} catch (GLib.Error e) {
throw_dbus_error (e);
}
#if ARM || ARM64
} finally {
helper.resume_syscall.begin (pid, null);
}
#endif
}

protected override async uint get_target_pid (Cancellable? cancellable) throws Error, IOError {
return pid;
}

protected override async string? load_source (Cancellable? cancellable) throws Error, IOError {
return null;
}
}

agent 里真正负责捕获 fork/specialize 的是 ForkMonitor。Android 下它会检测自己是不是 /system/bin/app_process 且 cmdline 是 zygote/zygote64/usap32/usap64,然后 hook:fork,vforkandroid_os_Process_setArgV0,selinux_android_setcontext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
	public class ForkMonitor : Object, Gum.InvocationListener {
public weak ForkHandler handler {
get;
construct;
}

private State state = IDLE;
private ChildRecoveryBehavior child_recovery_behavior = NORMAL;
private string? identifier;

private static void * fork_impl;
private static void * vfork_impl;

private enum State {
IDLE,
FORKING,
}

private enum ChildRecoveryBehavior {
NORMAL,
DEFERRED_UNTIL_SET_ARGV0
}

private enum HookId {
FORK,
SET_ARGV0,
SET_CTX
}

public ForkMonitor (ForkHandler handler) {
Object (handler: handler);
}

static construct {
unowned string libc = Gum.Process.query_libc_name ();
fork_impl = Gum.Module.find_export_by_name (libc, "fork");
vfork_impl = Gum.Module.find_export_by_name (libc, "vfork");
}

construct {
var interceptor = Gum.Interceptor.obtain ();

unowned Gum.InvocationListener listener = this;

#if ANDROID
if (get_executable_path ().has_prefix ("/system/bin/app_process")) {
try {
string cmdline;
FileUtils.get_contents ("/proc/self/cmdline", out cmdline);
if (cmdline == "zygote" || cmdline == "zygote64" || cmdline == "usap32" || cmdline == "usap64") {
var set_argv0 = Gum.Module.find_export_by_name ("libandroid_runtime.so", "_Z27android_os_Process_setArgV0P7_JNIEnvP8_jobjectP8_jstring");
if (set_argv0 != null) {
interceptor.attach (set_argv0, listener, (void *) HookId.SET_ARGV0);
child_recovery_behavior = DEFERRED_UNTIL_SET_ARGV0;
}

var setcontext = Gum.Module.find_export_by_name ("libselinux.so", "selinux_android_setcontext");
if (setcontext != null)
interceptor.attach (setcontext, listener, (void *) HookId.SET_CTX);
}
} catch (FileError e) {
}
}
#endif

interceptor.attach (fork_impl, listener, (void *) HookId.FORK);
interceptor.replace (vfork_impl, fork_impl);
}

其中 setArgV0 和 selinux_android_setcontext 用来拿到 App 的进程名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void on_set_argv0_enter (Gum.InvocationContext context) {
if (identifier == null) {
void *** env = context.get_nth_argument (0);
void * name_obj = context.get_nth_argument (2);

var env_vtable = *env;

var get_string_utf_chars = (GetStringUTFCharsFunc) env_vtable[169];
var release_string_utf_chars = (ReleaseStringUTFCharsFunc) env_vtable[170];

var name_utf8 = get_string_utf_chars (env, name_obj);

identifier = name_utf8;

release_string_utf_chars (env, name_obj, name_utf8);
}
}

我们再来回到spawn,在获取到system_server_agent后,会进行如下工作,对应下面这几行

  1. 查询目标包默认进程名;
  2. forceStopPackage 停掉已有进程;
  3. 调 startActivity 或 sendBroadcast 启动入口;
  4. 等待 zygote 侧捕获到对应进程。
1
2
3
4
5
6
7
var process_name = yield system_server_agent.get_process_name(...);

yield system_server_agent.stop_package(...);
yield system_server_agent.start_package(...);

pid = yield request.future.wait_async(...);

start_package,就是进行了一个startActivity的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public async void start_package (string package, PackageEntrypoint entrypoint, Cancellable? cancellable)
throws Error, IOError {
var package_node = new Json.Node.alloc ().init_string (package);
var uid_node = new Json.Node.alloc ().init_int (entrypoint.uid);

if (entrypoint is DefaultActivityEntrypoint) {
var activity_node = new Json.Node.alloc ().init_null ();

yield call ("startActivity", new Json.Node[] { package_node, activity_node, uid_node }, cancellable);
} else if (entrypoint is ActivityEntrypoint) {
var e = entrypoint as ActivityEntrypoint;

var activity_node = new Json.Node.alloc ().init_string (e.activity);

yield call ("startActivity", new Json.Node[] { package_node, activity_node, uid_node }, cancellable);
} else if (entrypoint is BroadcastReceiverEntrypoint) {
var e = entrypoint as BroadcastReceiverEntrypoint;

var receiver_node = new Json.Node.alloc ().init_string (e.receiver);
var action_node = new Json.Node.alloc ().init_string (e.action);

yield call ("sendBroadcast", new Json.Node[] { package_node, receiver_node, action_node, uid_node },
cancellable);
} else {
assert_not_reached ();
}
}

所以 Frida 的启动不是从 Launcher 图标点击开始,而是在 system_server 里模拟了一个“系统侧启动应用”的请求。后半段仍然是正常 Android 流程:AMS/ATMS -> Zygote -> App Process。

启动流程总结

Frida 把一个内部 agent 注入到 Android 的 system_server 进程里,
然后在 system_server 进程内部调用 Android Framework API。

1
2
3
4
5
Frida -> system_server 调 startActivity
system_server/AMS -> 发现需要新进程
AMS -> 请求 zygote 创建进程
zygote -> 按 Android 正常机制 fork
Frida -> 在 fork/specialize 过程中拦截并暂停新进程

那在hook拦截到fork后,到底发生了什么呢

1
2
3
4
5
6
Zygote 按 Android 正常机制 fork/specialize App;
Frida 的 ZygoteAgent 在 fork/specialize 边界识别新进程;
App 子进程继承/重建 Frida agent;
子进程向 frida-server 报到并阻塞等待;
Frida attach + load hook.js;
Frida resume,App 继续执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
正常:
Zygote fork
-> App 子进程 specialize
-> RuntimeInit / ZygoteInit
-> ActivityThread.main()
-> attachApplication()
-> bindApplication()
-> Application.attachBaseContext()
-> Application.onCreate()
-> Activity.onCreate()

Frida spawn:
Zygote fork
-> App 子进程 specialize
-> Frida 子进程 agent 报到
-> App 暂停等待 resume
-> frida-server attach(pid)
-> 加载 hook.js
-> Frida resume(pid)
-> RuntimeInit / ZygoteInit 继续
-> ActivityThread.main()
-> attachApplication()
-> bindApplication()
-> Application.attachBaseContext()
-> Application.onCreate()
-> Activity.onCreate()

1
2
3
4
5
6
7
8
9
10
11
12
正常启动:
Launcher -> AMS/ATMS -> Zygote -> App Process -> ActivityThread -> Application/Activity

Frida spawn:
Frida CLI -> frida-server
-> 预先注入 zygote/usap
-> 注入 system_server 发起 startActivity
-> AMS/ATMS -> Zygote -> App Process
-> Frida 暂停新进程
-> attach + load hook.js
-> resume
-> ActivityThread -> Application/Activity

总结来说,Frida 跳过 Launcher,直接在 system_server 内调用 startActivity(),
让 AMS/ATMS 正常走 App 启动流程。
同时 Frida 提前注入 zygote/usap,监控 fork/specialize。
当目标 App 子进程被 fork/specialize 出来时,Frida 让它在早期阻塞等待。
随后 Frida CLI 对这个 pid 建立 session、加载 hook.js,再 resume。
之后流程基本和正常 App 启动一致。

  • 标题: Frida学习-Frida spwn模式启动APP流程学习
  • 作者: 两只羊
  • 创建于 : 2026-05-09 23:11:50
  • 更新于 : 2026-05-11 15:53:20
  • 链接: https://twogoat.github.io/2026/05/09/Frida学习-Frida启动流程学习/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
Frida学习-Frida spwn模式启动APP流程学习