ELF文件加密

ELF头的各个字段如下:

    1. #define EI_NIDENT 16  
    2. typedef struct{  
    3.     unsigned char e_ident[EI_NIDENT];    //目标文件标识信息  
    4.     Elf32_Half e_type;                             //目标文件类型  
    5.     Elf32_Half e_machine;                       //目标体系结构类型  
    6.     Elf32_Word e_version;                      //目标文件版本  
    7.     Elf32_Addr e_entry;                          //程序入口的虚拟地址,若没有,可为0  
    8.     Elf32_Off e_phoff;                            //程序头部表格(Program Header Table)的偏移量(按字节计算),若没有,可为0  
    9.     Elf32_Off e_shoff;                            //节区头部表格(Section Header Table)的偏移量(按字节计算),若没有,可为0  
    10.     Elf32_Word e_flags;                        //保存与文件相关的,特定于处理器的标志。标志名称采用 EF_machine_flag的格式。  
    11.     Elf32_Half e_ehsize;                        //ELF 头部的大小(以字节计算)。  
    12.     Elf32_Half e_phentsize;                   //程序头部表格的表项大小(按字节计算)。  
    13.     Elf32_Half e_phnum;                      //程序头部表格的表项数目。可以为 0。  
    14.     Elf32_Half e_shentsize;                  //节区头部表格的表项大小(按字节计算)。  
    15.     Elf32_Half e_shnum;      //节区头部表格的表项数目。可以为 0。  
    16.     Elf32_Half e_shstrndx;  //节区头部表格中与节区名称字符串表相关的表项的索引。如果文件没有节区名称字符串表,此参数可以为 SHN_UNDEF。  
    17. }Elf32_Ehdr;  

关于头部我们要记住的有这几点,就可以根据其中部分条件找另外的值了:

e_phoff = sizeof(e_ehsize);

整个ELF文件大小 = e_shoff + e_shnum * sizeof(e_shentsize) + 1

通常情况下:e_shstrndx = e_shnum – 1
e_shstrndx字段的值跟strip有关。Strip之前:.shstrtab 并不是最后一个section.则 e_shstrndx = e_shnum – 1 – 2;
而经过strip之后,动态链接库末尾的.symtab和.strtab这两个section会被去掉. 则e_shstrndx = e_shnum – 1。

使用ndk生成在\libs\ armeabi\下的.so文件是经过strip的,也是被打包到apk中的。

tips:但是如果e_shoff和e_shnum都改成任意值,那么修正起来比较麻烦。

但貌似e_shoff、e_shnum等与section相关的信息任意修改,对.so文件的使用毫无影响。

能找到的一句如下:
1.elf如何装载
2.linker如何链接

基于上面的结论,再来分析下ELF头的字段。
1) e_ident[EI_NIDENT] 字段包含魔数、字节序、字长和版本,后面填充0。对于安卓的linker,通过verify_elf_object函数检验魔数,判定是否为.so文件。那么,我们可以向位置写入数据,至少可以向后面的0填充位置写入数据。遗憾的是,我在fedora 14下测试,是不能向0填充位置写数据,链接器报非0填充错误。
2) 对于安卓的linker,对e_type、e_machine、e_version和e_flags字段并不关心,是可以修改成其他数据的(仅分析,没有实测)
3) 对于动态链接库,e_entry 入口地址是无意义的,因为程序被加载时,设定的跳转地址是动态连接器的地址,这个字段是可以被作为数据填充的。
4) so装载时,与链接视图没有关系,即e_shoff、e_shentsize、e_shnum和e_shstrndx这些字段是可以任意修改的。被修改之后,使用readelf和ida等工具打开,会报各种错误,相信读者已经见识过了。
5) 既然so装载与装载视图紧密相关,自然e_phoff、e_phentsize和e_phnum这些字段是不能动的。

再来分析so修改
三、 基于特定section的加解密实现
基于section的加解密,是指将so文件的特定section进行加密,so文件被加载时解密。下面给出实例。
假设有一个shelldemo应用,调用一个native方法返回一个字符串供UI显示。在native方法中,又调用getString方法返回一个字符串供native方法返回。我需要将getString方法加密。这里,将getString方法存放在.mytext中(指定attribute((section (“.mytext”)));),即是需要对.mytext进行加密。
加密流程:
1) 从so文件头读取section偏移shoff、shnum和shstrtab
2) 读取shstrtab中的字符串,存放在str空间中
3) 从shoff位置开始读取section header, 存放在shdr
4) 通过shdr -> sh_name 在str字符串中索引,与.mytext进行字符串比较,如果不匹配,继续读取
5) 通过shdr -> sh_offset 和 shdr -> sh_size字段,将.mytext内容读取并保存在content中。
6) 为了便于理解,不使用复杂的加密算法。这里,只将content的所有内容取反,即 *content = ~(*content);
7) 将content内容写回so文件中
8) 为了验证第二节中关于section 字段可以任意修改的结论,这里,将shdr -> addr 写入ELF头e_shoff,将shdr -> sh_size 和 addr 所在内存块写入e_entry中,即ehdr.e_entry = (length << 16) + nsize。当然,这样同时也简化了解密流程,还有一个好处是:如果将so文件头修正放回去,程序是不能运行的。

