Android Hook - dl_iterate_phdr()增强

dl_iterate_phdr()是Linux/Android系统库提供的一个libc函数,用于遍历当前进程中加载的所有共享库(ELF)。

然而对于ARM32系统,dl_iterate_phdr()API 21 (Android 5.0)及以上版本才支持。

本文介绍通过读取proc/pid/maps文件,遍历已经加载到内存ELF并读取其程序表头的方案,从而兼容dl_iterate_phdr()在系统版本上的缺失。

另外,本文还将讲述如何修复dl_iterate_phdr()在Android低版本中的一些不足,从而提供对齐高版本特性的完整能力。

一、背景

1、dl_iterate_phdr的介绍

1.1、使用方法

Android Hook - 解析proc/pid/maps文件中介绍到,通过读取proc/[pid]/maps文件可以遍历已经被加载到内存中的动态库,实际Android/Linux系统提供了dl_iterate_phdr()方法用于更确切地实现这个功能。

从文档dl_iterate_phdr(3) --- Linux manual page你可以找到关于这个方法的详细说明,这里介绍一下重点。

c 复制代码
//该方法用于遍历已经加载到内存的动态库
int dl_iterate_phdr(
 //1、回调,每个动态库会被逐个回调给调用者处理。其中info就是当前遍历到的动态库信息;
 //size是dl_phdr_info的大小,通常用于确保回调函数的兼容性;
 //data是用户透传数据,这也是dl_iterate_phdr方法的第二个参数
 int (*callback)(struct dl_phdr_info *info,
                 size_t size, void *data),
 //2、data是用户透传数据
 void *data);

struct dl_phdr_info {
  //动态库在内存中的基地址
  ElfW(Addr) dlpi_addr;
  //动态库名称/路径,以\0结尾
  const char* dlpi_name;
  //段表
  const ElfW(Phdr)* dlpi_phdr;
  //段表表项数量
  ElfW(Half) dlpi_phnum;

  // These fields were added in Android R.
  //以下字段通常不会用到
  unsigned long long dlpi_adds;
  unsigned long long dlpi_subs;
  size_t dlpi_tls_modid;
  void* dlpi_tls_data;
};

从注释可以看出,dl_iterate_phdr()方法会通过回调来给调用者处理动态库的机会,而动态库的信息则是由struct dl_phdr_info来组织的。

一个简单的使用例子是:

c 复制代码
//1、头文件
#include <link.h>

static int callback(struct dl_phdr_info *info, size_t size, void *data){
  //3、在回调中逐个处理动态库信息,返回非0值表示马上结束遍历
}

int main(void){
  //2、传入回调,自定义数据没有就为NULL即可
   dl_iterate_phdr(callback, NULL);
}

除此之外,dl_iterate_phdr()方法还有一些注意点:

  1. 返回值dl_iterate_phdr()有返回值,这个返回值实际等于传入的callback的返回值,当返回值为0时表示继续遍历下一个动态库,而非0值时表示马上结束遍历。
  2. 当前进程的dl_phdr_info.namedl_phdr_info.name是当前遍历到的动态库的名称/路径。
  3. 段表 (dl_phdr_info.dlpi_phdrdl_phdr_info.dlpi_phnum)信息。通过段表我们就可以读取动态库各个段,例如.dynamic段等。段表的结构ElfW(Phdr)可以在elf.h中找到定义,而其作用则在Android Hook - 动态加载so库Android Hook - 解析proc/pid/maps文件分别有过介绍。
  4. 动态库在内存的基地址dl_phdr_info.dlpi_addr 。首先需要强调,dl_phdr_info.dlpi_addr并非表示动态库在内存中的首地址 ,而是基地址

1.2、load_bias

其中第4点即dl_phdr_info.dlpi_addr需要仔细说明。

在此之前,需要了解load_bias 的概念,在官方源码bionic/linker/linker_phdr.cpp的注释中有对此进行解释,这里拾人牙慧,也使用其中的例子。

根据Android Hook - 解析proc/pid/maps文件中的介绍,动态库只有PT_LOAD类型的段才会被映射到内存中,假设ELF文件中有两个连续的类型为PT_LOAD的段:

shell 复制代码
phdr0:    [ offset:0,      filesz:0x4000, memsz:0x4000, vaddr:0x30000 ],
phdr1:    [ offset:0x4000, filesz:0x2000, memsz:0x8000, vaddr:0x40000 ],

可以看出,如果这两个段的目标虚拟内存范围为:

shell 复制代码
# phdr0->vaddr = 0x30000
0x30000...0x34000
# phdr1->vaddr = 0x40000
0x40000...0x48000

当动态链接器将ELF文件真正加载到内存后,会分配真正的虚拟内存地址,给第一个段分配的起始地址称为phdr0_load_address,第二个称为phdr1_load_address,依次类推。

假设Linker决定将第一个段加载到起始内存地址0xa0000000中,那么两个段在内存中的真正地址范围为:

shell 复制代码
# phdr0_load_address = 0xa0030000
0xa0030000...0xa0034000
# phdr1_load_address = 0xa0040000
0xa0040000...0xa0048000

由于段之间的p_vaddr关系是固定的,因此段的真实内存地址和p_vaddr存在这样的关系:,

phdr0_load_address - phdr0->p_vaddr = phdr1_load_address - phdr1->p_vaddr = ... = load_bias

例子中,就是:

shell 复制代码
0xa0030000 - 0x30000 = 0xa0040000 - 0x40000 = 0xa0000000

这个固定的差值0xa0000000被称为load_bias

另外,段的phdr->p_vaddr不一定要起始于内存页的边界,而mmap加载ELF文件到内存时,则需要对齐内存页边界才能加载。

因此,实际的load_bias计算方式为:

load_bias = phdr0_load_address - page_start(phdr0->p_vaddr)

其中page_start ()是指求p_vaddr所在页的起始地址,假设内存页大小为4K,例子中就是:

shell 复制代码
# 内存页大小为4K,即0x1000,那么page_start(0x30000) = 0x30000 & (0x1000 - 1) = 0x30000
0xa0030000 - page_start(0x30000) = 0xa0000000

可以看出,由于0x30000本身是页大小对齐的,因此对load_bias的实际结果没有影响。

反过来说,使用load_biasp_vaddr,就可以计算出任意段在内存中的真实起始地址,即

phdr0_load_address = load_bias + phdr0->p_vaddr

phdr1_load_address = load_bias + phdr1->p_vaddr

...

1.3、dl_phdr_info.dlpi_addr

了解了load_bias 后,来说明dl_phdr_info.dlpi_addr值的含义,实际两者是一样的,dl_phdr_info.dlpi_addr就是load_bias

因为有load_bias,我们才可以计算段表中每个段在内存中的地址。

通过分析源码,可以确认这一点:

bionic/linker/linker_main.cpp

