标签:动态调试 nan idg run pat arm flag png head
title: so的装载与链接
categories: 逆向与协议分析
toc: true
mathjax: true
tags:
调用 dl_open 后,中间经过 dlopen_ext, 到达第一个主要函数 do_dlopen:
soinfo* do_dlopen(const char* name, int flags, const Android_dlextinfo* extinfo) {
protect_data(PROT_READ | PROT_WRITE);
soinfo* si = find_library(name, flags, extinfo); // 查找 SO
if (si != NULL) {
si->CallConstructors(); // 调用 SO 的 init 函数
}
protect_data(PROT_READ);
return si;
}
do_dlopen 调用了两个重要的函数,第一个是find_library, 第二个是 soinfo 的成员函数 CallConstructors,find_library 函数是 SO 装载链接的后续函数, 完成 SO 的装载链接后, 通过 CallConstructors 调用 SO 的初始化函数。
find_library 直接调用了 find_library_internal,下面直接看 find_library_internal函数:
static soinfo* find_library_internal(const char* name, int dlflags, const Android_dlextinfo* extinfo) {
if (name == NULL) {
return somain;
}
soinfo* si = find_loaded_library_by_name(name); // 判断 SO 是否已经加载
if (si == NULL) {
TRACE("[ ‘%s‘ has not been found by name. Trying harder...]", name);
si = load_library(name, dlflags, extinfo); // 继续 SO 的加载流程
}
if (si != NULL && (si->flags & FLAG_LINKED) == 0) {
DL_ERR("recursive link to \"%s\"", si->name);
return NULL;
}
return si;
}
find_library_internal 首先通过 find_loaded_library_by_name 函数判断目标 SO 是否已经加载,如果已经加载则直接返回对应的soinfo指针,没有加载的话则调用 load_library 继续加载流程,下面看 load_library 函数。
static soinfo* load_library(const char* name, int dlflags, const Android_dlextinfo* extinfo) {
int fd = -1;
...
// Open the file.
fd = open_library(name); // 打开 SO 文件,获得文件描述符 fd
ElfReader elf_reader(name, fd); // 创建 ElfReader 对象
...
// Read the ELF header and load the segments.
if (!elf_reader.Load(extinfo)) { // 使用 ElfReader 的 Load 方法,完成 SO 装载
return NULL;
}
soinfo* si = soinfo_alloc(SEARCH_NAME(name), &file_stat); // 为 SO 分配新的 soinfo 结构
if (si == NULL) {
return NULL;
}
si->base = elf_reader.load_start(); // 根据装载结果,更新 soinfo 的成员变量
si->size = elf_reader.load_size();
si->load_bias = elf_reader.load_bias();
si->phnum = elf_reader.phdr_count();
si->phdr = elf_reader.loaded_phdr();
...
if (!soinfo_link_image(si, extinfo)) { // 调用 soinfo_link_image 完成 SO 的链接过程
soinfo_free(si);
return NULL;
}
return si;
}
load_library 函数呈现了 SO 装载链接的整个流程,主要有3步:
在编译 SO 时,可以通过链接选项
-init
或是给函数添加属性__attribute__((constructor))
来指定 SO 的初始化函数,这些初始化函数在 SO 装载链接后便会被调用,再之后才会将 SO 的 soinfo 指针返回给 dl_open 的调用者。SO 层面的保护手段,有两个介入点, 一个是 jni_onload, 另一个就是初始化函数,比如反调试、脱壳等,逆向分析时经常需要动态调试分析这些初始化函数。
完成 SO 的装载链接后,返回到 do_dlopen 函数, do_open 获得 find_library 返回的刚刚加载的 SO 的 soinfo,在将 soinfo 返回给其他模块使用之前,最后还需要调用 soinfo 的成员函数 CallConstructors。
soinfo* do_dlopen(const char* name, int flags, const Android_dlextinfo* extinfo) {
...
soinfo* si = find_library(name, flags, extinfo);
if (si != NULL) {
si->CallConstructors();
}
return si;
...
}
CallConstructors 函数会调用 SO 的首先调用所有依赖的 SO 的 soinfo 的 CallConstructors 函数,接着调用自己的 soinfo 成员变量 init 和调用init_array 指定的函数,这两个变量在在解析 dynamic section 时赋值。
void soinfo::CallConstructors() {
//如果已经调用过,则直接返回
if (constructors_called) {
return;
}
// 调用依赖 SO 的 Constructors 函数
get_children().for_each([] (soinfo* si) {
si->CallConstructors();
});
// 调用 init_func
CallFunction("DT_INIT", init_func);
// 调用 init_array 中的函数
CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);
}
有了以上分析基础后,在需要动态跟踪初始化函数时,我们就知道可以将断点设在 do_dlopen 或是 CallConstructors。
在病毒和版权保护领域,“壳”一直扮演着极为重要的角色。通过加壳可以对代码进行压缩和加密,同时再辅以虚拟化、代码混淆和反调试等手段,达到防止静态和动态分析。
在 Android 环境中,Native 层的加壳主要是针对动态链接库 SO,SO 加壳的示意图如下:
加壳工具、loader、被保护SO。
下面对 SO 加壳的关键技术进行简单介绍。
Linker 加载完 loader 后,loader 需要将被保护的 SO 加载起来,这就要求 loader 的代码需要被执行,而且要在 被保护 SO 被使用之前,前文介绍了 SO 的初始化函数便可以满足这个要求,同时在 Android 系统下还可以使用 JNI_ONLOAD 函数,因此 loader 的执行时机有两个选择:
- SO 的 init 或 initarray
- jni_onload
loader 开始执行后,首先需要在内存中还原出 SO,SO 可以是经过加密、压缩、变换等手段,也可已单纯的以完全明文的数据存储,这与 SO 加壳的技术没有必要的关系,在此不进行讨论。
在内存中还原出 SO 后,loader 还需要执行装载和链接,这两个过程可以完全模仿 Linker 来实现,下面主要介绍一下相对 Linker,loader 执行这两个过程有哪些变化。
还原后的 SO 在内存中,所以装载时的主要变化就是从文件装载到从内存装载。
Linker 在装载 PT_LAOD segment时,使用 SO 文件的描述符 fd:
void* seg_addr = mmap(reinterpret_cast<void*>(seg_page_start),
file_length,
PFLAGS_TO_PROT(phdr->p_flags),
MAP_FIXED|MAP_PRIVATE,
fd_,
file_page_start);
按照 Linker 装载,PT_LAOD segment时,需要分为两步:
// 1、改用匿名映射
void* seg_addr = mmap(reinterpret_cast<void*>(seg_page_start),
file_length,
PFLAGS_TO_PROT(phdr->p_flags),
MAP_FIXED|MAP_PRIVATE,
-1,
0);
// 2、将内存中的 segment 复制到映射的内存中
memcpy(seg_addr+seg_page_offset, elf_data_buf + phdr->p_offset, phdr->p_filesz);
注意第2步复制 segment 时,目标地址需要加上 seg_page_offset,seg_page_offset 是 segment 相对与页面起始地址的偏移。
其他的步骤基本按照 Linker 的实现即可,只需要将一些从文件读取修改为从内存读取,比如读 elfheader和program header时。
soinfo 保存了 SO 装载链接和运行时需要的所有信息,为了维护相关的信息,loader 可以照搬 Linker 的 soinfo 结构,用于存储中间信息,装载链接结束后,还需要将 soinfo 的信息修复到 Linker 维护的soinfo。
链接过程完全是操作内存,不论是从文件装载还是内存装载,链接过程都是一样,完全模仿 Linker 即可。
另外链接后记得顺便调用 SO 初始化函数( init 和 init_array )。
SO 加壳的最关键技术点在于 soinfo 的修复,由于 Linker 加载的是 loader,而实际对外使用的是被保护的 SO,所以 Linker 维护的 soinfo 可以说是错误,loader 需要将自己维护的 soinfo 中的部分信息导出给 Linker 的soinfo。
修复过程如下:
self_soinfo.base = soinfo.base
。需要导出的主要有以下几项:Android Linker 与 SO 加壳技术 : https://blog.csdn.net/hgl868/article/details/52759921
IDA调试android so的.init_array数组:https://www.cnblogs.com/bingghost/p/6297325.html
Android NDK中.init段和.init_array段函数的定义方式:https://www.dllhook.com/post/213.html
Linker学习笔记:https://wooyun.js.org/drops/Android Linker学习笔记.html
标签:动态调试 nan idg run pat arm flag png head
原文地址:https://www.cnblogs.com/runope/p/13934175.html