已经很长一段时间没更新了,一方面本人技术一般,不知道能给技术网友分享点什么有价值的东西,一方面,有时候实验室事比较多,时间长了就分享的意识就单薄了,今天接着前面那个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){
-
- 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));
- }
-
-
-
- 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);
-
-
-
-
-
-
- for(elf32_dyn dyn : type_32.dynList){
- if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_HASH){
-
-
-
-
-
-
-
-
-
-
-
-
- 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);
-
- 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){
-
-
-
-
-
-
-
-
- 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);
-
-
- 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和反调试来防止动态分析,以后有机会进一步分析。
最后附件是所有相关的代码和文件如图所示:

附件代码:点击打开链接
转自:http://blog.youkuaiyun.com/feibabeibei_beibei/article/details/52642288