Android SO文件保护加固——加密篇(一)

这篇是一系列的关于SO文件保护的自我理解,SO文件保护分为加固,混淆以及最近炒的比较火的虚拟机,由于本人菜鸟,无力分析虚拟机,我相信以后会有机会。。。加固就是将真正的so代码保护起来,不让攻击者那么轻易的发现,至于混淆,由于ART机制的介入,使得O-LLVM越来越火,这以后有机会再分析,这次主要是基于有源码的so文件保护,下次介绍无源码的so文件保护,废话不多说,开搞

在这之前首先对elf文件结构有一定的了解,不一定完全了解,本菜鸟就不是完全懂,在文章开始之前有个知识点必须了解:

这两个节头要有所了解:

.init:可执行指令,构成进程的初始化代码,发生在main函数调用之前。

.fini:进程终止指令,发生在main函数调用之后。

以上这么分析感觉有点像c++的构造函数和析构函数,的确构造和析构是由此实现的。

并且结合GGC的可扩展机制:

__attribute__((section(".mytext")));可以把相应的函数和要保护的代码放在自己所定义的节里面。

这就引入了我们今天的主题,可以把我们关键的so文件中的核心函数放在自己所定义的节里面,然后进行加密保护,在合适的时机构造解密函数,当然解密函数可以用这个_attribute__((constructor))进行定义;类似于C++构造函数发生在main函数之前。

OK这个就是这篇文章的核心思想。

流程安排:

1.编写一个Native程序,对里面的关键函数放在自己所定义的节中,并且编写解密函数(当然这个是在你已知加密函数的基础上)

2.对得到的.so文件进行加密

3.加密后的替换验证

接下来走流程:

1.编写一个简单的计算器,把核心的代码放在.so文件里面如图:

这个比较简单很容易理解:

接下来是关键函数的自定义与解密函数:直接看代码:

[java] view plain copy
  1. #include "com_example_jni02_CallSo.h"  
  2. #include <jni.h>  
  3. #include <stdio.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. #include <Android/log.h>  
  11. //这里对 Java_com_example_jni02_CallSo_plus这个方法进行加密保护  
  12. jint JNICALL Java_com_example_jni02_CallSo_plus(JNIEnv* env, jobject obj, jint a, jint b)  __attribute__((section (".mytext")));  
  13. JNIEXPORT jstring JNICALL Java_com_example_jni02_CallSo_getString  
  14.   (JNIEnv* env, jobject obj){  
  15.     return (*env)->NewStringUTF(env,"Hello");  
  16. }  
  17. JNIEXPORT jint JNICALL Java_com_example_jni02_CallSo_plus  
  18.   (JNIEnv* env, jobject obj, jint a, jint b){  
  19.   
  20.     return a+b;  
  21. }  
  22. //在调用so文件进行解密  
  23. void init_Java_com_example_jni02_CallSo_plus() __attribute__((constructor));  
  24. unsigned long getLibAddr();  
  25.   
  26. void init_Java_com_example_jni02_CallSo_plus(){  
  27.     char name[15];  
  28.     unsigned int nblock;  
  29.     unsigned int nsize;  
  30.     unsigned long base;  
  31.     unsigned long text_addr;  
  32.     unsigned int i;  
  33.     Elf32_Ehdr *ehdr;  
  34.     Elf32_Shdr *shdr;  
  35.     base=getLibAddr();  
  36.     ehdr=(Elf32_Ehdr *)base;  
  37.     text_addr=ehdr->e_shoff+base;  
  38.     nblock=ehdr->e_entry >>16;  
  39.     nsize=ehdr->e_entry&0xffff;  
  40.     __android_log_print(ANDROID_LOG_INFO, "JNITag""nblock =  0x%d,nsize:%d", nblock,nsize);  
  41.     __android_log_print(ANDROID_LOG_INFO, "JNITag""base =  0x%x", text_addr);  
  42.     printf("nblock = %d\n", nblock);  
  43.    //修改内存权限  
  44.      if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){  
  45.        puts("mem privilege change failed");  
  46.         __android_log_print(ANDROID_LOG_INFO, "JNITag""mem privilege change failed");  
  47.      }  
  48.      //进行解密,是针对加密算法的  
  49.      for(i=0;i<nblock;i++){  
  50.          char *addr=(char*)(text_addr+i);  
  51.          *addr=~(*addr);  
  52.      }  
  53.       if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC) != 0){  
  54.         puts("mem privilege change failed");  
  55.       }  
  56.       puts("Decrypt success");  
  57. }  
  58. //获取到SO文件加载到内存中的起始地址,只有找到起始地址才能够进行解密;  
  59. unsigned long getLibAddr(){  
  60.     unsigned long ret=0;  
  61.     char name[]="libaddcomputer.so";  
  62.     char buf[4096];  
  63.     char *temp;  
  64.     int pid;  
  65.     FILE *fp;  
  66.     pid=getpid();  
  67.     sprintf(buf,"/proc/%d/maps",pid);  
  68.     fp=fopen(buf,"r");  
  69.     if(fp==NULL){  
  70.         puts("open failed");  
  71.         goto _error;  
  72.     }  
  73.     while (fgets(buf,sizeof(buf),fp)){  
  74.         if(strstr(buf,name)){  
  75.             temp = strtok(buf, "-");  
  76.             ret = strtoul(temp, NULL, 16);  
  77.             break;  
  78.         }  
  79.     }  
  80.     _error:  
  81.       fclose(fp);  
  82.       return ret;  
  83. }  
