0x00 : 前言与预备知识
frida
: frida是一个优秀的跨平台Dynamic instrumentation toolkit
,具体可以看官网介绍
GObject对象系统
GObject这个比较重要,因为frida框架底层的hook框架Frida-gum是纯c写的,为了实现一些面向对象的编程,使用了Gobject。
注:本篇主要是看interceptor
这种hook方式,针对函数头,之后会有一篇针对Stalker
模式的分析。
0x01 : 项目构架
直接拉下来的代码如下
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
| ╭─muhe@muheMacBookPro ~/Code/frida ‹master*› ╰─$ l total 137608 drwxr-xr-x 29 muhe staff 928B Nov 13 17:22 . drwxr-xr-x 90 muhe staff 2.8K Nov 5 16:44 .. drwxr-xr-x 15 muhe staff 480B Nov 15 18:23 .git -rw-r--r-- 1 muhe staff 383B Jan 21 2019 .gitignore -rw-r--r-- 1 muhe staff 886B Jan 21 2019 .gitmodules drwxr-xr-x 3 muhe staff 96B Nov 13 17:22 .vscode -rw-r--r-- 1 muhe staff 2.4K Jan 21 2019 COPYING -rw-r--r-- 1 muhe staff 1.2K Nov 7 18:11 Makefile -rw-r--r-- 1 muhe staff 28K Nov 7 18:11 Makefile.linux.mk -rw-r--r-- 1 muhe staff 28K Nov 7 18:11 Makefile.macos.mk -rw-r--r-- 1 muhe staff 21K Nov 7 18:11 Makefile.sdk.mk -rw-r--r-- 1 muhe staff 84K Apr 29 2019 Makefile.toolchain.mk -rw-r--r-- 1 muhe staff 1.7K Nov 7 18:11 README.md drwxr-xr-x 10 muhe staff 320B Nov 11 14:59 build drwxr-xr-x 61 muhe staff 1.9K Jan 21 2019 capstone -rw-r--r-- 1 muhe staff 1.0K Nov 7 18:11 config.mk drwxr-xr-x 9 muhe staff 288B Jan 21 2019 frida-clr drwxr-xr-x 21 muhe staff 672B Jan 21 2019 frida-core drwxr-xr-x 20 muhe staff 640B Nov 11 17:37 frida-gum drwxr-xr-x 15 muhe staff 480B Jan 21 2019 frida-node drwxr-xr-x 20 muhe staff 640B Jan 21 2019 frida-python drwxr-xr-x 27 muhe staff 864B Jan 21 2019 frida-qml drwxr-xr-x 10 muhe staff 320B Jan 21 2019 frida-swift drwxr-xr-x 12 muhe staff 384B Jan 21 2019 frida-tools -rw-r--r-- 1 muhe staff 25K Nov 7 18:11 frida.sln -rw-r--r-- 1 muhe staff 9.0K Nov 11 14:43 frida.srctrlbm -rw-r--r-- 1 muhe staff 67M Nov 11 14:43 frida.srctrldb -rw-r--r-- 1 muhe staff 6.1K Nov 11 14:35 frida.srctrlprj drwxr-xr-x 47 muhe staff 1.5K Nov 7 18:11 releng
|
frida-gum
是底层hook框架,跨平台;
frida-python
, frida-node
啥的是 bindings,暂时不管,不理解原理看也看不懂;
capstone
牛逼的反汇编框架,frida-gum
中用到了,用于指令的读;
releng
编译相关的;
frida-core
server/agent相关;
frida-tools
一些工具,比如frida-ps啥的。
重点是frida-gum
,这是理解这个框架的基础。
0x02 : 阅读frida-gum
(x86为例)
frida-gum
注释并不多,甚至可以说几乎没,好在他代码写得好,构架合理代码规范好,所以阅读起来多读几遍,总会看懂的。
2.1. 构架
这个的框架的构架如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| total 200 drwxr-xr-x 20 muhe staff 640B Nov 11 17:37 . drwxr-xr-x 29 muhe staff 928B Nov 13 17:22 .. -rw-r--r-- 1 muhe staff 34B Jan 21 2019 .git -rw-r--r-- 1 muhe staff 70B Jan 21 2019 .gitignore drwxr-xr-x 3 muhe staff 96B Nov 11 17:37 .vscode -rw-r--r-- 1 muhe staff 5.6K Jan 21 2019 COPYING drwxr-xr-x 5 muhe staff 160B Jan 21 2019 bindings -rw-r--r-- 1 muhe staff 2.1K Jan 21 2019 config.h.in drwxr-xr-x 3 muhe staff 96B Jan 21 2019 ext drwxr-xr-x 85 muhe staff 2.7K Jan 21 2019 gum -rw-r--r-- 1 muhe staff 5.1K Jan 21 2019 gum-32.vcxproj -rw-r--r-- 1 muhe staff 16K Jan 21 2019 gum-32.vcxproj.filters -rw-r--r-- 1 muhe staff 5.1K Jan 21 2019 gum-64.vcxproj -rw-r--r-- 1 muhe staff 16K Jan 21 2019 gum-64.vcxproj.filters -rw-r--r-- 1 muhe staff 8.5K Jan 21 2019 gum-common.props drwxr-xr-x 4 muhe staff 128B Jan 21 2019 libs -rw-r--r-- 1 muhe staff 6.8K Jan 21 2019 meson.build -rw-r--r-- 1 muhe staff 190B Jan 21 2019 meson_options.txt drwxr-xr-x 28 muhe staff 896B Jan 21 2019 tests drwxr-xr-x 7 muhe staff 224B Jan 21 2019 vapi
|
核心是在gum目录下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| gum ├── arch-arm ├── arch-arm64 ├── arch-mips ├── arch-x86 ├── backend-arm ├── backend-arm64 ├── backend-darwin ├── backend-dbghelp ├── backend-elf ├── backend-libdwarf ├── backend-libunwind ├── backend-linux ├── backend-mips ├── backend-posix ├── backend-qnx ├── backend-windows └── backend-x86 ....// gum下其他文件
|
这里有必要说一下,frida-gum
为了实现跨平台,抽象出来 构架无关/平台无关/系统无关
的api,比如一些内存操作,在frida-gum
里可能就是gum_xxxxx
,但是根据不同平台,调用到对应平台的api里去,正是做了很好的封装,上层代码才会看起来“平台无关”。
还有几个核心的对象,后面的代码里频繁提及:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| struct _GumInterceptor { GObject parent;
GRecMutex mutex;
GHashTable * function_by_address;
GumInterceptorBackend * backend; GumCodeAllocator allocator;
volatile guint selected_thread_id;
GumInterceptorTransaction current_transaction; };
|
从这个拦截器类索引出去的对象都需要好好注意,比如 GumInterceptorBackend
, 最好可以生成一个uml图,阅读代码的时候对比着看。
2.2. 代码阅读
2.2.1 准备工作
面对比较大的代码,重要的是找到一个入口,从这个点开始读,我这里大概看了下单元测试的代码,发现基本是: 初始化,测试各种功能,清理,退出。
那么我的阅读思路就是 :
- 初始化部分
- 各种功能,比如 内存模块,指令读写模块,代码修复模块
- 清理 这部分大概过一下就行
这里我参考了 jmpews
师傅的关于设计hook框架的文章,了解一个hook框架如何设计,分哪些模块,在阅读代码的时候能够有针对性一些。
- 内存分配 模块
- 指令写 模块
- 指令读 模块
- 指令修复 模块 relocator
- 跳板 模块
- 调度器 模块 enter_thunk部分实现
- 栈 模块
具体可以参考他的文章: 如何构建一款像 frida 一样的框架
2.2.2 hook从0到1
阅读顺序根据单元测试gum-test.c
确定的,具体的可以看代码
gum_interceptor_obtain()
这部分是 拦截器初始化
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
| GumInterceptor * gum_interceptor_obtain (void) { GumInterceptor * interceptor;
g_mutex_lock (&_gum_interceptor_lock);
if (_the_interceptor != NULL) { interceptor = GUM_INTERCEPTOR (g_object_ref (_the_interceptor)); } else { _the_interceptor = g_object_new (GUM_TYPE_INTERCEPTOR, NULL); g_object_weak_ref (G_OBJECT (_the_interceptor), the_interceptor_weak_notify, NULL);
interceptor = _the_interceptor; }
g_mutex_unlock (&_gum_interceptor_lock);
return interceptor; }
static void gum_interceptor_init (GumInterceptor * self) { g_rec_mutex_init (&self->mutex);
self->function_by_address = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) gum_function_context_destroy);
gum_code_allocator_init (&self->allocator, GUM_INTERCEPTOR_CODE_SLICE_SIZE);
self->backend = _gum_interceptor_backend_create (&self->allocator);
gum_interceptor_transaction_init (&self->current_transaction, self); }
|
因为GObject的使用,gum_interceptor_init
这个构造函数,在 interceptor
对象创建出来的时候触发。
重点看拦截器后端的初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| GumInterceptorBackend * _gum_interceptor_backend_create (GumCodeAllocator * allocator) { GumInterceptorBackend * backend;
backend = g_slice_new (GumInterceptorBackend); backend->allocator = allocator;
gum_x86_writer_init (&backend->writer, NULL); gum_x86_relocator_init (&backend->relocator, NULL, &backend->writer);
gum_interceptor_backend_create_thunks (backend);
return backend; }
|
这里初始化的writer
和relocator
分别用于指令写和指令恢复。
thunks
的初始化,这两个是用于调度执行,分别对应 进入hook和离开hook。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| static void gum_interceptor_backend_create_thunks (GumInterceptorBackend * self) { GumX86Writer * cw = &self->writer;
self->enter_thunk = gum_code_allocator_alloc_slice (self->allocator); gum_x86_writer_reset (cw, self->enter_thunk->data); gum_emit_enter_thunk (cw); gum_x86_writer_flush (cw); g_assert_cmpuint (gum_x86_writer_offset (cw), <=, self->enter_thunk->size);
self->leave_thunk = gum_code_allocator_alloc_slice (self->allocator); gum_x86_writer_reset (cw, self->leave_thunk->data); gum_emit_leave_thunk (cw); gum_x86_writer_flush (cw); g_assert_cmpuint (gum_x86_writer_offset (cw), <=, self->leave_thunk->size); }
|
因为原理类似,只举例enter_thunk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| static void gum_emit_enter_thunk (GumX86Writer * cw) { const gssize return_address_stack_displacement = 0; gum_emit_prolog (cw, return_address_stack_displacement);
gum_x86_writer_put_lea_reg_reg_offset (cw, GUM_REG_XSI, GUM_REG_XBP, GUM_FRAME_OFFSET_CPU_CONTEXT); gum_x86_writer_put_lea_reg_reg_offset (cw, GUM_REG_XDX, GUM_REG_XBP, GUM_FRAME_OFFSET_TOP); gum_x86_writer_put_lea_reg_reg_offset (cw, GUM_REG_XCX, GUM_REG_XBP, GUM_FRAME_OFFSET_NEXT_HOP);
gum_x86_writer_put_call_address_with_aligned_arguments (cw, GUM_CALL_CAPI, GUM_ADDRESS (_gum_function_context_begin_invocation), 4, GUM_ARG_REGISTER, GUM_REG_XBX, GUM_ARG_REGISTER, GUM_REG_XSI, GUM_ARG_REGISTER, GUM_REG_XDX, GUM_ARG_REGISTER, GUM_REG_XCX);
gum_emit_epilog (cw); }
|
gum_interceptor_attach_listener
1 2 3 4 5 6 7 8 9 10 11
| GumAttachReturn gum_interceptor_attach_listener (GumInterceptor * self, gpointer function_address, GumInvocationListener * listener, gpointer listener_function_data) {
...
}
|
gum_interceptor_transaction_begin
gum_interceptor_instrument ✨
这里要说的是 function_address 就是要hook的目标函数,frida-gum
把要hook的目标封装成了 GumFunctionContext
对象,方便操作
1 2 3
| function_address = gum_interceptor_resolve (self, function_address); function_ctx = gum_interceptor_instrument (self, function_address);
|
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
| static GumFunctionContext * gum_interceptor_instrument (GumInterceptor * self, gpointer function_address) { GumFunctionContext * ctx; ctx = (GumFunctionContext *) g_hash_table_lookup (self->function_by_address, function_address); if (ctx != NULL) return ctx; ctx = gum_function_context_new (self, function_address); if (ctx == NULL) return NULL; if (!_gum_interceptor_backend_create_trampoline (self->backend, ctx)) { gum_function_context_finalize (ctx); return NULL; }
g_hash_table_insert (self->function_by_address, function_address, ctx);
gum_interceptor_transaction_schedule_prologue_write ( &self->current_transaction, ctx, gum_interceptor_activate);
return ctx; }
|
这里贴一下跳板代码方便理解:
1 2 3 4 5 6 7 8 9 10
| 00C30200 mov al,byte ptr ds:[FF00C121h] 00C30205 xor eax,0C30200h 00C3020A jmp 00C30000 // 跳到上面的 enter_thunk 00C3020F push dword ptr ds:[0C30200h] 00C30215 jmp 00C30100 // 跳到 leave_thunk // 原函数修复的指令,7个字节 00C3021A push ebp 00C3021B mov ebp,esp 00C3021D cmp dword ptr [ebp+8],0 00C30221 jmp gum_test_target_function+7h (0D6FB97h) // 跳回原函数,因为写跳转用了7字节,所以+7
|
gum_interceptor_transaction_end
1 2 3
| gum_interceptor_transaction_schedule_prologue_write ( &self->current_transaction, ctx, gum_interceptor_activate);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| static void gum_interceptor_activate (GumInterceptor * self, GumFunctionContext * ctx, gpointer prologue) { if (ctx->destroyed) return;
g_assert (!ctx->activated); ctx->activated = TRUE;
_gum_interceptor_backend_activate_trampoline (self->backend, ctx, prologue); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| _gum_interceptor_backend_activate_trampoline (GumInterceptorBackend * self, GumFunctionContext * ctx, gpointer prologue) { GumX86Writer * cw = &self->writer; guint padding;
gum_x86_writer_reset (cw, prologue); cw->pc = GPOINTER_TO_SIZE (ctx->function_address); gum_x86_writer_put_jmp_address (cw, GUM_ADDRESS (ctx->on_enter_trampoline)); gum_x86_writer_flush (cw); g_assert_cmpint (gum_x86_writer_offset (cw), <=, GUM_INTERCEPTOR_REDIRECT_CODE_SIZE);
padding = ctx->overwritten_prologue_len - gum_x86_writer_offset (cw); for (; padding != 0; padding--) gum_x86_writer_put_nop (cw); gum_x86_writer_flush (cw); }
|
2.2.3 执行流程
通过设置函数返回地址(__gum_function_context_begin/end_invocation
),控制流程,这就是ROP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 原函数 ---------------------------------------------------- 跳板 02C80204 ---------------------------------------------------- `enter_chunk` // 首先要保存现场, 构造栈帧,随后进入下一个函数 ⬇️ `__gum_function_context_begin_invocation` // 通过设置栈(ret addr)控制执行流程 ---------------------------------------------------- replacement_function ---------------------------------------------------- 跳板 02C8020F ---------------------------------------------------- `leave_chunk` `__gum_function_context_end_invocation` ---------------------------------------------------- 继续执行
|
0x03 : 调试分析帮助理解
这里调试了单元测试中写hook和函数替换的逻辑,过程如下:
_gum_interceptor_backend_create()
后端初始化,初始化两个thunk
enter_thunk
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
| 00C30000 pushfd 00C30001 cld 00C30002 pushad 00C30003 lea esp,[esp-4] 00C3000A lea eax,[esp+2Ch] 00C30011 mov dword ptr [esp+10h],eax 00C30015 mov ebx,dword ptr [esp+28h] 00C30019 mov ebp,esp 00C3001B and esp,0FFFFFFF0h 00C30021 sub esp,200h 00C30027 fxsave [esp] 00C3002B lea esi,[ebp] 00C30031 lea edx,[ebp+2Ch] 00C30037 lea ecx,[ebp+28h] 00C3003D push ecx 00C3003E push edx 00C3003F push esi 00C30040 push ebx 00C30041 call __gum_function_context_begin_invocation (0CE8E1Fh) 00C30046 add esp,10h 00C30049 fxrstor [esp] 00C3004D mov esp,ebp 00C3004F lea esp,[esp+4] 00C30056 popad 00C30057 popfd 00C30058 ret
|
leave_thunk
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
| 00C30100 pushfd 00C30101 cld 00C30102 pushad 00C30103 lea esp,[esp-4] 00C3010A lea eax,[esp+28h] 00C30111 mov dword ptr [esp+10h],eax 00C30115 mov ebx,dword ptr [esp+28h] 00C30119 mov ebp,esp 00C3011B and esp,0FFFFFFF0h 00C30121 sub esp,200h 00C30127 fxsave [esp] 00C3012B lea esi,[ebp] 00C30131 lea edx,[ebp+28h] 00C30137 sub esp,4 00C3013A push edx 00C3013B push esi 00C3013C push ebx 00C3013D call __gum_function_context_end_invocation (0CEAB1Bh) 00C30142 add esp,0Ch 00C30145 add esp,4 00C30148 fxrstor [esp] 00C3014C mov esp,ebp 00C3014E lea esp,[esp+4] 00C30155 popad 00C30156 popfd 00C30157 ret
|
1 2 3 4 5 6 7 8
| GumAttachReturn gum_interceptor_attach (GumInterceptor * self, gpointer function_address, GumInvocationListener * listener, gpointer listener_function_data)
|
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
| GumAttachReturn gum_interceptor_attach (GumInterceptor * self, gpointer function_address, GumInvocationListener * listener, gpointer listener_function_data) { GumAttachReturn result = GUM_ATTACH_OK; GumFunctionContext * function_ctx;
if (gum_process_get_code_signing_policy () == GUM_CODE_SIGNING_REQUIRED) goto policy_violation;
gum_interceptor_ignore_current_thread (self); GUM_INTERCEPTOR_LOCK (self); gum_interceptor_transaction_begin (&self->current_transaction); self->current_transaction.is_dirty = TRUE; function_address = gum_interceptor_resolve (self, function_address); function_ctx = gum_interceptor_instrument (self, function_address); if (function_ctx == NULL) goto wrong_signature;
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;
policy_violation: { return GUM_ATTACH_POLICY_VIOLATION; } wrong_signature: { result = GUM_ATTACH_WRONG_SIGNATURE; goto beach; } already_attached: { result = GUM_ATTACH_ALREADY_ATTACHED; goto beach; } beach: { gum_interceptor_transaction_end (&self->current_transaction); GUM_INTERCEPTOR_UNLOCK (self); gum_interceptor_unignore_current_thread (self);
return result; } }
|
on_invoke_trampoline 跳板
1 2 3 4 5 6 7 8 9 10
| 00C30200 mov al,byte ptr ds:[FF00C121h] 00C30205 xor eax,0C30200h 00C3020A jmp 00C30000 // 跳到上面的 enter_thunk 00C3020F push dword ptr ds:[0C30200h] 00C30215 jmp 00C30100 // 跳到 leave_thunk // 原函数修复的指令,7个字节 00C3021A push ebp 00C3021B mov ebp,esp 00C3021D cmp dword ptr [ebp+8],0 00C30221 jmp gum_test_target_function+7h (0D6FB97h) // 跳回原函数,因为写跳转用了7字节,所以+7
|
gum_interceptor_transaction_end (&self->current_transaction);
调用 gum_interceptor_activate()
然后_gum_interceptor_backend_activate_trampolie()
随后,目标函数开头被修改:
1 2 3 4 5 6 7 8
| gpointer GUM_NOINLINE gum_test_target_function (GString * str) { 00D6FB90 jmp 00C30204 if (str != NULL) 00D6FB95 nop 00D6FB96 nop 00D6FB97 je gum_test_target_function+19h (0D6FBA9h)
|
直接跳转到 00C30204
, 其实就是 跳板,因为反汇编的地址差了点,所以开始的指令不太一样:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 00C30204 push dword ptr ds:[0C30200h] 00C3020A jmp 00C30000 00C3020F push dword ptr ds:[0C30200h] 00C30215 jmp 00C30100 00C3021A push ebp 00C3021B mov ebp,esp 00C3021D cmp dword ptr [ebp+8],0 00C30221 jmp gum_test_target_function+7h (0D6FB97h) 00C30226 add byte ptr [eax],al 00C30228 add byte ptr [eax],al 00C3022A add byte ptr [eax],al 00C3022C add byte ptr [eax],al 00C3022E add byte ptr [eax],al
|
调用流程调试分析,这里分两个情况,是否存在``replacement_function`
首先是不存在,只是打个hook(根据 TESTCASE(attach_one);
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| call 原函数 ---------------------------------------------------- 原函数 ---------------------------------------------------- 跳板 ---------------------------------------------------- `enter_chunk` // 首先要保存现场, 构造栈帧,随后进入下一个函数 ⬇️ `__gum_function_context_begin_invocation` // 通过设置栈(ret addr)控制执行流程 ---------------------------------------------------- 跳板+n (00C3021A) // 执行原函数的 修复的若干字节 ---------------------------------------------------- 原函数 ---------------------------------------------------- `leave_chunk` `__gum_function_context_end_invocation` ---------------------------------------------------- 继续执行....
|
存在替换的函数(TESTCASE(replace_one);
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 原函数 ---------------------------------------------------- 跳板 02C80204 ---------------------------------------------------- `enter_chunk` // 首先要保存现场, 构造栈帧,随后进入下一个函数 ⬇️ `__gum_function_context_begin_invocation` // 通过设置栈(ret addr)控制执行流程 ---------------------------------------------------- replacement_function ---------------------------------------------------- 跳板 02C8020F ---------------------------------------------------- `leave_chunk` `__gum_function_context_end_invocation` ---------------------------------------------------- 继续执行
|
replace_one 的跳板
1 2 3 4 5 6 7 8 9
| 02C80204 push dword ptr ds:[2C80200h] 02C8020A jmp 02C80000 02C8020F push dword ptr ds:[2C80200h] 02C80215 jmp 02C80100 02C8021A mov edi,edi 02C8021C push ebp 02C8021D mov ebp,esp 02C8021F jmp malloc+5h (01E5A7B5h)
|
0x04 : 结语
这个过程大概花了我一周 5天多的样子,挺难的个人感觉,需要捋清楚的话,配合调试会好很多,最开始我直接看的代码,看+做笔记,脑内debug,最后编译了工程,vs调试,清晰多了,还是建议边调试边看。
如果文中有任何问题,欢迎批评指正 : )
后面可能会在他基础上做点事情吧…这框架真牛逼 !
0x05 : 参考与引用
rida-gum源码解读
gobject c语言
如何构建一款像 frida 一样的框架