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

已经很长一段时间没更新了,一方面本人技术一般,不知道能给技术网友分享点什么有价值的东西,一方面,有时候实验室事比较多,时间长了就分享的意识就单薄了,今天接着前面那个so文件重要函数段加密,接着更,接下来开始书写。

一、原理篇

      在很多时候我们对一个.so文件中重要的函数段加密时,是无法拿到源码的,当然并不排除可能在以后随着Android开发与安全结合,出现逆向开发者,在开发的时候就进行一些重要的函数的保护,在上一篇中是对特定的section的加密,加密就可以根据section来进行查找加密不需要源码,而解密是利用linker在加载执行的时候利用__attribute__((constructor))的特性实行so加载时的自解密的特性; 

      在这一篇中,我们不需要源码,我们拿到待需要保护的.so文件进行加密,怎么加密呢?是基于特定函数进行加密,具体的原理后面会详解;我们知道任何函数在加密过后,在你加载执行的时候都是需要解密的,如果不进行解密的话是一定会报错的,因此这里面比较重要的就是解密时机,只要在加密函数被执行前进行解密完就ok,可以另外写一个.so文件为解密文件,只要在执行函数前解密就可以,原理就是这样。

基本上大致的步骤为:

1.首先要给你进行保护的.so文件中的重要函数加密;
2.逆向开发,自己写针对以上待解密的.so文件;
3.然后修改smali层进行调用解密so文件;

二、详解篇

这里重点讲解对于一个.so文件的重要函数进行加密逆向开发以及解密在下面实现篇进行介绍;

我们可以用IDA工具拿到要加密的函数名,比如本篇中:


接下来最重要的就是怎么在这个.so文件中找到这个函数名,需要对ELF文件有足够的了解:有一篇北大的关于ELF篇的详细分析文档 ,当然我都会传到后面附件中。

在加密之前要明白一点就是在ELF文件中一些关于动态链接的时候的一些重要的节区,观察下面这个表格:


可以看出这几个节区的重要性,因此接着看下面这个加密的流程;这一块是借鉴网上大神的一个源码,当然后面会给出分享,我只是在此做出一个梳理有利于读者的学习和理解:

加密流程如下:

1.解析文件头,获取e_phoff、e_phentsize和e_phnum等字段的信息,后面根据这些信息进一步得到p_offset和p_filesz;

2.根据程序头部的结构中的p_type得到Dynamic段的偏移值和大小;


关键代码段为:

 

3.遍历Dynamic段找到dynsym、.dynstr、.hash section文件中的偏移和.dynstr的大小;这块大家可能比较好奇为什么一个段中有这么多节,这是因为在执行时,把一些相同权限的节放在一起,以减少空间浪费

大家或许会问为什么有.hash节?

因为别的与此函数名相关的section的type有可能会相同)

比如看这个so文件


这个时候我们看到北大的ELF分析中讲到:(以下是对原内容的截取)


因此我们可以来分析hash .dynamic段一般用于动态链接的,所以.dynsym和.dynstr,.hash肯定包含在这里。我们可以解析了程序头信息之后,通过type获取到.dynamic程序头信息,然后获取到这个segment的偏移地址和大小



4.根据函数的方法名,计算所对应的hash值,根据hash值,找到下标hash % nbuckets的

bucket;根据bucket中的值,读取.dynsym中的对应索引的Elf32_Sym符号;从符号的

st_name所以找到在.dynstr中对应的字符串与函数名进行比较。若不等,则根据

chain[hash % nbuckets]找下一个Elf32_Sym符号,直到找到或者chain终止为止 

5.找到函数方法后进行加密;

三、实现篇

Number01:首先我们自己写一个样本

样本很简答,就是在本地层写一个算法,样本源码会在后面给出链接大致上如下:

