美文网首页
加壳VS脱壳(第二代)

加壳VS脱壳(第二代)

作者: M_天河 | 来源:发表于2020-09-01 09:47 被阅读0次

第二代壳,我的理解中有以下几种:
1,将源dex文件加密,而非整个apk,第一代中的解壳过程(Application.attachBaseContext中的方法)放到so中实现,这个原理和第一代大同小异,增加的难度就是增加了c代码的逆向。
2,将关键函数放到so中实现,然后将函数放到自定义的段中,然后对自定义段加密,在so文件加载入口init处对自定义段解密。
3,对so中的关键函数进行加密:原理与2相同,难点在于函数定位,具体函数的定位需要同hash表,首先通过dynamic段找到动态加载相关的三个重要段.dynsym(符号表).dynstr(动态字符串表).hash(哈希表),然后通过传入函数名使用hash表找到目标函数在符号表中的索引,然后再定位具体函数。

一,自定义段加密

1,编译apk,生成so文件,然后对so文件加密

  1. 如果是再java中编写的话需要自定义so文件格式解析的class,这里放在linux下编写,直接调用了elf.h头文件,自动解析elf文件格式;
  2. 读取so文件通过elf文件头中的字符串表索引;
  3. 通过节区头部表偏移获取节区头部表地址;
  4. 通过节区头部表的地址 + 节区字符串表索引 * 节区头部表固定大小,获取字符串表的地址;
  5. 将字符串表的内容复制出来;
  6. 然后通过对每个section的sh_name与我们的自定义section名字对比,找到自定义段的偏移地址和大小;
  7. 然后即可对偏移地址的内容进行加密算法运算;
  8. 将自定义段的偏移和大小写入elf头文件的section部分;
  9. 上一步的原因是elf加载时是基于装载视图,不会在意section部分的参数内容(注:Android7之后如果写在了e_shoff会报错,但是可以写在其他部分)。
#include <stdio.h>
#include <fcntl.h>
#include "elf.h"
#include <stdlib.h>
#include <string.h>

int main(int argc, char** argv){
  char *encodeSoName = "libdemo.so";
  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;
  
  fd = open(encodeSoName, 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);
  
  if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){
    puts("Read ELF section string table error");
    goto _error;
  }
  
  if((shstr = (char *) malloc(shdr.sh_size)) == NULL){
    puts("Malloc space for section string table failed");
    goto _error;
  }
  
  lseek(fd, shdr.sh_offset, SEEK_SET);
  if(read(fd, shstr, shdr.sh_size) != shdr.sh_size){
    puts("Read string table failed");
    goto _error;
  }

  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;
    }
  }
  
  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 = length / 4096 + (length % 4096 == 0 ? 0 : 1);
  printf("base = %x, length = %x\n", base, length);
  printf("nblock = %d, nsize = %d\n", nblock, nsize);
  printf("entry:%x\n",((length << 16) + nsize));

  ehdr.e_entry = (length << 16) + nsize;
  ehdr.e_shoff = base;
  
  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;
  }

  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;
}

2.编写jni项目,

  1. 使用__attribute__自定义section,将关键函数getString()写入自定义section中
  2. 然后在__attribute__中自定义init_getString()函数,此函数中完成so解密工作;
  3. 首先通过进程id,读取maps文件找到so基地址;
  4. 然后读取elf头中的写入的自定义section的偏移和长度,计算出实际地址,然后进行解密工作;
  5. 由于是内存中操作,需要对指定页进行mprotect函数修改权限操作;
#include <jni.h>
#include <stdio.h>
#include <string>
#include <android/log.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <elf.h>
#include <sys/mman.h>
jstring getString(JNIEnv*) __attribute__((section(".smmtext")));
jstring getString(JNIEnv* env){
    return env->NewStringUTF("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;
    __android_log_print(ANDROID_LOG_INFO, "JNITag", "0:Get in init_getString");

    base = getLibAddr();
    __android_log_print(ANDROID_LOG_INFO, "JNITag", "1:base =  0x%x", base);

    ehdr = (Elf32_Ehdr *)base;
    text_addr = ehdr->e_shoff + base;
    __android_log_print(ANDROID_LOG_INFO, "JNITag", "2:text_addr =  0x%x", text_addr);

    nblock = ehdr->e_entry >> 16;
    nsize = ehdr->e_entry & 0xffff;

    __android_log_print(ANDROID_LOG_INFO, "JNITag", "3:nblock =  0x%x,nsize:%d", nblock,nsize);
    __android_log_print(ANDROID_LOG_INFO, "JNITag", "4:base =  0x%x", text_addr);
    printf("nblock = %d\n", nblock);

    if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
        puts("mem privilege change failed");
        __android_log_print(ANDROID_LOG_INFO, "JNITag", "error:mem privilege change failed");
    }

    for(i=0;i< nblock; i++){
        char *addr = (char*)(text_addr + i);
        *addr = ~(*addr);
    }

    if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC) != 0){
        puts("mem privilege change failed");
    }
    puts("Decrypt success");
}

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

