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<ElfW(Phdr)*>(reinterpret_cast<char*>(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<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_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<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分配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 加载链接以及运行期间所需的各类信息,简单列举一下:

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

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

...

(点击此处查看下篇)

腾讯御安全团队

AndroidLinker与SO加壳技术之上篇的更多相关文章

  1. AndroidLinker与SO加壳技术之下篇

    点此查看上篇<AndroidLinker与SO加壳技术之上篇> 2.4 链接 链接过程由 soinfo_link_image 函数完成,主要可以分为四个主要步骤: 1. 定位 dynami ...

  2. 【腾讯Bugly干货分享】Android Linker 与 SO 加壳技术

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57e3a3bc42eb88da6d4be143 作者:王赛 1. 前言 Andr ...

  3. android apk 防止反编译技术第一篇-加壳技术

    做android framework方面的工作将近三年的时间了,现在公司让做一下android apk安全方面的研究,于是最近就在网上找大量的资料来学习.现在将最近学习成果做一下整理总结.学习的这些成 ...

  4. Android APK加壳技术方案

    Android APK加壳技术方案[1] Android APK加壳技术方案[2]

  5. Android Linker 与 SO 加壳技术

    1. 前言 Android 系统安全愈发重要,像传统pc安全的可执行文件加固一样,应用加固是Android系统安全中非常重要的一环.目前Android 应用加固可以分为dex加固和Native加固,N ...

  6. 什么是App加壳,以及App加壳的利与弊

    非著名程序员涩郎 非著名程序员,字耿左直右,号涩郎,爱搞机,爱编程,是爬行在移动互联网中的一名码匠!个人微信号:loonggg,微博:涩郎,专注于移动互联网的开发和研究,本号致力于分享IT技术和程序猿 ...

  7. 安卓Dex壳技术探讨(1)

    最近在研究安卓平台的加壳技术,以前以为只有原生层的代码才可以加壳,看了看网上的资料,才发现原来Java层也可以加壳,虽然与传统的壳有些区别,但就最终的效果来说,反静态分析的目的还是达到了的. 目前安卓 ...

  8. 关于javascript模块加载技术的一些思考

    前不久有个网友问我在前端使用requireJs和seajs的问题,我当时问他你们公司以前有没有自己编写的javascript库,或者javascript框架,他的回答是什么都没有,他只是听说像requ ...

  9. VMP虚拟机加壳的原理学习

    好久没有到博客写文章了,9月份开学有点忙,参加了一个上海的一个CHINA SIG信息比赛,前几天又无锡南京来回跑了几趟,签了阿里巴巴的安全工程师,准备11月以后过去实习,这之前就好好待在学校学习了. ...

随机推荐

  1. Foundation ----->NSArray

    .数组的创建     //注意:在OC的数组中,只能够存放对象 //    NSArray *array = [NSArray arrayWithObject:12];错误          //创建 ...

  2. JAVA学习笔记(33-53)

    33:java中的多维数组,以二位为例: 创建方法:int[][] a = new int[2][3]; 建立一个5*5的数组. 或者下面的建立方法也可以: int[][] c = { {1, 2, ...

  3. tracert与pathping

    trace route(windows命令为tracert,Linux命令为traceroute)命令可以列出本地计算机与目标计算机之间所有经过的计算机信息.可以输入目标计算机的名字(如www.bai ...

  4. Python算法-冒泡排序

    #coding:utf-8 """ 冒泡排序 原理:依次重复访问每一个需要排序的元素,每次比较相邻的两个元素是否符合顺序,若不符合就交换,直到没有不符合顺序的为止. &q ...

  5. Kafka报错-as it has seen zxid 0x83808 our last zxid is 0x0 client must try another server

    as it has seen zxid 0x83808 our last zxid is 0x0 client must try another server 停止zookeeper,删除datadi ...

  6. 关于 Dev中的GridControl 中 GridView 的 PopulateColumns() 方法

    最近使用Dev控件,Gridview绑定数据源后不能显示数据,于是在网上查询,说是使用PopulateColumns()方法,可以显示数据.试了一下,管用. 于是在所有更新数据源数据后,都用上了这句话 ...

  7. JetBrains激活

    https://www.imsxm.com/jetbrains-license-server/ 已经累计为大家激活1360577次 :) JetBrains授权服务器:http://idea.imsx ...

  8. linux 学习10 shell 基础

    10.1 Shell概述 .Shell是什么 Shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用Shell来启动.挂起.停止甚至是编写一 ...

  9. 用VC进行COM编程所必须掌握的理论知识

    一.为什么要用COM 软件工程发展到今天,从一开始的结构化编程,到面向对象编程,再到现在的COM编程,目标只有一个,就是希望软件能象积方块一样是累起来的,是组装起来的,而不是一点点编出来的.结构化编程 ...

  10. UICollectionReusableView 使用时的一些问题

    在使用UICollectionView 时, 设置分区头时, - (UICollectionReusableView *)collectionView:(UICollectionView *)coll ...