对抗静态分析——so文件的加密

【预备起~~~】
最近在忙找工作的事情,笔试~面试~笔试~面试。。。很久没有写(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
#define EI_NIDENT 16
typedef struct elf32_hdr{
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  unsigned char e_ident[EI_NIDENT];
  Elf32_Half e_type;
  Elf32_Half e_machine;
  Elf32_Word e_version;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Addr e_entry;
  Elf32_Off e_phoff;
  Elf32_Off e_shoff;
  Elf32_Word e_flags;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Half e_ehsize;
  Elf32_Half e_phentsize;
  Elf32_Half e_phnum;
  Elf32_Half e_shentsize;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Half e_shnum;
  Elf32_Half e_shstrndx;
} Elf32_Ehdr;


详细的就不说了,简单看下,开始的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
typedef struct elf32_phdr{
  Elf32_Word p_type;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Off p_offset;
  Elf32_Addr p_vaddr;
  Elf32_Addr p_paddr;
  Elf32_Word p_filesz;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Word p_memsz;
  Elf32_Word p_flags;
  Elf32_Word p_align;
} Elf32_Phdr;


section头部结构

[C]  纯文本查看 复制代码
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
typedef struct elf32_shdr {
  Elf32_Word sh_name;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Word sh_type;
  Elf32_Word sh_flags;
  Elf32_Addr sh_addr;
  Elf32_Off sh_offset;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Word sh_size;
  Elf32_Word sh_link;
  Elf32_Word sh_info;
  Elf32_Word sh_addralign;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Word sh_entsize;
} Elf32_Shdr;


注意这里都是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
fd = open(argv[1], O_RDWR);        //打开文件
if (fd < 0){
   printf ( "open %s failed\n" , argv[1]);
   goto _error;
}
 
if (read(fd, &ehdr, sizeof (Elf32_Ehdr)) != sizeof (Elf32_Ehdr)){        //读取头部,验证文件是否正确
   puts ( "Read ELF header error" );
   goto _error;
}
 
lseek(fd, ehdr.e_shoff + sizeof (Elf32_Shdr) * ehdr.e_shstrndx, SEEK_SET); //移动到shstrtab的头部
 
if (read(fd, &shdr, sizeof (Elf32_Shdr)) != sizeof (Elf32_Shdr)){ //读取shstrtab头部
   puts ( "Read ELF section string table error" );
   goto _error;
}
 
if ((shstr = ( char *) malloc (shdr.sh_size)) == NULL){ //开辟内存区域,这个用于保存shstrtab的字符串池
   puts ( "Malloc space for section string table failed" );
   goto _error;
}
 
lseek(fd, shdr.sh_offset, SEEK_SET);                //移动到shstrtab的字符串池
if (read(fd, shstr, shdr.sh_size) != shdr.sh_size){ //读取字符串池
   puts ( "Read string table failed" );
   goto _error;
}
 
lseek(fd, ehdr.e_shoff, SEEK_SET);                //移动到section头部数组的起始位置
for (i = 0; i < ehdr.e_shnum; i++){                //遍历section的头部
   if (read(fd, &shdr, sizeof (Elf32_Shdr)) != sizeof (Elf32_Shdr)){
     puts ( "Find section .text procedure failed" );
     goto _error;
   }
   if ( strcmp (shstr + shdr.sh_name, target_section) == 0){ //找到目标section
     base = shdr.sh_offset;
     length = shdr.sh_size;
     printf ( "Find section %s\n" , target_section);
     break ;
   }
}


这一段是从打开文件到找到制定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
lseek(fd, base, SEEK_SET);                //移动到目标section的内容上
content = ( char *) malloc (length);
if (content == NULL){
   puts ( "Malloc space for content failed" );
   goto _error;
}
if (read(fd, content, length) != length){ //读取出来
   puts ( "Read section .text failed" );
   goto _error;
}
 
nblock = length / block_size;
nsize = base / 4096 + (base % 4096 == 0 ? 0 : 1);
printf ( "base = %d, length = %d\n" , base, length);
printf ( "nblock = %d, nsize = %d\n" , nblock, nsize);
 
ehdr.e_entry = (length << 16) + nsize; //将sh_size和addr写到e_entry,简化解密流程
ehdr.e_shoff = base;
 
 
 
for (i=0;i<length;i++){
   content[/size][i][size=4] = ~content[/size][i][size=4]; //整体异或
}
 
 
 
lseek(fd, 0, SEEK_SET);
if (write(fd, &ehdr, sizeof (Elf32_Ehdr)) != sizeof (Elf32_Ehdr)){ //将头部写回
   puts ( "Write ELFhead to .so failed" );
   goto _error;
}
 
 
lseek(fd, base, SEEK_SET);
if (write(fd, content, length) != length){ //将内容写回
   puts ( "Write modified content to .so failed" );
   goto _error;
}


找到之后就修改加密了,完成后写回。这个so就加密完成了。

【三二三四】
下面我们来看解密代码,首先先看两个函数申明

[C]  纯文本查看 复制代码
?
1
2
void printLog() __attribute__((section( ".newsec" )));
void init_printLog() __attribute__((constructor));


这两个函数之后都有__attribute__,这是GCC的编译选项,用于设定函数属性。__attribute__((section(".newsec")))的意思就是说这个函数将被放到.newsec这个section中,我们前面所说的自己新建section就是这样实现的。。。那么printLog这个函数就是.newsec的唯一内容。
下面一个是解密函数,constructor属性可以让代码在main之前执行,保证在比较早的时间点执行解密函数,不影响后续的代码。

[C]  纯文本查看 复制代码
?
1
2
3
4
void printLog()
{
   ALOGD( "this is a log" );
}


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
void init_printLog()
{
   char name[15];
   unsigned int nblock;
   unsigned int nsize;
   unsigned long base;
   unsigned long text_addr;
   unsigned int i;
   Elf32_Ehdr *ehdr;
   Elf32_Shdr *shdr;
 
   base = getLibAddr();
 
   ehdr = (Elf32_Ehdr *)base;
   text_addr = ehdr->e_shoff + base;
 
   nblock = ehdr->e_entry >> 16;
   nsize = ehdr->e_entry & 0xffff;
 
   printf ( "nblock = %d\n" , nblock);
 
   if (mprotect(( void *) base, 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
     puts ( "mem privilege change failed" );
   }
 
   for (i=0;i< nblock; i++){
     char *addr = ( char *)(text_addr + i);
     *addr = ~(*addr);
   }
 
   if (mprotect(( void *) base, 4096 * nsize, PROT_READ | PROT_EXEC) != 0){
     puts ( "mem privilege change failed" );
   }
   puts ( "Decrypt success" );
}


解密过程,大多数差不多,需要注意两个地方一个是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
unsigned long getLibAddr(){
   unsigned long ret = 0;
   char name[] = "libdexloader.so" ;
   char buf[4096], *temp;
   int pid;
   FILE *fp;
   pid = getpid();
   sprintf (buf, "/proc/%d/maps" , pid);
   fp = fopen (buf, "r" );
   if (fp == NULL)
   {
     puts ( "open failed" );
     goto _error;
   }
   while ( fgets (buf, sizeof (buf), fp)){
     if ( strstr (buf, name)){
       temp = strtok (buf, "-" );
       ret = strtoul (temp, NULL, 16);
       break ;
     }
   }
_error:
   fclose (fp);
   return ret;
}


还有个是mprotect
这个函数用于修改内存页的权限,如果不修改,用户对于内存页的权限只有read,你是无法对内存中的数据进行修改的。这个和之前我们所说的segment的权限不一样,要注意区分。


【再来一次】
这种单独建一个section的方法简单粗暴易懂,但是只要解析一下就会知道多了一个section。所以实际上往往都是对固定的section进行加密解密,要注意的是这些section中有重要的信息,不能乱来,所以难度会大很多。大家有兴趣自己实现以下。
就酱~~~

转载于:https://www.cnblogs.com/ichunqiu/p/5999799.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值