extern "C" JNIEXPORT jstring JNICALL
Java_com_smm_mysecenc_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    return getString(env);
}

二,指定函数加密

  1. 函数的结构是保存在动态符号段.dynsym段中的,而函数名是保存在动态字符串段.dynstr中的,没有办法可以直接通过字符串段的函数名找到符号表的地址;
  2. 通过hash表可以找到对应的函数地址;
  3. .hash .dynsym .dynstr等都存在于.dynamic段中,这里来做下检查,打开一个so文件,找到他的哈希表、动态符号表和动态字符串表,分别找出它们的name、off、size字段,然后去他的dynamic段做比较,就会发现都在里面;
    .dynsym
    .dynstr
    .hash
    .dynamic
  4. 程序头phdr中的标记dynamic段和节区头shdr中的类型为SHT_DYNAMIC的段实际上是同一段;
  5. 首先找到.dynamic段,有很多种方法可以找到,最简单的直接遍历phdr中的type为PT_DYNAMIC (2)的段,因为这是唯一的;或者通过遍历节区表中的段名为.dynamic的段。
  6. 然后解析.dynamic段,在elf.h中有定义如下结构,用c的话可以直接调用,用java的话则需要自己手动解析一下,其中tag是标示这个dyn是什么类型的(.dynsym还是.dynstr),val是大小,ptr则是偏移
typedef struct dynamic {
  Elf32_Sword d_tag;
  union {
    Elf32_Sword d_val;
    Elf32_Addr d_ptr;
  } d_un;
} Elf32_Dyn;
  1. 遍历dynamic段找到 .hash .dynsym .dynstr这三个段;
  2. 然后再去定位我们的要加密的函数,过程如下
    (1). 复制出dynstr的字符串内容;
    (2). 根据传入函数名计算出hash值,例如传入函数名Java_com_smm_mysofuncenc_MainActivity_stringFromJNI,hash函数可以在Android源码链接器中找到:
static unsigned elfhash(const char *_name){
    const unsigned char *name = (const unsigned char *)_name;
    unsigned h = 0,g;
    while (*name){
        h = (h << 4) + *name++;
        g = h & 0xf0000000;
        h ^= g;
        h ^= g >> 24;
    }
    return h;
}

(3). 用计算出的hash值去对nbucket取模运算,得出的值乘4+8,然后再hash表中找到这个位置里面的值就是这个函数在符号表.dynsym中的索引;
(4). 然后通过索引值乘dynsym符号表的大小(elf.h文件中有定义Elf32_Sym是16),再加上符号表偏移即可确认目标函数的符号表
(5). 对比从符号表中取出的函数名是否正确,如果不正确就再去hash表中读取下一个,直到正确为止;
(6). 然后通过符号表读取目标函数的偏移和大小;

  1. 然后对目标区域自定义加密算法;
#include <stdio.h>
#include <fcntl.h>
#include "elf.h"
#include <stdlib.h>
#include <string.h>

typedef struct _funcInfo{
  Elf32_Addr st_value;
  Elf32_Word st_size;
}funcInfo;

Elf32_Ehdr ehdr;

//For Test
static void print_all(char *str, int len){
  int i;
  for(i=0;i<len;i++)
  {
    if(str[i] == 0)
      puts("");
    else
      printf("%c", str[i]);
  }
}

static unsigned elfhash(const char *_name)
{
    const unsigned char *name = (const unsigned char *) _name;
    unsigned h = 0, g;

    while(*name) {
        h = (h << 4) + *name++;
        g = h & 0xf0000000;
        h ^= g;
        h ^= g >> 24;
    }
    return h;
}

static Elf32_Off findTargetSectionAddr(const int fd, const char *secName){
  Elf32_Shdr shdr;
  char *shstr = NULL;
  int i;
  
  lseek(fd, 0, SEEK_SET);
  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);
  
  if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){
    puts("Read ELF section string table error");
    goto _error;
  }
  
  if((shstr = (char *) malloc(shdr.sh_size)) == NULL){
    puts("Malloc space for section string table failed");
    goto _error;
  }
  
  lseek(fd, shdr.sh_offset, SEEK_SET);
  if(read(fd, shstr, shdr.sh_size) != shdr.sh_size){
    puts(shstr);
    puts("Read string table failed");
    goto _error;
  }
  
  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, secName) == 0){
      printf("Find section %s, addr = 0x%x\n", secName, shdr.sh_offset);
      break;
    }
  }
  free(shstr);
  return shdr.sh_offset;
