背景

frida-server 发起 attach 后,到底谁在什么时候把 frida-agent.so 注进目标进程?这篇文章从代码层面,沿着时间线拆解整个注入链路,澄清几个常见误区。

总流程图(文字版)

frida-server(服务入口,接收请求)→ LinuxHostSession(组装参数,分配任务)→ Linjector + frida-helper(执行注入核心操作,ptrace/远程调用)→ bootstrapper(初始化注入环境)→ loader(加载agent.so,执行入口)→ agent(最终执行hook逻辑)

1. frida-server:只是“入口门卫”,不做实际注入

我一开始以为 frida-server 是注入的核心,负责直接操作目标进程。但看代码才发现,它的作用只是启动服务、接收客户端请求,然后把 attach 任务转交给下一级,自身不碰 ptrace 也不处理 so 注入。

private static int main (string[] args) {
		/*
		 * frida-server 进程入口:
		 *
		 * - 初始化 Frida Core(选择 GLib runtime、安装 log handler 等)
		 * - 解析命令行参数(listen/tls/token/verbose/...)
		 * - 构建 EndpointParameters(决定监听地址、TLS、鉴权、静态资源目录等)
		 * - 构建 ControlServiceOptions(预加载优化、崩溃上报等)
		 * - (部分平台)启用“policy softener”(iOS/tvOS 相关的策略放宽/兼容逻辑)
		 * - (非 Windows / 非 tvOS)可选 daemonize:fork 成后台服务并用 pipe 回传启动结果
		 * - 进入主循环:创建 ControlService 并开始监听;收到信号时停止并退出
		 *
		 * 控制链路的关键点在 run_application():
		 * 这里会 new ControlService(...),它对外暴露 HostSession 等 DBus 对象,远程客户端因此能调用
		 * spawn/attach/inject 等操作来“控制”目标 App(实际注入/attach 在 frida-core/src 下的 HostSession 实现里)。
		 */
		Environment.init ();

#if DARWIN
		// macOS/iOS 系上,frida-policyd 是另一个入口(同一二进制复用);如果是它则转去 Policyd._main().
		if (Path.get_basename (args[0]) == "frida-policyd") {
			return Policyd._main ();
		}
#endif

		try {
			// 解析 OptionEntry[],把值写入上面的静态变量(listen_address/token/...)。
			var ctx = new OptionContext ();
			ctx.set_help_enabled (true);
			ctx.add_main_entries (option_entries, null);
			ctx.parse (ref args);
		} catch (OptionError e) {
			printerr ("%s\n", e.message);
			printerr ("Run '%s --help' to see a full list of available command line options.\n", args[0]);
			return 1;
		}

		if (output_version) {
			// 纯信息模式:输出版本后立即退出(不会启动网络监听/控制服务)。
			stdout.printf ("%s\n", version_string ());
			return 0;
		}

		// 控制是否输出 debug 级别日志(具体 handler 在 server-glue.c 环境初始化时安装)。
		Environment.set_verbose_logging_enabled (verbose);

		EndpointParameters endpoint_params;
		try {
			/*
			 * endpoint_params 描述“控制平面”对外暴露的入口:
			 * - listen_address: 监听的地址/端口(port 这里传 0 表示由 listen_address/内部逻辑决定)
			 * - TLS certificate: 启用 TLS 时使用
			 * - origin: 允许的 Origin(用于浏览器/HTTP 访问场景的约束)
			 * - auth_service: token 不为空则要求先认证
			 * - asset_root: 可选静态文件目录(用于对外提供文件资源)
			 */
			endpoint_params = new EndpointParameters (listen_address, 0, parse_certificate (certpath), origin,
				(token != null) ? new StaticAuthenticationService (token) : null,
				(asset_root != null) ? File.new_for_path (asset_root) : null);
		} catch (GLib.Error e) {
			printerr ("%s\n", e.message);
			return 2;
		}

		var options = new ControlServiceOptions ();
		// 预加载可降低首次 attach/spawn 的延迟;崩溃上报影响 native crash reporter 的集成开关。
		options.enable_preload = enable_preload;
		options.report_crashes = report_crashes;

#if (IOS || TVOS) && !HAVE_EMBEDDED_ASSETS
		/*
		 * iOS/tvOS 且未内置资源时:推导 sysroot(用于定位运行时资源/依赖)。
		 * 通过遍历模块找到主可执行文件路径,再截取出 FRIDA_PREFIX 之前的前缀。
		 */
		string? program_path = null;
		Gum.Process.enumerate_modules (m => {
			uint32 * file_type = (uint32 *) (m.range.base_address + 12);
			const uint32 MH_EXECUTE = 2;
			if (*file_type == MH_EXECUTE) {
				program_path = m.path;
				return false;
			}
			return true;
		});
		int prefix_pos = program_path.last_index_of (Config.FRIDA_PREFIX + "/");
		if (prefix_pos != -1 && prefix_pos != 0) {
			options.sysroot = program_path[:prefix_pos];
		}
#endif

		PolicySoftenerFlavor softener_flavor = SYSTEM;
		if (softener_flavor_str != null) {
			try {
				// 选择策略放宽实现(枚举 nick:system|internal)。
				softener_flavor = PolicySoftenerFlavor.from_nick (softener_flavor_str);
			} catch (Error e) {
				printerr ("%s\n", e.message);
				return 3;
			}
		}

#if IOS || TVOS
		// iOS/tvOS 上可选启用 internal softener(与 jailbreak/策略限制相关)。
		if (softener_flavor == INTERNAL)
			InternalIOSTVOSPolicySoftener.enable ();
#endif

		ReadyHandler? on_ready = null;
#if !WINDOWS && !TVOS
		if (daemonize) {
			/*
			 * daemonize:父进程 fork 后退出;子进程成为 session leader,并把 stdio 重定向到 /dev/null。
			 * 用 pipe 同步:子进程在 ControlService 启动成功/失败后写回 0/1,父进程读取并以此作为 exit code。
			 */
			var sync_fds = new int[2];

			try {
				Unix.open_pipe (sync_fds, 0);
				Unix.set_fd_nonblocking (sync_fds[0], true);
				Unix.set_fd_nonblocking (sync_fds[1], true);
			} catch (GLib.Error e) {
				assert_not_reached ();
			}

			var sync_in = new UnixInputStream (sync_fds[0], true);
			var sync_out = new UnixOutputStream (sync_fds[1], true);

			var pid = Posix.fork ();
			if (pid != 0) {
				// 父进程:等待子进程写回启动结果(0=ready,非 0=失败)。
				try {
					var status = new uint8[1];
					sync_in.read (status);
					return status[0];
				} catch (GLib.Error e) {
					return 4;
				}
			}

			sync_in = null;
			on_ready = (success) => {
				// 子进程:服务启动成功后再 setsid + 重定向;并写回父进程。
				if (success) {
					Posix.setsid ();

					var null_in = Posix.open ("/dev/null", Posix.O_RDONLY);
					var null_out = Posix.open ("/dev/null", Posix.O_WRONLY);
					Posix.dup2 (null_in, Posix.STDIN_FILENO);
					Posix.dup2 (null_out, Posix.STDOUT_FILENO);
					Posix.dup2 (null_out, Posix.STDERR_FILENO);
					Posix.close (null_in);
					Posix.close (null_out);
				}

				var status = new uint8[1];
				status[0] = success ? 0 : 1;
				try {
					sync_out.write (status);
				} catch (GLib.Error e) {
				}
				sync_out = null;
			};
		}
#endif

		// 做一次平台相关的最终环境配置(例如 Android 上 patch SELinux policy)。
		Environment.configure ();

#if DARWIN
		/*
		 * Darwin 平台的主循环需要跑在 CFRunLoop;这里通过 worker 线程跑 GLib 主逻辑,
		 * 并在结束后停掉 run loop。
		 */
		var worker = new Thread<int> ("frida-server-main-loop", () => {
			var exit_code = run_application (endpoint_params, options, on_ready);

			_stop_run_loop ();

			return exit_code;
		});
		_start_run_loop ();

		var exit_code = worker.join ();

		return exit_code;
#else
		// 其它平台:直接运行应用(启动 ControlService 并进入 GLib MainLoop)。
		return run_application (endpoint_params, options, on_ready);
#endif
	}

