本文作者:杉木@涂鸦智能安全实验室
Frida源码阅读-frida-gum-插桩原理
最近被问到Frida的一些原理问题,发现确实还没深入看过Frida的源码,之前只是拿来用,一直说找个机会阅读一下源码。先简单了解一下Frida,Frida 是一款强大的动态代码插桩工具,常被用于逆向工程、动态分析以及自动化测试等领域。它支持多平台(如 Windows、macOS、Linux、iOS 和 Android)和多语言(如 Python、JavaScript),使其成为安全研究人员和开发者的理想工具之一。
Frida插桩原理
1. 架构概览
Frida 的架构主要包括三个部分:注入器(Injector) 、代理库(Frida Agent) 和 客户端库(Client Library) 。
- 注入器 :用来将 Frida 的代码注入目标进程中。
- 代理库 :一旦被注入目标进程,它就开始执行并提供API来操作目标进程(如hook函数、修改内存等)。
- 客户端库 :通常在你的电脑上运行,提供与代理库交互的接口。
2. 注入与交互
当 Frida 启动时,它首先通过其注入器将代理库(通常是一个共享库或动态链接库)注入到目标进程中。注入可以通过多种技术完成,如在Linux中利用ptrace,或者在Windows中使用DLL注入技术。
注入后,代理库会在目标进程中运行一个小型的HTTP服务器或使用socket通信等方式,以便接收客户端库发送的命令并返回结果。
关于ptrace,是 Linux 提供的一种系统调用,允许一个进程观察和控制另一个进程的执行,读写其内存,甚至改变其寄存器等状态。这是 Frida 和许多其他调试和逆向工程工具广泛使用的一种技术。
- Frida 通过调用
ptrace(PTRACE_ATTACH, pid, NULL, NULL)
请求附加到目标进程。这将使目标进程在操作系统级别暂停执行,等待进一步的命令。 - 完成初步附加后,Frida 可能会调用
waitpid()
等待进程状态改变,确认进程已经被正确暂停。 - 引申—调试与反调试的攻击与防御手段。
3. 脚本引擎
Frida 内部集成了一个JavaScript引擎,常用的是V8引擎。这意味着你可以使用JavaScript来编写脚本,通过这些脚本来控制目标进程的行为,比如插桩(hooking)、执行函数或者修改变量等。
4. 操作和反馈
用户通过客户端库(比如 Python 客户端)编写并执行脚本,这些脚本通过网络传递给代理库执行。执行结果也同样经过网络返回到客户端库,用户可以在这里接收和处理这些数据。
例如,如果你想查看一个函数何时被调用以及被传递了什么参数,你可以使用 Frida 的 JavaScript API 编写一个脚本来“hook”这个函数,并在函数被调用时打印出参数。
参考文章Frida 常用js API
5. 探测与修改
利用Frida不仅可以探测应用程序的行为(例如监控特定函数的调用),还可以在运行时修改应用程序的行为,如改变函数的执行逻辑,或者直接修改内存中的数据。
Frida 通过“Hooking”技术,即插入自定义的代码到目标应用程序的特定部分(如函数、方法或处理流程),来监控和修改应用程序的运行。以下是一个用Frida及JavaScript编写的简单示例,展示如何监控Android应用中特定函数的调用:
Java.perform(function () {var TargetClass = Java.use("com.example.target.ClassName");TargetClass.targetMethod.implementation = function (param1, param2) {console.log("targetMethod called with params: " + param1 + " and " + param2);var result = this.targetMethod(param1, param2);console.log("targetMethod returned: " + result);return result;};
});
上面的代码使用 Java.perform
函数创建了一个沙箱,在其中可以安全地调用目标应用的 Java 代码。它修改了 TargetClass
的 targetMethod
方法的实现,以打印调用参数和返回值。
修改内存中的字节数据
Frida 允许你直接读写进程的内存。这可以用于修改应用的行为,修复程序中的缺陷,或者改变游戏的状态。以下是一个使用 Frida 的示例,展示如何修改内存中的数据:
var baseAddress = ptr("0x12345678"); // 假设这是你想修改的内存地址
var size = 8; // 修改数据的大小,以字节为单位
var newData = [0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90]; // 需要写入的新数据// 读取当前内存内容
var originalData = Memory.readByteArray(baseAddress, size);
console.log("Original data: " + originalData);// 写入新数据
Memory.writeByteArray(baseAddress, newData);// 再次读取以验证更改
var modifiedData = Memory.readByteArray(baseAddress, size);
console.log("Modified data: " + modifiedData);
上例中使用了Memory.readByteArray
和Memory.writeByteArray
函数从指定内存地址读取和写入字节数组。这种方式在进行二进制补丁、修改游戏数据或执行内存相关的漏洞利用时非常有用。
文章分析的frida当前版本为16.5.9,Release Frida 16.5.9 · frida/frida
Frida源码阅读
frida-gum
是 Frida 的插桩引擎,实现了实际的代码插桩逻辑和机器码生成。这是整个frida的项目的目录结构,在github上是分成多个项目进行发布管理;
├─ frida-core: Frida 核心库
├─ frida-gum: inline-hook框架,提供代码追踪、内存监控、符合查找等能力├─ frida-python: python
├─ frida-node: Node.js
├─ frida-qml: Qml
├─ frida-swift: Swift
├─ frida-go: go
├─ frida-tools: CLI tools
└─ frida-clr
看完整个项目的结构后,单独把gum和core项目提取出来,gum是整个frida的基础框架,croe是核心库;
frida-gum提供inline-hook框架,提供inline hook 的封装 Interceptor、代码跟踪 Stalker、内存监控 MemoryAccessMonitor、以及符号查找、栈回溯实现、内存扫描、动态代码生成和重定位等;
frida-gum
frida-gum的项目目录结构如下;我这里代码分析跟踪基本上跟着大佬的分析进行[原创]Frida-gum 源代码速通笔记-软件逆向-看雪-安全社区|安全招聘|kanxue.com,大佬的分析写的非常详细,不再次赘述,只贴一下代码阅读时的备注;
├─frida-gum
│ ├─bindings //主要包含与 V8 JavaScript 引擎的绑定
│ │ ├─gumjs
│ │ └─gumpp
│ ├─docs
│ ├─ext //主要是win平台的调试相关
│ │ ├─dbghelp
│ │ └─symsrv
│ ├─gum //Frida Gum 的源码核心,包含所有关键逻辑实现。
//后端实现,包括对不同操作系统和硬件架构的支持。
│ │ ├─arch-arm
│ │ ├─arch-arm64
│ │ ├─arch-mips
│ │ ├─arch-x86
//arch-* 定义的是与 CPU 架构有关的代码,也就是汇编级操作, 比如汇编指令的读/写/修复.
│ │ ├─backend-arm
│ │ ├─backend-arm64
│ │ ├─backend-darwin
│ │ ├─backend-dbghelp
│ │ ├─backend-elf
│ │ ├─backend-freebsd
│ │ ├─backend-libdwarf
│ │ ├─backend-libunwind
│ │ ├─backend-linux
│ │ ├─backend-mips
│ │ ├─backend-posix
│ │ ├─backend-qnx
│ │ ├─backend-windows
│ │ ├─backend-x86
//backend-* 分两种情况: 1. 定义的是与操作系统有关的代码, 更多是一些内存/进程等操作 2. 对 arch 层级代码的封装成统一逻辑
│ │ └─devkit //用于支持Android的Dalvik虚拟机相关功能。
│ ├─libs //包含底层核心库的源代码
│ │ └─gum
│ ├─releng //编译相关
│ ├─subprojects
│ ├─tests //包含单元测试和集成测试的代码
│ │ ├─core
│ │ │ ├─arch-arm
│ │ │ ├─arch-arm64
│ │ │ ├─arch-x86
│ │ │ ├─swiftapiresolver
│ │ │ └─targetfunctions
│ │ ├─data
│ │ ├─gumjs
│ │ ├─gumpp
│ │ ├─heap
│ │ ├─prof
│ │ └─stubs
│ ├─tools
│ └─vapi
#ifdef HAVE_DARWIN
// 从测试用例开始
TESTCASE (interceptor_and_js_should_not_deadlock)
{GThread * worker_thread;int state = 0;if (!g_test_slow ()){g_print ("<skipping, run in slow mode> ");return;}
//调用interceptor_attacher_worker函数开始去hook对应函数,继续跟进看函数的实现worker_thread = g_thread_new ("script-test-worker-thread",interceptor_attacher_worker, &state);while (state == 0)g_usleep (G_USEC_PER_SEC / 200);COMPILE_AND_LOAD_SCRIPT ("const iterations = 100;""send('Start loop');""const threadSuspend = new NativeFunction("" Module.getExportByName(null, 'thread_suspend'),"" 'int', ['int'], { scheduling: 'exclusive' }"");""Interceptor.replace(threadSuspend, new NativeCallback((threadId) => {"" return threadSuspend(threadId);""}, 'int', ['int']));""Interceptor.flush();""setTimeout(() => {"" for (let i = 0; i !== iterations; i++)"" Thread.sleep(0.1);"" Interceptor.revert(threadSuspend);"" send('The end');""}, 0);");EXPECT_SEND_MESSAGE_WITH ("\"Start loop\"");g_usleep (G_USEC_PER_SEC / 25);g_thread_join (worker_thread);g_assert_cmpint (state, ==, 2);EXPECT_SEND_MESSAGE_WITH ("\"The end\"");EXPECT_NO_MESSAGES ();
}static gpointer
interceptor_attacher_worker (gpointer data)
{int * state = data;guint i;GumInterceptor * interceptor;GumInvocationListener * listener;GumAttachReturn result;*state = 1;interceptor = gum_interceptor_obtain ();listener = gum_make_call_listener (empty_invocation_callback,empty_invocation_callback, NULL, NULL);
//继续跟进gum_interceptor_attachfor (i = 0; i != 300; i++){result = gum_interceptor_attach (interceptor, target_function_int,GUM_INVOCATION_LISTENER (listener), NULL);if (result == GUM_ATTACH_OK){g_usleep (G_USEC_PER_SEC / 25);gum_interceptor_detach (interceptor, GUM_INVOCATION_LISTENER (listener));}}g_object_unref (listener);*state = 2;return NULL;
}static void
empty_invocation_callback (GumInvocationContext * context,gpointer user_data)
{
}#endif
// gum_interceptor_attach函数实现
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;function_address = gum_interceptor_resolve (self, function_address);//获取hook函数地址// 继续跟进gum_interceptor_instrumentfunction_ctx = gum_interceptor_instrument (self, GUM_INTERCEPTOR_TYPE_DEFAULT,function_address, &error);//尝试为指定函数地址进行插桩if (function_ctx == NULL)goto instrumentation_error;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:{gum_interceptor_transaction_end (&self->current_transaction);//结束事务,通过提交或回滚来保持系统一致性。GUM_INTERCEPTOR_UNLOCK (self);//解锁拦截器,允许其他线程进行操作。gum_interceptor_unignore_current_thread (self);//恢复对当前线程的正常拦截行为。return result;}
}
static GumFunctionContext *
gum_interceptor_instrument (GumInterceptor * self,GumInterceptorType type,gpointer function_address,GumInstrumentationError * error)
{GumFunctionContext * ctx;*error = GUM_INSTRUMENTATION_ERROR_NONE;//设定 *error 为 GUM_INSTRUMENTATION_ERROR_NONE,假设开始时没有错误。ctx = (GumFunctionContext *) g_hash_table_lookup (self->function_by_address,function_address);// 使用 g_hash_table_lookup 检查该函数地址是否已经有一个存在的上下文。if (ctx != NULL)// 如果找到 (ctx != NULL),检查现有上下文的类型是否匹配请求的类型 (ctx->type != type)。{if (ctx->type != type){// 如果类型不匹配,设置 *error 为 GUM_INSTRUMENTATION_ERROR_WRONG_TYPE 并返回 NULL。*error = GUM_INSTRUMENTATION_ERROR_WRONG_TYPE;return NULL;}return ctx;}
// 如果 self->backend 为空,就调用 _gum_interceptor_backend_create 来初始化拦截器的后台组件。if (self->backend == NULL){self->backend =_gum_interceptor_backend_create (&self->mutex, &self->allocator);}
// 用 gum_function_context_new 创建一个新的 GumFunctionContext。这个上下文将用于管理函数的拦截信息。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{if (!_gum_interceptor_backend_create_trampoline (self->backend, ctx))goto wrong_signature;}
//将新的函数上下文插入 self->function_by_address 哈希表g_hash_table_insert (self->function_by_address, function_address, ctx);
//更新任务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;}
}
// 创建并初始化一个新的 GumInterceptorBackend 实例
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_thunksgum_interceptor_backend_create_thunks (backend);}return backend;
}
// gum_interceptor_backend_create_thunks函数实现
static void
gum_interceptor_backend_create_thunks (GumInterceptorBackend * self)
{gsize page_size, code_size;GumMemoryRange range;page_size = gum_query_page_size ();code_size = page_size;
// 分配一块内存,用于存储 thunks,初始时设置为可读写(RW)self->thunks = gum_memory_allocate (NULL, code_size, page_size, GUM_PAGE_RW);range.base_address = GUM_ADDRESS (self->thunks);range.size = code_size;gum_cloak_add_range (&range);
// 在分配的内存中生成 thunks 代码
// gum_memory_patch_code 函数将调用 gum_emit_thunks 函数生成所需的代码
// 继续跟进gum_emit_thunksgum_memory_patch_code (self->thunks, 1024,(GumMemoryPatchApplyFunc) gum_emit_thunks, self);
}
// gum_emit_thunks函数实现
static void
gum_emit_thunks (gpointer mem,GumInterceptorBackend * self)
{GumArm64Writer * aw = &self->writer;// 设置 'enter_thunk' 的起始地址为 'self->thunks' 指向的内存起点self->enter_thunk = self->thunks;// 重置 ARM64 Writer 并指定新的内存位置进行代码生成gum_arm64_writer_reset (aw, mem);aw->pc = GUM_ADDRESS (self->enter_thunk);// 生成 'enter_thunk' 的代码// 继续跟进gum_emit_enter_thunk函数实现gum_emit_enter_thunk (aw);gum_arm64_writer_flush (aw);// 确定 'leave_thunk' 的起始地址self->leave_thunk =(guint8 *) self->enter_thunk + gum_arm64_writer_offset (aw);// 生成 'leave_thunk' 的代码gum_emit_leave_thunk (aw);gum_arm64_writer_flush (aw);
}
// gum_emit_enter_thunk函数实现
static void
gum_emit_enter_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_CPU_CONTEXT + G_STRUCT_OFFSET (GumCpuContext, lr));gum_arm64_writer_put_add_reg_reg_imm (aw, ARM64_REG_X3, ARM64_REG_SP,GUM_FRAME_OFFSET_NEXT_HOP);
// 最后跟进_gum_function_context_begin_invocation函数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);
}
gboolean
_gum_function_context_begin_invocation (GumFunctionContext * function_ctx,GumCpuContext * cpu_context,gpointer * caller_ret_addr,gpointer * next_hop)
最后的函数参考大佬的分析即可[原创]Frida-gum 源代码速通笔记-软件逆向-看雪-安全社区|安全招聘|kanxue.com
漏洞悬赏计划:涂鸦智能安全响应中心(https://src.tuya.com)欢迎白帽子来探索。