简单粗暴的so加解密实现

转:https://bbs.pediy.com/thread-191649.htm

 

以前一直对.so文件加载时解密不懂,不了解其工作原理和实现思路。最近翻看各种资料,有了一些思路。看到论坛没有类似帖子,故来一帖,也作为学习笔记。限于水平,本菜没有找到安卓平台一些具体实现思路,这些方法都是借鉴其他平台的实现思路和本菜的YY,肯定会有不少疏漏和错误之处,还请各位大牛指正,感激不尽!

简单粗暴的so加解密实现
一、  概述
利用动态链接库实现安卓应用的核心部分,能一定程度的对抗逆向。由于ida等神器的存在,还需要对核心部分进行加密。动态链接库的加密,在我看来,有两种实现方式:1. 有源码; 2、无源码。无源码的加密,类似window平台的加壳和对.dex文件的加壳,需要对文件进行分析,在合适的地方插入解密代码,并修正一些参数。而如果有源码,则可以构造解密代码,并让解密过程在.so被加载时完成。(当然,应用程序加载了.so文件后,内存中.so数据已经被解密,可直接dump分析。同时,也有一些对抗dump的方法,这里就不展开了)。
下文只针对有源码这种方式进行讨论,分析一些可行的实现方法。主要是包含对ELF header的分析(不是讨论各个字段含义); 基于特定section和特定函数的加解密实现(不讨论复杂的加密算法)。

二、  针对动态链接库的ELF头分析
网上有很多资料介绍ELF文件格式,而且写得很好很详细。我这里就不重复,不太了解的朋友,建议先看看。以下内容,我主要从链接视图和装载视图来分析ELF头的各个字段,希望能为读者提供一些ELF文件头的修正思路。
这里,我再罗嗦列出ELF头的各个字段:

typedef struct {
  unsigned char  e_ident[EI_NIDENT];  /* File identification. */
  Elf32_Half  e_type;    /* File type. */
  Elf32_Half  e_machine;  /* Machine architecture. */
  Elf32_Word  e_version;  /* ELF format version. */
  Elf32_Addr  e_entry;  /* Entry point. */
  Elf32_Off  e_phoff;  /* Program header file offset. */
  Elf32_Off  e_shoff;  /* Section header file offset. */
  Elf32_Word  e_flags;  /* Architecture-specific flags. */
  Elf32_Half  e_ehsize;  /* Size of ELF header in bytes. */
  Elf32_Half  e_phentsize;  /* Size of program header entry. */
  Elf32_Half  e_phnum;  /* Number of program header entries. */
  Elf32_Half  e_shentsize;  /* Size of section header entry. */
  Elf32_Half  e_shnum;  /* Number of section header entries. */
  Elf32_Half  e_shstrndx;  /* Section name strings section. */
} Elf32_Ehdr;


e_ident、e_type、e_machine、e_version、e_flags和e_ehsize字段比较固定;e_entry 入口地址与文件类型有关。e_phoff、e_phentsize和e_phnum与装载视图有关;e_shoff、e_shentsize、e_shnum和e_shstrndx与链接视图有关。目前e_ehsize = 52字节,e_shentsize = 40字节,e_phentsize = 32字节。
下面看看这两种视图的排列结构:

直接从图中,可以得到一些信息:Program header位于ELF header后面,Section Header位于ELF文件的尾部。那可以推出:
e_phoff = sizeof(e_ehsize);
整个ELF文件大小 = e_shoff + e_shnum * sizeof(e_shentsize) 
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中的。可以在\obj\local\armeabi\下找到未经过strip的.so文件。到这里,我们就可以把http://bbs.pediy.com/showthread.php?t=188793 帖子中提到的.so文件修正。如果e_shoff和e_shnum都改成任意值,那么修正起来比较麻烦。
感觉上好像e_shoff、e_shnum等与section相关的信息任意修改,对.so文件的使用毫无影响。的确是这样的,至少给出两个方面来佐证:
1.  so文件在内存中的映射

相信了解elf装载(执行)视图的朋友肯定清楚,.so文件是以segment为单位映射到内存的。图中红色区域的section是没有被映射的内存,当然也在segment中找不到。
2.  安卓linker源码
在linker.h源码中有一个重要的结构体soinfo,下面列出一些字段:

struct soinfo{
    const char name[SOINFO_NAME_LEN]; //so全名
    Elf32_Phdr *phdr;                 //Program header的地址
    int phnum;                        //segment 数量
    unsigned *dynamic;                //指向.dynamic,在section和segment中相同的
    
    //以下4个成员与.hash表有关
    unsigned nbucket;
    unsigned nchain;
    unsigned *bucket;
    unsigned *chain;
    
    //这两个成员只能会出现在可执行文件中
    unsigned *preinit_array;
    unsigned preinit_array_count;
  