#include <stdio.h>
#include <fcntl.h>
#include <elf.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char** argv){
  char target_section[] = ".mytext";
  char *shstr = NULL;
  char *content = NULL;
  Elf32_Ehdr ehdr;
  Elf32_Shdr shdr;
  int i;
  unsigned int base, length;
  unsigned short nblock;
  unsigned short nsize;
  unsigned char block_size = 16;

  int fd;

  if(argc < 2){
    puts("Input .so file");
    return -1;
  }

  fd = open(argv[1], O_RDWR);
  if(fd < 0){
    printf("open %s failed\n", argv[1]);
    goto _error;
  }
  //从so文件头读取section偏移shoff、shnum和shstrtab
  if(read(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)){
    puts("Read ELF header error");
    goto _error;
  }
  //off_t lseek(int handle, off_t offset, int fromwhere);
  lseek(fd, ehdr.e_shoff + sizeof(Elf32_Shdr) * ehdr.e_shstrndx, SEEK_SET);

  // ELF section string  table
  if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){
    puts("Read ELF section string table error");
    goto _error;
  }
  //分配str空间中
  if((shstr = (char *) malloc(shdr.sh_size)) == NULL){
    puts("Malloc space for section string table failed");
    goto _error;
  }
  //Read string table 
  lseek(fd, shdr.sh_offset, SEEK_SET);
  if(read(fd, shstr, shdr.sh_size) != shdr.sh_size){
    puts("Read string table failed");
    goto _error;
  }
  //通过shdr -> sh_name 在str字符串中索引,与.mytext进行字符串比较,如果不匹配,继续读取
  lseek(fd, ehdr.e_shoff, SEEK_SET);
  for(i = 0; i < ehdr.e_shnum; i++){
    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){
      base = shdr.sh_offset;
      length = shdr.sh_size;
      printf("Find section %s\n", target_section);
      break;
    }
  }
  //通过shdr -> sh_offset 和 shdr -> sh_size字段,将.mytext内容读取并保存在content中
  lseek(fd, base, SEEK_SET);
  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;
  ehdr.e_shoff = base;


  //将content的所有内容取反
  for(i=0;i<length;i++){
    content[i] = ~content[i];
  }



  lseek(fd, 0, SEEK_SET);
  if(write(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)){
    puts("Write ELFhead to .so failed");
    goto _error;
  }

  //将content内容写回so文件中
  lseek(fd, base, SEEK_SET);
  if(write(fd, content, length) != length){
    puts("Write modified content to .so failed");
    goto _error;
  }


  puts("Completed");
_error:
  free(content);
  free(shstr);
  close(fd);
  return 0;
}

我们来看解密对应的加密操作

解密时,需要保证解密函数在so加载时被调用,那函数声明为:init_getString attribute((constructor))。(也可以使用c++构造器实现, 其本质也是用attribute实现)
解密流程:
1) 动态链接器通过call_array调用init_getString
2) Init_getString首先调用getLibAddr方法,得到so文件在内存中的起始地址
3) 读取前52字节,即ELF头。通过e_shoff获得.mytext内存加载地址,ehdr.e_entry获取.mytext大小和所在内存块
4) 修改.mytext所在内存块的读写权限
5) 将[e_shoff, e_shoff + size]内存区域数据解密,即取反操作:*content = ~(*content);
6) 修改回内存区域的读写权限
(这里是对代码段的数据进行解密,需要写权限。如果对数据段的数据解密,是不需要更改权限直接操作的)

#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <elf.h>
#include <sys/mman.h>

jstring getString(JNIEnv*) __attribute__((section (".mytext")));
jstring getString(JNIEnv* env){
    return (*env)->NewStringUTF(env, "Native method return!");
};

void init_getString() __attribute__((constructor));
unsigned long getLibAddr();

void init_getString(){
  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");
}

unsigned long getLibAddr(){
  unsigned long ret = 0;
  char name[] = "libdemo.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;
}

JNIEXPORT jstring JNICALL
Java_com_example_shelldemo_MainActivity_getString( JNIEnv* env,
                                                  jobject thiz )
{
#if defined(__arm__)
  #if defined(__ARM_ARCH_7A__)
    #if defined(__ARM_NEON__)
      #define ABI "armeabi-v7a/NEON"
    #else
      #define ABI "armeabi-v7a"
    #endif
  #else
   #define ABI "armeabi"
  #endif
#elif defined(__i386__)
   #define ABI "x86"
#elif defined(__mips__)
   #define ABI "mips"
#else
   #define ABI "unknown"
#endif

    return getString(env);
}

注意:并不是所有的section都能全加,有些数据是不能加密的。比如直接对.text直接加密,会把与crt有关代码也加密,只能选择性的加密。

参考:http://bbs.pediy.com/thread-191649.htm

posted @ 2017-07-27 19:26  Tesi1a  阅读(1227)  评论(0编辑  收藏  举报