Android逆向之旅---基于对so中的section加密技术实现so加固

来源:互联网 发布:2004nba总决赛数据 编辑:程序博客网 时间:2024/05/22 11:54

转:http://blog.csdn.net/jiangwei0910410003/article/details/49962173

目录(?)[+]

一、前言

好长时间没有更新文章了,主要还是工作上的事,连续加班一个月,没有时间研究了,只有周末有时间,来看一下,不过我还是延续之前的文章,继续我们的逆向之旅,今天我们要来看一下如何通过对so加密,在介绍本篇文章之前的话,一定要先阅读之前的文章:

so文件格式详解以及如何解析一个so文件

http://blog.csdn.net/jiangwei0910410003/article/details/49336613

这个是我们今天这篇文章的基础,如果不了解so文件的格式的话,下面的知识点可能会看的很费劲

下面就来介绍我们今天的话题:对so中的section进行加密


二、技术原理

加密:在之前的文章中我们介绍了so中的格式,那么对于找到一个section的base和size就可以对这段section进行加密了

解密:因为我们对section进行加密之后,肯定需要解密的,不然的话,运行肯定是报错的,那么这里的重点是什么时候去进行解密,对于一个so文件,我们load进程序之后,在运行程序之前我们可以从哪个时间点来突破?这里就需要一个知识点:

__attribute__((constructor));

关于这个,属性的用法这里就不做介绍了,网上有相关资料,他的作用很简单,就是优先于main方法之前执行,类似于Java中的构造函数,当然其实C++中的构造函数就是基于这个属性实现的,我们在之前介绍elf文件格式的时候,有两个section会引起我们的注意:


对于这两个section,其实就是用这个属性实现的函数存在这里,

在动态链接器构造了进程映像,并执行了重定位以后,每个共享的目标都获得执行 某些初始化代码的机会。这些初始化函数的被调用顺序是不一定的,不过所有共享目标 初始化都会在可执行文件得到控制之前发生。
类似地,共享目标也包含终止函数,这些函数在进程完成终止动作序列时,通过 atexit() 机制执行。动态链接器对终止函数的调用顺序是不确定的。
共享目标通过动态结构中的 DT_INIT 和 DT_FINI 条目指定初始化/终止函数。通常 这些代码放在.init 和.fini 节区中。

这个知识点很重要,我们后面在进行动态调试so的时候,还会用到这个知识点,所以一定要理解。

所以,在这里我们找到了解密的时机,就是自己定义一个解密函数,然后用上面的这个属性声明就可以了。


三、实现流程

第一、我们编写一个简单的native代码,这里我们需要做两件事:

1、将我们核心的native函数定义在自己的一个section中,这里会用到这个属性:__attribute__((section (".mytext")));

其中.mytext就是我们自己定义的section.

说到这里,还记得我们之前介绍的一篇文章中介绍了,动态的给so添加一个section:

http://blog.csdn.net/jiangwei0910410003/article/details/49361281

2、需要编写我们的解密函数,用属性: __attribute__((constructor));声明

这样一个native程序就包含这两个重要的函数,使用ndk编译成so文件


第二、编写加密程序,在加密程序中我们需要做的是:

1、通过解析so文件,找到.mytext段的起始地址和大小,这里的思路是:

找到所有的Section,然后获取他的name字段,在结合String Section,遍历找到.mytext字段

2、找到.mytext段之后,然后进行加密,最后在写入到文件中。


四、技术实现

前面介绍了原理和实现方案,下面就开始coding吧,

第一、我们先来看看native程序