    //指向初始化代码,先于main函数之行,即在加载时被linker所调用,在linker.c可以看到:__linker_init -> link_image -> call_constructors -> call_array
    unsigned *init_array;
    unsigned init_array_count;
    void (*init_func)(void);
    //与init_array类似,只是在main结束之后执行
    unsigned *fini_array;
    unsigned fini_array_count;
    void (*fini_func)(void);
}


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

根据上述结论,做一个面目全非,各种工具打开报错的so文件就很easy了,读者可以试试,这里就不举例,你将在后续内容中看到。

三、  基于特定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 和 所在内存块写入e_entry中,即ehdr.e_entry = (length << 16) + nsize。  (nsize = base / 4096 + (base % 4096 == 0 ? 0 : 1);)当然,这样同时也简化了解密流程,还有一个好处是:如果将so文件头修正放回去,程序是不能运行的。

解密时,需要保证解密函数在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)  修改回内存区域的读写权限
(这里是对代码段的数据进行解密,需要写权限。如果对数据段的数据解密,是不需要更改权限直接操作的)

利用readelf查看加密后的so文件:

运行结果很简单,源码见附件

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

shellDemo的加密代码

shellAdder1.c

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

//./shellAdder1 libdemo.so
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 nsize;
  
  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;
  }
  
  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;
  }
  
  base = shdr.sh_addr;		//由于解密的是加载视图,此处应该是虚拟地址
  nsize = base / 4096 + (base % 4096 == 0 ? 0 : 1);
  printf("base = %d, length = %d\n", base, length);
  printf("nsize = %d\n",nsize);			//此节所在的内存块
  
  ehdr.e_entry = (length << 16) + nsize;
  ehdr.e_shoff = base;

  base = shdr.sh_offset;
  
  
  
  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;
}

makefile文件

all:
	gcc -o shellAdder1 shellAdder1.c
encrypt:
	./shellAdder1 libdemo.so

ShellDemo的jni解密代码

demo.c

#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 nLength;
  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;	//ehdr->e_shoff保存的是加密节的虚拟地址
  
  nLength = ehdr->e_entry >> 16;	//ehdr->e_entry 高16位为加密节的大小,低16位为加密节所在的内存块
  nsize = ehdr->e_entry & 0xffff;
  
  printf("nLength = %d\n", nLength);
  
  if(mprotect((void *) base, 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
    puts("mem privilege change failed");
  }
  
  for(i=0;i< nLength; 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;
}

Android.mk文件

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := demo
LOCAL_SRC_FILES := demo.c
include $(BUILD_SHARED_LIBRARY)


四、  基于特定函数的加解密实现
上面的加解密方式可谓简单粗暴。采用这种方式实现,如果ELF头section被恢复,则很容易被发现so多了一个section。那么,对ELF中已存在的section中的数据部分加密,可以达到一定的隐藏效果。
与上节例子类似,命名为shelldemo2,只是native直接返回字符串给UI。需要做的是对Java_com_example_shelldemo2_MainActivity_getString函数进行加密。加密和解密都是基于装载视图实现。需要注意的是,被加密函数如果用static声明,那么函数是不会出现在.dynsym中,是无法在装载视图中通过函数名找到进行解密的。当然,也可以采用取巧方式,类似上节,把地址和长度信息写入so头中实现。Java_com_example_shelldemo2_MainActivity_getString需要被调用,那么一定是能在.dynsym找到的。
加密流程:
1)  读取文件头,获取e_phoff、e_phentsize和e_phnum信息
2)  通过Elf32_Phdr中的p_type字段,找到DYNAMIC。从下图可以看出,其实DYNAMIC就是.dynamic section。从p_offset和p_filesz字段得到文件中的起始位置和长度

3)  遍历.dynamic,找到.dynsym、.dynstr、.hash section文件中的偏移和.dynstr的大小。在我的测试环境下,fedora 14和windows7 Cygwin x64中elf.h定义.hash的d_tag标示是:DT_GNU_HASH;而安卓源码中的是:DT_HASH。
4)  根据函数名称,计算hash值
5)  根据hash值,找到下标hash % nbuckets的bucket;根据bucket中的值,读取.dynsym中的对应索引的Elf32_Sym符号;从符号的st_name所以找到在.dynstr中对应的字符串与函数名进行比较。若不等,则根据chain[i](i为符号表索引)找下一个Elf32_Sym符号,直到找到或者chain终止为止。这里叙述得有些复杂,直接上代码。

for(i = bucket[funHash % nbucket]; i != 0; i = chain[i]){
   if(strcmp(dynstr + (funSym + i)->st_name, funcName) == 0){
      flag = 0;
      break;
   }
}


6)  找到函数对应的Elf32_Sym符号后,即可根据st_value和st_size字段找到函数的位置和大小
7)  后面的步骤就和上节相同了,这里就不赘述

