【预备起~~~】
最近在忙找工作的事情,笔试~面试~笔试~面试。。。很久没有写(pian)文(gao)章(fei)。忙了一阵子之后,终于~~~到了选offer的阶段(你家公司不是牛吗,老子不接你家offer,哈哈哈哈~~~),可以喘(出)口(口)气(恶)了(气)。。。来来来,继续讨论一下抗静态分析的问题,这回要说的是如何对so文件进行加密。
【一二三四】
so文件的作用不明觉厉~~~不对是不言而喻。各大厂商的加固方案都会选择将加固的代码放到native层,主要因为native层的逆向分析的难度更大,而且代码执行效率高,对性能影响小。但是总有些大牛,对这些方法是无感的,为了加大难度,这些厂商更加丧心病狂的对so文件进行加固,比如代码膨胀、ELF文件格式破坏、字节码加密等等。这篇文章就是主要讲简单粗暴的加密,来窥探一下这当中的原理。
首先,我们都知道so文件本质上也是一种ELF文件,ELF的文件头如下
[C] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 |
|
详细的就不说了,简单看下,开始的16字节是ELF文件魔数,然后是一些文件信息硬件、版本之类的,重点在几个变量
e_phoff、e_shoff、e_phentsize、e_phnum、e_shentsize、e_shnum、e_shstrndx
要知道这几个变量的含义首先要清楚,ELF文件的结构在链接时和执行时是不同的
<ignore_js_op>
一般情况下(也就是我们看到的情况),ELF文件内部分为多个section,每个section保存不同的信息,比如.shstrtab保存段信息的字符串,.text装载可执行代码等等。这些不同的section根据不同的内容和作用会有不同的读写和执行权限,但是这些section的权限是没有规律的,比如第一个section的权限是只读,第二个是读写、第三个又是只读。如果在内存当中直接以这种形式存在,那么文件在执行的时候会造成权限控制难度加大,导致不必要的消耗。所以当我们将so文件链接到内存中时,存在的不是section,而是segment,每个segment可以看作是相同权限的section的集合。也就是说在内存当中一个segment对应N个section(N>=0),而这些section和segment的信息都会被保存在文件中。
理解了这个,再看那几个变量。e_phoff是segment头部偏移的位置,e_phentsize是segment头部的大小,e_phnum指segment头部的个数(每个segment都有一个头部,这些头部是连续放在一起的,头部中有变量指向这些segment的具体内容)。同样e_shoff、e_shentsize、e_shnum分别表示section的头部偏移、头部大小、头部数量。最后一个e_shstrndx有点难理解。ELF文件中的每个section都是有名字的,比如.data、.text、.rodata,每个名字都是一个字符串,既然是字符串就需要一个字符串池来保存,而这个字符串池也是一个section,或者说准备一个section用来维护一个字符串池,这个字符串池保存了其他section以及它自己的名字。这个特殊的section叫做.shstrtab。由于这个section很特殊,所以把它单独标出来。我们也说了,所有section的头部是连续存放在一起的,类似一个数组,e_shstrndx变量是.shstrtab在这个数组中的下标。(希望我解释清楚了~~~)
segment头部结构
[C] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 |
|
section头部结构
[C] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 |
|
注意这里都是32位的。。。
在代码当中segment的命名是program,所以segment和program指的是同一个东西
Program header位于ELF header后面,Section Header位于ELF文件的尾部。那可以推出:
e_phoff = sizeof(e_ehsize);
整个ELF文件大小 = e_shoff + e_shnum * sizeof(e_shentsize) + 1
这里多讲一点与加密没有关系的知识。我们知道了在内存当中只有segment而没有section,那么如果section结构被破坏了,ELF文件是不是还能正常执行?答案:是
如何证明大家可以自己去寻找答案,这里不多说。但是由于这样,所以经常会破坏文件的section结构,让比如IDA、readelf等工具失效,这也是so加固的一种方式。
回到正题,我们继续说加密。加密的流程我们设想一下,可以是这样 解析ELF——>找到字节码——>对字节码加密
解密就是 解析ELF——>找到字节码——>对字节码解密
详细一点就是通过偏移、个数等信息找到section的头部,然后看是不是我们要找的section(通过名字)。找到后通过sh_offset(偏移)和sh_size(大小),就找到这个section的内容,整体加密。
【二二三四】
下面看加密的代码
[C] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
这一段是从打开文件到找到制定section的代码,我们为了减小实验难度,不会对一些重要的section加密(可能被玩坏),我们自己新建一个section,新建的方法之后说,所以这里的字符串target_section就是我们自己定义的section的名字。
[C] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
找到之后就修改加密了,完成后写回。这个so就加密完成了。
【三二三四】
下面我们来看解密代码,首先先看两个函数申明
[C] 纯文本查看 复制代码
1 2 |
|
这两个函数之后都有__attribute__,这是GCC的编译选项,用于设定函数属性。__attribute__((section(".newsec")))的意思就是说这个函数将被放到.newsec这个section中,我们前面所说的自己新建section就是这样实现的。。。那么printLog这个函数就是.newsec的唯一内容。
下面一个是解密函数,constructor属性可以让代码在main之前执行,保证在比较早的时间点执行解密函数,不影响后续的代码。
[C] 纯文本查看 复制代码
1 2 3 4 |
|
printLog代码很简单
[C] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
解密过程,大多数差不多,需要注意两个地方一个是getLibAddr,用于获得内存中so的位置
[C] 纯文本查看 复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
还有个是mprotect
这个函数用于修改内存页的权限,如果不修改,用户对于内存页的权限只有read,你是无法对内存中的数据进行修改的。这个和之前我们所说的segment的权限不一样,要注意区分。
【再来一次】
这种单独建一个section的方法简单粗暴易懂,但是只要解析一下就会知道多了一个section。所以实际上往往都是对固定的section进行加密解密,要注意的是这些section中有重要的信息,不能乱来,所以难度会大很多。大家有兴趣自己实现以下。
就酱~~~