[cpp] view plain copy
  1. #include <jni.h>  
  2. #include <stdio.h>  
  3. #include <android/log.h>  
  4. #include <stdlib.h>  
  5. #include <string.h>  
  6. #include <unistd.h>  
  7. #include <sys/types.h>  
  8. #include <elf.h>  
  9. #include <sys/mman.h>  
  10.   
  11. jstring getString(JNIEnv*) __attribute__((section (".mytext")));  
  12. jstring getString(JNIEnv* env){  
  13.     return (*env)->NewStringUTF(env, "Native method return!");  
  14. };  
  15.   
  16. void init_getString() __attribute__((constructor));  
  17. unsigned long getLibAddr();  
  18.   
  19. void init_getString(){  
  20.   char name[15];  
  21.   unsigned int nblock;  
  22.   unsigned int nsize;  
  23.   unsigned long base;  
  24.   unsigned long text_addr;  
  25.   unsigned int i;  
  26.   Elf32_Ehdr *ehdr;  
  27.   Elf32_Shdr *shdr;  
  28.     
  29.   base = getLibAddr();  
  30.     
  31.   ehdr = (Elf32_Ehdr *)base;  
  32.   text_addr = ehdr->e_shoff + base;  
  33.     
  34.   nblock = ehdr->e_entry >> 16;  
  35.   nsize = ehdr->e_entry & 0xffff;  
  36.   
  37.   __android_log_print(ANDROID_LOG_INFO, "JNITag""nblock =  0x%x,nsize:%d", nblock,nsize);  
  38.   __android_log_print(ANDROID_LOG_INFO, "JNITag""base =  0x%x", text_addr);  
  39.   printf("nblock = %d\n", nblock);  
  40.     
  41.   if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){  
  42.     puts("mem privilege change failed");  
  43.      __android_log_print(ANDROID_LOG_INFO, "JNITag""mem privilege change failed");  
  44.   }  
  45.     
  46.   for(i=0;i< nblock; i++){    
  47.     char *addr = (char*)(text_addr + i);  
  48.     *addr = ~(*addr);  
  49.   }  
  50.     
  51.   if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC) != 0){  
  52.     puts("mem privilege change failed");  
  53.   }  
  54.   puts("Decrypt success");  
  55. }  
  56.   
  57. unsigned long getLibAddr(){  
  58.   unsigned long ret = 0;  
  59.   char name[] = "libdemo.so";  
  60.   char buf[4096], *temp;  
  61.   int pid;  
  62.   FILE *fp;  
  63.   pid = getpid();  
  64.   sprintf(buf, "/proc/%d/maps", pid);  
  65.   fp = fopen(buf, "r");  
  66.   if(fp == NULL)  
  67.   {  
  68.     puts("open failed");  
  69.     goto _error;  
  70.   }  
  71.   while(fgets(buf, sizeof(buf), fp)){  
  72.     if(strstr(buf, name)){  
  73.       temp = strtok(buf, "-");  
  74.       ret = strtoul(temp, NULL, 16);  
  75.       break;  
  76.     }  
  77.   }  
  78. _error:  
  79.   fclose(fp);  
  80.   return ret;  
  81. }  
  82.   
  83. JNIEXPORT jstring JNICALL  
  84. Java_com_example_shelldemo_MainActivity_getString( JNIEnv* env,  
  85.                                                   jobject thiz )  
  86. {  
  87. #if defined(__arm__)  
  88.   #if defined(__ARM_ARCH_7A__)  
  89.     #if defined(__ARM_NEON__)  
  90.       #define ABI "armeabi-v7a/NEON"  
  91.     #else  
  92.       #define ABI "armeabi-v7a"  
  93.     #endif  
  94.   #else  
  95.    #define ABI "armeabi"  
  96.   #endif  
  97. #elif defined(__i386__)  
  98.    #define ABI "x86"  
  99. #elif defined(__mips__)  
  100.    #define ABI "mips"  
  101. #else  
  102.    #define ABI "unknown"  
  103. #endif  
  104.   
  105.     return getString(env);  
  106. }  
下面来分析一下代码:

1、定义自己的段

[cpp] view plain copy
  1. jstring getString(JNIEnv*) __attribute__((section (".mytext")));  
  2. jstring getString(JNIEnv* env){  
  3.     return (*env)->NewStringUTF(env, "Native method return!");  
  4. };  
这里的getString返回一个字符串,提供给Android上层,然后将getString定义在.mytext段中。

2、获取so加载到内存中的起始地址

[cpp] view plain copy
  1. unsigned long getLibAddr(){  
  2.   unsigned long ret = 0;  
  3.   char name[] = "libdemo.so";  
  4.   char buf[4096], *temp;  
  5.   int pid;  
  6.   FILE *fp;  
  7.   pid = getpid();  
  8.   sprintf(buf, "/proc/%d/maps", pid);  
  9.   fp = fopen(buf, "r");  
  10.   if(fp == NULL)  
  11.   {  
  12.     puts("open failed");  
  13.     goto _error;  
  14.   }  
  15.   while(fgets(buf, sizeof(buf), fp)){  
  16.     if(strstr(buf, name)){  
  17.       temp = strtok(buf, "-");  
  18.       ret = strtoul(temp, NULL, 16);  
  19.       break;  
  20.     }  
  21.   }  
  22. _error:  
  23.   fclose(fp);  
  24.   return ret;  
  25. }  
这里的代码其实就是读取设备的proc/<uid>/maps中的内容,因为这个maps中是程序运行的内存映像:


我们只有获取到so的起始地址,才能找到指定的Section然后进行解密。

3、解密函数

[cpp] view plain copy
  1. void init_getString(){  
  2.   char name[15];  
  3.   unsigned int nblock;  
  4.   unsigned int nsize;  
  5.   unsigned long base;  
  6.   unsigned long text_addr;  
  7.   unsigned int i;  
  8.   Elf32_Ehdr *ehdr;  
  9.   Elf32_Shdr *shdr;  
  10.     
  11.   //获取so的起始地址  
  12.   base = getLibAddr();  
  13.     
  14.   //获取指定section的偏移值和size  
  15.   ehdr = (Elf32_Ehdr *)base;  
  16.   text_addr = ehdr->e_shoff + base;  
  17.     
  18.   nblock = ehdr->e_entry >> 16;  
  19.   nsize = ehdr->e_entry & 0xffff;  
  20.   
  21.   __android_log_print(ANDROID_LOG_INFO, "JNITag""nblock =  0x%x,nsize:%d", nblock,nsize);  
  22.   __android_log_print(ANDROID_LOG_INFO, "JNITag""base =  0x%x", text_addr);  
  23.   printf("nblock = %d\n", nblock);  
  24.     
  25.   //修改内存的操作权限  
  26.   if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){  
  27.     puts("mem privilege change failed");  
  28.      __android_log_print(ANDROID_LOG_INFO, "JNITag""mem privilege change failed");  
  29.   }  
  30.   //解密  
  31.   for(i=0;i< nblock; i++){    
  32.     char *addr = (char*)(text_addr + i);  
  33.     *addr = ~(*addr);  
  34.   }  
  35.     
  36.   if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC) != 0){  
  37.     puts("mem privilege change failed");  
  38.   }  
  39.   puts("Decrypt success");  
  40. }  