JNIEXPORT jstring JNICALL Java_com_example_zbb_test01_MainActivity_getStringFromNative
        (JNIEnv *env, jobject obj, jstring str)
{
   // jstring   CharTojstring(JNIEnv* env,   char* str);
    //首先将string类型的转化为char类型的字符串
    const char *strAry=(*env)->GetStringUTFChars(env,str,0);
    if(strAry==NULL){
        return NULL;
    }
    int len=strlen(strAry);
    char* last=(char*)malloc((len+1)* sizeof(char));
    memset(last,0,len+1);
    //char buf[]={'z','h','a','o','b','e','i','b','e','i'};
    char* buf ="beibei";
    int buf_len=strlen(buf);
    int i;
    for(i=0;i<len;i++){
        last[i]=strAry[i]|buf[i%buf_len];
        if(last[i]==0){
            last[i]=strAry[i];
        }
    }
    last[len]=0;
    return (*env)->NewStringUTF(env, last);
}

Number02:对形成的.so文件的重要函数名进行加密

接着对形成的.so文件中的"Java_com_example_zbb_test01_MainActivity_getStringFromNative"进行加密,当然这块的加密方法读者可以自己来定义,具体的看以下的代码:

<span style="font-size:24px;">private static void encodeFunc(byte[] fileByteArys){
		//寻找Dynamic段的偏移值和大小
		int dy_offset = 0,dy_size = 0;
		for(elf32_phdr phdr : type_32.phdrList){
			if(Utils.byte2Int(phdr.p_type) == ElfType32.PT_DYNAMIC){
				dy_offset = Utils.byte2Int(phdr.p_offset);
				dy_size = Utils.byte2Int(phdr.p_filesz);
			}
		}
		System.out.println("dy_size:"+dy_size);
		int dynSize = 8;
		int size = dy_size / dynSize;
		System.out.println("size:"+size);
		byte[] dest = new byte[dynSize];
		for(int i=0;i<size;i++){
			System.arraycopy(fileByteArys, i*dynSize + dy_offset, dest, 0, dynSize);
			type_32.dynList.add(parseDynamic(dest));
		}
		
		//type_32.printDynList();
		
		byte[] symbolStr = null;
		int strSize=0,strOffset=0;
		int symbolOffset = 0;
		int dynHashOffset = 0;
		int funcIndex = 0;
		int symbolSize = 16;
		
		for(elf32_dyn dyn : type_32.dynList){
			if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_HASH){
				dynHashOffset = Utils.byte2Int(dyn.d_ptr);
			}else if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_STRTAB){
				System.out.println("strtab:"+dyn);
				strOffset = Utils.byte2Int(dyn.d_ptr);
			}else if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_SYMTAB){
				System.out.println("systab:"+dyn);
				symbolOffset = Utils.byte2Int(dyn.d_ptr);
			}else if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_STRSZ){
				System.out.println("strsz:"+dyn);
				strSize = Utils.byte2Int(dyn.d_val);
			}
		}
		
		symbolStr = Utils.copyBytes(fileByteArys, strOffset, strSize);
		//打印所有的Symbol Name,注意用0来进行分割,C中的字符串都是用0做结尾的
		/*String[] strAry = new String(symbolStr).split(new String(new byte[]{0}));
		for(String str : strAry){
			System.out.println(str);
		}*/
		
		for(elf32_dyn dyn : type_32.dynList){
			if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_HASH){
				//这里的逻辑有点绕
				/**
				 * 根据hash值,找到下标hash % nbuckets的bucket;根据bucket中的值,读取.dynsym中的对应索引的Elf32_Sym符号;
				 * 从符号的st_name所以找到在.dynstr中对应的字符串与函数名进行比较。若不等,则根据chain[hash % nbuckets]找下一个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;
					  }
					}
				 */
				int nbucket = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset, 4));
				int nchian = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+4, 4));
				int hash = (int)elfhash(funcName.getBytes());
				hash = (hash % nbucket);
				//这里的8是读取nbucket和nchian的两个值
				funcIndex = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+hash*4 + 8, 4));
				System.out.println("nbucket:"+nbucket+",hash:"+hash+",funcIndex:"+funcIndex+",chian:"+nchian);
				System.out.println("sym:"+Utils.bytes2HexString(Utils.int2Byte(symbolOffset)));
				System.out.println("hash:"+Utils.bytes2HexString(Utils.int2Byte(dynHashOffset)));
				
				byte[] des = new byte[symbolSize];
				System.arraycopy(fileByteArys, symbolOffset+funcIndex*symbolSize, des, 0, symbolSize);
				Elf32_Sym sym = parseSymbolTable(des);
				System.out.println("sym:"+sym);
				boolean isFindFunc = Utils.isEqualByteAry(symbolStr, Utils.byte2Int(sym.st_name), funcName);
				if(isFindFunc){
					System.out.println("find func....");
					return;
				}
				
				while(true){
					/**
					 *  lseek(fd, dyn_hash + 4 * (2 + nbucket + funIndex), SEEK_SET);
						if(read(fd, &funIndex, 4) != 4){
						  puts("Read funIndex failed\n");
						  goto _error;
						}
					 */
					//System.out.println("dyHash:"+Utils.bytes2HexString(Utils.int2Byte(dynHashOffset))+",nbucket:"+nbucket+",funIndex:"+funcIndex);
					funcIndex = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+4*(2+nbucket+funcIndex), 4));
					System.out.println("funcIndex:"+funcIndex);
					
					System.arraycopy(fileByteArys, symbolOffset+funcIndex*symbolSize, des, 0, symbolSize);
					sym = parseSymbolTable(des);
					
					isFindFunc = Utils.isEqualByteAry(symbolStr, Utils.byte2Int(sym.st_name), funcName);
					if(isFindFunc){
						System.out.println("find func...");
						int funcSize = Utils.byte2Int(sym.st_size);
						int funcOffset = Utils.byte2Int(sym.st_value);
						System.out.println("size:"+funcSize+",funcOffset:"+funcOffset);
						//进行目标函数代码部分进行加密
						//这里需要注意的是从funcOffset-1的位置开始
						byte[] funcAry = Utils.copyBytes(fileByteArys, funcOffset-1, funcSize);
						for(int i=0;i<funcAry.length-1;i++){
							funcAry[i] = (byte)(funcAry[i] ^ 0xFF);
						}
						Utils.replaceByteAry(fileByteArys, funcOffset-1, funcAry);
						break;
					}
				}
				break;
			}
			
		}
		
	}</span>
