打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
AndroidLinker与SO加壳技术之上篇

AndroidLinkerSO加壳技术之上篇

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 的成员函数 CallConstructorsfind_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, intdlflags, 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 byname.  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 thesegments.

    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,再对elfheader进行验证,之后读取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 tablesection header table等重要信息。
elf header 的验证包括:

magic字节

32/64 bit 与当前平台是否一致

大小端

类型:可执行文件、SO

版本:一般为 1,表示当前版本

平台:ARMx86amd64

有任何错误都会导致加载失败。

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<ElfW(Phdr)*>(reinterpret_cast<char*>(mmap_result)+ page_offset);

  return true;

}

program header 在内存中单独映射一份,用于解析programheader 时临时使用,在 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<uint8_t*>(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<uint8_t*>(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_endseg_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<void*>(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<void*>(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<void*>(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分配soinfosoinfo_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 加载链接以及运行期间所需的各类信息,简单列举一下:

装载链接期间主要使用的成员:

l   装载信息

const ElfW(Phdr)* phdr;

size_t phnum;

ElfW(Addr) base;

size_t size;

l   符号信息

const char* strtab;

ElfW(Sym)* symtab;

l   重定位信息

ElfW(Rel)* plt_rel;

size_t plt_rel_count;

ElfW(Rel)* rel;

size_t rel_count;

l   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;

运行期间主要使用的成员:

l   导出符号查找(dlsym:

const char* strtab;

ElfW(Sym)* symtab;

size_t nbucket;

size_t nchain;

unsigned* bucket;

unsigned* chain;

ElfW(Addr) load_bias;

l   异常处理:

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();

    ...

(下篇查看地址

 

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Intel平台下Linux中ELF文件动态链接的加载、解析及实例分析(一): 加载
程序内存映像、磁盘映像的理解,可执行文件的运行过程
vxWorks内核解读-5
高端内存映射
linux下实现在程序运行时的函数替换(热补丁)
problems from dma_alloc_coherent - wilson的日志 - 网易博客
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服