_error:
  return -1;
}

static char getTargetFuncInfo(int fd, const char *funcName, funcInfo *info){
  char flag = -1, *dynstr;
  int i;
  Elf32_Sym funSym;
  Elf32_Phdr phdr;
  Elf32_Off dyn_off;
  Elf32_Word dyn_size, dyn_strsz;
  Elf32_Dyn dyn;
  Elf32_Addr dyn_symtab, dyn_strtab, dyn_hash;
  unsigned funHash, nbucket, nchain, funIndex;
  
  lseek(fd, ehdr.e_phoff, SEEK_SET);
  for(i=0;i < ehdr.e_phnum; i++){
    if(read(fd, &phdr, sizeof(Elf32_Phdr)) != sizeof(Elf32_Phdr)){
      puts("Read segment failed");
      goto _error;
    }
    if(phdr.p_type ==  PT_DYNAMIC){
      dyn_size = phdr.p_filesz;
      dyn_off = phdr.p_offset;
      flag = 0;
      printf("Find section %s, size = 0x%x, addr = 0x%x\n", ".dynamic", dyn_size, dyn_off);
      break;
    }
  }
  if(flag){
    puts("Find .dynamic failed");
    goto _error;
  }
  flag = 0;

  printf("dyn_size:%d\n",dyn_size);
  printf("count:%d\n",(dyn_size/sizeof(Elf32_Dyn)));
  printf("off:%x\n",dyn_off);
  
  lseek(fd, dyn_off, SEEK_SET);
  for(i=0;i < dyn_size / sizeof(Elf32_Dyn); i++){
    int sizes = read(fd, &dyn, sizeof(Elf32_Dyn));
    if(sizes != sizeof(Elf32_Dyn)){
      puts("Read .dynamic information failed");
      //goto _error;
      break;
    }    
    if(dyn.d_tag == DT_SYMTAB){
      dyn_symtab = dyn.d_un.d_ptr;
      flag += 1;
      printf("Find .dynsym, addr = 0x%x, val = 0x%x\n", dyn_symtab, dyn.d_un.d_val);
    }

    if(dyn.d_tag == DT_HASH){
      dyn_hash = dyn.d_un.d_ptr;
      flag += 2;
      printf("Find .hash, addr = 0x%x\n", dyn_hash);
    }
    if(dyn.d_tag == DT_STRTAB){
      dyn_strtab = dyn.d_un.d_ptr;
      flag += 4;
      printf("Find .dynstr, addr = 0x%x\n", dyn_strtab);
    }
    if(dyn.d_tag == DT_STRSZ){
      dyn_strsz = dyn.d_un.d_val;
      flag += 8;
      printf("Find .dynstr size, size = 0x%x\n", dyn_strsz);
    }
  }

  if((flag & 0x0f) != 0x0f){
    puts("Find needed .section failed\n");
    goto _error;
  }
  
  dynstr = (char*) malloc(dyn_strsz);
  if(dynstr == NULL){
    puts("Malloc .dynstr space failed");
    goto _error;
  }
  
  lseek(fd, dyn_strtab, SEEK_SET);
  if(read(fd, dynstr, dyn_strsz) != dyn_strsz){
    puts("Read .dynstr failed");
    goto _error;
  }
  
  funHash = elfhash(funcName);
  printf("Function %s hashVal = 0x%x\n", funcName, funHash);
  
  lseek(fd, dyn_hash, SEEK_SET);
  if(read(fd, &nbucket, 4) != 4){
    puts("Read hash nbucket failed\n");
    goto _error;
  }
  printf("nbucket = %d\n", nbucket);
  
  if(read(fd, &nchain, 4) != 4){
    puts("Read hash nchain failed\n");
    goto _error;
  }
  printf("nchain = %d\n", nchain);
  
  funHash = funHash % nbucket;
  printf("funHash mod nbucket = %d \n", funHash);
  
  lseek(fd, funHash * 4, SEEK_CUR);
  if(read(fd, &funIndex, 4) != 4){
    puts("Read funIndex failed\n");
    goto _error;
  }

  printf("funcIndex:%d\n", funIndex);
  
  lseek(fd, dyn_symtab + funIndex * sizeof(Elf32_Sym), SEEK_SET);
  if(read(fd, &funSym, sizeof(Elf32_Sym)) != sizeof(Elf32_Sym)){
    puts("Read funSym failed");
    goto _error;
  }
  
  if(strcmp(dynstr + funSym.st_name, funcName) != 0){
    while(1){
        printf("hash:%x,nbucket:%d,funIndex:%d\n",dyn_hash,nbucket,funIndex);
        lseek(fd, dyn_hash + 4 * (2 + nbucket + funIndex), SEEK_SET);
        if(read(fd, &funIndex, 4) != 4){
          puts("Read funIndex failed\n");
          goto _error;
        }

        printf("funcIndex:%d\n", funIndex);
        
        if(funIndex == 0){
          puts("Cannot find funtion!\n");
          goto _error;
        }
        
        lseek(fd, dyn_symtab + funIndex * sizeof(Elf32_Sym), SEEK_SET);
        if(read(fd, &funSym, sizeof(Elf32_Sym)) != sizeof(Elf32_Sym)){
          puts("In FOR loop, Read funSym failed");
          goto _error;
        }
        
        if(strcmp(dynstr + funSym.st_name, funcName) == 0){
          break;
        }
    }
  }
  
  printf("Find: %s, offset = 0x%x, size = 0x%x\n", funcName, funSym.st_value, funSym.st_size);
  info->st_value = funSym.st_value;
  info->st_size = funSym.st_size;
  free(dynstr);
  return 0;
  
_error:
  free(dynstr);
  return -1;
}

