点此查看上篇《AndroidLinker与SO加壳技术之上篇》

2.4 链接

链接过程由 soinfo_link_image 函数完成,主要可以分为四个主要步骤:

1. 定位 dynamic section,
由函数 phdr_table_get_dynamic_section 完成,该函数会遍历 program header,找到为类型为 PT_DYNAMIC 的 header, 从中获取的是 dynamic section 的信息,主要就是虚拟地址和项数。

2. 解析 dynamic section
dynamic section本质上是类型为Elf32_Dyn的数组,Elf32_Dyn 结构如下

typedef struct {

Elf32_Sword d_tag;      /* 类型(e.g. DT_SYMTAB),决定 d_un 表示的意义*/

union {

Elf32_Word  d_val;  /* 根据 d_tag的不同,有不同的意义*/

Elf32_Addr  d_ptr;  /* 虚拟地址 */

} d_un;

} Elf32_Dyn;

Elf32_Dyn结构的d_tag属性表示该项的类型,类型决定了dun中信息的意义,e.g.:当d_tag = DT_SYMTAB表示该项存储的是符号表的信息,d_un.d_ptr 表示符号表的虚拟地址的偏移,当d_tag = DT_RELSZ时,d_un.d_val 表示重定位表rel的项数。
解析的过程就是遍历数组中的每一项,根据d_tag的不同,获取到不同的信息。
dynamic section 中包含的信息主要包括以下 3 类:

- 符号信息

- 重定位信息

- init&finit funcs

3. 加载 needed SO
调用 find_library 获取所有依赖的 SO 的 soinfo 指针,如果 SO 还没有加载,则会将 SO 加载到内存,分配一个soinfo*[]指针数组,用于存放 soinfo 指针。

4. 重定位
重定位SO 链接中最复杂同时也是最关键的一步。重定位做的工作主要是修复导入符号的引用,下面一节将对重定位过程进行详细分析。

soinfo_link_image 的示意代码:

