frida源码初探
# PREFACE:被拷打到 frida 和 xpose 的原理啥的,啥啥不会,爬来赶紧学学
# 总览
首先是一整个 frida 项目可以分为这些 subprojects:
- frida-clr:Frida .NET bindings
- frida-core:Frida core library intended for static linking into bindings
- frida-go:Frida Go bindings
- frida-gum:Cross-platform instrumentation and introspection library written in C
- frida-node:Frida Node.js bindings
- frida-python:Frida Python bindings
- frida-qml:Frida Qml plugin
- frida-swift:Frida Swift bindings
- frida-tools:Frida CLI tools
感觉网上许多文章都着重在看 frida-core 和 frida-gum,那么就先看看这两部分
# frida-gum
分析过程中感觉直接干看还是有点困难,这里先整理一些 frida 所实现的功能,对照着功能看进去
# 一、前置原理
# 1.1 inline hook
我们可以使用 Frida 的 API 在程序运行时拦截函数的调用,并且在拦截到函数调用时,通过修改函数的参数或者返回值来改变程序的行为。
这里的核心原理是:动态替换需要 Hook 的指令片段为一段经过设计的跳板指令,即 trampoline ,目标为我们设计好的一段 shellCode,这里可以看 [原创] Frida inlineHook 原理分析及简单设计一款 AArch64 inlineHook 工具 - Android 安全 - 看雪 - 安全社区 | 安全招聘 | kanxue.com 这篇文章对于 Frida 生成的 shellcode 进行了详细的研究,暂时没有进行复现,先纸面学习,大概需要注意的点是:
- frida 寻找前后 128MB 的地址获取一个空闲地址,将 hook 的点的前 16 个字节(或 4 字节)替换掉为 LDR 和 BR 指令,并且利用 x16 寄存器作为 trampoline 跳板
- 第一段跳板跳到 shellcode 后 mmap 一段匿名内存,保存当前寄存器状态与用于写入第二段跳板指令
- 然后是 onenter 和 onleave 的编写,注入 shellcode 实现需求的逻辑即可(这里是简易的核心原理,后面代码会复杂很多)
# 1.2 inline hook 检测
恰好学习的时候搜到这篇文章,一块记录了
既然 frida 会生成 trampoline 和 shellcode,那么有个很简单的方法:开一个线程,检查函数开头有没有被替换过,并且有没有被替换成类似 BR 到 shellcode 的格式
# 二、源码
readme 描述:
Cross-platform instrumentation and introspection library written in C.
This library is consumed by frida-core through its JavaScript bindings, GumJS.
然后文档有大概给出不同部分的功能(词汇量稍微有点不够咳咳)不过直接看着稍微有点没头绪,先看看别人的分析,这篇写的不错)
偷看一下别人的阅读思路:
首先有个 meson.build 构建文件,其中可以先看 gumtest.c 这一测试运行器(根据 testcase 递归看进代码)
runner_sources = [ | |
'gumtest.c', | |
'testutil.c', | |
'stubs' / 'dummyclasses.c', | |
'stubs' / 'fakebacktracer.c', | |
'stubs' / 'fakeeventsink.c', | |
'stalkerdummychannel.c', | |
'lowlevelhelpers.c', | |
] |
# gumtest.c
前面一大段代码根据宏来判断,是用于根据平台信息做初始化的
标记一个核心对象先:
struct _GumInterceptor | |
{ | |
#ifndef GUM_DIET | |
GObject parent; | |
#else | |
GumObject parent; | |
#endif | |
GRecMutex mutex; | |
GHashTable * function_by_address; | |
GumInterceptorBackend * backend; | |
GumCodeAllocator allocator; | |
volatile guint selected_thread_id; | |
GumInterceptorTransaction current_transaction; | |
}; |
拦截器初始化
GumInterceptor * | |
gum_interceptor_obtain (void) | |
{ | |
GumInterceptor * interceptor; | |
g_mutex_lock (&_gum_interceptor_lock); | |
#ifndef GUM_DIET | |
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; | |
} | |
#else | |
if (_the_interceptor != NULL) | |
{ | |
interceptor = gum_object_ref (_the_interceptor); | |
} | |
else | |
{ | |
_the_interceptor = g_new0 (GumInterceptor, 1); | |
_the_interceptor->parent.ref_count = 1; | |
_the_interceptor->parent.finalize = gum_interceptor_finalize; | |
gum_interceptor_init (_the_interceptor); | |
interceptor = _the_interceptor; | |
} | |
#endif | |
g_mutex_unlock (&_gum_interceptor_lock); | |
return interceptor; | |
} |
attach 上函数的方法:
传入地址,调用 gum_interceptor_resolve
,这里的 gum_interceptor_instrument
后面也会看
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); | |
function_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 gpointer | |
gum_interceptor_resolve (GumInterceptor * self, | |
gpointer address) | |
{ | |
address = gum_strip_code_pointer (address); | |
if (!gum_interceptor_has (self, address)) | |
{ | |
const gsize max_redirect_size = 16; | |
gpointer target; | |
gum_ensure_code_readable (address, max_redirect_size); | |
/* Avoid following grafted branches. */ | |
if (gum_process_get_code_signing_policy () == GUM_CODE_SIGNING_REQUIRED) | |
return address; | |
target = _gum_interceptor_backend_resolve_redirect (self->backend, | |
address); | |
if (target != NULL) | |
return gum_interceptor_resolve (self, target); | |
} | |
return address; | |
} |
可以进到:这部分是比较核心的生成跳转 shellcode 的部分,或许也可以从这部分下手来考虑对抗?(TODO+1)
gpointer | |
gum_arm64_reader_try_get_relative_jump_target (gconstpointer address) | |
{ | |
gpointer result = NULL; | |
csh capstone; | |
cs_insn * insn; | |
const uint8_t * code; | |
size_t size; | |
uint64_t pc; | |
const cs_arm64_op * ops; | |
cs_arch_register_arm64 (); | |
cs_open (CS_ARCH_ARM64, GUM_DEFAULT_CS_ENDIAN, &capstone); | |
cs_option (capstone, CS_OPT_DETAIL, CS_OPT_ON); | |
insn = cs_malloc (capstone); | |
code = address; | |
size = 16; | |
pc = GPOINTER_TO_SIZE (address); | |
#define GUM_DISASM_NEXT() \ | |
if (!cs_disasm_iter (capstone, &code, &size, &pc, insn)) \ | |
goto beach; \ | |
ops = insn->detail->arm64.operands | |
#define GUM_CHECK_ID(i) \ | |
if (insn->id != G_PASTE (ARM64_INS_, i)) \ | |
goto beach | |
#define GUM_CHECK_OP_TYPE(n, t) \ | |
if (ops[n].type != G_PASTE (ARM64_OP_, t)) \ | |
goto beach | |
#define GUM_CHECK_OP_REG(n, r) \ | |
if (ops[n].reg != G_PASTE (ARM64_REG_, r)) \ | |
goto beach | |
#define GUM_CHECK_OP_MEM(n, b, i, d) \ | |
if (ops[n].mem.base != G_PASTE (ARM64_REG_, b)) \ | |
goto beach; \ | |
if (ops[n].mem.index != G_PASTE (ARM64_REG_, i)) \ | |
goto beach; \ | |
if (ops[n].mem.disp != d) \ | |
goto beach | |
GUM_DISASM_NEXT (); | |
switch (insn->id) | |
{ | |
case ARM64_INS_B: | |
result = GSIZE_TO_POINTER (ops[0].imm); | |
break; | |
#ifdef HAVE_DARWIN | |
case ARM64_INS_ADRP: | |
{ | |
GumAddress target; | |
GUM_CHECK_OP_REG (0, X17); | |
target = ops[1].imm; | |
GUM_DISASM_NEXT (); | |
GUM_CHECK_ID (ADD); | |
GUM_CHECK_OP_REG (0, X17); | |
GUM_CHECK_OP_REG (1, X17); | |
GUM_CHECK_OP_TYPE (2, IMM); | |
target += ops[2].imm; | |
GUM_DISASM_NEXT (); | |
GUM_CHECK_ID (LDR); | |
GUM_CHECK_OP_REG (0, X16); | |
GUM_CHECK_OP_TYPE (1, MEM); | |
GUM_CHECK_OP_MEM (1, X17, INVALID, 0); | |
GUM_DISASM_NEXT (); | |
GUM_CHECK_ID (BRAA); | |
GUM_CHECK_OP_REG (0, X16); | |
GUM_CHECK_OP_REG (1, X17); | |
result = *((gpointer *) GSIZE_TO_POINTER (target)); | |
break; | |
} | |
#endif | |
default: | |
break; | |
} | |
beach: | |
cs_free (insn, 1); | |
cs_close (&capstone); | |
return result; | |
} |
创建拦截器后端以及分配器初始化
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); | |
gum_interceptor_transaction_init (&self->current_transaction, self); | |
} |
看到刚才 marked 的 gum_interceptor_instrument
,结合前面的代码,frida 似乎会将函数建哈希表用于寻址,并且进行了拦截器后端的初始化 _gum_interceptor_backend_create
以及生成跳板代码
static GumFunctionContext * | |
gum_interceptor_instrument (GumInterceptor * self, | |
GumInterceptorType type, | |
gpointer function_address, | |
GumInstrumentationError * error) | |
{ | |
GumFunctionContext * ctx; | |
*error = GUM_INSTRUMENTATION_ERROR_NONE; | |
// 要 hook 的函数,封装成了 GumFunctionContext,此时 | |
// 根据 地址,得到与之对应的 GunFunctionContext 对象 | |
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) | |
{ | |
self->backend = | |
_gum_interceptor_backend_create (&self->mutex, &self->allocator); | |
} | |
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; | |
} | |
// 设置完成后, 添加到哈希表 | |
// hash_table, key, value | |
//hook 函数地址,GumFunctionContext 对象对应, 方便查找 | |
g_hash_table_insert (self->function_by_address, function_address, ctx); | |
// 当前 transaction 添加到 任务中, 设置回调 函数 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; | |
} | |
} |
先偷一个跳板代码:
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
拦截器后端的初始化:这里初始化的 writer
和 relocator
分别用于指令写和指令恢复。
_gum_interceptor_backend_create (GRecMutex * mutex, | |
GumCodeAllocator * allocator) | |
{ | |
GumInterceptorBackend * backend; | |
backend = g_slice_new (GumInterceptorBackend); | |
backend->allocator = allocator; | |
gum_arm_writer_init (&backend->arm_writer, NULL); | |
backend->arm_writer.cpu_features = gum_query_cpu_features (); | |
gum_arm_relocator_init (&backend->arm_relocator, NULL, &backend->arm_writer); | |
gum_thumb_writer_init (&backend->thumb_writer, NULL); | |
gum_thumb_relocator_init (&backend->thumb_relocator, NULL, | |
&backend->thumb_writer); | |
gum_interceptor_backend_create_thunks (backend); | |
return backend; | |
} |
看到 thunks 的初始化,这里是关键函数,用来调度执行,对应进入 hook 和离开 hook
static void | |
gum_interceptor_backend_create_thunks (GumInterceptorBackend * self) | |
{ | |
GumArmWriter * aw = &self->arm_writer; | |
GumThumbWriter * tw = &self->thumb_writer; | |
self->arm_thunks = gum_code_allocator_alloc_slice (self->allocator); | |
gum_arm_writer_reset (aw, self->arm_thunks->data); | |
self->enter_thunk_arm = gum_arm_writer_cur (aw); | |
gum_emit_arm_enter_thunk (aw); | |
self->leave_thunk_arm = gum_arm_writer_cur (aw); | |
gum_emit_arm_leave_thunk (aw); | |
gum_arm_writer_flush (aw); | |
g_assert (gum_arm_writer_offset (aw) <= self->arm_thunks->size); | |
self->thumb_thunks = gum_code_allocator_alloc_slice (self->allocator); | |
gum_thumb_writer_reset (tw, self->thumb_thunks->data); | |
self->enter_thunk_thumb = gum_thumb_writer_cur (tw) + 1; | |
gum_emit_thumb_enter_thunk (tw); | |
self->leave_thunk_thumb = gum_thumb_writer_cur (tw) + 1; | |
gum_emit_thumb_leave_thunk (tw); | |
gum_thumb_writer_flush (tw); | |
g_assert (gum_thumb_writer_offset (tw) <= self->thumb_thunks->size); | |
} |
gum_emit_arm_enter_thunk:
static void | |
gum_emit_arm_enter_thunk (GumArmWriter * aw) | |
{ | |
// 用于保存返回地址以及栈、寄存器情况 | |
gum_emit_arm_prolog (aw); | |
// 构造自身函数栈以及寄存器 | |
gum_arm_writer_put_add_reg_reg_imm (aw, ARM_REG_R1, ARM_REG_SP, | |
GUM_FRAME_OFFSET_CPU_CONTEXT); | |
gum_arm_writer_put_sub_reg_reg_imm (aw, ARM_REG_R2, ARM_REG_R4, 4); | |
gum_arm_writer_put_add_reg_reg_imm (aw, ARM_REG_R3, ARM_REG_SP, | |
GUM_FRAME_OFFSET_NEXT_HOP); | |
// 调用 | |
gum_arm_writer_put_call_address_with_arguments (aw, | |
GUM_ADDRESS (_gum_function_context_begin_invocation), 4, | |
GUM_ARG_REGISTER, ARM_REG_R6, | |
GUM_ARG_REGISTER, ARM_REG_R1, | |
GUM_ARG_REGISTER, ARM_REG_R2, | |
GUM_ARG_REGISTER, ARM_REG_R3); | |
gum_emit_arm_epilog (aw); | |
} |
gum_emit_arm_leave_thunk:
static void | |
gum_emit_arm_leave_thunk (GumArmWriter * aw) | |
{ | |
gum_emit_arm_prolog (aw); | |
gum_arm_writer_put_add_reg_reg_imm (aw, ARM_REG_R1, ARM_REG_SP, | |
GUM_FRAME_OFFSET_CPU_CONTEXT); | |
gum_arm_writer_put_add_reg_reg_imm (aw, ARM_REG_R2, ARM_REG_SP, | |
GUM_FRAME_OFFSET_NEXT_HOP); | |
gum_arm_writer_put_call_address_with_arguments (aw, | |
GUM_ADDRESS (_gum_function_context_end_invocation), 3, | |
GUM_ARG_REGISTER, ARM_REG_R6, | |
GUM_ARG_REGISTER, ARM_REG_R1, | |
GUM_ARG_REGISTER, ARM_REG_R2); | |
gum_emit_arm_epilog (aw); | |
} |
拦截器激活:
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); | |
} |
核心 bl 处:
void _gum_interceptor_backend_activate_trampoline (GumInterceptorBackend * self, | |
GumFunctionContext * ctx, | |
gpointer prologue) | |
{ | |
GumAddress function_address; | |
GumArmFunctionContextData * data = GUM_FCDATA (ctx); | |
function_address = GUM_ADDRESS ( | |
_gum_interceptor_backend_get_function_address (ctx)); | |
if (FUNCTION_CONTEXT_ADDRESS_IS_THUMB (ctx)) | |
{ | |
GumThumbWriter * tw = &self->thumb_writer; | |
// 设置 base、pc | |
gum_thumb_writer_reset (tw, prologue); | |
tw->pc = function_address; | |
if (ctx->trampoline_deflector != NULL) | |
{ | |
if (data->redirect_code_size == GUM_INTERCEPTOR_THUMB_LINK_REDIRECT_SIZE) | |
{ | |
gum_emit_thumb_push_cpu_context_high_part (tw); | |
gum_thumb_writer_put_bl_imm (tw, // 写 bl,进行跳板操作 | |
GUM_ADDRESS (ctx->trampoline_deflector->trampoline)); | |
} | |
else | |
{ | |
g_assert (data->redirect_code_size == | |
GUM_INTERCEPTOR_THUMB_TINY_REDIRECT_SIZE); | |
gum_thumb_writer_put_b_imm (tw, | |
GUM_ADDRESS (ctx->trampoline_deflector->trampoline)); | |
} | |
} | |
else if (ctx->type == GUM_INTERCEPTOR_TYPE_FAST) | |
{ | |
gum_thumb_writer_put_ldr_reg_address (tw, ARM_REG_PC, | |
GUM_ADDRESS (ctx->replacement_function)); | |
} | |
else | |
{ | |
gum_thumb_writer_put_ldr_reg_address (tw, ARM_REG_PC, | |
GUM_ADDRESS (ctx->on_enter_trampoline)); | |
} | |
gum_thumb_writer_flush (tw); | |
g_assert (gum_thumb_writer_offset (tw) <= data->redirect_code_size); | |
} | |
else | |
{ | |
GumArmWriter * aw = &self->arm_writer; | |
gum_arm_writer_reset (aw, prologue); | |
aw->pc = function_address; | |
if (ctx->trampoline_deflector != NULL) | |
{ | |
g_assert (data->redirect_code_size == | |
GUM_INTERCEPTOR_ARM_TINY_REDIRECT_SIZE); | |
gum_arm_writer_put_b_imm (aw, | |
GUM_ADDRESS (ctx->trampoline_deflector->trampoline)); | |
} | |
else if (ctx->type == GUM_INTERCEPTOR_TYPE_FAST) | |
{ | |
gum_arm_writer_put_ldr_reg_address (aw, ARM_REG_PC, | |
GUM_ADDRESS (ctx->replacement_function)); | |
} | |
else | |
{ | |
gum_arm_writer_put_ldr_reg_address (aw, ARM_REG_PC, | |
GUM_ADDRESS (ctx->on_enter_trampoline)); | |
} | |
gum_arm_writer_flush (aw); | |
g_assert (gum_arm_writer_offset (aw) == data->redirect_code_size); | |
} | |
} |
# 三、执行流程
偷一个(上面我们走的是 arm 的流程,函数名略有不同,但流程是一样的)
# 先这样~有机会再来调试
# frida-core
# 一、前置知识
# 1、vala 语言
介绍:Projects/Vala - GNOME Wiki!:Vala is a programming language using modern high level abstractions without imposing additional runtime requirements and without using a different ABI compared to applications and libraries written in C. Vala uses the GObject type system and has additional code generation routines that make targeting the GNOME stack simple. Vala has many other uses where native binaries are required.
vala 语言是 GNOME 中使用的一个高级语言,和传统高级语言不同的是 vala 代码会被编译器先编译成 C 代码,然后再编译成二进制文件,因此也可以认为 vala 语言是 C 的一个语法糖拓展。其使用使用 glib 的 GObject
类型系统来构造类和接口以实现面向对象,其语法有点类似于 C#
,支持许多现代语言的高级特性,包括但不限于接口、属性、内存管理、异常、lambda、信号等等。Vala 既可以通过 API 文件访问已有的 C 库文件,也可以从 C 中很容易调用 Vala 的方法。
直接看也能大概看懂
# 2、进程问题
无论在 Android 还是 iOS 或者其他平台,进程和进程直接相互是透明的,就算对 Frida 来说目标进程并非透明的,但是对目标进程来说,至少 Frida 也该是透明的。但是现实情况显然是,二者往往需要相互之间识别并交互。
# 二、源码分析
vala 插件点不进去啊急了,看不动先鸽了 ××× 等 selinux 看看再回来