private static int run_application (EndpointParameters endpoint_params, ControlServiceOptions options, ReadyHandler on_ready) {
		// 选择临时目录根(用于放置提取/生成的辅助二进制、agent 等),并注入 sysroot 影响资源定位。
		TemporaryDirectory.always_use ((directory != null) ? directory : DEFAULT_DIRECTORY);
		TemporaryDirectory.use_sysroot (options.sysroot);

		// 核心:ControlService 会启动网络入口,并在连接上注册 DBus 对象(HostSession 等),远程控制从这里开始。
		application = new Application (new ControlService (endpoint_params, options));

		// 收到信号时优雅停服(停止监听、断开连接、释放资源),并退出主循环。
		Posix.signal (Posix.Signal.INT, (sig) => {
			application.stop ();
		});
		Posix.signal (Posix.Signal.TERM, (sig) => {
			application.stop ();
		});

		if (on_ready != null) {
			// daemonize 模式下:在 service.start() 结果出来后通知父进程。
			application.ready.connect (success => {
				on_ready (success);
				on_ready = null;
			});
		}

		return application.run ();
	}

看点:仅启动服务、接收请求,转发任务,无实际注入逻辑。

结论:frida-server 仅负责服务启动和请求转发,是整个注入链的“入口”,不执行任何注入相关的核心操作。