这里我们获取到so文件的头部,然后获取指定section的偏移地址和size

[cpp] view plain copy
  1. //获取so的起始地址  
  2. base = getLibAddr();  
  3.   
  4. //获取指定section的偏移值和size  
  5. ehdr = (Elf32_Ehdr *)base;  
  6. text_addr = ehdr->e_shoff + base;  
  7.   
  8. nblock = ehdr->e_entry >> 16;  
  9. nsize = ehdr->e_entry & 0xffff;  
这里可能会有困惑?为什么这里是这么获取offset和size的,其实这里我们做了一点工作,就是我们在加密的时候顺便改写了so的头部信息,将offset和size值写到了头部中,这样加大破解难度。后面在说到加密的时候在详解。

text_addr是起始地址+偏移值,就是我们的section在内存中的绝对地址

nsize是我们的section占用的页数

然后修改这个section的内存操作权限

[cpp] view plain copy
  1. //修改内存的操作权限  
  2. if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){  
  3.     puts("mem privilege change failed");  
  4.     __android_log_print(ANDROID_LOG_INFO, "JNITag""mem privilege change failed");  
  5. }  
这里调用了一个系统函数:mprotect

第一个参数:需要修改内存的起始地址

必须需要页面对齐,也就是必须是页面PAGE_SIZE(0x1000=4096)的整数倍

第二个参数:需要修改的大小

占用的页数*PAGE_SIZE

第三个参数:权限值


最后读取内存中的section内容,然后进行解密,在将内存权限修改回去。

然后使用ndk编译成so即可,这里我们用到了系统的打印log信息,所以需要用到共享库,看一下编译脚本Android.mk

[html] view plain copy
  1. LOCAL_PATH := $(call my-dir)  
  2.   
  3. include $(CLEAR_VARS)  
  4. LOCAL_MODULE :demo  
  5. LOCAL_SRC_FILES :demo.c  
  6. LOCAL_LDLIBS := -llog  
  7. include $(BUILD_SHARED_LIBRARY)  

关于如何使用ndk,这里就不做介绍了,参考这篇文章:

http://blog.csdn.net/jiangwei0910410003/article/details/17710243


第二、加密程序

1、加密程序(Java版)

我们获取到上面的so文件,下面我们就来看看如何进行加密的:

