admin管理员组

文章数量:1639675

一、前言

经过上次写完在ELF文件中根据函数名找函数,就准备开始编写so文件函数加密,这里这是对代码进行加密,还没有对函数名做混淆,会放到下次写。还有本次的测试机是nexus4,操作系统为android 4.4。

二、函数加密

一般在android中,各种核心的东西都会放在so文件中,因为native层的代码分析难度大,执行效率高。本文选择对so文件的核心函数进行加密,用来对抗静态分析。在加密之前,需要知道加密的原理和流程,在我们对so文件的核心函数加密后,我们需要对其解密,如果不解密的话,你的程序肯定会崩溃。解密的时机肯定是在函数运行之前,本文选择了在 JNIOnLoad函数里执行解密操作,当然JNIOnLoad不是最早执行的函数,但是在库加载时就运行了,是满足我们的要求的。

首先介绍一下对so文件里的函数的加密,要对函数加密当然要先找到函数的地址,本人以前写过一个根据函数名找函数地址的文章子,不了解的同学可以先看看这篇文章,文章地址:https://blog.csdn/qq_16812035/article/details/87866217。找到函数地址后和函数大小后,事情就变得简单了。本人做的加密就是对函数的每个字节码进行异或操作,然后保存为新的so文件。

void entryCode(size_t code_base, size_t code_size) {
	for (size_t i = 0; i < code_size; ++i) {
		((char*)code_base)[i] = ((char*)code_base)[i] ^ 0xA;
	}
}

生成新的so文件后,需要替换掉原so文件,替换掉后,重新打包,签名,安装到手机上,运行,直接崩溃,是因为程序执行到你的函数时,因为是加密的,执行了乱七八糟的指令,所以崩溃了,所以我们要编写解密代码。

三、解 密 

我们要在so文件里定义一个JNIOnLoad来覆盖默认的JNIOnload函数,然后开始写我们的解密程序,第一步当然是找到我们的so函数的基地址,在linux中,有一句名言,"一切皆文件",进程也是。我们随便进入一个进程的目录,执行cat maps,可以看到liblog.so的地址范围在0x400e6000-0x400e9000之间,所以它的基地址就是0x400e6000。

接下来就是代码实现部分,首先找到本进程的maps文件,打开并且遍历文件的每一行,找到要查找的库,最后提取它的基地址。代码如下。

//获取目标库文件基地址
unsigned long get_lib_addr(char* libname){
    char buf[4096];
    char *temp;
    unsigned long ret;
    //获取pid
    int pd = getpid();
    //生成进程maps路径
    sprintf(buf,"/proc/%d/maps",pd);
    //打开maps文件
    FILE* fp = fopen(buf,"r");
    if(fp==NULL){
        puts("open fail");
        fclose(fp);
        return -1;
    }
    //按行读取
    while (fgets(buf, sizeof(buf),fp)){
        //根据目标函数名找到对应库信息
        if(strstr(buf,libname)){
            //字符串切割,返回库函数基地址
            temp = strtok(buf, "-");
            //将字符串转为无符号整数
            ret = strtoul(temp, NULL, 16);
            __android_log_print(ANDROID_LOG_INFO, "JNITag", "base =  0x%x", ret);
            break;
        }
    }
        fclose(fp);
        return ret;
}

找到基地址后,我们要对so文件进行解析,这时候so文件已经加载到内存了,我们不能像以前一样查找我们的函数,我们需要了解一下ELF文件格式的其它信息,在ELF文件头中,有两个字段,指向程序头的偏移e_phoff和程序头数组的数量e_phnum。

程序头中包含一个结构数组,用来描述与程序执行直接相关的目标文件结构信息。

