Frida 注入流程源码解析:从底层server到 Agent 加载
背景
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。
更多推荐



所有评论(0)