Frida学习-Frida spwn模式启动APP流程学习
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后,会进行如下工作,对应下面这几行
查询目标包默认进程名;
forceStopPackage 停掉已有进程;
调 startActivity 或 sendBroadcast 启动入口;
等待 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 启动一致。