在这里重点解释这个解密函数:
首先看到的是getLibAddr()这个函数:在介绍这个函数之前首先了解一个内存映射问题:

Linux一样,Android提供了基于/proc的“伪文件”系统来作为查看用户进程内存映像的接口(cat /proc/pid/maps)。可以说,这是Android系统内核层开放给用户层关于进程内存信息的一扇窗户。通过它,我们可以查看到当前进程空间的内存映射情况,模块加载情况以及虚拟地址和内存读写执行(rwxp)属性等。

接下来包括内存权限的修改以及函数的解密算法,最后包括内存权限的修改回去,应该都比较好理解。ok,以上编写完以后就编译生成.so文件。

2.对得到的.so文件进行加密:

这一块也是一个重点,大致上逻辑我们可以这么认为:先找到那个我们自己所定义的节,然后找到对应的offset和size,最后进行加密,加密完以后重新的写到另一个新的.so文件中,这块是需要建立在对ELF了解的基础上

这里重点了解一下这个加密函数,在自己写的时候可以在这个基础上进行改进。

首先看一下这个核心加密代码:

[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.                     sectionAry[i]=(byte) ~sectionAry[i];  
  21.                 }  
  22.                 Utils.replaceByteAry(fileByteArys, mySectionOffset, sectionAry);  
  23.             }  
  24.         }  
  25.   
  26.         //修改Elf Header中的entry和offset值  
  27.         int nSize = mySectionSize/4096 + (mySectionSize%4096 == 0 ? 0 : 1);  
  28.         byte[] entry = new byte[4];  
  29.         entry = Utils.int2Byte((mySectionSize<<16) + nSize);  
  30.         Utils.replaceByteAry(fileByteArys, 24, entry);  
  31.         byte[] offsetAry = new byte[4];  
  32.         offsetAry = Utils.int2Byte(mySectionOffset);  
  33.         Utils.replaceByteAry(fileByteArys, 32, offsetAry);  
  34.     }  
以上加密是没有问题的,但是对于最后so文件头的修改简单的说明一下:

修改so文件为什么不会报错的原因进行简单的说明:

我们在这考虑一个问题就是Section与Segment的区别,由于OS在映射ELF到内存时,每一个段会占用是页的整数倍,这样会产生浪费,在操作系统的层面来讲,可以吧相同权限的section放在一起成为一个Segment再进行映射,这样一来减少浪费,但是在映射的时候会有一部分信息不会映射到内存中,可以看这个图:

因此来说修改这些不会报错。

3.对于文件替换后没有什么问题,运行结果为:

总结:

该篇是在有源码的基础上进行对特定的section进行加密,但是试想一下,有多少情况下才能有源码,因此局限性比较大,

下一篇是基于二进制级别的特定函数的加密,链接为:点击打开链接 源码是:http://download.csdn.NET/detail/feibabeibei_beibei/9532172

转自:http://blog.csdn.net/feibabeibei_beibei/article/details/51498285


  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值