c++ 复制代码
//1、soinfo表示Linker加载的一个动态库
static void init_link_map_head(soinfo& info) {
  auto& map = info.link_map_head;
  //2、记住这里,把load_bias赋值给map.l_addr
  map.l_addr = info.load_bias;
  map.l_name = const_cast<char*>(info.get_realpath());
  phdr_table_get_dynamic_section(info.phdr, info.phnum, info.load_bias, &map.l_ld, nullptr);
}
  • 动态链接器加载动态库到内存后,会记录为soinfo,并对其进行初始化。其中就会计算load_bias

bionic/linker/linker.cpp

c++ 复制代码
//3、dl_iterate_phdr()会调用do_dl_iterate_phdr()
int do_dl_iterate_phdr(int (*cb)(dl_phdr_info* info, size_t size, void* data), void* data) {
  int rv = 0;
  for (soinfo* si = solist_get_head(); si != nullptr; si = si->next) {
    dl_phdr_info dl_info;
    //4、看到,和步骤2对应,将map.l_addr赋值给dlpi_addr,因此load_bias就是dl_info.dlpi_addr
    dl_info.dlpi_addr = si->link_map_head.l_addr;
    ...
  }
  return rv;
}
  • dl_iterate_phdr()实际是遍历所有的soinfo,可以看出dl_info.dlpi_addr值最终来源于soinfo.load_bias

2、dl_iterate_phdr的版本差异

由于Android系统的不断演进,不同版本间的dl_iterate_phdr()实现略有不同,甚至部分低版本不提供dl_iterate_phdr()方法。

首先说明支持的最低目标系统版本定为API 16(Android 4.0)

以下是不同版本间区别的详细介绍。

2.1、Android 4.0 - Android 4.4

  1. Android 4.0(API 16) - Android 4.4(API 19)版本在ARM架构不提供dl_iterate_phdr方法
  2. 从Android 4.0(API 16)开始,dl_iterate_phdr回调中的dl_phdr_info.dlpi_namebasename(动态库名)而非pathname(动态库完整路径)
  3. libdl.so是动态链接器的接口库,对外提供dlfcn方法,但是运行时该库实际不会被加载到内存中
    • 在Android 4.0(API 16)等版本中,libdl.so对应的libdl_info是在代码中写死的,并且作为Linker已经加载的动态库列表solist的链表头
    • 在动态库加载时,会先从solist中查找是否已经加载过,因此libdl.so实际不会被加载。
  4. solist中第二个节点为somain(当前进程) ,并且对应包名dl_phdr_info.dlpi_name应用包名而不是app_process(64)

linker.cpp 4.4.4

c++ 复制代码
//变更1
#ifdef ANDROID_ARM_LINKER
...
//1.1、可以看出,ARM架构下不提供dl_iterate_phdr方法,X86架构则提供
#elif defined(ANDROID_X86_LINKER) || defined(ANDROID_MIPS_LINKER)
int
dl_iterate_phdr(int (*cb)(dl_phdr_info *info, size_t size, void *data),
                void *data)
{
    int rv = 0;
  	//1.2、solist就是动态链接器已经加载的动态库列表,因此dl_iterate_phdr实际就是遍历这个链表
    for (soinfo* si = solist; si != NULL; si = si->next) {
        dl_phdr_info dl_info;
        dl_info.dlpi_addr = si->link_map.l_addr;
        dl_info.dlpi_name = si->link_map.l_name;
        dl_info.dlpi_phdr = si->phdr;
        dl_info.dlpi_phnum = si->phnum;
        rv = cb(&dl_info, sizeof(dl_phdr_info), data);
        if (rv != 0) {
            break;
        }
    }
    return rv;
}

#endif

...

//变更2
static soinfo *load_library(const char *name)
{
  //2.1、加载目标动态库
  int fd = open_library(name);
  ...
  //2.2、库名只获取basename的部分(即从最后一个/开始的部分)
  const char* bname = strrchr(name, '/');
	soinfo* si = soinfo_alloc(bname ? bname + 1 : name);
  ...
  return si;
}

//变更3
//3.1、solist是Linker已经加载的动态库列表,是一个链表结构,表头是libdl_info
static soinfo* solist = &libdl_info;

//变更4
static Elf32_Addr __linker_init_post_relocation(KernelArgumentBlock& args, Elf32_Addr linker_base) {
  ...
  //4.1、使用第一个参数,即应用名来构造soinfo,并且是在linker初始化就完成的,因此这是链表的第二个节点
  soinfo* si = soinfo_alloc(args.argv[0]);
  ...
  link_map_t* map = &(si->link_map);

  map->l_addr = 0;
  map->l_name = args.argv[0];
  map->l_prev = NULL;
  ...
  //4.2、可以看出创建的节点就是somain,即代表主进程
  somain = si;
  ...
}

dlfcn.cpp

c++ 复制代码
//变更3
//3.2、可以看出,libdl.so对应的libdl_info实际是直接写死的。在动态库加载时,会先从solist中查找是否已经加载过,因此libdl.so实际不会被加载。
soinfo libdl_info = {
    "libdl.so",

    phdr: 0, phnum: 0,
    entry: 0, base: 0, size: 0,
    unused1: 0, dynamic: 0, unused2: 0, unused3: 0,
    next: 0,

    flags: FLAG_LINKED,

    strtab: ANDROID_LIBDL_STRTAB,
    symtab: gLibDlSymtab,

    ....
    //3.3、link_map,因此对应的dl_phdr_info.l_name是空字符串
    { l_addr: 0, l_name: 0, l_ld: 0, l_next: 0, l_prev: 0, },
  	...
};

2.2、Android 5.0 - Android 5.1

  1. 从Android 5.0(API 21)开始,在所有架构 上提供dl_iterate_phdr方法。

linker.cpp 5.1.1

c 复制代码
//变更1
//1.1、去掉了defined(ANDROID_X86_LINKER)等宏的限制,因此支持ARM架构
int dl_iterate_phdr(int (*cb)(dl_phdr_info* info, size_t size, void* data), void* data) {
  int rv = 0;
  for (soinfo* si = solist; si != nullptr; si = si->next) {
    dl_phdr_info dl_info;    
    dl_info.dlpi_addr = si->link_map_head.l_addr;
    dl_info.dlpi_name = si->link_map_head.l_name;
    dl_info.dlpi_phdr = si->phdr;
    dl_info.dlpi_phnum = si->phnum;
    rv = cb(&dl_info, sizeof(dl_phdr_info), data);
    if (rv != 0) {
      break;
    }
  }
  return rv;
}

2.3、Android 6.0 - Android 6.0

  1. Android 6.0(API 23)开始,调用dl_iterate_phdr前需要加锁g_dl_mutex
    • 其他dlfcn,包括dlopen、dlsym等,都要加这个锁才能调用,这一点在Android 4.4版本就已经实现了,但是dl_iterate_phdr则要到Android 6.0才加这个锁。
  2. Android 6.0(API 23)开始,dl_iterate_phdr回调中的dl_phdr_info.dlpi_name是完整的pathname

dlfcn.cpp 6.0.1

c++ 复制代码
static pthread_mutex_t g_dl_mutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;

//变更1
int dl_iterate_phdr(int (*cb)(dl_phdr_info* info, size_t size, void* data), void* data) {
  //1.1、使用RAII加一个全局锁g_dl_mutex
  ScopedPthreadMutexLocker locker(&g_dl_mutex);
  //1.2、调用do_dl_iterate_phdr
  return do_dl_iterate_phdr(cb, data);
}

