libdl在安卓系统中如何与链接器一起工作?

4

众所周知,/system/bin/linker 负责动态链接机制,但 libdl 中有存根函数,这些函数实际上在动态链接器 (dlfcn.c) 中定义,并在运行时被劫持,如下所示:

#include <dlfcn.h>
/* These are stubs for functions that are actually defined
 * in the dynamic linker (dlfcn.c), and hijacked at runtime.
 */
void *dlopen(const char *filename, int flag) { return 0; }
const char *dlerror(void) { return 0; }
void *dlsym(void *handle, const char *symbol) { return 0; }
int dladdr(const void *addr, Dl_info *info) { return 0; }
int dlclose(void *handle) { return 0; }

void android_update_LD_LIBRARY_PATH(const char* ld_library_path) { }

#if defined(__arm__)

void *dl_unwind_find_exidx(void *pc, int *pcount) { return 0; }

#elif defined(__i386__) || defined(__mips__)

/* we munge the cb definition so we don't have to include any headers here.
 * It won't affect anything since these are just symbols anyway */
int dl_iterate_phdr(int (*cb)(void *info, void *size, void *data), void *data) { return 0; }

#else
#error Unsupported architecture. Only mips, arm and x86 are supported.
#endif

那么劫持发生的时间和方式是什么?如果您能在Android开源中向我展示代码,将不胜感激。

1个回答

8
从你的问题中不清楚你感兴趣的是哪个Android版本,但似乎你正在查看旧版Android(因为它使用dlfcn.c而不是dlfcn.cpp)。我将基于Android 6讨论劫持过程。
对于较新的Android版本,过程基本相同,但某些方法名称和文件名已更改。
bionic/README.md中,可以找到以下描述:
引用: libdl/ --- libdl.so
动态链接器接口库。这实际上只是一堆存根,动态链接器会在运行时将其替换为指向自己实现的指针。这就是dlopen(3)等内容所在的位置。 linker/ --- /system/bin/linker和/system/bin/linker64
动态链接器。当您运行动态链接的可执行文件时,其ELF文件有一个DT_INTERP条目,表示“使用以下程序启动我”。在Android上,这要么是linker,要么是linker64(具体取决于是否为32位或64位可执行文件)。它负责将ELF可执行文件加载到内存中,并解析符号引用(以便当您的代码尝试跳转到fopen(3)等位置时,它能够正确地起作用)。
你发布的代码可以在bionic/libdl/libdl.c中找到。
// These are stubs for functions that are actually defined
// in the dynamic linker and hijacked at runtime.
void* dlopen(const char* filename __unused, int flag __unused) { return 0; }

我们可以验证关于 ELF 二进制文件中解释入口的声明:

$ readelf --string-dump=.interp system/bin/vold 

String dump of section '.interp':
  [     0]  /system/bin/linker64
linkerlinker64的高级入口点可以在bionic/linker/linker.cpp中找到(对于汇编级入口点,您需要查看特定于架构的文件,例如bionic/linker/arch/x86_64/begin.S):
/*
 * This is the entry point for the linker, called from begin.S. This
 * method is responsible for fixing the linker's own relocations, and
 * then calling __linker_init_post_relocation().
 */
extern "C" ElfW(Addr) __linker_init(void* raw_args) {

这个__linker_init函数初始化了全局变量static soinfo* solist;,它还初始化了其他内容,其中solist = get_libdl_info();将其赋值为返回的链接库信息。

struct soinfobionic/linker/linker.h中定义,并通过成员soinfo* next;表示链表中的一个节点。此链表中的每个节点都保存有关共享对象的信息,包括符号表,通过成员symtab_

get_libdl_info返回一个链表,其中只有一个条目,表示libdl.so共享对象。但是,此节点的符号表没有使用libdl.so中的存根函数指针进行初始化,而是使用了真正的实现:成员symtab_使用__libdl_info->symtab_ = g_libdl_symtab;进行了初始化。g_libdl_symtab表在这里进行了初始化,其中包含指向真正的dlopen等的指针。

所以我们找到了劫持发生的点:链接器初始化共享对象信息列表,其中第一个元素是一个带有符号表的条目,指向libdl.so的实际实现的符号表,而不是桩代码。本节其余部分介绍了如何使用此链接列表来理解为什么此劫持有效。 __linker_init返回__linker_init_post_relocation的地址给调用汇编代码,然后跳转到这个方法(例如在bionic/linker/arch/x86_64/begin.S中)。
__linker_init_post_relocation中,初始化一个soinfo结构体,其中argv[0]是要执行的二进制文件。
  soinfo* si = soinfo_alloc(args.argv[0], nullptr, 0, RTLD_GLOBAL);

对于这个soinfo,它将会调用:

  if (!si->prelink_image()) {

prelink_image 函数提取二进制文件中的 dynamic 表指针,以及其他内容:

bool soinfo::prelink_image() {
  /* Extract dynamic section */
  ElfW(Word) dynamic_flags = 0;
  phdr_table_get_dynamic_section(phdr, phnum, load_bias, &dynamic, &dynamic_flags);

可以使用以下命令行检查 dynamic 表:

$ readelf -d system/bin/vold

Dynamic section at offset 0x781a0 contains 46 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libbase.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc++.so]
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so]
...

将指向动态表的指针初始化后,for_each_dt_needed辅助函数变得可用。该函数会在动态表中的每个NEEDED条目上运行指定的操作。回到__linker_init_post_relocation中,使用此辅助函数来收集我们需要加载的共享库的名称:

  for_each_dt_needed(si, [&](const char* name) {
    needed_library_name_list.push_back(name);
    ++needed_libraries_count;
  });

接下来,需要的库列表将传递给find_libraries。在那里,为每个库创建一个LoadTask
  // Step 0: prepare.
  LoadTaskList load_tasks;
  for (size_t i = 0; i < library_names_count; ++i) {
    const char* name = library_names[i];
    load_tasks.push_back(LoadTask::create(name, start_with));
  }

对于每个这样的加载任务,它将加载二进制文件,并将所加载库的依赖项附加到“load_tasks”列表的末尾。换句话说,它对依赖图执行广度优先遍历。
  // Step 1: load and pre-link all DT_NEEDED libraries in breadth first order.
  for (LoadTask::unique_ptr task(load_tasks.pop_front());
      task.get() != nullptr; task.reset(load_tasks.pop_front())) {

对于每个这样的加载任务,它会调用find_library_internal来进行实际的加载。该函数首先调用find_loaded_library_by_soname,通过遍历全局变量solist来检查库是否已被加载。
  for (soinfo* si = solist; si != nullptr; si = si->next) {
    const char* soname = si->get_soname();
    if (soname != nullptr && (strcmp(name, soname) == 0)) {

这就是最初被劫持的libdl.so条目填充的确切内容,指向了非存根实现的dlopen等。因此,每当一个二进制文件在其dynamic部分的NEEDED列表中具有libdl.so时,加载进程将始终发现libdl.so已经被加载,并返回被劫持的soinfo

如果在solist中找不到库,则find_library_internal函数会调用load_library读取实际的库文件。它为该库创建一个新的soinfo条目,并使用soinfo_alloc(使用始于solist的列表末端的sonext全局变量)将其附加到全局solist的末尾。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接