我们要找的.hash、symstr、dynsym节在程序执行的时候会合并到一个类型为.dynamic的段中。代码很简单,遍历程序头数组,检查e_type类型,找到对应动态节区数组。

        Elf32_Ehdr* ehdr = (Elf32_Ehdr*)addr;
        Elf32_Phdr* phdr = (Elf32_Phdr*)(ehdr->e_phoff + addr);
        Elf32_Dyn* dyn;
        for(size_t i = 0;i<ehdr->e_phnum;++i){
            if(PT_DYNAMIC == phdr->p_type){
                dyn = (Elf32_Dyn*)(phdr->p_vaddr + addr);
                break;
            }
            phdr++;
        }

下面是动态节区结构体,d_tag对应节区头的e_type,d_ptr为虚拟地址的偏移。

根据动态节区节区结构体就可以找到我们要的.hash,dynstr,dynsym的内容,代码如下。

        size_t dyncount = phdr->p_memsz/ sizeof(Elf32_Dyn);
        size_t hashoff = 0;
        size_t symtaboff = 0;
        size_t strtaboff = 0;

        for(size_t i = 0;i<dyncount;++i){
            if(DT_HASH == dyn->d_tag){
                hashoff = dyn->d_un.d_ptr;
            }else if(DT_SYMTAB == dyn->d_tag){
                symtaboff = dyn->d_un.d_ptr;
            }else if(DT_STRTAB == dyn->d_tag){
                strtaboff = dyn->d_un.d_ptr;
            }
            dyn++;
        }

找到这三个节后,事情就好办了,根据.hash节遍历找到目标符号表,然后找到函数偏移地址和大小,可以参考前面链接中的文章。

        unsigned long eflag = ELFHash(name);
        Elf32_Word* bucketchain = (Elf32_Word*)(hashoff + addr);
        Elf32_Word bucket_count =  bucketchain[0];
        Elf32_Word chain_count = bucketchain[1];
        Elf32_Word* bucket = &bucketchain[2];
        Elf32_Word* chain = &bucketchain[2 + bucket_count];


        Elf32_Sym* sym = (Elf32_Sym*)(symtaboff+ addr);
        char* str = (char*)(strtaboff + addr);

        funInfo fun_info;
        size_t mod = eflag%bucket_count;
        for(size_t i = bucket[mod];i!=0;i = chain[i]){
            char* findstr = (char*)(str + sym[i].st_name);
            if(!strcmp(findstr,name)){
                size_t code_revision = sym[i].st_value;
                if(code_revision&0x00000001){
                    code_revision--;
                }
                fun_info.code_offset = code_revision;
                fun_info.size = sym[i].st_size;
                break;
            }
        }

得到函数地址后,我们可以进行解密了,因为之前加密的方法是异或0xA,所以这时候我们同样多加密的字节码异或上0xA,这叫做异或的自反性,A^B = C,可以得到C^B = A。但是我们不能像加密一样,直接改,因为有内存保护属性,代码段的权限通常只有读和执行,并没有写,强行写的话,程序会崩溃。所以我们要先修改内存保护属性,然后解密,解密完,将内存保护属性改为原来的权限。linux里提供了一个修改内存权限的函数mprotect。

int mprotect(const void *start, size_t len, int prot)

可以看到函数一共有三个参数:

  1.    start 起始地址
  2.    len 大小, 必须是页大小的倍数
  3.    prot 权限

成功时返回0,失败返回-1。代码如下。

 size_t pagesize = (fun_info.code_offset/PAGE_SIZE + 1)*PAGE_SIZE;
 mprotect(libBase, pagesize, PROT_EXEC|PROT_READ|PROT_WRITE);
 for(size_t i = 0;i < fun_info.size;++i){
       ((char*)(fun_info.code_offset + libBase))[i]^=0xA;
 }
 mprotect(libBase,pagesize,PROT_EXEC|PROT_READ);

下面看下效果,加密前。

加密之后。

四、总结

在nexus4手机 android4.4中正常运行,其它机型和操作系统没试过,可能会有很多问题,以后有时间测试一下,加密后加大了逆向分析的难度,但这种程度的加密,还是很容易破解的,继续学习点猥琐的招数吧。

代码下载:https://github/newhasaki/soEncryption

 

本文标签: 函数文件android