static void* dlopen_ext(const char* filename, int flags, const android_dlextinfo* extinfo) {
  //1.3、其他dlfcn,包括dlopen、dlsym等,都要加这个锁才能调用。
  ScopedPthreadMutexLocker locker(&g_dl_mutex);
 	... 
}

//变更2
static soinfo* load_library(int fd, off64_t file_offset,
                            LoadTaskList& load_tasks,
                            const char* name, int rtld_flags,
                            const android_dlextinfo* extinfo) {
  ...
  std::string realpath = name;
  //2.1、找到动态库文件真实的完整路径
  if (!realpath_fd(fd, &realpath)) {
    ...
    //2.2、查找失败才使用name兜底
    realpath = name;
  }
  ...
  //2.3、构造soinfo时,使用的是realpath
  soinfo* si = soinfo_alloc(realpath.c_str(), &file_stat, file_offset, rtld_flags);
  ...
  return si;
}

2.3、Android 7.0 - Android 7.1

  1. somain(当前进程)对应的dl_phdr_info.dlpi_name应用包名 改为app_process(64)路径

linker.cpp

c++ 复制代码
static const char* get_executable_path() {
  static std::string executable_path;
  if (executable_path.empty()) {
    char path[PATH_MAX];
    //变更1
    //1.1.1、获取/proc/self/exe这个符号链接对应的可执行文件路径,实际就是/system/bin/app_process(64)
    ssize_t path_len = readlink("/proc/self/exe", path, sizeof(path));
    if (path_len == -1 || path_len >= static_cast<ssize_t>(sizeof(path))) {
      __libc_fatal("readlink('/proc/self/exe') failed: %s", strerror(errno));
    }
    executable_path = std::string(path, path_len);
  }

  return executable_path.c_str();
}

//变更1
static ElfW(Addr) __linker_init_post_relocation(KernelArgumentBlock& args, ElfW(Addr) linker_base) {
  ...
  //1.1、获取可执行程序路径
  const char* executable_path = get_executable_path();
  ...
  //1.2、使用这个路径构造somain
  soinfo* si = soinfo_alloc(&g_default_namespace, executable_path, &file_stat, 0, RTLD_GLOBAL);
  ...
  somain = si;
}

2.4、Android 8.0 - Android 15

  1. 从Android 8.0开始,dl_iterate_phdr回调中包含Linker本身
    • 之前的版本是不包含Linker本身的,虽然它也是一个动态库。
    • 虽然这个特性Google从Android 8.0开始支持,但是部分厂商要到Android 8.1才支持
  2. 从Android 8.0开始,libdl.so将会被真正的加载到内存
    • Linker本身对应的soinfo取代了libdl.so作为solist的链表头。

linker_main.cpp

c++ 复制代码
#if defined(__LP64__)
static char kLinkerPath[] = "/system/bin/linker64";
#else
static char kLinkerPath[] = "/system/bin/linker";
#endif

static soinfo* solist;

//变更1
//变更2
extern "C" ElfW(Addr) __linker_init(void* raw_args) {
  ...
  //1.1、初始化solist,传入动态链接器路径
  //2.1、初始化solist时,不再使用libdl.so来构造soinfo,而是使用linker本身。因此会正常加载libdl.so到内存中。
  sonext = solist = get_libdl_info(kLinkerPath);
  ...
}

dlfcn.cpp

c++ 复制代码
static soinfo* __libdl_info = nullptr;

//变更1
soinfo* get_libdl_info(const char* linker_path) {
  if (__libdl_info == nullptr) {
    //1.2、构造动态链接器对应的soinfo    
    __libdl_info = new (__libdl_info_buf) soinfo(&g_default_namespace, linker_path, nullptr, 0, 0);
    ...
    __libdl_info->soname_ = "ld-android.so";
    ...
  }
  
  return __libdl_info;
}

3、结论与目标

3.1、例子

接下看两个dl_iterate_phdr()输出的实际例子,来具体了解一下不同版本之间的差异,首先是Android 5.1:

shell 复制代码
# Android 5.1
# 1、第一个输出,代表代码中写死的libdl.so本身
info->dlpi_name: (null)
info->dlpi_addr: 0x0
info->dlpi_phdr: 0x0
info->dlpi_phnum: 0
# 2、第二个输出是当前进程名,这里是应用包名
info->dlpi_name: com.muye.dl_iterate_phdr_enhance
info->dlpi_addr: 0x7b50a18000
info->dlpi_phdr: 0x7b50a18040
# 3、第三个输出是libandroid_runtime.so,但只有basename而不是pathname
info->dlpi_name: libandroid_runtime.so
info->dlpi_addr: 0x7b50859000
info->dlpi_phdr: 0x7b50859040
info->dlpi_phnum: 6
...
  1. 可以看到,输出的第一个dl_phdr_infodlpi_namedlpi_addrdlpi_phdr等都是0。这和我们通过源码分析的一致,即在Android 4.0版本开始,solist链表的第一个元素就是libdl.so本身,并且值都被初始化为0。
  2. 第二个输出是当前进程,在7.0之前dlpi_name是包名。
  3. 第三个是一个动态库,dlpi_name是basename而不是pathname。

接下来对比Android 14的输出:

shell 复制代码
# Android 14
# 1、Android 8.0开始,solist的第一个节点变成了linker本身
info->dlpi_name: /system/bin/linker64
info->dlpi_addr: 0x7454015000
info->dlpi_phdr: 0x7454015040
info->dlpi_phnum: 10
# 2、Android Android 7.0开始,改进程的包名为app_process(64)
info->dlpi_name: /system/bin/app_process64
info->dlpi_addr: 0x5db5520000
info->dlpi_phdr: 0x5db5520040
info->dlpi_phnum: 11
# 3、Android 6.0开始,动态库的dlpi_name为pathname
info->dlpi_name: /system/lib64/libandroid_runtime.so
info->dlpi_addr: 0x7427f38000
info->dlpi_phdr: 0x7427f38040
info->dlpi_phnum: 10
...
  1. 输出的第一节点变成Linker本身。
  2. 第二个输出是当前进程,并且dlpi_name变为/system/bin/app_process(64)
  3. 第三个是一个动态库,dlpi_name已经是完整的pathname

3.2、总结

综上所述,使用表格整理dl_iterate_phdr()方法的版本情况:

版本 差异
Android 4.0 - Android 4.4 1、不支持32位ARM架构 2、dl_iterate_phdr回调basename 3、libdl.so不会被加载到内存
Android 5.0 - Android 5.1 1、dl_iterate_phdr支持所有架构
Android 6.0 - Android 7.1 1、dl_iterate_phdr需要加锁g_dl_mutex 2、dl_iterate_phdr回调pathname
Android 7.0 - Android 7.1 1、当前进程对应的dl_phdr_info.dlpi_name改为app_process(64)路径。
Android 8.0 - Android 15 1、dl_iterate_phdr回调中包含Linker本身 2、libdl.so会被加载到内存