[java] view plain copy
  1. package com.jiangwei.encodesection;  
  2.   
  3. import com.jiangwei.encodesection.ElfType32.Elf32_Sym;  
  4. import com.jiangwei.encodesection.ElfType32.elf32_phdr;  
  5. import com.jiangwei.encodesection.ElfType32.elf32_shdr;  
  6.   
  7. public class EncodeSection {  
  8.       
  9.     public static String encodeSectionName = ".mytext";  
  10.       
  11.     public static ElfType32 type_32 = new ElfType32();  
  12.       
  13.     public static void main(String[] args){  
  14.           
  15.         byte[] fileByteArys = Utils.readFile("so/libdemo.so");  
  16.         if(fileByteArys == null){  
  17.             System.out.println("read file byte failed...");  
  18.             return;  
  19.         }  
  20.           
  21.         /** 
  22.          * 先解析so文件 
  23.          * 然后初始化AddSection中的一些信息 
  24.          * 最后在AddSection 
  25.          */  
  26.         parseSo(fileByteArys);  
  27.           
  28.         encodeSection(fileByteArys);  
  29.           
  30.         parseSo(fileByteArys);  
  31.           
  32.         Utils.saveFile("so/libdemos.so", fileByteArys);  
  33.           
  34.     }  
  35.       
  36.     private static void encodeSection(byte[] fileByteArys){  
  37.         //读取String Section段  
  38.         System.out.println();  
  39.           
  40.         int string_section_index = Utils.byte2Short(type_32.hdr.e_shstrndx);  
  41.         elf32_shdr shdr = type_32.shdrList.get(string_section_index);  
  42.         int size = Utils.byte2Int(shdr.sh_size);  
  43.         int offset = Utils.byte2Int(shdr.sh_offset);  
  44.   
  45.         int mySectionOffset=0,mySectionSize=0;  
  46.         for(elf32_shdr temp : type_32.shdrList){  
  47.             int sectionNameOffset = offset+Utils.byte2Int(temp.sh_name);  
  48.             if(Utils.isEqualByteAry(fileByteArys, sectionNameOffset, encodeSectionName)){  
  49.                 //这里需要读取section段然后进行数据加密  
  50.                 mySectionOffset = Utils.byte2Int(temp.sh_offset);  
  51.                 mySectionSize = Utils.byte2Int(temp.sh_size);  
  52.                 byte[] sectionAry = Utils.copyBytes(fileByteArys, mySectionOffset, mySectionSize);  
  53.                 for(int i=0;i<sectionAry.length;i++){  
  54.                     sectionAry[i] = (byte)(sectionAry[i] ^ 0xFF);  
  55.                 }  
  56.                 Utils.replaceByteAry(fileByteArys, mySectionOffset, sectionAry);  
  57.             }  
  58.         }  
  59.   
  60.         //修改Elf Header中的entry和offset值  
  61.         int nSize = mySectionSize/4096 + (mySectionSize%4096 == 0 ? 0 : 1);  
  62.         byte[] entry = new byte[4];  
  63.         entry = Utils.int2Byte((mySectionSize<<16) + nSize);  
  64.         Utils.replaceByteAry(fileByteArys, 24, entry);  
  65.         byte[] offsetAry = new byte[4];  
  66.         offsetAry = Utils.int2Byte(mySectionOffset);  
  67.         Utils.replaceByteAry(fileByteArys, 32, offsetAry);  
  68.     }  
  69.       
  70.     private static void parseSo(byte[] fileByteArys){  
  71.         //读取头部内容  
  72.         System.out.println("+++++++++++++++++++Elf Header+++++++++++++++++");  
  73.         parseHeader(fileByteArys, 0);  
  74.         System.out.println("header:\n"+type_32.hdr);  
  75.   
  76.         //读取程序头信息  
  77.         //System.out.println();  
  78.         //System.out.println("+++++++++++++++++++Program Header+++++++++++++++++");  
  79.         int p_header_offset = Utils.byte2Int(type_32.hdr.e_phoff);  
  80.         parseProgramHeaderList(fileByteArys, p_header_offset);  
  81.         //type_32.printPhdrList();  
  82.   
  83.         //读取段头信息  
  84.         //System.out.println();  
  85.         //System.out.println("+++++++++++++++++++Section Header++++++++++++++++++");  
  86.         int s_header_offset = Utils.byte2Int(type_32.hdr.e_shoff);  
  87.         parseSectionHeaderList(fileByteArys, s_header_offset);  
  88.         //type_32.printShdrList();  
  89.           
  90.         //这种方式获取所有的Section的name  
  91.         /*byte[] names = Utils.copyBytes(fileByteArys, offset, size); 
  92.         String str = new String(names); 
  93.         byte NULL = 0;//字符串的结束符 
  94.         StringTokenizer st = new StringTokenizer(str, new String(new byte[]{NULL})); 
  95.         System.out.println( "Token Total: " + st.countTokens() ); 
  96.         while(st.hasMoreElements()){ 
  97.             System.out.println(st.nextToken()); 
  98.         } 
  99.         System.out.println("");*/  
  100.   
  101.         /*//读取符号表信息(Symbol Table) 
  102.         System.out.println(); 
  103.         System.out.println("+++++++++++++++++++Symbol Table++++++++++++++++++"); 
  104.         //这里需要注意的是:在Elf表中没有找到SymbolTable的数目,但是我们仔细观察Section中的Type=DYNSYM段的信息可以得到,这个段的大小和偏移地址,而SymbolTable的结构大小是固定的16个字节 
  105.         //那么这里的数目=大小/结构大小 
  106.         //首先在SectionHeader中查找到dynsym段的信息 
  107.         int offset_sym = 0; 
  108.         int total_sym = 0; 
  109.         for(elf32_shdr shdr : type_32.shdrList){ 
  110.             if(Utils.byte2Int(shdr.sh_type) == ElfType32.SHT_DYNSYM){ 
  111.                 total_sym = Utils.byte2Int(shdr.sh_size); 
  112.                 offset_sym = Utils.byte2Int(shdr.sh_offset); 
  113.                 break; 
  114.             } 
  115.         } 
  116.         int num_sym = total_sym / 16; 
  117.         System.out.println("sym num="+num_sym); 
  118.         parseSymbolTableList(fileByteArys, num_sym, offset_sym); 
  119.         type_32.printSymList(); 
  120.  
  121.         //读取字符串表信息(String Table) 
  122.         System.out.println(); 
  123.         System.out.println("+++++++++++++++++++Symbol Table++++++++++++++++++"); 
  124.         //这里需要注意的是:在Elf表中没有找到StringTable的数目,但是我们仔细观察Section中的Type=STRTAB段的信息,可以得到,这个段的大小和偏移地址,但是我们这时候我们不知道字符串的大小,所以就获取不到数目了 
  125.         //这里我们可以查看Section结构中的name字段:表示偏移值,那么我们可以通过这个值来获取字符串的大小 
  126.         //可以这么理解:当前段的name值 减去 上一段的name的值 = (上一段的name字符串的长度) 
  127.         //首先获取每个段的name的字符串大小 
  128.         int prename_len = 0; 
  129.         int[] lens = new int[type_32.shdrList.size()]; 
  130.         int total = 0; 
  131.         for(int i=0;i<type_32.shdrList.size();i++){ 
  132.             if(Utils.byte2Int(type_32.shdrList.get(i).sh_type) == ElfType32.SHT_STRTAB){ 
  133.                 int curname_offset = Utils.byte2Int(type_32.shdrList.get(i).sh_name); 
  134.                 lens[i] = curname_offset - prename_len - 1; 
  135.                 if(lens[i] < 0){ 
  136.                     lens[i] = 0; 
  137.                 } 
  138.                 total += lens[i]; 
  139.                 System.out.println("total:"+total); 
  140.                 prename_len = curname_offset; 
  141.                 //这里需要注意的是,最后一个字符串的长度,需要用总长度减去前面的长度总和来获取到 
  142.                 if(i == (lens.length - 1)){ 
  143.                     System.out.println("size:"+Utils.byte2Int(type_32.shdrList.get(i).sh_size)); 
  144.                     lens[i] = Utils.byte2Int(type_32.shdrList.get(i).sh_size) - total - 1; 
  145.                 } 
  146.             } 
  147.         } 
  148.         for(int i=0;i<lens.length;i++){ 
  149.             System.out.println("len:"+lens[i]); 
  150.         } 
  151.         //上面的那个方法不好,我们发现StringTable中的每个字符串结束都会有一个00(传说中的字符串结束符),那么我们只要知道StringTable的开始位置,然后就可以读取到每个字符串的值了 
  152.        */  
  153.     }  
  154.       
  155.     /** 
  156.      * 解析Elf的头部信息 
  157.      * @param header 
  158.      */  
  159.     private static void  parseHeader(byte[] header, int offset){  
  160.         if(header == null){  
  161.             System.out.println("header is null");  
  162.             return;  
  163.         }  
  164.         /** 
  165.          *  public byte[] e_ident = new byte[16]; 
  166.             public short e_type; 
  167.             public short e_machine; 
  168.             public int e_version; 
  169.             public int e_entry; 
  170.             public int e_phoff; 
  171.             public int e_shoff; 
  172.             public int e_flags; 
  173.             public short e_ehsize; 
  174.             public short e_phentsize; 
  175.             public short e_phnum; 
  176.             public short e_shentsize; 
  177.             public short e_shnum; 
  178.             public short e_shstrndx; 
  179.          */  
  180.         type_32.hdr.e_ident = Utils.copyBytes(header, 016);//魔数  
  181.         type_32.hdr.e_type = Utils.copyBytes(header, 162);  
  182.         type_32.hdr.e_machine = Utils.copyBytes(header, 182);  
  183.         type_32.hdr.e_version = Utils.copyBytes(header, 204);  
  184.         type_32.hdr.e_entry = Utils.copyBytes(header, 244);  
  185.         type_32.hdr.e_phoff = Utils.copyBytes(header, 284);  
  186.         type_32.hdr.e_shoff = Utils.copyBytes(header, 324);  
  187.         type_32.hdr.e_flags = Utils.copyBytes(header, 364);  
  188.         type_32.hdr.e_ehsize = Utils.copyBytes(header, 402);  
  189.         type_32.hdr.e_phentsize = Utils.copyBytes(header, 422);  
  190.         type_32.hdr.e_phnum = Utils.copyBytes(header, 44,2);  
  191.         type_32.hdr.e_shentsize = Utils.copyBytes(header, 46,2);  
  192.         type_32.hdr.e_shnum = Utils.copyBytes(header, 482);  
  193.         type_32.hdr.e_shstrndx = Utils.copyBytes(header, 502);  
  194.     }  
  195.       
  196.     /** 
  197.      * 解析程序头信息 
  198.      * @param header 
  199.      */  
  200.     public static void parseProgramHeaderList(byte[] header, int offset){  
  201.         int header_size = 32;//32个字节  
  202.         int header_count = Utils.byte2Short(type_32.hdr.e_phnum);//头部的个数  
  203.         byte[] des = new byte[header_size];  
  204.         for(int i=0;i<header_count;i++){  
  205.             System.arraycopy(header, i*header_size + offset, des, 0, header_size);  
  206.             type_32.phdrList.add(parseProgramHeader(des));  
  207.         }  
  208.     }  
  209.       
  210.     private static elf32_phdr parseProgramHeader(byte[] header){  
  211.         /** 
  212.          *  public int p_type; 
  213.             public int p_offset; 
  214.             public int p_vaddr; 
  215.             public int p_paddr; 
  216.             public int p_filesz; 
  217.             public int p_memsz; 
  218.             public int p_flags; 
  219.             public int p_align; 
  220.          */  
  221.         ElfType32.elf32_phdr phdr = new ElfType32.elf32_phdr();  
  222.         phdr.p_type = Utils.copyBytes(header, 04);  
  223.         phdr.p_offset = Utils.copyBytes(header, 44);  
  224.         phdr.p_vaddr = Utils.copyBytes(header, 84);  
  225.         phdr.p_paddr = Utils.copyBytes(header, 124);  
  226.         phdr.p_filesz = Utils.copyBytes(header, 164);  
  227.         phdr.p_memsz = Utils.copyBytes(header, 204);  
  228.         phdr.p_flags = Utils.copyBytes(header, 244);  
  229.         phdr.p_align = Utils.copyBytes(header, 284);  
  230.         return phdr;  
  231.           
  232.     }  
  233.       
  234.     /** 
  235.      * 解析段头信息内容 
  236.      */  
  237.     public static void parseSectionHeaderList(byte[] header, int offset){  
  238.         int header_size = 40;//40个字节  
  239.         int header_count = Utils.byte2Short(type_32.hdr.e_shnum);//头部的个数  
  240.         byte[] des = new byte[header_size];  
  241.         for(int i=0;i<header_count;i++){  
  242.             System.arraycopy(header, i*header_size + offset, des, 0, header_size);  
  243.             type_32.shdrList.add(parseSectionHeader(des));  
  244.         }  
  245.     }  
  246.       
  247.     private static elf32_shdr parseSectionHeader(byte[] header){  
  248.         ElfType32.elf32_shdr shdr = new ElfType32.elf32_shdr();  
  249.         /** 
  250.          *  public byte[] sh_name = new byte[4]; 
  251.             public byte[] sh_type = new byte[4]; 
  252.             public byte[] sh_flags = new byte[4]; 
  253.             public byte[] sh_addr = new byte[4]; 
  254.             public byte[] sh_offset = new byte[4]; 
  255.             public byte[] sh_size = new byte[4]; 
  256.             public byte[] sh_link = new byte[4]; 
  257.             public byte[] sh_info = new byte[4]; 
  258.             public byte[] sh_addralign = new byte[4]; 
  259.             public byte[] sh_entsize = new byte[4]; 
  260.          */  
  261.         shdr.sh_name = Utils.copyBytes(header, 04);  
  262.         shdr.sh_type = Utils.copyBytes(header, 44);  
  263.         shdr.sh_flags = Utils.copyBytes(header, 84);  
  264.         shdr.sh_addr = Utils.copyBytes(header, 124);  
  265.         shdr.sh_offset = Utils.copyBytes(header, 164);  
  266.         shdr.sh_size = Utils.copyBytes(header, 204);  
  267.         shdr.sh_link = Utils.copyBytes(header, 244);  
  268.         shdr.sh_info = Utils.copyBytes(header, 284);  
  269.         shdr.sh_addralign = Utils.copyBytes(header, 324);  
  270.         shdr.sh_entsize = Utils.copyBytes(header, 364);  
  271.         return shdr;  
  272.     }  
  273.       
  274.     /** 
  275.      * 解析Symbol Table内容  
  276.      */  
  277.     public static void parseSymbolTableList(byte[] header, int header_count, int offset){  
  278.         int header_size = 16;//16个字节  
  279.         byte[] des = new byte[header_size];  
  280.         for(int i=0;i<header_count;i++){  
  281.             System.arraycopy(header, i*header_size + offset, des, 0, header_size);  
  282.             type_32.symList.add(parseSymbolTable(des));  
  283.         }  
  284.     }  
  285.       
  286.     private static ElfType32.Elf32_Sym parseSymbolTable(byte[] header){  
  287.         /** 
  288.          *  public byte[] st_name = new byte[4]; 
  289.             public byte[] st_value = new byte[4]; 
  290.             public byte[] st_size = new byte[4]; 
  291.             public byte st_info; 
  292.             public byte st_other; 
  293.             public byte[] st_shndx = new byte[2]; 
  294.          */  
  295.         Elf32_Sym sym = new Elf32_Sym();  
  296.         sym.st_name = Utils.copyBytes(header, 04);  
  297.         sym.st_value = Utils.copyBytes(header, 44);  
  298.         sym.st_size = Utils.copyBytes(header, 84);  
  299.         sym.st_info = header[12];  
  300.         //FIXME 这里有一个问题,就是这个字段读出来的值始终是0  
  301.         sym.st_other = header[13];  
  302.         sym.st_shndx = Utils.copyBytes(header, 142);  
  303.         return sym;  
  304.     }  
  305.       
  306.   
  307. }  