解密流程为加密逆过程,大体相同,只有一些细微的区别,具体如下:
1)  找到so文件在内存中的起始地址
2)  也是通过so文件头找到Phdr;从Phdr找到PT_DYNAMIC后,需取p_vaddr和p_filesz字段,并非p_offset,这里需要注意。
3)  后续操作就加密类似,就不赘述。对内存区域数据的解密,也需要注意读写权限问题。

加密后效果:

运行结果与上节相同,就不贴了。

ShellDemo2的加密代码

shellAdder2.c

#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 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, 0, SEEK_SET);
  if(read(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)){
    puts("Read ELF header error");
    goto _error;
  }

  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, offset = 0x%x\n", ".dynamic", dyn_size, dyn_off);
      break;
    }
  }
  if(flag){
    puts("Find .dynamic failed");
    goto _error;
  }
  flag = 0;
  
    /* Dynamic structure */
    //typedef struct {
	//Elf32_Sword	d_tag;		/* controls meaning of d_val */
	//union {
	//	Elf32_Word	d_val;	/* Multiple meanings - see d_tag */
	//	Elf32_Addr	d_ptr;	/* program virtual address */
	//} d_un;
    //} Elf32_Dyn;
  lseek(fd, dyn_off, SEEK_SET);
  for(i=0;i < dyn_size / sizeof(Elf32_Dyn); i++){
    if(read(fd, &dyn, sizeof(Elf32_Dyn)) != sizeof(Elf32_Dyn)){
      puts("Read .dynamic information failed");
      goto _error;
    }    
    if(dyn.d_tag == DT_SYMTAB){
      dyn_symtab = dyn.d_un.d_ptr;
      flag += 1;
      printf("Find .dynsym, addr = 0x%x\n", dyn_symtab);
    }
//    if(dyn.d_tag == DT_GNU_HASH){ //For Android: DT_HASH
    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;
  }
  
  //DT_SYMTAB,DT_HASH,DT_STRTAB virtual address is in the first load segment range in so,so virtual address == file offset
  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;
  }
//  print_all(dynstr, dyn_strsz);
  
  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;
  }
  
  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){
	lseek(fd, dyn_hash + 4 * (2 + nbucket + funIndex), SEEK_SET);
	if(read(fd, &funIndex, 4) != 4){
	  puts("Read funIndex failed\n");
	  goto _error;
	}
	
	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 funcName[] = "Java_com_example_shelldemo2_MainActivity_getString";
  char *content = NULL;
  int fd, i;
  funcInfo info;
  
  if(argc < 2){
    puts("Usage: shellAdder2 libxxx.so");
    return -1;
  }
  fd = open(argv[1], O_RDWR);
  if(fd < 0){
    printf("open %s failed\n", argv[1]);
    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;
  }
  //the export function is in the .text,.text is the first load segment range,so virtual address == file offset. 
  // 22: 0000135d    24 FUNC    GLOBAL DEFAULT    7 Java_com_example_shelldemo2_MainActivity_getString
  //the function is thumb format,so info.st_value - 1
  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 - 1) != info.st_size - 1){
    puts("Write modified content to .so failed");
    goto _error;
  }
  puts("Complete!");
  
_error:
  free(content);
  close(fd);
  return 0;
}

makefile文件

all:
	gcc -o shellAdder2 shellAdder2.c
encrypt:
	./shellAdder2 libdemo.so

jni解密部分

demo.c

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

#define DEBUG

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


void init_getString() __attribute__((constructor));

static void print_debug(const char *msg){
#ifdef DEBUG
	__android_log_print(ANDROID_LOG_INFO, "JNITag", "%s", msg);
#endif
}

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 unsigned int getLibAddr(){
  unsigned int 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;
}