至此,我们的目标就明确为以最新版本能力为准,对齐所有版本dl_iterate_phdr方法的能力,主要包括:

  1. 所有版本和架构都支持的dl_iterate_phdr()方法 。即需要实现在Android 4.0 - Android 4.4版本上的dl_iterate_phdr()方法。
  2. 所有版本dl_iterate_phdr()统一返回pathname。即兼容Android 4.0 - Android 5.1版本。
  3. 所有版本dl_iterate_phdr()调用前都加上锁。即兼容Android 4.0 - Android 5.1版本。
  4. 所有版本dl_iterate_phdr()回调的当前进程dlpi_name,都改为app_process(64)。即兼容Android 4.0 - Android 6.0版本。
  5. 所有版本dl_iterate_phdrd()的回调中都将包含Linker本身。即兼容Android 4.0 - Android 7.1版本。

下面将介绍如何完成每个目标。

二、dl_iterate_phdr低版本兼容

这里的低版本兼容是指在Android 4.0 - Android 4.4版本上实现dl_iterate_phdr方法的效果。

正如文章开头所说,动态库被Linker加载到内存后,会体现在proc/[pid]/maps文件的内容中。因此,通过遍历proc/[pid]/maps文件内容,找到每个动态库的信息,并在构造出对应的dl_phdr_info数据结构,就可以模拟dl_iterate_phdr方法。

1、找到动态库

我们可以使用在Android Hook - 解析proc/pid/maps文件中总结的工具库MapsVisitor来遍历proc/[pid]/maps文件内容,问题在于怎么识别哪些内存对应的是动态库

首先来看一下maps文件的例子,并且观察其特点:

shell 复制代码
12c00000-12e01000 rw-p 00000000 00:01 2213      /dev/ashmem/dalvik-main space (deleted)
...
# 1、特殊的内存空间,以[开头
7b4307e000-7b4317a000 rw-p 00000000 00:00 0     [stack:11818]
# 2、动态库,第一块内存权限为r-xp,即是可执行的
7b4317a000-7b4317c000 r-xp 00000000 fe:00 978   /system/lib64/libwebviewchromium_loader.so
7b4317c000-7b4318b000 ---p 00000000 00:00 0 
7b4318b000-7b4318c000 r--p 00001000 fe:00 978   /system/lib64/libwebviewchromium_loader.so
7b4318c000-7b4318d000 rw-p 00002000 fe:00 978   /system/lib64/libwebviewchromium_loader.so
...
# 3、一些字体
7b43a00000-7b43a40000 r--p 00000000 fe:00 652   /system/fonts/NotoSerif-BoldItalic.ttf
...
# 4、特殊内存
7b50a2a000-7b50a2b000 r-xp 00000000 00:00 0                              [vdso]
...

从上文可以看出,动态库对应的内存有以下特征:

  1. 内存对应动态库文件路径
  2. 内存具有可执行权限
  3. 内存对应的文件偏移是0

根据这些特征,就可以筛选出所有可能的动态库了。

事实上,加载到内存的动态库并非一定需要执行权限,例如部分.oat文件,但是在Android 4.4及以前,没有.oat文件,因此忽略这种可能性。

2、解析内存中的ELF信息

找到动态库所在的起始地址 void *handle后,就需要使用这个信息构造出dl_phdr_info

由于我们筛选出的内存对应的文件偏移是0,也就是对应ELF文件的文件头,因此可以通过ELF的Magic Number去初步检验,这块内存是否对应ELF文件信息。即

c 复制代码
#define ELFMAG "\177ELF"
#define SELFMAG 4
if (memcmp(handle, ELFMAG, SELFMAG) != 0) {
  //检验不通过
  return;
}

如果校验通过,说明可以进一步把内存按照ELF文件头的格式读取出来,为了存储这些信息,设计一个MemoryElf数据结构:

c 复制代码
typedef struct MemoryElf {
    const char *file_path;
    uintptr_t load_bias;
    const ElfW(Ehdr) *ehdr;
    const ElfW(Phdr) *phdr;
} MemoryElf;

接下来就可以根据ELF文件格式去读取了(如果不了解ELF文件格式,先阅读Android Hook - 动态加载so库):

c 复制代码
//1、handle是遍历maps时找到的可能是动态库的内存起始地址,filePath则是对应的文件路径。
MemoryElf *memory_elf_create(void *handle, const char *filePath){
  //魔数校验
  ...
  MemoryElf *memoryElf = (MemoryElf *) calloc(1, sizeof(MemoryElf));
  ...
  //2、强转成elf文件头
  memoryElf->ehdr = (const ElfW(Ehdr) *) handle;
  //3、根据段表偏移,找到段表
  memoryElf->phdr = (const ElfW(Phdr) *) ((uintptr_t) handle + memoryElf->ehdr->e_phoff);
  ...
}

对比dl_phdr_info的结构,目前已经有以下信息:

  1. dl_phdr_info.dlpi_name对应memoryElf.file_path
  2. dl_phdr_info.dlpi_phdr对应memoryElf.phdr
  3. dl_phdr_info.dlpi_phnum对应memoryElf.ehdr.e_phnum

剩下的工作就是计算dl_phdr_info.dlpi_addr,即load_bias

根据前文对load_bias的定义,它是第一个加载的段的地址phdr0_load_address,减去 phdr0->p_vaddr

memoryElf.phdr是段表,因此我们可以遍历段表找到p_vaddr最小的段,它就是加载到内存的第一个段。

在Linker源码中,加载ELF文件后也是这样做的,因此可以参考bionic/linker/linker_phdr.cpp中的phdr_table_get_load_size()来实现:

c 复制代码
static ElfW(Addr) phdr_table_get_load_size(const ElfW(Phdr) *phdr, ElfW(Half) e_phnum) {
    ElfW(Addr) min_addr = UINTPTR_MAX;
    bool found_pt_load = false;
  	//1、遍历段表
    for (int i = 0; i < e_phnum; i++) {
        const ElfW(Phdr)* phdr_i = &phdr[i];
      	//2、只处理类型为PT_LOAD的段,只要它们才会被加载到内存
        if(phdr_i->p_type != PT_LOAD){
            continue;
        }
        found_pt_load = true;
      	//3、找到p_vaddr的最小值
        if (phdr_i->p_vaddr < min_addr) {
            min_addr = phdr_i->p_vaddr;
        }
    }
  	//4、如果没有段会加载到内存,默认是0
    if(!found_pt_load){
        return 0;
    }
  	//5、需要对齐内存页边界
    min_addr = page_start(min_addr);  	
    return min_addr;
}

有了phdr0->p_vaddr以后,由于我们知道第一个加载的段在内存中的起始地址为void* handle,即phdr0_load_address,因此:

shell 复制代码
load_bias = (uintptr_t)handle - min_addr(即phdr0->p_vaddr)

至此,我们成功把maps中遍历到的ELF文件内存,使用它的起始地址handle和文件路径filepath两个参数,映射成dl_phdr_info数据结构,进而可以回调给外部使用。

三、dl_iterate_phdr增强