在这里,我需要解析so文件的头部信息,程序头信息,段头信息

[java] view plain copy
  1. //读取头部内容  
  2. System.out.println("+++++++++++++++++++Elf Header+++++++++++++++++");  
  3. parseHeader(fileByteArys, 0);  
  4. System.out.println("header:\n"+type_32.hdr);  
  5.   
  6. //读取程序头信息  
  7. //System.out.println();  
  8. //System.out.println("+++++++++++++++++++Program Header+++++++++++++++++");  
  9. int p_header_offset = Utils.byte2Int(type_32.hdr.e_phoff);  
  10. parseProgramHeaderList(fileByteArys, p_header_offset);  
  11. //type_32.printPhdrList();  
  12.   
  13. //读取段头信息  
  14. //System.out.println();  
  15. //System.out.println("+++++++++++++++++++Section Header++++++++++++++++++");  
  16. int s_header_offset = Utils.byte2Int(type_32.hdr.e_shoff);  
  17. parseSectionHeaderList(fileByteArys, s_header_offset);  
  18. //type_32.printShdrList();  

关于这个解析的工作说明这里就不解析了,看之前解析elf文件的那篇文章。

获取这些信息之后,下面就来开始寻找我们的段了,只需要遍历Section列表,找到名字是.mytext的section即可,然后获取offset和size,对内容进行加密,回写到文件中。

