根据您的问题,不清楚您对哪个 Android 版本感兴趣,但您似乎正在查看较旧的 Android 版本(因为它使用 dlfcn.c 而不是 dlfcn.cpp)。我将讨论基于Android 6的劫持过程。
对于较新的 Android 版本,流程基本相同,只是部分方法名和文件名发生了变化。
在bionic/README.md中可以找到如下描述:
libdl/ --- libdl.so
动态链接器接口库。这实际上只是一堆存根
动态链接器用指向它自己的实现的指针替换
运行。这就是dlopen(3) 之类的东西所在的地方。
链接器/ --- /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
linker 和 linker64 的高级入口点可以在 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 函数使用solist = get_libdl_info(); 初始化全局变量static soinfo* solist;。
struct soinfo 在bionic/linker/linker.h 中定义,表示链表中的一个节点(通过有一个成员soinfo* next;)。这种链表中的每个节点都通过成员symtab_ 保存有关共享对象的信息,包括符号表。
get_libdl_info 返回一个带有单个条目的链表,表示libdl.so 共享对象。然而,这个节点的符号表不是用指向来自libdl.so的存根函数的指针初始化的,而是用真正的实现:symtab_成员用__libdl_info->symtab_ = g_libdl_symtab;初始化。 g_libdl_symtab 表被初始化为here,并带有指向真实dlopen 等的指针。
所以我们找到了劫持发生的地方:链接器初始化了一个共享对象信息列表,其中包含libdl.so的条目作为第一个元素,符号表指向dlopen的实际实现等。 ,而不是存根。本节的其余部分是关于如何使用这个链表来理解为什么这种劫持是有效的。
__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]
...
随着指向dynamic 表的指针初始化,for_each_dt_needed 辅助函数变得可用,它为dynamic 表中的每个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 列表的末尾。换句话说,它做了一个依赖图的Breadth-first traversal。
// 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)) {
这正是solist,最初填充了libdl.so 的劫持条目,指向dlopen 等的非存根实现。因此,每当二进制文件在NEEDED 列表中具有libdl.so 时在其dynamic部分,加载过程总会发现libdl.so已经被加载并返回被劫持的soinfo。
如果在solist 中没有找到库,find_library_internal 函数会调用load_library 来读取实际的库文件。它为此库创建一个新的soinfo 条目,并使用soinfo_alloc 将其附加到全局solist 的末尾(使用始终指向以solist 开头的列表末尾的sonext 全局变量) .