增强是指补偿原生dl_iterate_phdr()在部分版本中的不足。

为了可以额外处理dl_iterate_phdr()的结果,可对回调进行包装,即按照如下处理顺序:

c 复制代码
static int dl_iterate_phdr_by_linker_callback(struct dl_phdr_info *info, size_t size, void *dataArr) {
  //4、把复合的参数拆开,从而拿到外部真正的入参
  uintptr_t *params = (uintptr_t *) (dataArr);
  dl_iterate_phdr_cb_t callback = (dl_iterate_phdr_cb_t) (params[0]);
  void *data = (void *) (params[1]);
  MapsVisitor_t *mapVisitor = (MapsVisitor_t *) (params[2]);
  ...
  //5、对info进行加工
  ...
}

//dl_iterate_phdr_by_system()会对dl_iterate_phdr()进行增强
static int dl_iterate_phdr_by_system(dl_iterate_phdr_cb_t callback, void *data) {
  ...
  //1、MapsVisitor_t用于遍历maps文件
  MapsVisitor_t *mapVisitor = maps_visitor_create(0);
  ...
  //2、将外部传入的callback、data等参数,合成一个参数
  uintptr_t params[] = {(uintptr_t) callback, (uintptr_t) data, (uintptr_t) mapVisitor};
  ...
  //3、调用系统dl_iterate_phdr()方法,传入我们自己的dl_iterate_phdr_by_linker_callback,这样我们在callback给外部前,就可以对dl_iterate_phdr()方法的结果进行加工
	result = dl_iterate_phdr(dl_iterate_phdr_by_linker_callback, params);
}

整体的处理流程如上,接下来关注每个加工细节。

1、回调Linker本身

正如背景中的描述,在Android 4.0 - Android 7.1版本中,dl_iterate_phdr()不会回调Linker本身。

为此,我们可以主动构造 Linker本身对应的dl_phdr_info,然后回调给外部

而构造一个dl_phdr_info,只需要两个参数,其中Linker对应的filepath是固定的,即:

c 复制代码
#ifndef __LP64__
//32位系统
#define LINKER_PATH "/system/bin/linker"
#else
//64位系统
#define LINKER_PATH "/system/bin/linker64"
#endif

问题在于获取Linker在内存中的起始地址。

和前面一样,这样可以通过遍历proc/[pid]/maps文件内容得到,只需要匹配filepath是否Linker即可。

这里提供另外一个更简单的方式,即通过getauxval()方法。

getauxval() 是一个在 Unix-like 操作系统(如 Linux)中用于获取进程特定的辅助值(auxiliary values)的函数。它提供了一种通过程序运行时的附加信息来访问系统和进程的额外数据。通常这些数据是操作系统为当前进程提供的硬件、系统、或者运行时环境信息。

其函数原型为:

c 复制代码
#include <sys/auxv.h>
#include <stdint.h>

//type: 需要查询的辅助值的类型标识符。这个参数指定了想要获取的辅助信息的类型,getauxval() 会根据它返回相应的值。
//返回值:返回指定类型的辅助值,如果没有该类型的信息,则返回 0
unsigned long getauxval(unsigned long type);

因此,使用getauxval(AT_BASE)就可以获得动态链接器的起始地址。

AT_BASE 只是其中一种type,它的具体含义和其他类型type,可以从文档中了解。

而在Android 系统中,getauxval()方法需要在Android 5.0以后才存在,因此在之前的版本可以使用遍历maps的方式兜底。

至此,构造Linker对应的dl_phdr_info的要素都已齐全。

2、回调pathname

正如背景中的描述,在Android 4.0 - Android 5.1版本,dl_iterate_phdr()回调的dl_phdr_info.dlpi_namebasename

因此目标就是通过dl_phdr_info找到其对应pathname,即动态库的完整文件路径。

这一点同样可以通过遍历proc/[pid]/maps文件内容做到。

前面我们遍历maps时,通过ELF文件在内存的起始地址,进而计算出load_bias,反过来,通过dl_phdr_info.dlpi_addr(也就是load_bias),同样可以计算出ELF文件在内存的起始地址。

即:

shell 复制代码
dl_phdr_info.dlpi_addr(load_bias) + min_addr = handle(起始地址)

其中min_addr的计算方法和之前的一致。

如果这个起始地址在maps的某行记录(记录内容包括内存起始地址、结束地址、filepath等)的内存范围内,那么这行记录对应的filepath,就是动态库的路径。

具体来说,伪代码如下:

c 复制代码
//1、计算出起始地址,使用dl_phdr_info.dlpi_addr(load_bias) + min_addr
uintptr_t base = (uintptr_t) (get_ehdr_from_dl_phdr_info(info));
maps_visitor_reset(mapVisitor);
//2、遍历maps文件内容
while (maps_visitor_has_next(mapVisitor)) {
    MapItem mapItem = {0};
    if (maps_visitor_next(mapVisitor, &mapItem) == NULL) {
        continue;
    }
  	//3、起始地址不在范围内,继续往下找
    if (mapItem.start_address > base) {
        break;
    }
    if (mapItem.end_address <= base) {
        continue;
    }
  	//4、在范围内,此时path就是动态库的完整路径,可以覆盖原dlpi_name
    info->dlpi_name = mapItem.path;
    break;
}

3、修改当前进程名为app_process(64)

Android 4.0 - Android 6.0版本中,dl_iterate_phdr()回调当前进程的dl_phdr_info.dlpi_name时,会返回包名而不是可执行文件的路径(即app_process(64))。

例如:

shell 复制代码
...
# 第二个输出是当前进程名,这里是应用包名
info->dlpi_name: com.muye.dl_iterate_phdr_enhance
info->dlpi_addr: 0x7b50a18000
info->dlpi_phdr: 0x7b50a18040
...

这里需要统一成返回可执行文件的路径。

实际上,步骤2回调pathname 就可以同时达到这个目的,我们可以不只是对系统版本进行判断,才进行basename到pathname的转换,而是直接判断dl_phdr_info.dlpi_name是否完整路径。

即:

c 复制代码
//1、不是以/开头,说明不是完整路径,进行转换。另外,其他非动态库对应的特殊内存,则以[开头,也不需要处理。
if (info->dlpi_name[0] != '/' && info->dlpi_name[0] != '[') {
  //2、执行步骤2的basename到pathname的转换流程
  ...
}

这样当遍历到info->dlpi_name: com.muye.dl_iterate_phdr_enhance时,就会替换成/system/bin/app_process(64)了。

四、dl_iterate_phdr锁

Android 6.0及之后中,dl_iterate_phdr()会在获得**g_dl_mutex**锁后执行,这就需要我们弥补Android 4.0 - Android 5.1版本中的这一缺陷。

对于Android 4.0 - 4.4版本,是通过遍历maps来模拟dl_iterate_phdr()方法,因此只需要加一个我们自己定义的锁即可。

而对于Android 5.0 - 5.1版本,则需要找到**g_dl_mutex**锁的指针,在调用dl_iterate_phdr()方法前后进行加锁和解锁。

问题是怎么找到它?这需要进一步了解ELF文件结构。

g_dl_mutex的symbol

