1.背景
Android 系统安全愈发重要,像传统pc安全的可执行文件加固一样,应用加固是Android系统安全中非常重要的一环。目前Android 应用加固可以分为dex加固和Native加固,Native 加固的保护对象为 Native 层的 SO 文件,使用加壳、反调试、混淆、VM 等手段增加SO文件的反编译难度。目前最主流的 SO 文件保护方案还是加壳技术, 在SO文件加壳和脱壳的攻防技术领域,最重要的基础的便是对于 Linker 即装载链接机制的理解。对于非安全方向开发者,深刻理解系统的装载与链接机制也是进阶的必要条件。
本文详细分析了 Linker 对 SO 文件的装载和链接过程,最后对 SO 加壳的关键技术进行了简要的介绍。对于 Linker 的学习,还应该包括 Linker 自举、可执行文件的加载等技术,但是限于本人的技术水平,本文的讨论范围限定在 SO 文件的加载,也就是在调用dlopen("libxx.SO")之后,Linker 的处理过程。
本文基于 Android 5.0 AOSP 源码,仅针对 ARM 平台,为了增强可读性,文中列举的源码均经过删减,去除了其他 CPU 架构的相关源码以及错误处理。
另:阅读本文的读者需要对 ELF 文件结构有一定的了解。

2. SO 的装载与链接
2.1 整体流程说明
2.1.1 do_dlopen
调用 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 的初始化函数。

2.1.2 find_library_internal
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 函数。

2.13 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步:
1装载:创建ElfReader对象,通过 ElfReader 对象的 Load 方法将 SO 文件装载到内存
2分配soinfo:调用 soinfo_alloc 函数为 SO 分配新的 soinfo 结构,并按照装载结果更新相应的成员变量
3链接: 调用 soinfo_link_image 完成 SO 的链接
通过前面的分析,可以看到, load_library 函数中包含了 SO 装载链接的主要过程, 后文主要通过分析 ElfReader 类和 soinfo_link_image 函数, 来分别介绍 SO 的装载和链接过程。

2.2 装载
在 load_library 中, 首先初始化 elf_reader 对象, 第一个参数为 SO 的名字, 第二个参数为文件描述符 fd:
ElfReader elf_reader(name, fd)
之后调用 ElfReader 的 load 方法装载 SO。

 ...    // Read the ELF header and load the segments.    if (!elf_reader.Load(extinfo)) {        return NULL;    }    ...

ElfReader::Load 方法如下:

bool ElfReader::Load(const Android_dlextinfo* extinfo) {  return ReadElfHeader() &&             // 读取 elf header         VerifyElfHeader() &&           // 验证 elf header         ReadProgramHeader() &&         // 读取 program header         ReserveAddressSpace(extinfo) &&// 分配空间         LoadSegments() &&              // 按照 program header 指示装载 segments         FindPhdr();                    // 找到装载后的 phdr 地址}

ElfReader::Load 方法首先读取 SO 的elf header,再对elf header进行验证,之后读取program header,根据program header 计算 SO 需要的内存大小并分配相应的空间,紧接着将 SO 按照以 segment 为单位装载到内存,最后在装载到内存的 SO 中找到program header,方便之后的链接过程使用。
下面深入 ElfReader 的这几个成员函数进行详细介绍。

2.2.1 read&verify elfheader

bool ElfReader::ReadElfHeader() {  ssize_t rc = read(fd_, &header_, sizeof(header_));  if (rc != sizeof(header_)) {    return false;  }  return true;}

ReadElfHeader 使用 read 直接从 SO 文件中将 elfheader 读取 header 中,header_ 为 ElfReader 的成员变量,类型为 Elf32_Ehdr,通过 header 可以方便的访问 elf header中各个字段,elf header中包含有 program header table、section header table等重要信息。
对 elf header 的验证包括:
magic字节
32/64 bit 与当前平台是否一致
大小端
类型:可执行文件、SO …
版本:一般为 1,表示当前版本
平台:ARM、x86、amd64 …
有任何错误都会导致加载失败。

2.2.2 Read ProgramHeader

bool ElfReader::ReadProgramHeader() {  phdr_num_ = header_.e_phnum;      // program header 数量  // mmap 要求页对齐  ElfW(Addr) page_min = PAGE_START(header_.e_phoff);  ElfW(Addr) page_max = PAGE_END(header_.e_phoff + (phdr_num_ * sizeof(ElfW(Phdr))));  ElfW(Addr) page_offset = PAGE_OFFSET(header_.e_phoff);  phdr_size_ = page_max - page_min;  // 使用 mmap 将 program header 映射到内存  void* mmap_result = mmap(NULL, phdr_size_, PROT_READ, MAP_PRIVATE, fd_, page_min);  phdr_mmap_ = mmap_result;  // ElfReader 的成员变量 phdr_table_ 指向program header table  phdr_table_ = reinterpret_cast(reinterpret_cast(mmap_result) + page_offset);  return true;}

将 program header 在内存中单独映射一份,用于解析program header 时临时使用,在 SO 装载到内存后,便会释放这块内存,转而使用装载后的 SO 中的program header。

2.2.3 reserve space & 计算 load size

bool ElfReader::ReserveAddressSpace(const Android_dlextinfo* extinfo) {  ElfW(Addr) min_vaddr;  // 计算 加载SO 需要的空间大小  load_size_ = phdr_table_get_load_size(phdr_table_, phdr_num_, &min_vaddr);  // min_vaddr 一般情况为零,如果不是则表明 SO 指定了加载基址  uint8_t* addr = reinterpret_cast(min_vaddr);  void* start;  int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS;  start = mmap(addr, load_size_, PROT_NONE, mmap_flags, -1, 0);  load_start_ = start;  load_bias_ = reinterpret_cast(start) - addr;  return true;}

首先调用 phdr_table_get_load_size 函数获取 SO 在内存中需要的空间load_size,然后使用 mmap 匿名映射,预留出相应的空间。

关于loadbias: SO 可以指定加载基址,但是 SO 指定的加载基址可能不是页对齐的,这种情况会导致实际映射地址和指定的加载地址有一个偏差,这个偏差便是 load_bias_,之后在针对虚拟地址进行计算时需要使用 load_bias_ 修正。普通的 SO 都不会指定加载基址,这时min_vaddr = 0,则 load_bias_ = load_start_,即load_bias_ 等于加载基址,下文会将load_bias_ 直接称为基址。
 
下面深入phdr_table_get_load_size分析一下 load_size 的计算:使用成员变量 phdr_table 遍历所有的program header, 找到所有类型为 PT_LOAD 的 segment 的 p_vaddr 的最小值,p_vaddr + p_memsz 的最大值,分别作为 min_vaddr 和 max_vaddr,在将两个值分别对齐到页首和页尾,最终使用对齐后的 max_vaddr - min_vaddr 得到 load_size。

size_t phdr_table_get_load_size(const ElfW(Phdr)* phdr_table, size_t phdr_count,                                ElfW(Addr)* out_min_vaddr,                                ElfW(Addr)* out_max_vaddr) {  ElfW(Addr) min_vaddr = UINTPTR_MAX;  ElfW(Addr) max_vaddr = 0;  bool found_pt_load = false;  for (size_t i = 0; i < phdr_count; ++i) {    const ElfW(Phdr)* phdr = &phdr_table[i];    if (phdr->p_type != PT_LOAD) {      continue;    }    found_pt_load = true;    if (phdr->p_vaddr < min_vaddr) {      min_vaddr = phdr->p_vaddr;         // 记录最小的虚拟地址    }    if (phdr->p_vaddr + phdr->p_memsz > max_vaddr) {      max_vaddr = phdr->p_vaddr + phdr->p_memsz;  // 记录最大的虚拟地址    }  }  if (!found_pt_load) {    min_vaddr = 0;  }  min_vaddr = PAGE_START(min_vaddr);      // 页对齐  max_vaddr = PAGE_END(max_vaddr);      // 页对齐  if (out_min_vaddr != NULL) {    *out_min_vaddr = min_vaddr;  }  if (out_max_vaddr != NULL) {    *out_max_vaddr = max_vaddr;  }  return max_vaddr - min_vaddr;         // load_size = max_vaddr - min_vaddr}

2.2.4 Load Segments
遍历 program header table,找到类型为 PT_LOAD 的 segment:
计算 segment 在内存空间中的起始地址 segstart 和结束地址 seg_end,seg_start 等于虚拟偏移加上基址load_bias,同时由于 mmap 的要求,都要对齐到页边界得到 seg_page_start 和 seg_page_end。
计算 segment 在文件中的页对齐后的起始地址 file_page_start 和长度 file_length。
使用 mmap 将 segment 映射到内存,指定映射地址为 seg_page_start,长度为 file_length,文件偏移为 file_page_start。

bool ElfReader::LoadSegments() {  for (size_t i = 0; i < phdr_num_; ++i) {    const ElfW(Phdr)* phdr = &phdr_table_[i];    if (phdr->p_type != PT_LOAD) {      continue;    }    // Segment 在内存中的地址.    ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;    ElfW(Addr) seg_end   = seg_start + phdr->p_memsz;    ElfW(Addr) seg_page_start = PAGE_START(seg_start);    ElfW(Addr) seg_page_end   = PAGE_END(seg_end);    ElfW(Addr) seg_file_end   = seg_start + phdr->p_filesz;    // 文件偏移    ElfW(Addr) file_start = phdr->p_offset;    ElfW(Addr) file_end   = file_start + phdr->p_filesz;    ElfW(Addr) file_page_start = PAGE_START(file_start);    ElfW(Addr) file_length = file_end - file_page_start;    if (file_length != 0) {      // 将文件中的 segment 映射到内存      void* seg_addr = mmap(reinterpret_cast(seg_page_start),                            file_length,                            PFLAGS_TO_PROT(phdr->p_flags),                            MAP_FIXED|MAP_PRIVATE,                            fd_,                            file_page_start);    }    // 如果 segment 可写, 并且没有在页边界结束,那么就将 segemnt end 到页边界的内存清零。    if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) {      memset(reinterpret_cast(seg_file_end), 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end));    }    seg_file_end = PAGE_END(seg_file_end);    // 将 (内存长度 - 文件长度) 对应的内存进行匿名映射    if (seg_page_end > seg_file_end) {      void* zeromap = mmap(reinterpret_cast(seg_file_end),                           seg_page_end - seg_file_end,                           PFLAGS_TO_PROT(phdr->p_flags),                           MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE,                           -1,                           0);    }  }  return true;}

2.3 分配 soinfo
load_library 在调用 load_segments 完成装载后,接着调用 soinfo_alloc 函数为目标SO分配soinfo,soinfo_alloc 函数实现如下:

static soinfo* soinfo_alloc(const char* name, struct stat* file_stat) {  soinfo* si = g_soinfo_allocator.alloc();  //分配空间,可以简单理解为 malloc  // Initialize the new element.  memset(si, 0, sizeof(soinfo));  strlcpy(si->name, name, sizeof(si->name));  si->flags = FLAG_NEW_SOINFO;  sonext->next = si;    // 加入到存有所有 soinfo 的链表中  sonext = si;  return si;}

Linker 为 每个 SO 维护了一个soinfo结构,调用 dlopen时,返回的句柄其实就是一个指向该 SO 的 soinfo 指针。soinfo 保存了 SO 加载链接以及运行期间所需的各类信息,简单列举一下:
装载链接期间主要使用的成员:

  • 装载信息

const ElfW(Phdr)* phdr;
size_t phnum;
ElfW(Addr) base;
size_t size;

  • 符号信息

const char* strtab;
ElfW(Sym)* symtab;

  • 重定位信息

ElfW(Rel)* plt_rel;
size_t plt_rel_count;
ElfW(Rel)* rel;
size_t rel_count;

  • init 函数和 finit 函数

Linker_function_t* init_array;
size_t init_array_count;
Linker_function_t* fini_array;
size_t fini_array_count;
Linker_function_t init_func;
Linker_function_t fini_func;
运行期间主要使用的成员:

  • 导出符号查找(dlsym):

const char* strtab;
ElfW(Sym)* symtab;
size_t nbucket;
size_t nchain;
unsigned* bucket;
unsigned* chain;
ElfW(Addr) load_bias;

  • 异常处理:

unsigned* ARM_exidx;
size_t ARM_exidx_count;
load_library 在为 SO 分配 soinfo 后,会将装载结果更新到 soinfo 中,后面的链接过程就可以直接使用soinfo的相关字段去访问 SO 中的信息。

...
si->base = elf_reader.load_start();
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();
...

(下篇将继续更新) 

                                                  (腾讯御安全团队)

更多相关文章

  1. android 如何动态的加载类----app插件技术
  2. Android动态加载jar/dex[转]
  3. android单元测试中的多线程以及handler消息传递
  4. android窗体加载过程剖析之消息处理的注册机制
  5. [Android]关于换肤功能的遐想篇
  6. Android热更新简介
  7. Android高德地图加载WMS服务应用实践
  8. Android开源中国客户端学习 截屏模块
  9. android UI进阶之实现listview的下拉加载

随机推荐

  1. 用git下载Android自带app的源代码
  2. Android之蓝牙startDiscovery()搜索不到
  3. Android开发者指南(4) —— Application
  4. Android(安卓)Studio 视图解析
  5. Android(安卓)整理常用的第三方库
  6. Android在代码中查看系统版本
  7. 关于android监听H5发送的事件实现方法。
  8. 【Android】如何方便地将代码抛到主线程
  9. Android(安卓)点击EditText外部区域失去
  10. Android-解决AutoCompleteText下拉提示的