【预备起~~~】
最近在忙找工作的事情,笔试~面试~笔试~面试。。。很久没有写(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文件的加密的更多相关文章

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

    对抗静态分析——运行时修复dex   本文来源:i春秋社区-分享你的技术,为安全加点温度 零.写在前面   这个系列本来题目想写对抗反编译,可是想想对抗反编译的这个范围有点大,总结如下 灵魂作图   ...

  2. 使用Windows EFS(怎么给文件夹加密)进行文件加密

    和Windows BitLocker一样,Encrypting File System(EFS,加密文件系统)是Windows内置的一套基于公共密钥的加密机制,可以加密NTFS分区上的文件和文件夹,能 ...

  3. 一键自动发布ipa(更新svn,拷贝资源,压缩资源,加密图片资源,加密数据文件,加密lua脚本,编译代码,ipa签名,上传ftp)

    一键自动发布ipa(更新svn,拷贝资源,压缩资源,加密图片资源,加密数据文件,加密lua脚本,编译代码,ipa签名,上传ftp) 程序员的生活要一切自动化,更要幸福^_^. 转载请注明出处http: ...

  4. C# 文件夹加密

    可以加密文件内容,也可以对文件夹本身进行加密,本文对文件夹加密. 一.指定或生成一个密钥 1)指定的密钥 /// <summary> /// 密钥,这个密码可以随便指定 /// </ ...

  5. ZIP文件伪加密

    题目给出图片,那当然是从图片下手啦! 首先下载图片,在Linux系统下用binwalk工具打开,果然不出所料,里面藏有文件! 用dd把它分解出来! 'txt' 格式的文件提取出来!会看到一个Zip压缩 ...

  6. Java实现文件的加密与解密

    最近在做一个项目,需要将资源文件(包括图片.动画等类型)进行简单的加密后再上传至云上的服务器,而在应用程序中对该资源使用前先将读取到的文件数据进行解密以得到真正的文件信息.此策略的原因与好处是将准备好 ...

  7. 某返利网站admin目录index.php文件混淆加密算法分析

    ---恢复内容开始--- 文件已经加密,可以在此下载:index.php 文件内容打开大概如此: 简单字符替换之后,发现字符串用base64_decode仍无法解码. 找到一个解码网站:找源码 解码后 ...

  8. [AS3]as3用ByteArray来对SWF文件编码加密实例参考

    [AS3]as3用ByteArray来对SWF文件编码加密实例参考,简单来说,就是将 swf 以 binary 的方式读入,并对 ByteArray 做些改变,再重新存成 swf 档.这个作业当然也可 ...

  9. mac系统下给文件夹加密方法

    电脑里我们往往会有许多隐私的文件,不希望被别人看到,在过去的Windows电脑里,我们习惯性的会在文件夹中将该文件隐藏,但是这个隐藏是不安全的,遇到稍微会点电脑技术的人就可以给你解开,安全性不高,ma ...

随机推荐

  1. 打开 Mac OS X 隐藏的用户资源库文件夹的方法

    在较高版本的 Mac OS X 中,用户的资源库文件夹(/Users/username/Library)默认被系统隐藏了,从 Finder 窗口中不能直接打开. 但是通过下面这个方法可以快速打开用户资 ...

  2. SLAM拾萃(3):siftGPU

    前言 本周博客我们给大家介绍一下SiftGPU.由于特征匹配是SLAM中非常耗时间的一步,许多人都想把它的时间降至最短,因此目前ORB成了非常受欢迎的特征.而老牌SIFT,则一直给人一种“很严谨很精确 ...

  3. zmq 学习笔记

    0. PUB/SUB, XPUB/XSUB filtering happens at publisher sides when sockets are using a connected protoc ...

  4. 【软件分析与挖掘】Vision of Software Clone Management: Past, Present, and Future (Keynote Paper)

    abstract: 代码克隆的综述 S1    INTRODUCTION AND MOTIVATION 代码克隆的利弊: 利:可以有效地去耦合,避免其他一些可能的错误: 弊:当被复制的那段code中带 ...

  5. winform用户控件

    用途用户控件包含Time控件和一个lable控件,一个ToolStrip控件,每隔一秒显示一次时间     1. 生成用户控件   新建一个项目类型为用户控件   注意定义类名,此类名为以后工具箱中显 ...

  6. 基于html5页面滚动背景图片动画效果

    基于html5页面滚动背景图片动画效果是一款带索引按钮的页面滚动动画特效代码.效果图如下: 在线预览   源码下载 实现的代码. html代码: <div id="fullpage&q ...

  7. ASP.NET Web API 的简单示例

    Demo1: HTML: <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> ...

  8. 【Java】一次SpringMVC+ Mybatis 配置多数据源经历

    需求 现在在维护的是学校的一款信息服务APP的后台,最近要开发一些新功能,其中一个就是加入学校电影院的在线购票.在线购票实际上已经有一套系统了,但是是外包给别人开发的,我们拿不到代码只能拿到数据库,并 ...

  9. [C#] 谈谈异步编程async await

    为什么需要异步,异步对可能起阻止作用的活动(例如,应用程序访问 Web 时)至关重要. 对 Web 资源的访问有时很慢或会延迟. 如果此类活动在同步过程中受阻,则整个应用程序必须等待. 在异步过程中, ...

  10. Spring应用——事务管理

    事务基础:请参看:http://www.cnblogs.com/solverpeng/p/5720306.html 一.Spring 事务管理 1.前提:事务管理器 在使用 Spring 声明式事务管 ...