核心部分代码已经在上面介绍;

加密过后再IDA中表现为:


Number03:接下来我们进行逆向开发解密文件的分析:

解密流程为加密逆过程,大体相同,只有一些细微的区别,具体如下:

1)  找到so文件在内存中的起始地址
2)  也是通过so文件头找到Phdr;从Phdr找到PT_DYNAMIC后,需取p_vaddr和p_filesz字段,并非p_offset,这里需要注意。
3)  后续操作就加密类似,就不赘述。对内存区域数据的解密,也需要注意读写权限问题。

#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>
#include <android/log.h>
//解密函数

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

void clearcache(char* begin, char *end)
{
	const int syscall = 0xf0002;
	__asm __volatile (
		"mov	 r0, %0\n"
		"mov	 r1, %1\n"
		"mov	 r7, %2\n"
		"mov     r2, #0x0\n"
		"svc     0x00000000\n"
		:
		:	"r" (begin), "r" (end), "r" (syscall)
		:	"r0", "r1", "r7"
		);
}

void init_getStringFromNative(){
  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;

  if(mprotect((void *) base, 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
	  __android_log_print(ANDROID_LOG_INFO, "JNITag", "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){
	  __android_log_print(ANDROID_LOG_INFO, "JNITag", "mem privilege change failed");
  }

  clearcache((char*)text_addr, (char*)(text_addr + nblock -1));

  __android_log_print(ANDROID_LOG_INFO, "JNITag", "Decrypt success");
}

unsigned long getLibAddr(){
  unsigned long ret = 0;
  char name[] = "libegg.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;
}
Number04:然后修改smali层进行调用解密so文件:


然后在AK中重打包,在手机上运行,结果是OK的,但是在模拟器上出问题了,具体的原因还需进一步分析,如果网友有知道的望告知。

四、总结篇

通过这两篇的有无源码的so文件中特定setion还是无源码特定函数的加密,在一定程度上都能够防一些静态分析,但是防不了动态分析,只要加密无论是多么复杂的加密方法,但是就一定在执行的时候以解密后的形式在内存中完整的出现,因此只要在IDA中找到开始和结束的libegg.so的起始地址,dump出来就可以,因此下一步就是反dump和反调试来防止动态分析,以后有机会进一步分析。

最后附件是所有相关的代码和文件如图所示:


附件代码:点击打开链接

 

  • 3
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值