1、g_dl_mutex

先来看看g_dl_mutex在Android 5.1中的相关代码。

bionic/linker/dlfcn.cpp

c++ 复制代码
static pthread_mutex_t g_dl_mutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;

由此可见,g_dl_mutex是一个静态变量 ,类型为pthread_mutex_t,被初始化为可重入互斥锁。

对于已经初始化的静态变量,通常会存储在ELF文件的.data节中。这里的存储是指ELF文件会有一块空间,记录着静态变量的初始值。

当ELF文件被加载到内存后,对应的也会在进程内存有一块存储着这个值的内存区域,这就是运行时"活着"的静态变量。

2、符号

符号是ELF文件、动态链接中的一个重要概念,它用于标识程序中的函数、变量、类型或其他代码实体的元数据。

这么说仍然是太抽象了,来看一下使用代码怎么描述符号具备哪些属性。

2.1、符号的组成

elf.h

c 复制代码
//符号
typedef struct elf64_sym {
  //1、符号名,实际是名称在字符串表中的偏移
  Elf64_Word st_name;		/* Symbol name, index in string tbl */
  //2、低4位为type类型,剩余高位为bind绑定属性
  unsigned char	st_info;	/* Type and binding attributes */
  //3、记录符号可见性
  unsigned char	st_other;	/* No defined meaning, 0 */
  //4、符号所在节的序号
  Elf64_Half st_shndx;		/* Associated section index */
  //5、符号值,函数为地址,数据为数据值地址
  Elf64_Addr st_value;		/* Value of the symbol */
  //6、符号大小。如果符号没有大小或未知大小,则为0
  Elf64_Xword st_size;		/* Associated symbol size */
} Elf64_Sym;

其中比较重要的属性有:

  • 符号名 st_namest_name是符号名称在字符串表中的偏移量。

  • 符号值 st_value。对于函数,符号值是函数地址 ,对应数据,符号值是数据值所在的地址

  • 符号所在节 st_shndx。表示符号所在节在节表中的下标。对于某些特殊符号,st_shndx有特殊值。

    • 尤其需要关注st_shndx值为SHN_UNDF(0)的情况,这表示该符号在本目标文件被引用到,但是定义在其他文件中
  • 符号类型和绑定属性 st_info

    • 绑定属性记录在高位,可能的枚举值为:

      shell 复制代码
      //本地符号。对于目标文件的外部不可见。相同的局部符号可以存在于多个文件中而不会互相干扰。
      #define STB_LOCAL 0
      //全局符号。这种符号可以被程序中的任何其他翻译单元引用或修改,意味着它在整个程序范围内是可见的。
      #define STB_GLOBAL 1
      //弱符号。弱符号是相对于强符号的,特点是如果存在多个同名符号,链接器会选择 强符号,如果没有强符号,则使用弱符号。
      #define STB_WEAK 2
    • 符号类型 记录在st_info的低4位中,可能的类型有:

      shell 复制代码
      //未知类型
      #define STT_NOTYPE 0
      //该符号是个数据对象,比如变量、数组等
      #define STT_OBJECT 1
      //该符号是函数或其他可执行代码
      #define STT_FUNC 2
      //该符号表示一个段,这种符号必须是STB_LOCAL
      #define STT_SECTION 3
      //该符号表示文件名,一般都是该目标文件对应的源文件名
      #define STT_FILE 4
      //该符号标记未初始化的通用块
      #define STT_COMMON 5
      //该符号指定线程局部存储实体
      #define STT_TLS 6

对于ELF文件而言,符号是非常重要的概念,可以理解为全局变量、全局方法等都会在编译时生成对应的符号,而动态库之前的依赖调用,就是通过符号作为桥梁来连接的。

对于符号更详细的介绍,可以见官方文档符号节表

2.2、符号表

ELF所有的符号记录在符号表中,符号表是ELF文件中的一个节(从编译角度看)。

Android Hook - 动态加载so库中已经介绍过ELF文件的初步格式,包括文件头、节表和段表。

符号表就是类型为SYMTAB的节:

elf.h

shell 复制代码
#define SHT_SYMTAB 2

我们可以使用readelf010Editor 等工具(在Android Hook - 动态加载so库中有介绍)查看ELF文件的节表信息。

这里选择使用readelf来查看Android 5.1版本的linker的节信息(linker位于/system/bin/linker64,读者可以自行从模拟器中导出):

shell 复制代码
/Users/XXX/Library/Android/sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-readelf -S linker64                
There are 20 section headers, starting at offset 0x122f0:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  ...
  [ 2] .dynsym           DYNSYM          00000000000001e8 0001e8 000180 18   A  3   3  8
  [ 3] .dynstr           STRTAB          0000000000000368 000368 000069 00   A  0   0  1
  ...
  [13] .data             PROGBITS        0000000000022000 012000 0001f8 00  WA  0   0 32
  ...
  [17] .shstrtab         STRTAB          0000000000000000 012240 0000ab 00      0   0  1
  [18] .symtab           SYMTAB          0000000000000000 0127f0 005670 18     19 915  8
  [19] .strtab           STRTAB          0000000000000000 017e60 002922 00      0   0  1
...
  • .symtab.dynsym类型分别SYMTABDYNSYM
    • .dynsym记录的是动态链接 过程中会使用到的符号,因此被称为动态符号表。动态链接过程中并不需要使用到所有的符号。
    • .symtab则记录的是全部符号,即.dynsym.symtab子集。通常用于静态链接时要解析的符号。
  • .dynstr.strtab都是类型为STRTAB 的节。
    • 同理,.dynstr存储.symtab的符号名称,.strtab存储.symtab的符号名称。

接下来,还可以使用readelf查看符号表的具体内容:

shell 复制代码
/Users/XXX/Library/Android/sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-readelf -s linker64
# 动态符号表
Symbol table '.dynsym' contains 16 entries:
   Num:    Value          Size Type    Bind   Vis       Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT   UND 
     1: 0000000000001540     0 SECTION LOCAL  DEFAULT     5 .text
     2: 0000000000021570     0 SECTION LOCAL  DEFAULT    10 .data.rel.ro
     3: 0000000000023b40     0 NOTYPE  LOCAL  DEFAULT    14 _bss_end__
     4: 00000000000221f8     0 NOTYPE  LOCAL  DEFAULT    14 __bss_start__
     5: 0000000000023b40     0 NOTYPE  LOCAL  DEFAULT    14 __bss_end__
     6: 00000000000221f8     0 NOTYPE  LOCAL  DEFAULT    14 __bss_start
     7: 0000000000023b40     0 NOTYPE  LOCAL  DEFAULT    14 __end__
     8: 00000000000221f8     0 NOTYPE  LOCAL  DEFAULT    13 _edata
     9: 0000000000023b40     0 NOTYPE  LOCAL  DEFAULT    14 _end
    10: 0000000000002494    20 FUNC    GLOBAL DEFAULT     5 malloc
    11: 00000000000024d0    20 FUNC    GLOBAL DEFAULT     5 calloc
    12: 00000000000075fc     4 FUNC    GLOBAL DEFAULT     5 rtld_db_dlactivity
    13: 0000000000001694    12 FUNC    GLOBAL DEFAULT     5 _start
    14: 00000000000024bc    20 FUNC    GLOBAL DEFAULT     5 realloc
    15: 00000000000024a8    20 FUNC    GLOBAL DEFAULT     5 free