2. LinuxHostSession:参数“组装工”,衔接服务与注入

我曾误解 LinuxHostSession 会直接调用 ptrace 注入 so,实际它的核心工作是组装 attach 和注入所需的参数,然后调用 Linjector,把注入任务交给 frida-helper 去执行。

protected override async Future<IOStream> perform_attach_to (uint pid, HashTable<string, Variant> options,
				Cancellable? cancellable, out Object? transport) throws Error, IOError {
			uint id;
			string entrypoint = "frida_agent_main";
			string parameters = make_agent_parameters (pid, "", options);
			AgentFeatures features = CONTROL_CHANNEL;
			var linjector = (Linjector) injector;
#if HAVE_EMBEDDED_ASSETS
			id = yield linjector.inject_library_resource (pid, agent, entrypoint, parameters, features, cancellable);
#else
			id = yield linjector.inject_library_file_with_template (pid, PathTemplate (Config.FRIDA_AGENT_PATH), entrypoint,
				parameters, features, cancellable);
#endif
			injectee_by_pid[pid] = id;

			var stream_request = new Promise<IOStream> ();
			IOStream stream = yield linjector.request_control_channel (id, cancellable);
			stream_request.resolve (stream);

			transport = null;

			return stream_request.future;
		}

看点:仅组装参数、调用Linjector,不执行ptrace或远程代码。

结论:LinuxHostSession 是“控制面衔接者”,负责参数组装和任务分发,连接 frida-server 和实际的注入执行模块。

3. frida-helper:注入“执行者”,真正的ptrace操作者

之前一直困惑“谁来执行 ptrace”,跟到 frida-helper 的代码才明白,它是注入的核心执行者——接收 Linjector 的请求,通过 ptrace 操作目标进程,完成注入的核心步骤。

public async void inject_library (uint pid, UnixInputStream library_so, string entrypoint, string data,
				AgentFeatures features, uint id, Cancellable? cancellable) throws Error, IOError {
			var spec = new InjectSpec (library_so, entrypoint, data, features, id);
			var task = new InjectTask (this, spec);
			RemoteAgent agent = yield perform (task, pid, cancellable);
			take_agent (agent);
		}

		private class InjectTask : Object, Task<RemoteAgent> {
			private weak LinuxHelperBackend backend;
			private InjectSpec spec;

			public InjectTask (LinuxHelperBackend backend, InjectSpec spec) {
				this.backend = backend;
				this.spec = spec;
			}

			public async RemoteAgent run (uint pid, Cancellable? cancellable) throws Error, IOError {
				PausedSyscallSession? pss = backend.paused_syscalls[pid];
				if (pss != null)
					yield pss.interrupt (cancellable);
				var session = yield InjectSession.open (pid, cancellable);
				RemoteAgent agent = yield session.inject (spec, cancellable);
				session.close ();
				return agent;
			}
		}

看点:InjectTask.run 中包含ptrace核心逻辑,是实际注入的执行者。

