从你的问题中不清楚你感兴趣的是哪个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
中找到。
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
):
extern "C" ElfW(Addr) __linker_init(void* raw_args) {
这个__linker_init
函数初始化了全局变量static soinfo* solist;
,它还初始化了其他内容,其中solist = get_libdl_info();
将其赋值为返回的链接库信息。
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
表在这里进行了初始化,其中包含指向真正的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() {
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
:
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”列表的末尾。换句话说,它对依赖图执行广度优先遍历。
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
的末尾。