# 符号表
Symbol table '.symtab' contains 922 entries:
   Num:    Value          Size Type    Bind   Vis       Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT   UND 
     1: 0000000000000190     0 SECTION LOCAL  DEFAULT     1 .hash
		...
    50: 0000000000022120    40 OBJECT  LOCAL  DEFAULT    13 __dl__ZL10g_dl_mutex
    ...

表中每项的数据分别表示符号值Value符号大小Size符号类型Type绑定属性Bind可见性Vis符号所在节Ndx符号名Name

读者可以自行对比struct elf64_sym来理解。

也可以选择更直观的010Editor来查看,效果如下:

2.3、符号的例子

符号是ELF文件中的概念,在我们使用**c/c++**编程时,变量和函数等会被编译成符号,下面来看一个例子,说明不同类型的变量和函数,会编译成怎么样的符号。

代码如下:

c 复制代码
#include <stdio.h>

//1、函数默认就是强符号
void strong_func(){
    printf("强符号strong_func函数\n");
}

//2、__attribute__((weak)) 会强制把函数变为弱符号
__attribute__((weak)) void weak_func() {
    printf("库中的弱符号默认实现\n");
}

//3、已经初始化的全部变量,是强符号
int global_var_init = 100;

//4、未初始化的全部变量,是强符号
int global_var_un_init;

//5、已经初始化的静态变量。生成动态库时,默认是不会保留静态变量的符号的,这里使用__attribute__((used))来强行保留它。
static int static_var_init __attribute__((used)) = 200;

//6、未初始化的静态变量。生成动态库时,默认是不会保留静态变量的符号的,这里使用__attribute__((used))来强行保留它。
static int static_var_un_init __attribute__((used));

//7、静态方法。生成动态库时,默认是不会保留静态变量的符号的,这里使用__attribute__((used))来强行保留它。
static __attribute__((used)) void static_func () {
  	//8、局部变量,不会生成符号
  	int local_var = 100;
    printf("本地符号函数static_func() static_var_init = %d, static_var_un_init = %d, scope_var = %d\n", static_var_init, static_var_un_init, local_var);
}

然后进行交叉编译成动态库:

shell 复制代码
/Users/XXX/Library/Android/sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android34-clang -fPIC -shared -o libsymbol.so symbol.c

再使用readelf来查看libsymbol.so中的符号:

c 复制代码
Symbol table '.dynsym' contains 9 entries:
   Num:    Value          Size Type    Bind   Vis       Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT   UND 
     ...
     //1、动态符号表中,没有静态变量和静态方法
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND printf@LIBC
     5: 00000000000017c8    28 FUNC    GLOBAL DEFAULT    12 strong_func
     6: 00000000000017e4    28 FUNC    WEAK   DEFAULT    12 weak_func
     7: 0000000000003a50     4 OBJECT  GLOBAL DEFAULT    18 global_var_init
     8: 0000000000003a5c     4 OBJECT  GLOBAL DEFAULT    19 global_var_un_init

Symbol table '.symtab' contains 44 entries:
   Num:    Value          Size Type    Bind   Vis       Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT   UND 
     ...
    //2、文件名称,也是一个符号,类型为FILE
    23: 0000000000000000     0 FILE    LOCAL  DEFAULT   ABS symbol.c
    ...
    //3、静态方法和变量和符号,被强行保留了,绑定属性为LOCAL
    25: 0000000000001800    44 FUNC    LOCAL  DEFAULT    12 static_func
    26: 0000000000003a54     4 OBJECT  LOCAL  DEFAULT    18 static_var_init
    27: 0000000000003a58     4 OBJECT  LOCAL  DEFAULT    19 static_var_un_init
    ...
    //4、全局方法是全局符号,类型为FUNC,绑定属性为GLOBAL,
    39: 00000000000017c8    28 FUNC    GLOBAL DEFAULT    12 strong_func
    //5、printf是外部导入的方法,Ndx为UND,说明需要在动态链接时查找
    40: 0000000000000000     0 FUNC    GLOBAL DEFAULT   UND printf
    //6、可以强制把函数定义为弱符号
    41: 00000000000017e4    28 FUNC    WEAK   DEFAULT    12 weak_func
    //7、全局方法是全局符号,类型为OBJECT, 绑定属性为GLOBAL
    42: 0000000000003a50     4 OBJECT  GLOBAL DEFAULT    18 global_var_init
    43: 0000000000003a5c     4 OBJECT  GLOBAL DEFAULT    19 global_var_un_init

对比源码和符号表,可以看出:

  • 静态变量、静态方法 默认不会保留符号。
    1. 静态变量、静态方法只会在源文件中使用,在编译时只需要转换成偏移/地址即可访问,因此默认不需要保留符号。
    2. 为了展示效果,正如源码中的注释,使用了__attribute__((used))来保留静态变量、静态方法的符号。
    3. 即使保留了符号,静态变量、静态方法的绑定属性为LOCAL ,说明这是一个本地符号,只在源文件中使用。
    4. static_var_init的Ndx为18,static_var_un_init则为19,分别对应.data.bss,说明静态变量是否初始化,会影响它应该存储在哪个节。
  • 全局变量、方法 默认为全局符号强符号
    1. 全局符号 ,是相对于本地符号而言的。意味着对整个程序都可见。
    2. 强符号 是相对于弱符号而言的。强符号在程序中只能有一个(即不能同名),如果出现同名则出现符号冲突。而弱符号则可能存在多个同名符号,编译时会按照规则裁决使用哪个符号。
    3. 符号类型为OBJECT 表示变量,FUNC表示函数。
  • printf导入符号
    • 导入符号 是相对导出符号 而言的。通常在都记录在.dynsym表中,而导入符号是在当前模块(比如一个共享库或可执行文件)中使用的符号,但其实际的定义和实现是在其他地方的共享库或目标文件中
    • 导入符号 特征是Ndx值为UND ,其他都是导出符号,即其他动态库可以在动态链接时引用当前动态库的符号。
  • 局部变量不会生成对应符号。

关于符号详细的知识,可以参考程序员的自我修养---链接、装载与库中的符号相关篇章。

2.4、g_dl_mutex对应的符号

在了解什么是符号后,终于可以来查看g_dl_mutex对应的符号了。

shell 复制代码
/Users/XXX/Library/Android/sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-readelf -s /Users/chenkaiyi/Downloads/linker64 | grep g_dl_mutex
    50: 0000000000022120    40 OBJECT  LOCAL  DEFAULT    13 __dl__ZL10g_dl_mutex