结论:frida-helper 是注入链的“执行核心”,负责 ptrace 操作、远程内存写入和 dlopen 调用,是真正完成 so 注入的模块。

4. bootstrapper:注入“初始化器”,搭建agent运行环境

我曾混淆 bootstrapper 和 loader 的作用,以为它们是一回事。实际 bootstrapper 是在 so 加载后,先初始化注入环境,为 loader 后续加载 agent 做好准备,不直接执行 agent 入口。

frida_bootstrap (FridaBootstrapContext * ctx)
{
  /*
   * 该函数在“目标进程内部”执行,是 bootstrap 阶段入口。
   * 作用是为下一阶段 loader 准备运行环境,并把结果写回
   * FridaBootstrapContext,供 helper 侧读取。
   */
  FridaLibcApi * libc = ctx->libc;
  FridaProcessLayout process;

  /* 从 auxv 和进程映射中探测解释器/libc/动态链接器状态。 */
  if (!frida_probe_process (ctx->page_size, &process))
    return FRIDA_BOOTSTRAP_AUXV_NOT_FOUND;

  /* 回填动态链接器元数据,供 helper 侧状态机判断下一步动作。 */
  ctx->rtld_flavor = process.rtld_flavor;
  ctx->rtld_base = process.interpreter;
  ctx->r_brk = process.r_brk;

  /*
   * 动态链接器已出现,但 libc 还没映射完成。
   * 这种情况下返回 TOO_EARLY,让外层先恢复执行一段再重试。
   */
  if (process.interpreter != NULL && process.libc == NULL)
    return FRIDA_BOOTSTRAP_TOO_EARLY;

  /* 解释器和 libc 都没找到:尝试兜底加载 libc,失败则快速返回错误。 */
  if (process.interpreter == NULL && process.libc == NULL)
  {
    frida_try_load_libc_and_raise (ctx);
    return FRIDA_BOOTSTRAP_LIBC_LOAD_ERROR;
  }

  /* 解析远端 libc(及链接器相关)API,供 loader/运行时胶水逻辑使用。 */
  if (!frida_resolve_libc_apis (&process, libc))
    return FRIDA_BOOTSTRAP_LIBC_UNSUPPORTED;

  /* 在目标进程申请可执行内存,后续 helper 会把 loader.bin 写到这里并执行。 */
  ctx->loader_base = mmap (NULL, ctx->loader_size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (ctx->loader_base == MAP_FAILED)
    return FRIDA_BOOTSTRAP_MMAP_ERROR;

  /* 可选创建控制 socketpair,用于后续 agent 与 helper 的快速接管。 */
  ctx->ctrlfds[0] = -1;
  ctx->ctrlfds[1] = -1;
  if (ctx->enable_ctrlfds)
    frida_socketpair (AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, ctx->ctrlfds);

  return FRIDA_BOOTSTRAP_SUCCESS;
}
private async BootstrapResult bootstrap (size_t loader_size, Cancellable? cancellable) throws Error, IOError {
			var result = new BootstrapResult ();

			unowned uint8[] bootstrapper_code = Frida.Data.HelperBackend.get_bootstrapper_bin_blob ().data;
			uint64 bootstrapper_base = 0;
			size_t bootstrapper_size = round_size_to_page_size (bootstrapper_code.length);

			uint64 remote_mmap = 0;
			uint64 remote_munmap = 0;
			ProcMapsEntry? remote_libc = ProcMapsEntry.find_by_path (pid, local_libc.path);
			bool same_libc = remote_libc != null && remote_libc.identity == local_libc.identity;
			if (same_libc) {
				remote_mmap = remote_libc.base_address + mmap_offset;
				remote_munmap = remote_libc.base_address + munmap_offset;
			}

			if (remote_mmap != 0) {
				bootstrapper_base = yield allocate_memory (remote_mmap, bootstrapper_size,
					Posix.PROT_READ | Posix.PROT_WRITE | Posix.PROT_EXEC, cancellable);
			}

			try {
				if (bootstrapper_base != 0)
					write_memory (bootstrapper_base, bootstrapper_code);

				HelperBootstrapStatus status = SUCCESS;
				do {
					uint64 code_start;
					ProcessCodeSwapScope? code_swap = null;
					if (bootstrapper_base != 0) {
						code_start = bootstrapper_base;
					} else {
						code_swap = yield new ProcessCodeSwapScope (this, bootstrapper_code, cancellable);
						code_start = code_swap.code_start;
					}
					uint64 code_end = code_start + bootstrapper_size;

					maybe_fixup_helper_code (code_start, bootstrapper_code);

					var call_builder = new RemoteCallBuilder (code_start, saved_regs);

					unowned uint8[] fallback_ld_data = fallback_ld.data;
					unowned uint8[] fallback_libc_data = fallback_libc.data;

					uint64 libc_api_location, bootstrap_ctx_location, fallback_ld_location, fallback_libc_location;
					call_builder
						.reserve_stack_space (sizeof (HelperLibcApi), out libc_api_location)
						.reserve_stack_space (sizeof (HelperBootstrapContext), out bootstrap_ctx_location)
						.reserve_stack_space (fallback_ld_data.length + 1, out fallback_ld_location)
						.reserve_stack_space (fallback_libc_data.length + 1, out fallback_libc_location);

					var bootstrap_ctx = HelperBootstrapContext ();
					bootstrap_ctx.page_size = Gum.query_page_size ();
					bootstrap_ctx.fallback_ld = (string *) fallback_ld_location;
					bootstrap_ctx.fallback_libc = (string *) fallback_libc_location;
					bootstrap_ctx.loader_size = loader_size;
					bootstrap_ctx.enable_ctrlfds = PidFileDescriptor.getfd_is_supported ();
					bootstrap_ctx.libc = (HelperLibcApi *) libc_api_location;
					write_memory (bootstrap_ctx_location, (uint8[]) &bootstrap_ctx);
					unowned uint8[] fallback_ld_cstr = fallback_ld_data[:fallback_ld_data.length + 1];
					unowned uint8[] fallback_libc_cstr = fallback_libc_data[:fallback_libc_data.length + 1];
					write_memory (fallback_ld_location, fallback_ld_cstr);
					write_memory (fallback_libc_location, fallback_libc_cstr);
					call_builder.add_argument (bootstrap_ctx_location);

					RemoteCall bootstrap_call = call_builder.build (this);
					RemoteCallResult bootstrap_result = yield bootstrap_call.execute (cancellable);
					status = (HelperBootstrapStatus) bootstrap_result.return_value;

					bool restart_after_libc_load =
						bootstrap_result.status == RAISED_SIGNAL && bootstrap_result.stop_signal == Posix.Signal.STOP;
					if (restart_after_libc_load) {
						bootstrap_result = yield bootstrap_call.execute (cancellable);
						status = (HelperBootstrapStatus) bootstrap_result.return_value;
					}

					if (!(bootstrap_result.status == COMPLETED && (status == SUCCESS || status == TOO_EARLY))) {
						if (bootstrap_result.status == COMPLETED) {
							throw new Error.NOT_SUPPORTED ("Bootstrapper failed due to '%s'; " +
								"please file a bug",
								Marshal.enum_to_nick<HelperBootstrapStatus> (status));
						} else {
							uint64 pc = bootstrap_result.regs.program_counter;
							if (pc >= code_start && pc < code_end) {
								throw new Error.NOT_SUPPORTED (
									"Bootstrapper crashed with signal %d at offset 0x%x; please file a bug\n%s",
									bootstrap_result.stop_signal,
									(uint) (pc - code_start),
									bootstrap_result.regs.to_string ());
							} else {
								throw new Error.NOT_SUPPORTED ("Bootstrapper crashed with signal %d; please file a bug\n%s",
									bootstrap_result.stop_signal,
									bootstrap_result.regs.to_string ());
							}
						}
					}

					uint8[] output_context = read_memory (bootstrap_ctx_location, sizeof (HelperBootstrapContext));
					Memory.copy (&result.context, output_context, output_context.length);

					uint8[] output_libc = read_memory (libc_api_location, sizeof (HelperLibcApi));
					Memory.copy (&result.libc, output_libc, output_libc.length);

					result.context.libc = &result.libc;

					if (result.context.rtld_flavor == ANDROID && result.libc.dlopen == null) {
						ProcMapsEntry? remote_ld = ProcMapsEntry.find_by_address (pid, (uintptr) result.context.rtld_base);
						bool same_ld = remote_ld != null && local_android_ld != null && remote_ld.identity == local_android_ld.identity;
						if (!same_ld)
							throw new Error.NOT_SUPPORTED ("Unable to locate Android dynamic linker; please file a bug");
						result.libc.dlopen = rebase_pointer ((uintptr) dlopen, local_android_ld, remote_ld);
						result.libc.dlclose = rebase_pointer ((uintptr) dlclose, local_android_ld, remote_ld);
						result.libc.dlsym = rebase_pointer ((uintptr) dlsym, local_android_ld, remote_ld);
						result.libc.dlerror = rebase_pointer ((uintptr) dlerror, local_android_ld, remote_ld);
					}

					if (code_swap != null)
						code_swap.revert ();

					if (status == TOO_EARLY)
						yield resume_until_execution_reaches ((uint64) result.context.r_brk, cancellable);
				} while (status == TOO_EARLY);

				if (bootstrapper_base != 0)
					yield deallocate_memory (remote_munmap, bootstrapper_base, bootstrapper_size, cancellable);
			} catch (GLib.Error e) {
				if (bootstrapper_base != 0) {
					try {
						yield deallocate_memory (remote_munmap, bootstrapper_base, bootstrapper_size, null);
					} catch (GLib.Error e) {
					}
				}

				throw_api_error (e);
			}

			return result;
		}

看点:仅初始化环境,衔接helper和loader,不执行agent逻辑。


结论:bootstrapper 负责注入环境初始化,是 frida-helper 和 loader 之间的“桥梁”,分工明确,不参与 agent 加载和执行。

5. loader:agent“加载器”,触发agent入口执行

我之前以为 helper 加载 so 后就直接执行 agent 逻辑,实际是 loader 负责通过 dlopen 加载 frida-agent.so,通过 dlsym 找到入口函数,最终触发 agent 执行。

__attribute__ ((section (".text.entrypoint")))
__attribute__ ((visibility ("default")))
void
frida_load (FridaLoaderContext * ctx)
{
  ctx->libc->pthread_create (&ctx->worker, NULL, frida_main, ctx);
}
if (ctx->agent_handle == NULL)
{
  agent_codefd = frida_receive_fd (ctrlfd, libc);
  libc->sprintf (agent_path, "/proc/self/fd/%d", agent_codefd);

  ctx->agent_handle = libc->dlopen (agent_path, libc->dlopen_flags, pretend_caller_addr);
  ctx->agent_entrypoint_impl = libc->dlsym (ctx->agent_handle, ctx->agent_entrypoint, pretend_caller_addr);
}

ctx->agent_entrypoint_impl (ctx->agent_data, &unload_policy, &injector_state);

看点:核心是dlopen加载agent.so、dlsym找到入口,触发agent执行。


结论:loader 是 agent 的“启动器”,负责加载 agent.so 并触发入口执行,是连接注入流程和 agent 逻辑的关键。

6. agent:最终“工作者”,执行实际hook逻辑

我曾误以为 agent 是被动执行的“脚本”,实际它是独立的逻辑模块,启动后会初始化 Frida API,注册 hook 逻辑,完成最终的插桩任务,且真正的 hook 逻辑不在此处,而在 frida-gum。

namespace Frida.Agent {
  public void main (string agent_parameters, ref Frida.UnloadPolicy unload_policy, void * injector_state) {
    if (Runner.shared_instance == null)
      Runner.create_and_run (agent_parameters, ref unload_policy, injector_state);
public Gum.ScriptBackend get_script_backend (ScriptRuntime runtime) throws Error {
  switch (runtime) {
    case QJS:
      qjs_backend = Gum.ScriptBackend.obtain_qjs ();
      return qjs_backend;
    case V8:
      v8_backend = Gum.ScriptBackend.obtain_v8 ();
      return v8_backend;

看点:agent初始化Frida环境,注册hook,不包含核心hook实现。


结论:agent 是最终的“业务执行者”,负责注册 hook 逻辑,但真正的 hook 实现(如 interceptor)在 frida-gum 中,不在 frida-core。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