static bool soinfo_link_image(soinfo* si, const Android_dlextinfo* extinfo) {

...

// 1. 获取 dynamic section 的信息,si->dynamic 指向 dynamic section

phdr_table_get_dynamic_section(phdr, phnum, base, &si->dynamic,

&dynamic_count, &dynamic_flags);

...

// 2. 解析dynamic section

uint32_t needed_count = 0;

for (ElfW(Dyn)* d = si->dynamic; d->d_tag != DT_NULL; ++d) {

switch (d->d_tag) {

// 以下为符号信息

case DT_HASH:

si->nbucket = reinterpret_cast<uint32_t*>(base + d->d_un.d_ptr)[0];

si->nchain = reinterpret_cast<uint32_t*>(base + d->d_un.d_ptr)[1];

si->bucket = reinterpret_cast<uint32_t*>(base + d->d_un.d_ptr + 8);

si->chain = reinterpret_cast<uint32_t*>(base + d->d_un.d_ptr + 8 + si->nbucket * 4);

break;

case DT_SYMTAB:

si->symtab = reinterpret_cast<ElfW(Sym)*>(base + d->d_un.d_ptr);

break;

case DT_STRTAB:

si->strtab = reinterpret_cast<const char*>(base + d->d_un.d_ptr);

break;

// 以下为重定位信息

case DT_JMPREL:

si->plt_rel = reinterpret_cast<ElfW(Rel)*>(base + d->d_un.d_ptr);

break;

case DT_PLTRELSZ:

si->plt_rel_count = d->d_un.d_val / sizeof(ElfW(Rel));

break;

case DT_REL:

si->rel = reinterpret_cast<ElfW(Rel)*>(base + d->d_un.d_ptr);

break;

case DT_RELSZ:

si->rel_count = d->d_un.d_val / sizeof(ElfW(Rel));

break;

// 以下为 init&finit funcs

case DT_INIT:

si->init_func = reinterpret_cast<Linker_function_t>(base + d->d_un.d_ptr);

break;

case DT_FINI:

...

case DT_INIT_ARRAY:

si->init_array = reinterpret_cast<Linker_function_t*>(base + d->d_un.d_ptr);

break;

case DT_INIT_ARRAYSZ:

...

case DT_FINI_ARRAY:

...

case DT_FINI_ARRAYSZ:

...

// SO 依赖

case DT_NEEDED:

...

...

}

// 3. 加载依赖的SO

for (ElfW(Dyn)* d = si->dynamic; d->d_tag != DT_NULL; ++d) {

if (d->d_tag == DT_NEEDED) {

soinfo* lsi = find_library(library_name, 0, NULL);

si->add_child(lsi);

*pneeded++ = lsi;

}

}

*pneeded = NULL;

...

// 4. 重定位

soinfo_relocate(si, si->plt_rel, si->plt_rel_count, needed);

soinfo_relocate(si, si->rel, si->rel_count, needed);

...

// 设置已链接标志

si->flags |= FLAG_LINKED;

DEBUG("[ finished linking %s ]", si->name);

2.4.1 重定位 relocate

Android ARM 下需要处理两个重定位表,plt_rel 和 rel,plt 指的是延迟绑定,但是 Android 目前并不对延迟绑定做特殊处理,直接与普通的重定位同时处理。两个重定位的表都由 soinfo_relocate 函数处理。
soinfo_relocate 函数需要遍历重定位表,处理每个重定位项,每个重定位项的处理过程可已分为 4 步:
1. 解析重定位项和导入符号的信息

重定位项的结构如下

typedef struct {

Elf32_Addr  r_offset;   /* 需要重定位的位置的偏移 */

Elf32_Word  r_info;     /* 高24位为符号在符号表中的index,低8位为重定位类型 */

} Elf32_Rel;

首先从重定位项获取的信息如下:

· 重定位的类型 type

· 符号在符号表中的索引号 sym,sym 为0表示为本SO内部的重定位,如果不为0,意味着该符号为导入符号

· 重定位的目标地址 reloc,使用r_offset + si_load_bias,相当于 偏移地址+基地址

符号表表项的结构为elf32_sym:

typedef struct elf32_sym {

Elf32_Word  st_name;    /* 名称 - index into string table */

Elf32_Addr  st_value;   /* 偏移地址 */

Elf32_Word  st_size;    /* 符号长度( e.g. 函数的长度) */

unsigned char   st_info;    /* 类型和绑定类型 */

unsigned char   st_other;   /* 未定义 */

Elf32_Half  st_shndx;   /* section header的索引号,表示位于哪个 section 中 */

} Elf32_Sym;

2. 如果 sym 不为0,则查找导入符号的信息
如果 sym 不为0,则继续使用 sym 在符号表中获取符号信息,从符号信息中进一步获取符号的名称。随后调用 soinfo_do_lookup 函数在所有依赖的 SO 中根据符号名称查找符号信息,返回值类型为 elf32_sym,同时还会返回含有该符号的 SO 的 soinfo( lsi ),如果查找成功则该导入符号的地址为:
sym_addr = s->st_value + lsi->load_bias;

3. 修正需要重定位的地址
根据重定位类型的不同,修正重定位地址,具体的重定位类型定义和计算方法可以参考 aaelf 文档的 4.6.1.2 节。
对于导入符号,则使用根据第二步得到 sym_addr 去修正,对于 SO 内部的相对偏移修正,则直接将reloc的地址加上 SO 的基址。

static int soinfo_relocate(soinfo* si, ElfW(Rel)* rel, unsigned count, soinfo* needed[]) {

ElfW(Sym)* s;

soinfo* lsi;

// 遍历重定位表

for (size_t idx = 0; idx < count; ++idx, ++rel) {

// 1. 解析重定位项和导入符号的信息

// 重定位类型

unsigned type = ELFW(R_TYPE)(rel->r_info);

// 导入符号在符号表中的 index,可以为0,(修正 SO 内部的相对偏移)

unsigned sym = ELFW(R_SYM)(rel->r_info);

// 需要重定位的地址

ElfW(Addr) reloc = static_cast<ElfW(Addr)>(rel->r_offset + si->load_bias);

ElfW(Addr) sym_addr = 0;

const char* sym_name = NULL;

if (type == 0) { // R_*_NONE

continue;

}

if (sym != 0) {

// 2. 如果 sym 有效,则查找导入符号

// 从符号表中获得符号信息,在根据符号信息从字符串表中获取字符串名

sym_name = reinterpret_cast<const char*>(si->strtab + si->symtab[sym].st_name);

// 在依赖的 SO 中查找符号,返回值为 Elf32_Sym 类型

s = soinfo_do_lookup(si, sym_name, &lsi, needed);

if (s == NULL) {}

// 查找失败,不关心

} else {

// 查找成功,最终的符号地址 = s->st_value + lsi->load_bias

// s->st_value 是符号在依赖 SO 中的偏移,lsi->load_bias 为依赖 SO 的基址

sym_addr = static_cast<ElfW(Addr)>(s->st_value + lsi->load_bias);}

} else {

s = NULL;}

// 3. 根据重定位类型,修正需要重定位的地址

switch (type) {

// 判断重定位类型,将需要重定位的地址 reloc 修正为目标符号地址

// 修正导入符号

case R_ARM_JUMP_SLOT:

*reinterpret_cast<ElfW(Addr)*>(reloc) = sym_addr;

break;

case R_ARM_GLOB_DAT:

*reinterpret_cast<ElfW(Addr)*>(reloc) = sym_addr;

break;

case R_ARM_ABS32:

*reinterpret_cast<ElfW(Addr)*>(reloc) += sym_addr;

break;

case R_ARM_REL32:

*reinterpret_cast<ElfW(Addr)*>(reloc) += sym_addr - rel->r_offset;

break;

// 不支持

case R_ARM_COPY:

/*

* ET_EXEC is not supported SO this should not happen.

*/

DL_ERR("%s R_ARM_COPY relocations are not supported", si->name);

return -1;

// SO 内部的偏移修正

case R_ARM_RELATIVE:

if (sym) {

DL_ERR("odd RELATIVE form...");

return -1;

}

*reinterpret_cast<ElfW(Addr)*>(reloc) += si->base;

break;

default:

DL_ERR("unknown reloc type %d @ %p (%zu)", type, rel, idx);

return -1;}

}

return 0;

}

2.5 CallConstructors

在编译 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。

3. 加壳技术

在病毒和版权保护领域,“壳”一直扮演着极为重要的角色。通过加壳可以对代码进行压缩和加密,同时再辅以虚拟化、代码混淆和反调试等手段,达到防止静态和动态分析。

在 Android 环境中,Native 层的加壳主要是针对动态链接库 SO,SO 加壳的示意图如下:

加壳工具、loader、被保护SO。

· SO: 即被保护的目标 SO。

· loader: 自身也是一个 SO,系统加载时首先加载 loader,loader 首先还原出经过加密、压缩、变换的 SO,再将 SO 加载到内存,并完成链接过程,使 SO 可以正常被其他模块使用。

· 加壳工具: 将被保护的 SO 加密、压缩、变换,并将结果作为数据与 loader 整合为 packed SO。

下面对 SO 加壳的关键技术进行简单介绍。

3.1 loader 执行时机

Linker 加载完 loader 后,loader 需要将被保护的 SO 加载起来,这就要求 loader 的代码需要被执行,而且要在 被保护 SO 被使用之前,前文介绍了 SO 的初始化函数便可以满足这个要求,同时在 Android 系统下还可以使用 JNI_ONLOAD 函数,因此 loader 的执行时机有两个选择:

· SO 的 init 或 initarray

· jni_onload

3.2 loader 完成 SO 的加载链接

loader 开始执行后,首先需要在内存中还原出 SO,SO 可以是经过加密、压缩、变换等手段,也可已单纯的以完全明文的数据存储,这与 SO 加壳的技术没有必要的关系,在此不进行讨论。
在内存中还原出 SO 后,loader 还需要执行装载和链接,这两个过程可以完全模仿 Linker 来实现,下面主要介绍一下相对 Linker,loader 执行这两个过程有哪些变化。

3.2.1 装载

还原后的 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时。

3.2.2 分配 soinfo

soinfo 保存了 SO 装载链接和运行时需要的所有信息,为了维护相关的信息,loader 可以照搬 Linker 的 soinfo 结构,用于存储中间信息,装载链接结束后,还需要将 soinfo 的信息修复到 Linker 维护的soinfo,3.3节进行详细说明。

3.2.3 链接

链接过程完全是操作内存,不论是从文件装载还是内存装载,链接过程都是一样,完全模仿 Linker 即可。
另外链接后记得顺便调用 SO 初始化函数( init 和 init_array )。

3.3 soinfo 修复

SO 加壳的最关键技术点在于 soinfo 的修复,由于 Linker 加载的是 loader,而实际对外使用的是被保护的 SO,所以 Linker 维护的 soinfo 可以说是错误,loader 需要将自己维护的 soinfo 中的部分信息导出给 Linker 的soinfo。

修复过程如下:

00001. 获取 Linker 维护的 soinfo,可以通过 dlopen 打开自己来获得:self_soinfo = dlopen(self)。

00002. 将 loader soinfo 中的信息导出到 self_soinfo,最简单粗暴的方式就是直接赋值,比如:self_soinfo.base = soinfo.base。需要导出的主要有以下几项:

· SO地址范围:base、size、load_bias

· 符号信息:sym_tab、str_tab、

· 符号查找信息:nbucket、nchain、bucket、chain

· 异常处理:ARM_exidx、ARM_exidx_count

参考

· <<Linkers and loaders>>

· <<ELF for the ARM Architecture>>

腾讯御安全团队

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

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

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

  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. VMP虚拟机加壳的原理学习

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

  9. Android中的Apk的加固(加壳)原理解析和实现

    一.前言 今天又到周末了,憋了好久又要出博客了,今天来介绍一下Android中的如何对Apk进行加固的原理.现阶段.我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk, ...

随机推荐

  1. 由python为入口回顾C++的lambda

    lambda是一种匿名函数,python  lambda可以使简单的函数简洁的表达,,C++的lambda使类似嵌套函数的功能得以实现 python的lambda lambda [arg1[,arg2 ...

  2. tp框架总结(二)

    一.函数库和类库 项目中的常用的函数库要封装到项目Common/function.php中  在项目中可以直接调用  [ 函数();] import方法是ThinkPHP内建的类库导入方法,提供了方便 ...

  3. xml ---DOM操作

    package day03.xml; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; ...

  4. 通过反射获得 spring 的 RequestMapping value值

    package demo import java.lang.reflect.Method; import org.springframework.web.bind.annotation.Request ...

  5. How-to: Enable User Authentication and Authorization in Apache HBase

    With the default Apache HBase configuration, everyone is allowed to read from and write to all table ...

  6. javascript检查移动设备是否支持重力方向感应

    javascript如何检查移动设备,如手机平台是否支持重力或者方向感应. 可以使用html5提供的重力和方向感应接口来判断. html5 中针对高端手机提供了重力感应和重力加速的接口,开发可以利用这 ...

  7. 微软ASP.NET技术“乱谈”

    微软ASP.NET技术“乱谈” 2014新年了,顺手写的一点文字,主要谈谈我对当前微软ASP.NET技术的看法,比较随意,大伙儿随便看看吧. 1 当前微软Web平台技术全貌 从2002年发布.NET ...

  8. 【转】 HTMLCollection和NodeList的区别

    1 HTMLCollection类型 下面的每个项目(以及它们指定的属性)都返回 HTMLCollection(基类)或者它的派生类: Document (images, applets, links ...

  9. 通过profiler对unity进行针对性优化

    转 : http://user.qzone.qq.com/289422269/blog/1453815629?ptlang=2052 通过profiler对unity进行针对性优化  1. CPU U ...

  10. 321android浏览器

    [应用简介] 321浏览器是IT蓝豹创始人在2012年的时候业余时间研发的项目,现在很多各大网站都扒了我们的源码, 并且还插入了他们的广告,这是一件特别令人痛恨的事,如果需要下载整版的同学们就上IT蓝 ...