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

Posted i春秋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对抗静态分析——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中有重要的信息,不能乱来,所以难度会大很多。大家有兴趣自己实现以下。
就酱~~~
























































以上是关于对抗静态分析——so文件的加密的主要内容,如果未能解决你的问题,请参考以下文章

对抗静态分析——运行时修复dex

2019-2020-2 20175310奚晨妍《网络对抗技术》Exp4 恶意代码分析

2019-2020-2 20175303柴轩达《网络对抗技术》Exp4 恶意代码分析

20145207李祉昂《网络对抗技术》恶意代码分析

静态分析第三发 so文件分析(小黄人快跑)

20155201 李卓雯 《网络对抗技术》实验一 逆向及Bof基础