likes
comments
collection
share

如何实现内存分配函数的DispatchTable Hook

作者站长头像
站长
· 阅读数 8

故事背景

今天给大家带来一种Native Hook的分享,它叫做DispatchTable Hook。DispatchTable Hook的应用非常广泛,比如谷歌Android系统的malloc debug,malloc hook,gwp-asan都有用到针对内存分配相关函数的DispatchTable Hook,同时在国内大厂字节中,部分方案也使用了DispatchTable Hook。那么这个秘密武器究竟是什么,我们应用开发者将如何使用呢?

可能大部分开发者对这个概念有点陌生,同时相关资料也比较少,不过不要紧,下面我们将从DispatchTable Hook的原理出发到实践,完成一个内存分配函数的DispatchTable Hook 框架。

本文涉及的所有代码均开源,位于我的项目JniHook中。

DispatchTable是什么

DispatchTable 顾名思义,分配表,它是一种把函数表示与真正实现隔离的一种思想。

如何实现内存分配函数的DispatchTable Hook

比如我们在Android调用一个malloc函数,它的实现并不一定是libc 的malloc,这个概念很重要,也有可能是添加了特殊功能的malloc实现,比如用于检测分配内存的gwp-asan-malloc 或者malloc hook 。主要取决于当前DispatchTable的实现。

内存分配相关函数对应的Dispatch是一个结构体,叫做MallocDisptach,里面包含了内存分配函数malloc,calloc,free,realloc等

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

__LIBC_HIDDEN__ constinit WriteProtected<libc_globals> __libc_globals;
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;
};
  1. current_dispatch_table:指向当前应用程序使用的DispatchTable的指针

  2. default_dispatch_table:指向默认DispatchTable的指针

  3. malloc_dispatch_table:一般等同于default_dispatch_table,只不过default_dispatch_table是一个指针,而malloc_dispatch_table是DispatchTable的值

DispatchTable Hook

优点对比

我们拿DispatchTable Hook 与常见的got表hook 与inline hook进行一个小对比

DispatchTable Hookgot表hookinline 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等关键分配函数进行内存踩踏校验

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等关键结构实现

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关键代码其实就是以下三行

  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。

static inline const MallocDispatch* GetDispatchTable() {
  return atomic_load_explicit(&__libc_globals->current_dispatch_table, memory_order_acquire);
}

为什么要这么判断呢,这是因为默认情况下,Android系统的第一个DispatchTable,给到了malloc limit的DispatchTable实现,这里的目的就是限制malloc或者calloc这些函数分配的内存需要处于一个正常的范围之内,防止恶意的内存分配

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限制

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,让其继续执行分配内存

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 hook,那么我们只需要改变默认的dispatchtable即可。

实现

实现dispatchtable hook有两个关键步骤

  1. 要获取到当前程序的__libc_global对象,因为这个对象里面才持有dispatchtable
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;
};
  1. 我们可能不需要完整替换所有disptachtable的内容, 因此我们需要获取原本默认的disptachtable, 只改变自己需要替换的函数
  2. 原子替换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

如何实现内存分配函数的DispatchTable Hook

值得注意的是,我们可以通过dlopen直接打开libc,但是我们不能够直接通过dlsym去找到该符号,因为dlsym的查找范围是从 .dynsym 中查询 “动态链接符号”,而我们的__libc_globals符号并不在此,而是在.symtab中位于2568(不同手机这里索引可能不一样,但是都包含在这里)。

如何实现内存分配函数的DispatchTable Hook

因此我们需要遍历其他所有能拿到symbol的section,通过解析Elf文件我们可以做到这点,这里我们可以直接通过xDL这个开源库解析ELF文件获取到dynsym或者.symtab这些额外的符号,这个库也被广泛运用到字节inline hook解决方案shadow hook中。

遍历dynsym采取xdl_sym方法,遍历symtab采取xdl_dsym方法

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;
}
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查找符号调用即可

const MallocDispatch* NativeAllocatorDispatch() {
  return &__libc_malloc_default_dispatch;
}

对应的符号是

_Z23NativeAllocatorDispatchv

代码如下:

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方法即可

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三个函数

    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~

转载自:https://juejin.cn/post/7353542036232634377
评论
请登录