static char getTargetFuncInfo(unsigned long base, const char *funcName, funcInfo *info){
	char flag = -1, *dynstr;
	int i;
	Elf32_Ehdr *ehdr;
	Elf32_Phdr *phdr;
	Elf32_Off dyn_vaddr;
	Elf32_Word dyn_size, dyn_strsz;
	Elf32_Dyn *dyn;
    Elf32_Addr dyn_symtab, dyn_strtab, dyn_hash;
	Elf32_Sym *funSym;
	unsigned funHash, nbucket;
	unsigned *bucket, *chain;

    ehdr = (Elf32_Ehdr *)base;
	phdr = (Elf32_Phdr *)(base + ehdr->e_phoff);
//    __android_log_print(ANDROID_LOG_INFO, "JNITag", "phdr =  0x%p, size = 0x%x\n", phdr, ehdr->e_phnum);
	for (i = 0; i < ehdr->e_phnum; ++i) {
//		__android_log_print(ANDROID_LOG_INFO, "JNITag", "phdr =  0x%p\n", phdr);
		if(phdr->p_type ==  PT_DYNAMIC){
			flag = 0;
			print_debug("Find .dynamic segment");
			break;
		}
		phdr ++;
	}
	if(flag)
		goto _error;
	dyn_vaddr = phdr->p_vaddr + base;
	dyn_size = phdr->p_filesz;
	__android_log_print(ANDROID_LOG_INFO, "JNITag", "dyn_vadd =  0x%x, dyn_size =  0x%x", dyn_vaddr, dyn_size);
	flag = 0;
	for (i = 0; i < dyn_size / sizeof(Elf32_Dyn); ++i) {
		dyn = (Elf32_Dyn *)(dyn_vaddr + i * sizeof(Elf32_Dyn));
		if(dyn->d_tag == DT_SYMTAB){
			dyn_symtab = (dyn->d_un).d_ptr;
			flag += 1;
			__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find .dynsym section, addr = 0x%x\n", dyn_symtab);
		}
		if(dyn->d_tag == DT_HASH){
			dyn_hash = (dyn->d_un).d_ptr;
			flag += 2;
			__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find .hash section, addr = 0x%x\n", dyn_hash);
		}
		if(dyn->d_tag == DT_STRTAB){
			dyn_strtab = (dyn->d_un).d_ptr;
			flag += 4;
			__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find .dynstr section, addr = 0x%x\n", dyn_strtab);
		}
		if(dyn->d_tag == DT_STRSZ){
			dyn_strsz = (dyn->d_un).d_val;
			flag += 8;
			__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find strsz size = 0x%x\n", dyn_strsz);
		}
	}
	if((flag & 0x0f) != 0x0f){
		print_debug("Find needed .section failed\n");
		goto _error;
	}
	dyn_symtab += base;
	dyn_hash += base;
	dyn_strtab += base;

	funHash = elfhash(funcName);
	funSym = (Elf32_Sym *) dyn_symtab;
	dynstr = (char*) dyn_strtab;
	nbucket = *((int *) dyn_hash);
	bucket = (int *)(dyn_hash + 8);
	chain = (unsigned int *)(dyn_hash + 4 * (2 + nbucket));

	flag = -1;
	__android_log_print(ANDROID_LOG_INFO, "JNITag", "hash = 0x%x, nbucket = 0x%x\n", funHash, nbucket);
	for(i = bucket[funHash % nbucket]; i != 0; i = chain[i]){
		__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find index = %d\n", i);
		if(strcmp(dynstr + (funSym + i)->st_name, funcName) == 0){
			flag = 0;
			__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find %s\n", funcName);
			break;
		}
	}
	if(flag) goto _error;
	info->st_value = (funSym + i)->st_value;
	info->st_size = (funSym + i)->st_size;
	__android_log_print(ANDROID_LOG_INFO, "JNITag", "st_value = %d, st_size = %d", info->st_value, info->st_size);
	return 0;
_error:
	return -1;
}

//decrypt function
void init_getString(){
	const char target_fun[] = "Java_com_example_shelldemo2_MainActivity_getString";
	funcInfo info;
	int i;
	unsigned int npage, base = getLibAddr();

	__android_log_print(ANDROID_LOG_INFO, "JNITag", "base addr =  0x%x", base);
	if(getTargetFuncInfo(base, target_fun, &info) == -1){
	  print_debug("Find Java_com_example_shelldemo2_MainActivity_getString failed");
	  return ;
	}
	npage = info.st_size / PAGE_SIZE + ((info.st_size % PAGE_SIZE == 0) ? 0 : 1);
	if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), npage, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
		print_debug("mem privilege change failed");
	}

	for(i=0;i< info.st_size - 1; i++){
		char *addr = (char*)(base + info.st_value -1 + i);
		*addr = ~(*addr);
	}

	if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), npage, PROT_READ | PROT_EXEC) != 0){
		print_debug("mem privilege change failed");
	}

}

JNIEXPORT jstring JNICALL
Java_com_example_shelldemo2_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 (*env)->NewStringUTF(env, "Native method return!");
}


Android.mk文件

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := demo
LOCAL_SRC_FILES := demo.c
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)


五、  参考资料
http://blog.csdn.net/forlong401/article/details/12060605
《ELF文件格式》
Android linker源码:bionic\linker
Android libc源码:bionic\libc\bionic
谷歌:ELF链接视图与装载视图相关资料
------------------------------------------------------------------------
基于上面的方法,我写了一个CrackMe.apk的注册机程序供大家玩耍。输入3~10位的username和regcodes,8位的校验码,字符范围:A~Z、a~z、0~9。若校验通过,则提示:congratulation! You crack it!. 
附件:
 shelldemo.zip
 shelldemo2.zip
 CrackMe.apk

上个pdf,这个太乱了。
 简单粗暴的so加解密实现.pdf

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值