基于对so中的函数加密技术实现so加固

来源:互联网 发布:根域名服务器分布 编辑:程序博客网 时间:2024/06/15 21:46

一、技术原理

这篇和之前的那篇文章唯一的不同点就是如何找到指定的函数的偏移地址和大小

在so文件中,每个函数的结构描述是存放在.dynsym段中的。每个函数的名称保存在.dynstr段中的,类似于之前说过的每个section的名称都保存在.shstrtab段中,所以在前面的文章中我们找到指定段的时候,就是通过每个段的sh_name字段到.shstrtab中寻找名字即可,而且我们知道.shstrtab这个段在头文件中是有一个index的,就是在所有段列表中的索引值,所以很好定位.shstrtab.
注意
parseSectionHeaderList(fileByteArys, s_header_offset);
找到节头表,由表项名字对应找到指定的节头,进而可定位指定节(section)。

但是在这篇文章我们可能遇到一个问题,就是不能按照这种方式去查找指定函数名了:
有人说可以通过section的type来获取.dynsym和.dynstr。我们看到上图中.dynsym类型是:DYNSYM,.dynstr类型是STRTAB,但是这种方法是不行的,因为这个type不是唯一的,也就说不同的section,type可能相同,我们没办法区分,比如.shstrtab和.dynstr的type都是STRTAB.其实从这里我们就知道这两个段的区别了:
.shstrtab值存储段的名称,.dynstr是存储so中的所有符号名称。
我们看到有一个.hash段,在上图中我们也可以看到的:
由 Elf32_Word 对象组成的哈希表支持符号表访问。

这里写图片描述

上面的描述感觉有点复杂,其实说的简单点就是:

用目标函数名在用hash函数得到一个hash值,然后再做一些计算就可以得到这个函数在.dynsym段中这个函数对应的条目了。关于这个hash函数,是公用的,我们在Android中的bonic/linker.c源码中也是可以找到的:

unsigned long elf_hash (const unsigned char *name) {unsigned long h = 0, g; while (*name){    h=(h<<4)+*name++; if (g = h & 0xf0000000)    h^=g>>24; h&=-g;}return h; }

那么我们只要得到.hash段即可,但是我们怎么获取到这个section中呢?elf中并没有对这个段进行数据结构的描述,有人可能想到了我们在上图看到.hash段的type是HASH,那么我们再通过这个type来获取?但是之前说了,这个type不是唯一的,通过他来获取section是不靠谱的?那么我们该怎么办呢?这时候我们就要看一下程序头信息了:
这里写图片描述
我们知道程序头信息是最后so被加载到内存中的映像描述,这里我们看到有一个.dynamic段。我们再看看so文件的装载视图和链接视图:
这里写图片描述
这个我们在之前也说过,在so被加载到内存之后,就没有section了,对应的是segment了,也就是程序头中描述的结构,而且一个segment可以包含多个section,.dynamic段一般用于动态链接的,所以.dynsym和.dynstr,.hash肯定包含在这里。
注意:
第一、在程序头信息中,type标示.dynamic段是唯一的,所以可以通过type来进行寻找
第二、我们看到上面的链接视图和装载视图发现,我们这种通过程序头中的信息来查找.dysym等section靠谱点,因为当so被加载到内存中,就不存在了section了,只有segment了。

我们可以解析了程序头信息之后,通过type获取到.dynamic程序头信息,然后获取到这个segment的偏移地址和大小,在进行解析成elf32_dyn结构。

二、实现方案

需要做的是对Java_com_example_shelldemo2_MainActivity_getString函数进行加密,需要注意的是,被加密函数如果用static声明,那么函数是不会出现在.dynsym中,是无法在装载视图中通过函数名找到进行解密的。当然,也可以采用取巧方式,类似上节,把地址和长度信息写入so头中实现。Java_com_example_shelldemo2_MainActivity_getString需要被调用,那么一定是能在.dynsym找到的。
加密流程:
1) 读取文件头,获取e_phoff、e_phentsize和e_phnum信息
2) 通过Elf32_Phdr中的p_type字段,找到DYNAMIC。从下图可以看出,其实DYNAMIC就是.dynamic section。从p_offset和p_filesz字段得到文件中的起始位置和长度
3) 遍历.dynamic,找到.dynsym、.dynstr、.hash section文件中的偏移和.dynstr的大小。在我的测试环境下,fedora 14和windows7 Cygwin x64中elf.h定义.hash的d_tag标示是:DT_GNU_HASH;而安卓源码中的是:DT_HASH。
4) 根据函数名称,计算hash值
5) 根据hash值,找到下标hash % nbuckets的bucket;根据bucket中的值,读取.dynsym中的对应索引的Elf32_Sym符号;从符号的st_name所以找到在.dynstr中对应的字符串与函数名进行比较。若不等,则根据chain[hash % nbuckets]找下一个Elf32_Sym符号,直到找到或者chain终止为止。这里叙述得有些复杂,直接上代码。

for(i = bucket[funHash % nbucket]; i != 0; i = chain[i]){  if(strcmp(dynstr + (funSym + i)->st_name, funcName) == 0){    flag = 0;    break;  }}

6) 找到函数对应的Elf32_Sym符号后,即可根据st_value和st_size字段找到函数的位置和大小
7) 后面的步骤就和上节相同了,这里就不赘述
解密流程为加密逆过程,大体相同,只有一些细微的区别,具体如下:
1) 找到so文件在内存中的起始地址
2) 也是通过so文件头找到Phdr;从Phdr找到PT_DYNAMIC后,需取p_vaddr和p_filesz字段,并非p_offset,这里需要注意。
3) 后续操作就加密类似,就不赘述。对内存区域数据的解密,也需要注意读写权限问题。

上面就介绍了完了,下面我们就可以来开始coding了。

三、代码实现

注意:
寻找函数偏移及大小:
1.so文件中每个函数的结构描述存放在.dynsym(此节区包含了动态链接符号表。)段中。每个函数的名称保存在.dynstr段中,类似于之前每个section的名称都保存在.shstrtab段中。
2.hash节区包含了一个符号哈希表。
由Elf32_word对象组成的哈希表支持符号表(.dynsym)访问。
bucket 数组包含 nbucket 个项目,chain 数组包含 nchain 个项目,下标都是从
0 开始。bucket 和 chain 中都保存符号表索引。Chain 表项和符号表存在对应。符号
表项的数目应该和 nchain 相等,所以符号表的索引也可用来选取 chain 表项。哈希
函数能够接受符号名并且返回一个可以用来计算 bucket 的索引。
因此,如果哈希函数针对某个名字返回了数值 X,则 bucket[X%nbucket] 给出了
一个索引 y,该索引可用于符号表,也可用于 chain 表。如果符号表项不是所需要的,
那么 chain[y] 则给出了具有相同哈希值的下一个符号表项。我们可以沿着 chain 链
一直搜索,直到所选中的符号表项包含了所需要的符号,或者 chain 项中包含值
STN_UNDEF。
3.根据hash值,找到下标hash % nbuckets的bucket;根据bucket中的值,读取.dynsym中的对应索引的Elf32_Sym符号;从符号的st_name索引找到在.dynstr中对应的字符串与函数名进行比较。若不等,则根据chain[hash % nbuckets]找下一个Elf32_Sym符号,直到找到或者chain终止为止。

hash->Elf32_Sym符号->符号的st_name索引->.dynstr中对应的字符串->函数名比较->chain[hash % nbuckets]找下一个Elf32_Sym符号->直到找到或者chain终止为止。