int main(int argc, char **argv){
  char secName[] = ".dynamic";
  char funcName[] = "Java_com_example_shelldemo2_MainActivity_getString";
  char *soName = "libdemo.so";
  char *content = NULL;
  int fd, i;
  Elf32_Off secOff;
  funcInfo info;

  unsigned a = elfhash(funcName);
  printf("a:%d\n", a);
  
  fd = open(soName, O_RDWR);
  if(fd < 0){
    printf("open %s failed\n", argv[1]);
    goto _error;
  }
  
  secOff = findTargetSectionAddr(fd, secName);
  if(secOff == -1){
    printf("Find section %s failed\n", secName);
    goto _error;
  }
  if(getTargetFuncInfo(fd, funcName, &info) == -1){
    printf("Find function %s failed\n", funcName);
    goto _error;
  }
  
  content = (char*) malloc(info.st_size);
  if(content == NULL){
    puts("Malloc space failed");
    goto _error;
  }
  
  lseek(fd, info.st_value - 1, SEEK_SET);
  if(read(fd, content, info.st_size) != info.st_size){
    puts("Malloc space failed");
    goto _error;
  }
  
  for(i=0;i<info.st_size -1;i++){
    content[i] = ~content[i];
  }
  
  lseek(fd, info.st_value-1, SEEK_SET);
  if(write(fd, content, info.st_size) != info.st_size){
    puts("Write modified content to .so failed");
    goto _error;
  }
  puts("Complete!");
  
_error:
  free(content);
  close(fd);
  return 0;
}

相关文章

  • 加壳VS脱壳(第二代)

    第二代壳,我的理解中有以下几种:1,将源dex文件加密,而非整个apk,第一代中的解壳过程(Application...

  • 六 iOS逆向 - 脱壳

    加壳脱壳基本概念 使用dumpdecrypted工具进行iOSApp脱壳 一 加壳脱壳基本概念 一 什么是加壳? ...

  • 加壳脱壳

    脱壳存根(stub) 脱壳存根执行了以下三个步骤: (1)将原始程序脱壳到内存中 (2)解析原始可执行文件的所有导...

  • 八、加壳脱壳

    从之前的知识可知,通过class-dump或hopper就可以知道一个app的头文件信息。 加壳的app:但如果是...

  • 手工加壳脱壳

    PE文件的Magic code(魔数、幻数)是什么? MZ头、PE头 PE文件中文件头的信息有哪些? 运行平台、时...

  • Android加壳脱壳

    Android 加固与脱壳 加固与脱壳常用加固 so 文件特征分析 apk 加固动态调试分析 apk 加固-环境搭...

  • 加壳脱壳(04)

    1.什么是脱壳? 摘掉壳程序,将未加密的可执行文件还原出来(有些人也称为“砸壳”) 脱壳主要有2种方法:硬脱壳、动...

  • iOS逆向:脱壳

    目录一,加壳二,脱壳三,工具四,验证 一,加壳 1,介绍 App Store会对ipa包中的可执行文件进行加壳操作...

  • iOS逆向与安全8.1:砸壳、初识Theos

    砸壳 软件脱壳,顾名思义,就是对软件加壳的逆操作,把软件上存在的壳去掉(解密)。 砸壳原理 应用加壳(加密)提交给...

  • 应用砸壳

    砸壳 软件脱壳,顾明思义,就是对软件加壳的逆操作,把软件上存在的壳去掉(解密) 砸壳原理 应用加壳(加密) 应用砸...

网友评论

      本文标题:加壳VS脱壳(第二代)

      本文链接:https://www.haomeiwen.com/subject/infwdktx.html