故事背景
今天给大家带来一种Native Hook的分享,它叫做DispatchTable Hook。DispatchTable Hook的应用非常广泛,比如谷歌Android系统的malloc debug,malloc hook,gwp-asan都有用到针对内存分配相关函数的DispatchTable Hook,同时在国内大厂字节中,部分方案也使用了DispatchTable Hook。那么这个秘密武器究竟是什么,我们应用开发者将如何使用呢?
可能大部分开发者对这个概念有点陌生,同时相关资料也比较少,不过不要紧,下面我们将从DispatchTable Hook的原理出发到实践,完成一个内存分配函数的DispatchTable Hook 框架。
本文涉及的所有代码均开源,位于我的项目JniHook中。
DispatchTable是什么
DispatchTable 顾名思义,分配表,它是一种把函数表示与真正实现隔离的一种思想。
比如我们在Android调用一个malloc函数,它的实现并不一定是libc 的malloc,这个概念很重要,也有可能是添加了特殊功能的malloc实现,比如用于检测分配内存的gwp-asan-malloc 或者malloc hook 。主要取决于当前DispatchTable的实现。
内存分配相关函数对应的Dispatch是一个结构体,叫做MallocDisptach,里面包含了内存分配函数malloc,calloc,free,realloc等
ini
struct MallocDispatch {
MallocCalloc calloc;
MallocFree free;
MallocMallinfo mallinfo;
MallocMalloc malloc;
MallocMallocUsableSize malloc_usable_size;
MallocMemalign memalign;
MallocPosixMemalign posix_memalign;
#if defined(HAVE_DEPRECATED_MALLOC_FUNCS)
MallocPvalloc pvalloc;
#endif
MallocRealloc realloc;
#if defined(HAVE_DEPRECATED_MALLOC_FUNCS)
MallocValloc valloc;
#endif
MallocIterate malloc_iterate;
MallocMallocDisable malloc_disable;
MallocMallocEnable malloc_enable;
MallocMallopt mallopt;
MallocAlignedAlloc aligned_alloc;
MallocMallocInfo malloc_info;
} __attribute__((aligned(32)));
在libc初始化时,会有一个全局变量libc_globals,它里面就描述了当前程序的DisptachTable
arduino
__LIBC_HIDDEN__ constinit WriteProtected<libc_globals> __libc_globals;
c
struct libc_globals {
vdso_entry vdso[VDSO_END];
long setjmp_cookie;
uintptr_t heap_pointer_tag;
_Atomic(bool) decay_time_enabled;
_Atomic(bool) memtag;
// In order to allow a complete switch between dispatch tables without
// the need for copying each function by function in the structure,
// use a single atomic pointer to switch.
// The current_dispatch_table pointer can only ever be set to a complete
// table. Any dispatch table that is pointed to by current_dispatch_table
// cannot be modified after that. If the pointer changes in the future,
// the old pointer must always stay valid.
// The malloc_dispatch_table is modified by malloc debug, malloc hooks,
// and heaprofd. Only one of these modes can be active at any given time.
_Atomic(const MallocDispatch*) current_dispatch_table;
// This pointer is only used by the allocation limit code when both a
// limit is enabled and some other hook is enabled at the same time.
_Atomic(const MallocDispatch*) default_dispatch_table;
MallocDispatch malloc_dispatch_table;
};
-
current_dispatch_table:指向当前应用程序使用的DispatchTable的指针
-
default_dispatch_table:指向默认DispatchTable的指针
-
malloc_dispatch_table:一般等同于default_dispatch_table,只不过default_dispatch_table是一个指针,而malloc_dispatch_table是DispatchTable的值
DispatchTable Hook
优点对比
我们拿DispatchTable Hook 与常见的got表hook 与inline hook进行一个小对比
DispatchTable Hook | got表hook | inline hook |
---|---|---|
针对callee,即被调用函数的hook,无需处理多so或者so加载的问题 | 针对caller,即调用函数的hook,需要处理so加载的问题,比如bhook为了监听增量hook采取hook__loader_dlopen方式 | 针对callee即被调用函数的hook,无需处理多so或者so加载的问题 |
对于MallocDispatch,只针对规定的内存分配函数,应用范围小 | 针对具有外部函数,需要重新定向的函数才生效,即函数确定必须依赖got表确定的外部函数 | 几乎能够针对大部分函数进行处理,范围最广 |
稳定,hook方式较为简洁 | 稳定高效,实现不复杂 | 稳定性比其他两种低,实现依赖指令集改写 |
谷歌官方例子
我们可以在AndroidAOSP中看到DispatchTable Hook的各种例子,比如gwp-asan中通过自定义MallocDispatch从而实现对calloc,free,malloc等关键分配函数进行内存踩踏校验
scss
const MallocDispatch gwp_asan_dispatch __attribute__((unused)) = {
gwp_asan_calloc,
gwp_asan_free,
Malloc(mallinfo),
gwp_asan_malloc,
gwp_asan_malloc_usable_size,
Malloc(memalign),
Malloc(posix_memalign),
#if defined(HAVE_DEPRECATED_MALLOC_FUNCS)
Malloc(pvalloc),
#endif
gwp_asan_realloc,
#if defined(HAVE_DEPRECATED_MALLOC_FUNCS)
Malloc(valloc),
#endif
gwp_asan_malloc_iterate,
gwp_asan_malloc_disable,
gwp_asan_malloc_enable,
Malloc(mallopt),
Malloc(aligned_alloc),
Malloc(malloc_info),
};
初始化代码如下,通过改写 __libc_globals 的default_dispatch_table与malloc_dispatch_table等关键结构实现
scss
bool MaybeInitGwpAsan(libc_globals* globals,
const android_mallopt_gwp_asan_options_t& mallopt_options) {
...
// GWP-ASan's initialization is always called in a single-threaded context, so
// we can initialize lock-free.
// Set GWP-ASan as the malloc dispatch table.
globals->malloc_dispatch_table = gwp_asan_dispatch;
atomic_store(&globals->default_dispatch_table, &gwp_asan_dispatch);
// If malloc_limit isn't installed, we can skip the default_dispatch_table
// lookup.
if (GetDispatchTable() == nullptr) {
atomic_store(&globals->current_dispatch_table, &gwp_asan_dispatch);
}
GwpAsanInitialized = true;
prev_dispatch = NativeAllocatorDispatch();
GuardedAlloc.init(options);
__libc_shared_globals()->gwp_asan_state = GuardedAlloc.getAllocatorState();
__libc_shared_globals()->gwp_asan_metadata = GuardedAlloc.getMetadataRegion();
__libc_shared_globals()->debuggerd_needs_gwp_asan_recovery = NeedsGwpAsanRecovery;
__libc_shared_globals()->debuggerd_gwp_asan_pre_crash_report = GwpAsanPreCrashHandler;
__libc_shared_globals()->debuggerd_gwp_asan_post_crash_report = GwpAsanPostCrashHandler;
return true;
}
};
原理分析
实现malloc的DispatchTable Hook关键代码其实就是以下三行
scss
globals->malloc_dispatch_table = gwp_asan_dispatch;
atomic_store(&globals->default_dispatch_table, &gwp_asan_dispatch);
// If malloc_limit isn't installed, we can skip the default_dispatch_table
// lookup.
if (GetDispatchTable() == nullptr) {
atomic_store(&globals->current_dispatch_table, &gwp_asan_dispatch);
}
前两行代码不用多说,其实就是把原本libc_global的DispatchTable替换为自己的DispatchTable。而第三句代码是否替换current_dispatch_table有一个前提条件,就是GetDispatchTable函数返回null。
GetDispatchTable函数其实就是原子加载出当前libc_global的current_dispatch_table。
scss
static inline const MallocDispatch* GetDispatchTable() {
return atomic_load_explicit(&__libc_globals->current_dispatch_table, memory_order_acquire);
}
为什么要这么判断呢,这是因为默认情况下,Android系统的第一个DispatchTable,给到了malloc limit的DispatchTable实现,这里的目的就是限制malloc或者calloc这些函数分配的内存需要处于一个正常的范围之内,防止恶意的内存分配
arduino
static constexpr MallocDispatch __limit_dispatch
__attribute__((unused)) = {
LimitCalloc,
LimitFree,
LimitMallinfo,
LimitMalloc,
LimitUsableSize,
LimitMemalign,
LimitPosixMemalign,
#if defined(HAVE_DEPRECATED_MALLOC_FUNCS)
LimitPvalloc,
#endif
LimitRealloc,
#if defined(HAVE_DEPRECATED_MALLOC_FUNCS)
LimitValloc,
#endif
LimitIterate,
LimitMallocDisable,
LimitMallocEnable,
LimitMallopt,
LimitAlignedAlloc,
LimitMallocInfo,
};
Malloc limit的实现会在真正调用分配函数前,进行检查,比如当前分配的内存是否超过了限制,超过后就让这次分配失败,通过__builtin_mul_overflow 内建函数与CheckLimit限制
arduino
static inline bool CheckLimit(size_t bytes) {
uint64_t total;
if (__predict_false(__builtin_add_overflow(
atomic_load_explicit(&gAllocated, memory_order_relaxed), bytes, &total) ||
total > gAllocLimit)) {
return false;
}
return true;
}
当分配的内存并没有超过分配内存gAllocLimit的限制时,就会通过GetDefaultDisptachTable方法,获取默认的DispatchTable,让其继续执行分配内存
scss
void* LimitCalloc(size_t n_elements, size_t elem_size) {
size_t total;
if (__builtin_mul_overflow(n_elements, elem_size, &total) || !CheckLimit(total)) {
warning_log("malloc_limit: calloc(%zu, %zu) exceeds limit %" PRId64, n_elements, elem_size,
gAllocLimit);
return nullptr;
}
auto dispatch_table = GetDefaultDispatchTable();
if (__predict_false(dispatch_table != nullptr)) {
return IncrementLimit(dispatch_table->calloc(n_elements, elem_size));
}
return IncrementLimit(Malloc(calloc)(n_elements, elem_size));
}
因此我们可以总结以下流程
我们如果要实现dispatchtable hook,那么我们只需要改变默认的dispatchtable即可。
实现
实现dispatchtable hook有两个关键步骤
- 要获取到当前程序的__libc_global对象,因为这个对象里面才持有dispatchtable
c
struct libc_globals {
_Atomic(const MallocDispatch*) current_dispatch_table;
// This pointer is only used by the allocation limit code when both a
// limit is enabled and some other hook is enabled at the same time.
_Atomic(const MallocDispatch*) default_dispatch_table;
MallocDispatch malloc_dispatch_table;
};
- 我们可能不需要完整替换所有disptachtable的内容, 因此我们需要获取原本默认的disptachtable, 只改变自己需要替换的函数
- 原子替换default_dispatch_table 为我们自定义的MallocDispatch对象指针,还有替换malloc_dispatch_table,如果当前current_disptach_table为null的话(一般不会为null,有malloclimit的disptachtable占位),也替换为自定义的MallocDispatch对象指针,前提是current_disptach_table为null。
下面我们按照上面两个步骤,实现disptachtable hook
获取到当前程序的__libc_global对象
因为系统没有开发接口给应用程序使用,我们没有一个方法能够方便获取__libc_global对象,但是我们留意到__libc_global是一个全局对象,它有自己的符号,我们可以通过查找libc里面的符号,找到__libc_global对应的符号
readelf -sW libc.so
它的符号就是
__libc_globals
值得注意的是,我们可以通过dlopen直接打开libc,但是我们不能够直接通过dlsym去找到该符号,因为dlsym的查找范围是从 .dynsym
中查询 "动态链接符号",而我们的__libc_globals符号并不在此,而是在.symtab中位于2568(不同手机这里索引可能不一样,但是都包含在这里)。
因此我们需要遍历其他所有能拿到symbol的section,通过解析Elf文件我们可以做到这点,这里我们可以直接通过xDL这个开源库解析ELF文件获取到dynsym
或者.symtab
这些额外的符号,这个库也被广泛运用到字节inline hook解决方案shadow hook中。
遍历dynsym采取xdl_sym方法,遍历symtab采取xdl_dsym方法
arduino
static void *find_symbol(void *handle, const char *sym_name) {
void *addr = xdl_sym(handle, sym_name, NULL);
if (NULL == addr) {
addr = xdl_dsym(handle, sym_name, NULL);
}
return addr;
}
ini
void *handle = xdl_open("libc.so", XDL_DEFAULT);
struct libc_globals *c_global = (struct libc_globals *) find_symbol(handle,
"__libc_globals");
通过符号获取,我们就能够拿到指向了__libc_global对象的指针
获取默认的DispatchTable
获取原先默认的dispatchtable可以通过NativeAlloctorDispatch方法获取,我们可以通过dlsym查找符号调用即可
arduino
const MallocDispatch* NativeAllocatorDispatch() {
return &__libc_malloc_default_dispatch;
}
对应的符号是
_Z23NativeAllocatorDispatchv
代码如下:
ini
void *handle = xdl_open("libc.so", XDL_DEFAULT);
struct MallocDispatch *c_dispatcher = ((struct MallocDispatch *(*)()) find_symbol(
handle, "_Z23NativeAllocatorDispatchv"))();
if (c_dispatcher == NULL) {
return;
}
替换default_dispatch_table 为我们自定义的MallocDispatch对象指针
默认情况下__libc_globals是不可写的,因此我们需要针对它进行权限改写,通过mprotect方法即可
ini
if (mprotect(c_global, PAGE_SIZE, PROT_WRITE | PROT_READ) == -1) {
return 0;
}
switch (type) {
case MALLOC: {
*callee = pika_dispatch_table->malloc;
dynamic->malloc = hook_func;
break;
}
case CALLOC: {
*callee = pika_dispatch_table->calloc;
dynamic->calloc = hook_func;
break;
}
case FREE: {
*callee = pika_dispatch_table->free;
dynamic->free = hook_func;
break;
}
// You can add any function in here which should at the dispatch table
default: {
return 0;
}
}
//替换dispatch_table为我们自定义的dispatch table
c_global->malloc_dispatch_table = *dynamic;
atomic_store(&c_global->default_dispatch_table, dynamic);
if (c_global->current_dispatch_table == NULL) {
atomic_store(&c_global->current_dispatch_table,
dynamic);
}
if (mprotect(c_global, PAGE_SIZE, PROT_READ) == -1) {
return 0;
}
return 1;
dynamic为我们自定义生成的MallocDispatch对象,针对default_dispatch_table字段,我们需要采取原子方法替换,因为这个变量是_Atomic修饰的,因此替换原子变量,我们可以通过atomic_store替换即可。这里我们简单只修改malloc ,free与calloc三个函数
ini
static struct MallocDispatch *dynamic;
dynamic = malloc(sizeof(struct MallocDispatch));
dynamic->calloc = pika_dispatch_table->calloc;
dynamic->free = pika_dispatch_table->free;
dynamic->mallinfo = pika_dispatch_table->mallinfo;
dynamic->malloc = pika_dispatch_table->malloc;
dynamic->malloc_usable_size = pika_dispatch_table->malloc_usable_size;
dynamic->memalign = pika_dispatch_table->memalign;
dynamic->posix_memalign = pika_dispatch_table->posix_memalign;
#if defined(HAVE_DEPRECATED_MALLOC_FUNCS)
dynamic->pvalloc = predispatcher->pvalloc;
#endif
dynamic->realloc = pika_dispatch_table->realloc;
#if defined(HAVE_DEPRECATED_MALLOC_FUNCS)
dynamic->valloc = predispatcher->valloc;
#endif
dynamic->malloc_iterate = pika_dispatch_table->malloc_iterate;
dynamic->malloc_disable = pika_dispatch_table->malloc_disable;
dynamic->malloc_enable = pika_dispatch_table->malloc_enable;
dynamic->mallopt = pika_dispatch_table->mallopt;
dynamic->aligned_alloc = pika_dispatch_table->aligned_alloc;
dynamic->malloc_info = pika_dispatch_table->malloc_info;
至此,我们就完成了整个DispatchTable的全过程,完整代码可以通过我的github项目JniHook 查看
总结
针对malloc的DispatchTableHook 能够让我们快速且简单的完成针对内存分配相关函数的监控,在此之上我们可以添加自己的分配逻辑,从而实现更多的方案,NativeHook在性能优化领域中不可缺少,希望本期的DispatchTableHook能够让Android开发者们有所帮助~我是Pika,一个神奇的Android开发,Bye~