下面来看看核心方法:

[java] view plain copy
  1. private static void encodeSection(byte[] fileByteArys){  
  2.     //读取String Section段  
  3.     System.out.println();  
  4.   
  5.     int string_section_index = Utils.byte2Short(type_32.hdr.e_shstrndx);  
  6.     elf32_shdr shdr = type_32.shdrList.get(string_section_index);  
  7.     int size = Utils.byte2Int(shdr.sh_size);  
  8.     int offset = Utils.byte2Int(shdr.sh_offset);  
  9.   
  10.     int mySectionOffset=0,mySectionSize=0;  
  11.     for(elf32_shdr temp : type_32.shdrList){  
  12.         int sectionNameOffset = offset+Utils.byte2Int(temp.sh_name);  
  13.         if(Utils.isEqualByteAry(fileByteArys, sectionNameOffset, encodeSectionName)){  
  14.             //这里需要读取section段然后进行数据加密  
  15.             mySectionOffset = Utils.byte2Int(temp.sh_offset);  
  16.             mySectionSize = Utils.byte2Int(temp.sh_size);  
  17.             byte[] sectionAry = Utils.copyBytes(fileByteArys, mySectionOffset, mySectionSize);  
  18.             for(int i=0;i<sectionAry.length;i++){  
  19.                 sectionAry[i] = (byte)(sectionAry[i] ^ 0xFF);  
  20.             }  
  21.             Utils.replaceByteAry(fileByteArys, mySectionOffset, sectionAry);  
  22.         }  
  23.     }  
  24.   
  25.     //修改Elf Header中的entry和offset值  
  26.     int nSize = mySectionSize/4096 + (mySectionSize%4096 == 0 ? 0 : 1);  
  27.     byte[] entry = new byte[4];  
  28.     entry = Utils.int2Byte((mySectionSize<<16) + nSize);  
  29.     Utils.replaceByteAry(fileByteArys, 24, entry);  
  30.     byte[] offsetAry = new byte[4];  
  31.     offsetAry = Utils.int2Byte(mySectionOffset);  
  32.     Utils.replaceByteAry(fileByteArys, 32, offsetAry);  
  33. }  