可以看出:

  1. g_dl_mutex变量对应的符号名为__dl__ZL10g_dl_mutex。这是由于linker是C++编写的,C++在编译时为了防止动态链接过程中的符号冲突(即存在多个同名的强符号) ,会对变量名进行名字修饰(mangling),从而减少冲突的可能。
  2. __dl__ZL10g_dl_mutex是一个本地符号,这和源码中的static一致。这同时说明__dl__ZL10g_dl_mutex不在.dynsym中,而是在symtab
  3. 0x0000000000022120是指符号对应的变量(类型为OBJECT,即变量)所在的地址。这个地址尤其重要,和段的p_vaddr一样,结合load_bias,就可以计算出变量在内存中的地址

3、找到g_dl_mutex

在了解符号后,就可以使用这个思路找g_dl_mutex在内存中的地址了。

具体来说:

  1. 找到Linker在内存中的起始地址(这在前面已经做到了,使用getauxval()),进而转成ELF文件头

  2. 使用ELF文件头,找到节表 。这里需要注意,节表是编译时使用的,因此不会被加载到内存,为了找到它,我们需要在Linker对应的磁盘文件(即system/bin/linker(64))中查找,。

  3. 使用节表,找到符号表 .symtab__dl__ZL10g_dl_mutex符号是本地符号,因此存在符号表.symtab中,通用符号表.symtab是不会被加载到内存的,因此需要读取磁盘文章。

  4. 遍历符号表,通过符号名 匹配是否为__dl__ZL10g_dl_mutex,从而找到对应的符号,读取st_value,即符号对应的变量所在的地址。

    1. 要找到符号名称,则需要找到字符串表.dynstr.strtab 节的类型都是STRTAB ,因此单靠类型无法区分两者,需要依赖节的名称。所有节的名称都存储在.shstrtab节,而.shstrtab节则由ELF文件头的e_shstrndx属性决定,该属性表明.shstrtab节在节表中的序号。
  5. st_value记录的地址,加上load_bias就是变量所在内存的地址,即变量的地址 。使用这个指针,强转成pthread_mutex_t,就可以实现加锁和解锁了。

接下来是伪代码,鼓励读者自行实现:

c 复制代码
typedef struct LinkerElf {
    MemoryElf* memoryElf;
    const ElfW(Shdr) *shdr;
    const ElfW(Sym) *symtab;
    size_t symCnt;
    const char *strtab;
    const char *shstrtab;
} LinkerElf;

LinkerElf *linker_elf_create(void *handle, const char *filePath) {
  ...
  LinkerElf *linkerElf = (LinkerElf *) calloc(1, sizeof(LinkerElf));
  ...
  //1、通过Linker的起始地址和名称,构造memoryElf,从而读取ELF文件头
  linkerElf->memoryElf = memory_elf_create(handle, filePath);
  ...
  //2、将Linker文件读取到内存
  FILE* file = fopen(filePath, "rb");

  //3、初始化section table
  initSectionTable(linkerElf, file);
  //4、Section名称字符串表
  initSectionStringTable(linkerElf, file);
  //5、字符串表
  initStringTable(linkerElf, file);
  //6、符号表
  initSymbolTable(linkerElf, file);
}

首先打开文件,将所需的各个表读取到内存。

以从文件中读取节表(Section Table)为例:

c 复制代码
static void initSectionTable(LinkerElf *linkerElf, FILE* file) {
  	//1、根据elf文件头记录的节表偏移e_shoff找到节表起始位置
    if(fseek(file, linkerElf->memoryElf->ehdr->e_shoff, SEEK_SET) != 0){
        return;
    }
  	//2、根据elf文件头记录的节表项大小e_shentsize和节表项数量e_shnum,计算节表总大小
    size_t size = linkerElf->memoryElf->ehdr->e_shentsize * linkerElf->memoryElf->ehdr->e_shnum;
    if(size == 0){
        return;
    }
    ElfW(Shdr)* shdr = malloc(size);
    if(shdr == NULL){
        return;
    }
  	//3、读取节表达内存
    if(fread(shdr, 1, size, file) != size){
        free(shdr);
        return;
    }
    linkerElf->shdr = shdr;
}

其他表读取的方式大同小异,不赘述,可以从**DlIteratePhdrEnhance**中了解。

当所需表都被读取到内存后,就可以遍历符号表找到目标符号:

c 复制代码
const ElfW(Sym) *linker_elf_look_up_symbol(LinkerElf *linkerElf, const char *symbolName){
    //1、遍历符号表
    for(size_t i = 0; i < linkerElf->symCnt; i++){
      	//2、访问每个符号
        const ElfW(Sym) * symbol = linkerElf->symtab + i;
        if(!SYMTAB_IS_EXPORT_SYM(symbol->st_shndx)){
            continue;
        }
      	//3、从字符串表找到符号对应的符号名
        const char* name = linkerElf->strtab + symbol->st_name;
      	//4、匹配符号名,判断是否目标符号
        if(strcmp(name, symbolName) == 0) {
            return symbol;
        }
    }
    return NULL;
}

#define XDL_LINKER_SYM_G_DL_MUTEX           "__dl__ZL10g_dl_mutex"
LinkerElf *linkerElf = linker_elf_create((void *) base, LINKER_PATH);
//5、使用上述方法,找到g_dl_mutex对应的符号
const ElfW(Sym) * symbol = linker_elf_look_up_symbol(linkerElf,XDL_LINKER_SYM_G_DL_MUTEX);
//6、根据内存load_bias,计算变量g_dl_mutex的地址
linker_mutex = (pthread_mutex_t *) (linkerElf->memoryElf->load_bias + symbol->st_value);

至此,我们顺利访问Linker中的变量g_dl_mutex,继而可以使用pthread_mutex_lock()pthread_mutex_unlock()进行加锁和解锁了。

五、总结

本文讲述了以下核心内容:

  1. 介绍了dl_iterate_phdr()方法的作用和系统版本之间的差异。差异部分主要通过分析不同版本的源码和结合模拟器测试得出。
  2. 修复/增强dl_iterate_phdr()在低版本上的能力,核心思路是通过proc/[pid]/maps来补足dl_phdr_info的信息。其中关键是如何从内存中解析出ELF文件结构和计算load_bias
  3. 介绍了符号 的相关概念,进而成功找到内存中动态库的变量/函数 ,这是一个重要的技巧

其中对符号的理解是最重要的,本文只介绍了使用到的部分,读者可以结合符号节表程序员的自我修养---链接、装载与库深入理解。

六、写在最后

1、源码下载

DlIteratePhdrEnhance

2、免责声明

本文涉及的代码,旨在展示和描述方案的可行性,可能存bug或者性能问题。

不建议未经修改验证,直接使用于生产环境。

3、转载声明

本文欢迎转载,转载请注明出处

4、留言讨论

你是否也在现实开发中遇到类似的场景或者应用,是否有更多的意见和想法,欢迎留言一起学习讨论。

5、一键三连

创作不易,期待点赞收藏

如果你对更多的Android Hook开发技巧、思路感兴趣的,欢迎关注我的栏目。

后续将提供更多优质内容,硬核干货。

相关推荐
kk爱闹1 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空2 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭3 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日4 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安4 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑4 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟8 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡9 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0010 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体
zhangphil11 小时前
Android理解onTrimMemory中ComponentCallbacks2的内存警戒水位线值
android