首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
逆向工程
发新帖
44
21
[原创]Frida-gum 源代码速通笔记
发表于: 2023-8-15 19:43
24186
[原创]Frida-gum 源代码速通笔记
Tokameine
3
2023-8-15 19:43
24186
# 前言 最近做一些逆向的时候卡住了,感觉自己对 frida 的了解过于浅薄了,由于自己对安全研究的一些坏习惯,因此不读一下 frida 的源码就理解不了它的实现原理,于是直接就拿起来开始看了。(尽管现在做的不是安全研究,但希望这种习惯能延续下去吧。) 不得不说,frida 的代码写的是真的赏心悦目,像我这样阅读代码的苦手都能大致通过语义理解原理,我只能说,非常有感觉! # 源代码目录 Frida 的源代码目录结构按照模块进行区分,本文只选择其中几个笔者认为比较重要的部分进行分析: - **frida-core** / frida 的主要功能实现模块 - **frida-gum** / 提供 inline hook 、代码跟踪、内存监控、符号查找以及其他多个上层基础设施实现 - **gum-js** / 为 frida-gum 提供 JavaScript 语言的接口 出于完整性考虑,笔者也把其他比较重要的模块的介绍贴在这里。如有需要,读者可以自行去深入了解: > **frida-python**: Frida Python bindings > **frida-node**: Frida Node.js bindings > **frida-qml**: Frida Qml plugin > **frida-swift**: Frida Swift bindings > **frida-tools**: Frida CLI tools 本文中,笔者将按照自顶向下的方法去分析对应模块的功能实现。但本篇仅涉及到 Frida-gum部分,Frida-core将在另外一篇文章中介绍。 # frida-gum frida-gum 的实现结果是跨架构跨平台的,为此它抽象出了架构无关/平台无关/系统无关的 api 供用户使用。 该模块中有几个较为关心的子模块: - Interceptor: 提供 inline hook 的封装 - Stalker: 用于跟踪指令 - MemoryAccessMonitor: 内存监控 ## Interceptor 我们从测试样例开始: ```c TESTLIST_BEGIN (interceptor_arm64) TESTENTRY (attach_to_thunk_reading_lr) TESTENTRY (attach_to_function_reading_lr) TESTLIST_END () ``` 样例分为了对部分代码或整体函数进行钩取两种,似乎没什么区别,不妨先从函数开始。 ```c TESTCASE (attach_to_function_reading_lr) { const gsize code_size_in_pages = 1; gsize code_size; GumEmitLrFuncContext ctx; code_size = code_size_in_pages * gum_query_page_size (); ctx.code = gum_alloc_n_pages (code_size_in_pages, GUM_PAGE_RW); ctx.run = NULL; ctx.func = NULL; ctx.caller_lr = 0; gum_memory_patch_code (ctx.code, code_size, gum_emit_lr_func, &ctx); g_assert_cmphex (ctx.run (), ==, ctx.caller_lr); interceptor_fixture_attach (fixture, 0, ctx.func, '>', '<'); g_assert_cmphex (ctx.run (), !=, ctx.caller_lr); g_assert_cmpstr (fixture->result->str, ==, "><"); interceptor_fixture_detach (fixture, 0); gum_free_pages (ctx.code); } ``` frida 从 `interceptor_fixture_attach` 函数开始去 hook 对应函数,向下跟进可以找到实现函数: ```c static GumAttachReturn interceptor_fixture_try_attach (InterceptorFixture * h, guint listener_index, gpointer test_func, gchar enter_char, gchar leave_char) { GumAttachReturn result; Arm64ListenerContext * ctx; ctx = h->listener_context[listener_index]; if (ctx != NULL) { arm64_listener_context_free (ctx); h->listener_context[listener_index] = NULL; } ctx = g_slice_new0 (Arm64ListenerContext); ctx->listener = test_callback_listener_new (); ctx->listener->on_enter = (TestCallbackListenerFunc) arm64_listener_context_on_enter; ctx->listener->on_leave = (TestCallbackListenerFunc) arm64_listener_context_on_leave; ctx->listener->user_data = ctx; ctx->fixture = h; ctx->enter_char = enter_char; ctx->leave_char = leave_char; result = gum_interceptor_attach (h->interceptor, test_func, GUM_INVOCATION_LISTENER (ctx->listener), NULL); if (result == GUM_ATTACH_OK) { h->listener_context[listener_index] = ctx; } else { arm64_listener_context_free (ctx); } return result; } ``` 可以注意到,其中 `on_enter` 和 `on_leave` 是可以由用户自行重载的。然后再从 `gum_interceptor_attach` 进入,该函数包括了布置 hook 并启动 hook 的任务: ```c GumAttachReturn gum_interceptor_attach (GumInterceptor * self, gpointer function_address, GumInvocationListener * listener, gpointer listener_function_data) { GumAttachReturn result = GUM_ATTACH_OK; GumFunctionContext * function_ctx; GumInstrumentationError error; gum_interceptor_ignore_current_thread (self); GUM_INTERCEPTOR_LOCK (self); gum_interceptor_transaction_begin (&self->current_transaction); self->current_transaction.is_dirty = TRUE; //1. 获得需要钩取的函数地址 function_address = gum_interceptor_resolve (self, function_address); //2. 此处用于构造跳板、布置 hook,并将该函数抽象为一个 ctx 结构体 //后续对该函数的引用都将使用 ctx 指代该函数 function_ctx = gum_interceptor_instrument (self, GUM_INTERCEPTOR_TYPE_DEFAULT, function_address, &error); if (function_ctx == NULL) goto instrumentation_error; //3. 添加监听器 if (gum_function_context_has_listener (function_ctx, listener)) goto already_attached; gum_function_context_add_listener (function_ctx, listener, listener_function_data); goto beach; instrumentation_error: { switch (error) { case GUM_INSTRUMENTATION_ERROR_WRONG_SIGNATURE: result = GUM_ATTACH_WRONG_SIGNATURE; break; case GUM_INSTRUMENTATION_ERROR_POLICY_VIOLATION: result = GUM_ATTACH_POLICY_VIOLATION; break; case GUM_INSTRUMENTATION_ERROR_WRONG_TYPE: result = GUM_ATTACH_WRONG_TYPE; break; default: g_assert_not_reached (); } goto beach; } already_attached: { result = GUM_ATTACH_ALREADY_ATTACHED; goto beach; } beach: { //4. 注入跳板 gum_interceptor_transaction_end (&self->current_transaction); GUM_INTERCEPTOR_UNLOCK (self); gum_interceptor_unignore_current_thread (self); return result; } } ``` 笔者已经在上述代码的诸事中大致描述了关键部分的代码功能,在这里需要为此做一些额外说明。 所谓 **inline hook** 的工作原理是:将函数开头的指令替换为跳转指令,使得函数在执行时先跳转到 hook 到 `on_entry` 函数中,然后再从中返回执行原函数。 其中,用于从函数开头跳转到 `on_entry` 中的指令被称之为 `跳板`,而构造跳板首先需要获得 hook 函数在内存中的地址。这个操作在本文中不会详细介绍,若读者有兴趣了解原理可以自行阅读代码。 而监听器(Listener)的作用则是一个用于记录相关监视数据的结构体,对于已经被 hook 过的函数是不需要添加两个监听器的。 接下来我们跟入 `gum_interceptor_instrument` : ```c static GumFunctionContext * gum_interceptor_instrument (GumInterceptor * self, GumInterceptorType type, gpointer function_address, GumInstrumentationError * error) { GumFunctionContext * ctx; *error = GUM_INSTRUMENTATION_ERROR_NONE; //1. 获得需要 hook 的函数对象 //该对象在第一次调用 gum_interceptor_instrument 进行初始化入表 //此处由于第一次调用的缘故,在表里查询不到 ctx //因此会继续往下器创建该 ctx ctx = (GumFunctionContext *) g_hash_table_lookup (self->function_by_address, function_address); if (ctx != NULL) { if (ctx->type != type) { *error = GUM_INSTRUMENTATION_ERROR_WRONG_TYPE; return NULL; } return ctx; } if (self->backend == NULL) { //2. 构造三级跳板 self->backend = _gum_interceptor_backend_create (&self->mutex, &self->allocator); } //3. 初始化 ctx ctx = gum_function_context_new (self, function_address, type); if (gum_process_get_code_signing_policy () == GUM_CODE_SIGNING_REQUIRED) { if (!_gum_interceptor_backend_claim_grafted_trampoline (self->backend, ctx)) goto policy_violation; } else { //4. 构造二级跳板 if (!_gum_interceptor_backend_create_trampoline (self->backend, ctx)) goto wrong_signature; } //5. 函数入表,表示已经完成基本操作 g_hash_table_insert (self->function_by_address, function_address, ctx); //6. 添加任务,设置回调函数 gum_interceptor_activate 用于激活跳板 gum_interceptor_transaction_schedule_update (&self->current_transaction, ctx, gum_interceptor_activate); return ctx; policy_violation: { *error = GUM_INSTRUMENTATION_ERROR_POLICY_VIOLATION; goto propagate_error; } wrong_signature: { *error = GUM_INSTRUMENTATION_ERROR_WRONG_SIGNATURE; goto propagate_error; } propagate_error: { gum_function_context_finalize (ctx); return NULL; } } ``` 在注释中,笔者提到了二级跳板和三级跳板,那么一级跳板是什么? 一级跳板其实就是函数开头的跳转指令,该指令将会让程序跳转到二级跳板中,而二级跳板会转入三级跳板,最后由三级跳板分发,选择用户提供的 `on_entry` 函数进行调用。 ### _gum_interceptor_backend_create 首先创建的是三级跳板,因此我们跟到 `_gum_interceptor_backend_create` 里看看它是如何实现的。该函数是平台相关的具体函数,由于笔者打算分析 arm64 下的实现,因此这里的源代码应为 frida-gum/gum/backend-arm64/guminterceptor-arm64.c: ```c GumInterceptorBackend * _gum_interceptor_backend_create (GRecMutex * mutex, GumCodeAllocator * allocator) { GumInterceptorBackend * backend; backend = g_slice_new0 (GumInterceptorBackend); backend->mutex = mutex; backend->allocator = allocator; if (gum_process_get_code_signing_policy () == GUM_CODE_SIGNING_OPTIONAL) { gum_arm64_writer_init (&backend->writer, NULL); gum_arm64_relocator_init (&backend->relocator, NULL, &backend->writer); gum_interceptor_backend_create_thunks (backend); } return backend; } ``` 跟入 `gum_interceptor_backend_create_thunks` : ```c static void gum_interceptor_backend_create_thunks (GumInterceptorBackend * self) { gsize page_size, code_size; page_size = gum_query_page_size (); code_size = page_size; self->thunks = gum_memory_allocate (NULL, code_size, page_size, GUM_PAGE_RW); gum_memory_patch_code (self->thunks, 1024, (GumMemoryPatchApplyFunc) gum_emit_thunks, self); } ``` 此处通过调用 `gum_memory_patch_code` 把 `gum_emit_thunks` 的实现写入到 `self->thunks` 中,因此我们这里跟入 `gum_emit_thunks` : ```c static void gum_emit_thunks (gpointer mem, GumInterceptorBackend * self) { GumArm64Writer * aw = &self->writer; self->enter_thunk = self->thunks; gum_arm64_writer_reset (aw, mem); aw->pc = GUM_ADDRESS (self->enter_thunk); gum_emit_enter_thunk (aw);//1. 此处创建三级跳板 enter_thunk gum_arm64_writer_flush (aw); self->leave_thunk = (guint8 *) self->enter_thunk + gum_arm64_writer_offset (aw); gum_emit_leave_thunk (aw);//2. 此处创建三级跳板 leave_thunk gum_arm64_writer_flush (aw); } ``` 此处涉及到了具体的三级跳板的创建,分别由 `gum_emit_enter_thunk` 和 `gum_emit_leave_thunk` 完成,这里笔者先从 `gum_emit_enter_thunk` 进行分析: ```c static void gum_emit_enter_thunk (GumArm64Writer * aw) { gum_emit_prolog (aw);//1. 保存上下文信息 //2. add x1,sp,0 gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X1, ARM64_REG_SP, GUM_FRAME_OFFSET_CPU_CONTEXT); //3. add x2,sp,G_STRUCT_OFFSET (GumCpuContext, lr) gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X2, ARM64_REG_SP, GUM_FRAME_OFFSET_CPU_CONTEXT + G_STRUCT_OFFSET (GumCpuContext, lr)); //4. add x3,sp,sizeof (GumCpuContext) gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X3, ARM64_REG_SP, GUM_FRAME_OFFSET_NEXT_HOP); //5. call _gum_function_context_begin_invocation(x17,x1,x2,x3) gum_arm64_writer_put_call_address_with_arguments (aw, GUM_ADDRESS (_gum_function_context_begin_invocation), 4, GUM_ARG_REGISTER, ARM64_REG_X17, GUM_ARG_REGISTER, ARM64_REG_X1, GUM_ARG_REGISTER, ARM64_REG_X2, GUM_ARG_REGISTER, ARM64_REG_X3); gum_emit_epilog (aw); } ``` 此处主要负责调用四级跳板 `_gum_function_context_begin_invocation` 并进行传参,跟入该函数: ```c void _gum_function_context_begin_invocation (GumFunctionContext * function_ctx, GumCpuContext * cpu_context, gpointer * caller_ret_addr, gpointer * next_hop) ``` 注意,此处第三个参数 `caller_ret_addr` 代表的是**被 hook** 函数用于储存返回地址的内存地址,而第四个参数则是四级跳板返回时执行的下一个函数地址。 稍微向下看看函数的实现:(省略部分) ```c //1. 如果替换了函数实现,或注册了 on_leave 则设置 will_trap_on_leave will_trap_on_leave = function_ctx->replacement_function != NULL || (invoke_listeners && function_ctx->has_on_leave_listener); if (will_trap_on_leave) { //2. 如果设置了 will_trap_on_leave,就需要保存原本的返回地址,这样在 on_leave 时能给正确返回 stack_entry = gum_invocation_stack_push (stack, function_ctx, *caller_ret_addr); invocation_ctx = &stack_entry->invocation_context; } else if (invoke_listeners) { //3. 如果没设置 will_trap_on_leave,但有注册 linsters,那么在这里把原本的函数地址保存到栈里 stack_entry = gum_invocation_stack_push (stack, function_ctx, function_ctx->function_address); invocation_ctx = &stack_entry->invocation_context; } ``` 这里不难理解: - 如果我们替换了函数实现,或者设置了 on_leave ,那么在返回以前到原本的执行流之前就需要先保存当前的返回内容,它会在后续被用于指向正确的返回地址。 - 如果我们只是想钩一些调用点,那么执行流应该从这里返回到原本的函数去恢复执行。 此处只是先在栈中保存数据。 然后接下来会调用注册的 on_enter: ```c if (listener_entry->listener_interface->on_enter != NULL) { listener_entry->listener_interface->on_enter ( listener_entry->listener_instance, invocation_ctx); } ``` 后续过程中: ```c if (will_trap_on_leave) { *caller_ret_addr = function_ctx->on_leave_trampoline; } if (function_ctx->replacement_function != NULL) { stack_entry->calling_replacement = TRUE; stack_entry->cpu_context = *cpu_context; stack_entry->original_system_error = system_error; invocation_ctx->cpu_context = &stack_entry->cpu_context; invocation_ctx->backend = &interceptor_ctx->replacement_backend; invocation_ctx->backend->data = function_ctx->replacement_data; *next_hop = function_ctx->replacement_function; } else { *next_hop = function_ctx->on_invoke_trampoline; } ``` 可以看到此处会将 `on_leave_trampoline` 二级跳板写入到用于储存返回地址的内存中去。也就是说被 hook 函数在执行完毕以后会返回到 `on_leave_trampoline` 。 然后如果需要替换函数实现,那么就要把用于替换的实现代码地址写入当前函数的返回地址去,否则就将跳板注入进去。 因此在该函数结束后会根据这一步选择接下来是执行 `function_ctx->replacement_function` 还是 `function_ctx->on_invoke_trampoline` 。 前者就不难理解了,接下来就是调用我们自己实现的函数,并在返回的时候回到二级跳板。 我们看看后者的实现: ```c (gdb) x/17i function_ctx->on_invoke_trampoline 0x7fb6c82a30: stp x29, x30, [sp, #-16]! 0x7fb6c82a34: mov x29, sp 0x7fb6c82a38: sub sp, sp, #0x10 0x7fb6c82a3c: mov x8, #0x0 // #0 0x7fb6c82a40: ldr x16, 0x7fb6c82a48 0x7fb6c82a44: br x16 0x7fb6c82a48: cbnz x12, 0x7fb6cb7ae8//执行原本的函数 ``` 此处用于调用原本的函数。 这里我们留个疑问,先不管二级跳板 `on_leave_trampoline` 的实现是什么,现在再跟一下 `leave_chunk`: ```c static void gum_emit_leave_thunk (GumArm64Writer * aw) { gum_emit_prolog (aw); gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X1, ARM64_REG_SP, GUM_FRAME_OFFSET_CPU_CONTEXT); gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X2, ARM64_REG_SP, GUM_FRAME_OFFSET_NEXT_HOP); gum_arm64_writer_put_call_address_with_arguments (aw, GUM_ADDRESS (_gum_function_context_end_invocation), 3, GUM_ARG_REGISTER, ARM64_REG_X17, GUM_ARG_REGISTER, ARM64_REG_X1, GUM_ARG_REGISTER, ARM64_REG_X2); gum_emit_epilog (aw); } ``` 和前一个 chunk 的结构差不多,我们跟入 `_gum_function_context_end_invocation` : ```c void _gum_function_context_end_invocation (GumFunctionContext * function_ctx, GumCpuContext * cpu_context, gpointer * next_hop) { gint system_error; InterceptorThreadContext * interceptor_ctx; GumInvocationStackEntry * stack_entry; GumInvocationContext * invocation_ctx; GPtrArray * listener_entries; guint i; #ifdef HAVE_WINDOWS system_error = gum_thread_get_system_error (); #endif gum_tls_key_set_value (gum_interceptor_guard_key, function_ctx->interceptor); #ifndef HAVE_WINDOWS system_error = gum_thread_get_system_error (); #endif interceptor_ctx = get_interceptor_thread_context (); stack_entry = gum_invocation_stack_peek_top (interceptor_ctx->stack); //1. 此处将函数返回时的地址设置为真正的返回值。该值在 enter_chunk 中被保存 *next_hop = gum_sign_code_pointer (stack_entry->caller_ret_addr); //此处省略...... #ifndef GUM_DIET if (listener_entry->listener_interface->on_leave != NULL) { //2. 此处调用注册的 on_leave listener_entry->listener_interface->on_leave ( listener_entry->listener_instance, invocation_ctx); } } ``` 接下来回到最开始我们跳过的地方,按照代码的顺序,其实在完成上述跳板设置以后才开始准备二级跳板: ```c gboolean _gum_interceptor_backend_create_trampoline (GumInterceptorBackend * self, GumFunctionContext * ctx) { ctx->on_enter_trampoline = gum_sign_code_pointer (gum_arm64_writer_cur (aw)); //此处省略 gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X17, GUM_ADDRESS (ctx)); gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X16, GUM_ADDRESS (gum_sign_code_pointer (self->enter_thunk))); gum_arm64_writer_put_br_reg (aw, ARM64_REG_X16); ctx->on_leave_trampoline = gum_arm64_writer_cur (aw); gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X17, GUM_ADDRESS (ctx)); gum_arm64_writer_put_ldr_reg_address (aw, ARM64_REG_X16, GUM_ADDRESS (gum_sign_code_pointer (self->leave_thunk))); gum_arm64_writer_put_br_reg (aw, ARM64_REG_X16); gum_arm64_writer_flush (aw);//(6) g_assert (gum_arm64_writer_offset (aw) <= ctx->trampoline_slice->size); ctx->on_invoke_trampoline = gum_sign_code_pointer (gum_arm64_writer_cur (aw)); ``` 该代码大致会实现如下结构: ```c on_enter_trampoline: ldr x17 context_addr ldr x16 enter_thunk br x16 on_leave_trampoline: ldr x17 context_addr ldr x16 leave_thunk br x16 //处写入代码,下面三个字存放地址 context_addr: .dword address enter_thunk: .dword address leave_thunk: .dword address on_invoke_trampoline: ``` 到这一步其实流程就清晰很多了。 - 一级跳板进入到二级跳板 `on_enter_trampoline` - 二级跳板再跳转到三级跳板 `enter_thunk` - 三级跳板中再调用四级跳板 `_gum_function_context_begin_invocation` - 四级跳板将会调用注册的 `on_enter` 函数,并设置真正的返回地址,同时决定接下来要执行谁 - 如果执行 `on_invoke_trampoline` ,那么将调用原本的函数 - 否则将会调用我们用于替换的函数 - 接下来执行流将返回到 `on_leave_trampoline` - 然后从该处跳转入三级跳板 `leave_thunk` - 再从三级跳板进入四级跳板 `_gum_function_context_end_invocation` - 在四级跳板中,将恢复真正的返回地址,并调用注册好的 `on_leave` 函数,最后从中返回 而如果用户没有注册 on_leave 函数,那么钩子的步骤将会减少很多。在 `_gum_function_context_begin_invocation` 中将不会修改真正的返回地址,并直接让 `next_hop` 设置为 `on_invoke_trampoline` ,此时程序将直接离开钩子,因为后续不会再进入到 `leave_chunk` 了。 ### 额外的问题 在上文中可以发现,几个跳板的实现其实是走了固定的寄存器的。因此如果程序本身本来就要使用这两个寄存器进行工作的话,去 hook 那些函数会导致非预期的结果。这个地方可能需要注意一下吧,毕竟大多数时候使用 frida 感觉 hook 基本上都是透明的,容易忽略到这种级别的问题。 ## Stalker 其实笔者没怎么用过这个功能,它所实现的 “代码跟踪” 能力其实和调试器差不多,而如果能使用调试器进行调试的话,大部分问题其实都能解决,而就算不能使用调试器,靠 frida 的 hook 也能解决不少问题了,这导致笔者基本上没用过它。 不过没用过不影响看看原理。因为 Stalker 是靠代码插桩的方式实现跟踪的,这和事情的 Interceptor 有点相似。 Stalker 的测试样例就比较多了,但我们只想对源代码的实现有所了解,因此不需要每个都看,选几个比较有意思的就行。这里笔者选了 `TESTENTRY(call)` 作为分析样例: ```c TESTCASE (call) { StalkerTestFunc func; GumCallEvent * ev; func = invoke_flat (fixture, GUM_CALL); g_assert_cmpuint (fixture->sink->events->len, ==, 2); g_assert_cmpint (g_array_index (fixture->sink->events, GumEvent, 0).type, ==, GUM_CALL); ev = &g_array_index (fixture->sink->events, GumEvent, 0).call; GUM_ASSERT_CMPADDR (ev->location, ==, fixture->last_invoke_calladdr); GUM_ASSERT_CMPADDR (ev->target, ==, gum_strip_code_pointer (func)); } ``` 关键的实现在 `invoke_flat` 中,这里我们跟入:`invoke_flat` - `invoke_flat_expecting_return_value` ```c static StalkerTestFunc invoke_flat_expecting_return_value (TestArm64StalkerFixture * fixture, GumEventType mask, guint expected_return_value) { StalkerTestFunc func; gint ret; func = (StalkerTestFunc) test_arm64_stalker_fixture_dup_code (fixture, flat_code, sizeof (flat_code)); fixture->sink->mask = mask; ret = test_arm64_stalker_fixture_follow_and_invoke (fixture, func, -1); g_assert_cmpint (ret, ==, expected_return_value); return func; } ``` 再跟入 `test_arm64_stalker_fixture_follow_and_invoke`: ```c static gint test_arm64_stalker_fixture_follow_and_invoke (TestArm64StalkerFixture * fixture, StalkerTestFunc func, gint arg) { GumAddressSpec spec; guint8 * code; GumArm64Writer cw; gint ret; GCallback invoke_func; spec.near_address = gum_strip_code_pointer (gum_stalker_follow_me); spec.max_distance = G_MAXINT32 / 2; //1. 创建新代码页用来储存接下来将要生成的代码 code = gum_alloc_n_pages_near (1, GUM_PAGE_RW, &spec); gum_arm64_writer_init (&cw, code); //2. 保存寄存器 gum_arm64_writer_put_push_reg_reg (&cw, ARM64_REG_X29, ARM64_REG_X30); gum_arm64_writer_put_mov_reg_reg (&cw, ARM64_REG_X29, ARM64_REG_SP); //3. 调用 gum_stalker_follow_me gum_arm64_writer_put_call_address_with_arguments (&cw, GUM_ADDRESS (gum_stalker_follow_me), 3, GUM_ARG_ADDRESS, GUM_ADDRESS (fixture->stalker), GUM_ARG_ADDRESS, GUM_ADDRESS (fixture->transformer), GUM_ARG_ADDRESS, GUM_ADDRESS (fixture->sink)); /* call function -int func(int x)- and save address before and after call */ gum_arm64_writer_put_ldr_reg_address (&cw, ARM64_REG_X0, GUM_ADDRESS (arg)); fixture->last_invoke_calladdr = gum_arm64_writer_cur (&cw); //4. 调用原本的代码 gum_arm64_writer_put_call_address_with_arguments (&cw, GUM_ADDRESS (func), 0); fixture->last_invoke_retaddr = gum_arm64_writer_cur (&cw); gum_arm64_writer_put_ldr_reg_address (&cw, ARM64_REG_X1, GUM_ADDRESS (&ret)); gum_arm64_writer_put_str_reg_reg_offset (&cw, ARM64_REG_W0, ARM64_REG_X1, 0); //5. 取消跟踪 gum_arm64_writer_put_call_address_with_arguments (&cw, GUM_ADDRESS (gum_stalker_unfollow_me), 1, GUM_ARG_ADDRESS, GUM_ADDRESS (fixture->stalker)); gum_arm64_writer_put_pop_reg_reg (&cw, ARM64_REG_X29, ARM64_REG_X30); gum_arm64_writer_put_ret (&cw); gum_arm64_writer_flush (&cw); gum_memory_mark_code (cw.base, gum_arm64_writer_offset (&cw)); gum_arm64_writer_clear (&cw); invoke_func = GUM_POINTER_TO_FUNCPTR (GCallback, gum_sign_code_pointer (code)); invoke_func (); gum_free_pages (code); return ret; } ``` 逻辑比较清晰,相当于将原本的代码注入到另外一片内存,然后对其进行插桩执行,并在插桩代码中记录覆盖率相关的信息。这里我们先从 `gum_stalker_follow_me` 开始,它是一个由汇编实现的函数: ```asm #ifdef __APPLE__ .globl _gum_stalker_follow_me _gum_stalker_follow_me: #else .globl gum_stalker_follow_me .type gum_stalker_follow_me, %function gum_stalker_follow_me: #endif stp x29, x30, [sp, -16]! mov x29, sp mov x3, x30 #ifdef __APPLE__ bl __gum_stalker_do_follow_me #else bl _gum_stalker_do_follow_me #endif ldp x29, x30, [sp], 16 br x0 ``` 此处对于 Apple 架构和其他架构选用了两个不同的函数,不过我在源代码中并没有找到 `__gum_stalker_do_follow_me` 的声明或实现,这里我们将就这用 `_gum_stalker_do_follow_me` 进行理解吧: ```c gpointer _gum_stalker_do_follow_me (GumStalker * self, GumStalkerTransformer * transformer, GumEventSink * sink, gpointer ret_addr) { GumExecCtx * ctx; gpointer code_address; ctx = gum_stalker_create_exec_ctx (self, gum_process_get_current_thread_id (), transformer, sink); g_private_set (&gum_stalker_exec_ctx_private, ctx); ctx->current_block = gum_exec_ctx_obtain_block_for (ctx, ret_addr, &code_address); if (gum_exec_ctx_maybe_unfollow (ctx, ret_addr)) { gum_stalker_destroy_exec_ctx (self, ctx); return ret_addr; } gum_event_sink_start (ctx->sink); ctx->sink_started = TRUE; return code_address + GUM_RESTORATION_PROLOG_SIZE; } ``` 关键内容跟入 `gum_event_sink_start` ,里面是用于记录覆盖率信息的具体函数,分别有两套实现,一套是用 `quickjs` ,另外一套是 `v8` 的实现,细节这里笔者就不深究了,大致逻辑如图:  ## 内存监控 这部分内容也不是笔者关心的重点,但笔者找了一圈似乎没找到 arm64 下的实现,倒是有 x86 平台下的测试样例。因此本文也就不过多赘述了,大致原理就是设置内存页的读写权限,从而在读写监控页面的时候引发中断来监视内容。 实现的内容分了 Windows 平台和 posix 平台两种,如下代码为 posix 平台: ```c GumMemoryAccessMonitor * gum_memory_access_monitor_new (const GumMemoryRange * ranges, guint num_ranges, GumPageProtection access_mask, gboolean auto_reset, GumMemoryAccessNotify func, gpointer data, GDestroyNotify data_destroy) { GumMemoryAccessMonitor * monitor; guint i; monitor = g_object_new (GUM_TYPE_MEMORY_ACCESS_MONITOR, NULL); monitor->ranges = g_memdup (ranges, num_ranges * sizeof (GumMemoryRange)); monitor->num_ranges = num_ranges; monitor->access_mask = access_mask; monitor->auto_reset = auto_reset; monitor->pages_total = 0; for (i = 0; i != num_ranges; i++) { GumMemoryRange * r = &monitor->ranges[i]; gsize aligned_start, aligned_end; guint num_pages; aligned_start = r->base_address & ~((gsize) monitor->page_size - 1); aligned_end = (r->base_address + r->size + monitor->page_size - 1) & ~((gsize) monitor->page_size - 1); r->base_address = aligned_start; r->size = aligned_end - aligned_start; num_pages = r->size / monitor->page_size; g_atomic_int_add (&monitor->pages_remaining, num_pages); monitor->pages_total += num_pages; } monitor->notify_func = func; monitor->notify_data = data; monitor->notify_data_destroy = data_destroy; return monitor; } ``` ```c gboolean gum_memory_access_monitor_enable (GumMemoryAccessMonitor * self, GError ** error) { if (self->enabled) return TRUE; // ... self->exceptor = gum_exceptor_obtain (); gum_exceptor_add (self->exceptor, gum_memory_access_monitor_on_exception, self); // ... } ``` ### gum-js 这部分主要是做一个扫盲。细节可以参考 evilpan 大佬的文章,里面也大致介绍了 gum-js 的实现。 简单来说就说,V8 支持对 JavaScript 的动态解析,并能够将其抽象到 C 语言层面进行调用。 ```cpp Local<String> attach_name = String::NewFromUtf8Literal(GetIsolate(), "Attach"); // 判断对象是否存在,以及类型是否是函数 Local<Value> attach_val; if (!context->Global()->Get(context, attach_name).ToLocal(&attach_val) || !attach_val->IsFunction()) { return false; } // 如果是,则转换为函数类型 Local<Function> attach_func = attach_val.As<Function>(); // 将调用参数封装为 JS 对象 Local<Object> obj = templ->NewInstance(GetIsolate()->GetCurrentContext()).ToLocalChecked(); obj->SetInternalField(0, 0xdeadbeef); // 使用自定义的参数调用该 JS 函数,并获取返回结果 TryCatch try_catch(GetIsolate()); const int argc = 1; Local<Value> argv[argc] = {obj}; Local<Value> result; attach_func->Call(context, context->Global(), argc, argv).ToLocal(&result); ``` 通过这种交互面板,就能给允许用户动态传入脚本进行执行了。之所以选择的是 JavaScript 而不是其他语言,就笔者估计来看,大致上有两个原因(以下内容为笔者的猜测,各位读者可以当看个乐子): - 首先编译型的语言肯定是不行了,因为它不支持动态调整,每次都要编译一份再运行肯定不太灵活 - 而在解释型语言里就比较看重运行效率了。笔者曾写过一篇 V8 优化引擎 Turbofan 的原理分析,从其中可以大致理解到,V8 对 JavaScript 的优化效率几乎已经达到理论上限了,这意味着在各种解释型语言里,JS 的效率可能是最高的(这是区分场景的,但在 Frida 里,可能它是最合适的) > Turbofan 可参考本文:https://bbs.kanxue.com/thread-273791.htm # 参考 <mark class="encrypted">143K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6W2N6X3W2D9M7r3q4F1i4K6u0W2j5$3!0E0i4K6u0r3x3U0l9J5x3W2)9J5c8U0l9@1i4K6u0r3x3o6g2Q4x3V1k6X3M7X3W2V1j5g2)9J5k6r3W2F1N6r3g2J5L8X3q4D9i4K6u0r3i4K6t1K6M7%4c8S2L8r3E0W2M7R3`.`.</mark> <mark class="encrypted">715K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6*7K9s2g2S2L8X3I4S2L8W2)9J5k6i4A6Z5K9h3S2#2i4K6u0W2j5$3!0E0i4K6u0r3M7q4)9J5c8U0j5H3x3K6M7I4y4K6p5I4z5l9`.`.</mark> <mark class="encrypted">ba4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6G2x3s2S2E0N6h3S2W2i4K6u0W2k6$3W2@1K9s2g2T1i4K6u0W2K9h3!0Q4x3V1j5J5x3o6p5&6i4K6u0r3x3e0q4Q4x3V1j5I4y4g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1k6%4g2E0i4K6t1#2c8e0c8Q4x3U0g2n7b7W2)9J5y4f1p5K6i4K6t1#2c8e0N6Q4x3U0g2m8x3q4)9J5y4e0R3I4i4K6t1#2c8e0W2Q4x3U0f1&6z5q4)9J5y4e0R3#2i4K6t1#2c8e0S2Q4x3U0g2m8c8W2)9J5y4f1u0n7i4K6u0r3i4K6t1K6x3W2)9J5k6o6u0Q4x3X3b7J5i4K6u0V1K9r3!0G2K9#2)9J5y4f1f1@1i4K6t1#2b7V1u0Q4x3U0f1^5c8e0m8Q4x3U0g2q4y4g2)9J5y4e0R3^5i4K6t1#2b7U0l9I4</mark>
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于
2023-8-15 19:43 被Tokameine编辑 ,原因:
#调试逆向
#其他内容
收藏
・
44
点赞
・
21
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
mb_fhpfcoqp
感谢你的贡献,论坛因你而更加精彩!
2026-4-29 21:24
zzzhangyu
谢谢你的细致分析,受益匪浅!
2026-1-26 18:20
mb_iesvmzqc
感谢你的积极参与,期待更多精彩内容!
2026-1-22 18:17
卡卡骨
感谢你的积极参与,期待更多精彩内容!
2026-1-15 10:21
xianyuuuan
谢谢你的细致分析,受益匪浅!
2025-12-7 15:01
xilooo
谢谢你的细致分析,受益匪浅!
2025-8-12 19:34
git_21210wzzzh2025
+1
为你点赞!
2025-7-26 10:10
螺丝兔
+1
期待更多优质内容的分享,论坛有你更精彩!
2025-3-5 16:07
三草儿
感谢你的贡献,论坛因你而更加精彩!
2025-2-9 01:22
g0mx
+1
你的分享对大家帮助很大,非常感谢!
2024-7-11 16:59
PLEBFE
为你点赞~
2023-12-30 00:56
新气象
为你点赞~
2023-12-22 23:15
tfflowers
为你点赞~
2023-12-20 18:26
shuxuliang
为你点赞~
2023-9-25 13:27
值得怀疑
为你点赞~
2023-8-24 21:34
mb_yfioexda
为你点赞~
2023-8-22 10:35
pureGavin
为你点赞~
2023-8-18 09:42
0x指纹
为你点赞~
2023-8-17 14:20
iamshy
为你点赞~
2023-8-17 13:52
东方玻璃
为你点赞~
2023-8-16 14:13
你瞒我瞒
为你点赞~
2023-8-16 11:08
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
6
)
pureGavin
雪 币:
18182
活跃值:
(21429)
能力值:
( LV12,RANK:310 )
在线值:
发帖
104
回帖
1466
粉丝
324
关注
私信
pureGavin
3
2
楼
感谢分享
2023-8-18 09:42
0
小希希
雪 币:
2857
活跃值:
(5655)
能力值:
( LV2,RANK:10 )
在线值:
发帖
17
回帖
360
粉丝
18
关注
私信
小希希
3
楼
感谢分享
2023-10-17 11:00
0
wx_coddvccwyh
雪 币:
4
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
126
粉丝
0
关注
私信
wx_coddvccwyh
4
楼
想问下对于frida源码的分析,应该如何进行调试分析环境的部署?
2024-10-6 02:15
0
梦_魇
雪 币:
1330
活跃值:
(7660)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
36
粉丝
1
关注
私信
梦_魇
5
楼
感谢楼主分享 顺便问一下楼主 你说的一级跳板的具体写入的实现代码在哪里呢 我看没到您说明 我在源码里面也没有找到具体的位置
2025-4-17 16:17
0
thouger_
雪 币:
9
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
34
粉丝
0
关注
私信
thouger_
6
楼
同问
2025-9-28 11:38
0
artake
雪 币:
10
活跃值:
(2681)
能力值:
( LV4,RANK:40 )
在线值:
发帖
13
回帖
185
粉丝
1
关注
私信
artake
7
楼
666
2025-12-1 15:38
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
Tokameine
3
17
发帖
85
回帖
470
RANK
关注
私信
他的文章
[原创]VirtualApp 原理速览 - 总集篇
17545
[原创] Activity 原生启动流程分析
15846
[原创]强网杯2023Final-D8利用分析——从越界读到任意代码执行(CVE-2023-4427)
14261
[原创] KCTF2023 第十二题 深入内核 writeup
5052
[原创] Frida-Core 源代码速通笔记
20159
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部