我们知道Section中的sh_name字段的值是这个section段的name在StringSection中的索引值,这里offset就是StringSection在文件中的偏移值。当然我们需要知道的一个知识点就是:StringSection中的每个name都是以\0结尾的,所以我们只需要判断字符串到结束符就可以了,判断方法是Utils.isEqualByteAry:

[java] view plain copy
  1. public static boolean isEqualByteAry(byte[] src, int start, String destStr){  
  2.     if(destStr == null){  
  3.         return false;  
  4.     }  
  5.     byte[] dest = destStr.getBytes();  
  6.     if(src == null || dest == null){  
  7.         return false;  
  8.     }  
  9.     if(dest.length == 0 || src.length == 0){  
  10.         return false;  
  11.     }  
  12.     if(start >= src.length){  
  13.         return false;  
  14.     }  
  15.   
  16.     int len = 0;  
  17.     byte temp = src[start];  
  18.     while(temp != 0){  
  19.         len++;  
  20.         temp = src[start+len];  
  21.     }  
  22.   
  23.     byte[] sonAry = copyBytes(src, start, len);  
  24.     if(sonAry == null || sonAry.length == 0){  
  25.         return false;  
  26.     }  
  27.     if(sonAry.length != dest.length){  
  28.         return false;  
  29.     }  
  30.     String sonStr = new String(sonAry);  
  31.     if(destStr.equals(sonStr)){  
  32.         return true;  
  33.     }  
  34.     return false;  
  35. }  
这里我们加密的方法很简单,加密完成之后,我们需要做的是回写到so文件中,当然这里我们还需要做一件事,就是将我们加密的.mytext段的偏移值和pageSize保存到头部信息中:

[java] view plain copy
  1. //修改Elf Header中的entry和offset值  
  2. int nSize = mySectionSize/4096 + (mySectionSize%4096 == 0 ? 0 : 1);  
  3. byte[] entry = new byte[4];  
  4. entry = Utils.int2Byte((mySectionSize<<16) + nSize);  
  5. Utils.replaceByteAry(fileByteArys, 24, entry);  
这里又有一个知识点需要说明?大家可能会困惑,我们这样修改了so的头部信息的话,在加载运行so文件的时候不会报错吗?这个就要看看Android底层是如何解析so文件,然后将so文件映射到内存中的了,下面我们来看看系统是如何解析so文件的?

源代码的位置:Android linker源码:bionic\linker

在linker.h源码中有一个重要的结构体soinfo,下面列出一些字段:

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

指向初始化代码,先于main函数之行,即在加载时被linker所调用,在linker.c可以看到:__linker_init -> link_image -> 

[cpp] view plain copy
  1. call_constructors -> call_array  
  2. unsigned *init_array;  
  3. unsigned init_array_count;  
  4. void (*init_func)(void);  
  5. //与init_array类似,只是在main结束之后执行  
  6. unsigned *fini_array;  
  7. unsigned fini_array_count;  
  8. void (*fini_func)(void);  
  9. }  

另外,linker.c中也有许多地方可以佐证。其本质还是linker是基于装载视图解析的so文件的。
基于上面的结论,再来分析下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中的有些信息在运行的时候是没有用途的,有些东西是不能改的。


2、加密程序(C版)

上面说的是Java版本的,下面再来一个C版本的:

[cpp] view plain copy
  1. #include <stdio.h>  
  2. #include <fcntl.h>  
  3. #include "elf.h"  
  4. #include <stdlib.h>  
  5. #include <string.h>  
  6.   
  7. int main(int argc, char** argv){  
  8.   char *encodeSoName = "libdemo.so";  
  9.   char target_section[] = ".mytext";  
  10.   char *shstr = NULL;  
  11.   char *content = NULL;  
  12.   Elf32_Ehdr ehdr;  
  13.   Elf32_Shdr shdr;  
  14.   int i;  
  15.   unsigned int base, length;  
  16.   unsigned short nblock;  
  17.   unsigned short nsize;  
  18.   unsigned char block_size = 16;  
  19.     
  20.   int fd;  
  21.     
  22.   fd = open(encodeSoName, O_RDWR);  
  23.   if(fd < 0){  
  24.     printf("open %s failed\n", argv[1]);  
  25.     goto _error;  
  26.   }  
  27.     
  28.   if(read(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)){  
  29.     puts("Read ELF header error");  
  30.     goto _error;  
  31.   }  
  32.     
  33.   lseek(fd, ehdr.e_shoff + sizeof(Elf32_Shdr) * ehdr.e_shstrndx, SEEK_SET);  
  34.     
  35.   if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){  
  36.     puts("Read ELF section string table error");  
  37.     goto _error;  
  38.   }  
  39.     
  40.   if((shstr = (char *) malloc(shdr.sh_size)) == NULL){  
  41.     puts("Malloc space for section string table failed");  
  42.     goto _error;  
  43.   }  
  44.     
  45.   lseek(fd, shdr.sh_offset, SEEK_SET);  
  46.   if(read(fd, shstr, shdr.sh_size) != shdr.sh_size){  
  47.     puts("Read string table failed");  
  48.     goto _error;  
  49.   }  
  50.   
  51.   lseek(fd, ehdr.e_shoff, SEEK_SET);  
  52.   for(i = 0; i < ehdr.e_shnum; i++){  
  53.     if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){  
  54.       puts("Find section .text procedure failed");  
  55.       goto _error;  
  56.     }  
  57.     if(strcmp(shstr + shdr.sh_name, target_section) == 0){  
  58.       base = shdr.sh_offset;  
  59.       length = shdr.sh_size;  
  60.       printf("Find section %s\n", target_section);  
  61.       break;  
  62.     }  
  63.   }  
  64.     
  65.   lseek(fd, base, SEEK_SET);  
  66.   content = (char*) malloc(length);  
  67.   if(content == NULL){  
  68.     puts("Malloc space for content failed");  
  69.     goto _error;  
  70.   }  
  71.   if(read(fd, content, length) != length){  
  72.     puts("Read section .text failed");  
  73.     goto _error;  
  74.   }  
  75.     
  76.   nblock = length / block_size;  
  77.   nsize = length / 4096 + (length % 4096 == 0 ? 0 : 1);  
  78.   printf("base = %x, length = %x\n", base, length);  
  79.   printf("nblock = %d, nsize = %d\n", nblock, nsize);  
  80.   printf("entry:%x\n",((length << 16) + nsize));  
  81.   
  82.   ehdr.e_entry = (length << 16) + nsize;  
  83.   ehdr.e_shoff = base;  
  84.     
  85.   for(i=0;i<length;i++){  
  86.     content[i] = ~content[i];  
  87.   }  
  88.   
  89.   lseek(fd, 0, SEEK_SET);  
  90.   if(write(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)){  
  91.     puts("Write ELFhead to .so failed");  
  92.     goto _error;  
  93.   }  
  94.   
  95.   lseek(fd, base, SEEK_SET);  
  96.   if(write(fd, content, length) != length){  
  97.     puts("Write modified content to .so failed");  
  98.     goto _error;  
  99.   }  
  100.     
  101.   puts("Completed");  
  102. _error:  
  103.   free(content);  
  104.   free(shstr);  
  105.   close(fd);  
  106.   return 0;  
  107. }  

这里就不做详细解释了

我们在上面加密完成之后,我们可以验证一下,使用readelf命令查看一下:


哈哈,加密成功,我们在用IDA查看一下:


会有错误提示,但是我们点击OK,还是成功打开了so文件,但是我们ctrl+s查看段信息的时候:


也是没有看到我们的段信息,我们可以看一下我们没有加密前的效果:


既然加密成功了,那么下面我们得验证一下能否运行成功


第三、Android测试demo

我们在获取加密之后的so文件之后,我们用Android工程测试一下:

[java] view plain copy
  1. package com.example.shelldemo;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5. import android.view.Menu;  
  6. import android.view.MenuItem;  
  7. import android.widget.TextView;  
  8.   
  9. public class MainActivity extends Activity {  
  10.   
  11.     private TextView tv;  
  12.     private native String getString();  
  13.       
  14.     static{  
  15.         System.loadLibrary("demo");  
  16.     }  
  17.     @Override  
  18.     protected void onCreate(Bundle savedInstanceState) {  
  19.         super.onCreate(savedInstanceState);  
  20.         setContentView(R.layout.activity_main);  
  21.           
  22.         tv = (TextView) findViewById(R.id.tv);  
  23.         tv.setText(getString());  
  24.     }  
  25. }  
运行结果:


看到了,运行成功了。


案例下载地址:http://download.csdn.net/detail/jiangwei0910410003/9288051


五、技术总结

1、Elf文件格式的深入了解

2、两个属性的了解:__attribute__((constructor)); __attribute__((section (".mytext")));

3、程序的maps内存映像了解

4、修改内存属性方法

5、Android系统如何解析so文件linker源码


六、梳理流程步骤

加密流程:
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文件头修正放回去,程序是不能运行的。

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


六、总结

这篇文章主要介绍了如何对so中的section进行加密,然后将我们的native函数存到这个section中,从而达到对我们函数的实现的加密,这样对于后续的破解工作加大难度,但是还是那句话,没有绝对的安全,这种方式还是很容易破解的,动态调试so,在init出下断点,就可以跟到我们这里的init_getString函数的实现了。关于动态调试的知识点大家不要着急,后续我会详细讲解的,所以说攻与防是永不停息的战争。下一篇我会继续介绍如何对指定的函数进行加密,难度加大。。期待~~


1 0
原创粉丝点击