安卓加固之so文件加固
Posted ciyze
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了安卓加固之so文件加固相关的知识,希望对你有一定的参考价值。
一、前言
最近在学习安卓加固方面的知识,看到了jiangwei212的博客,其中有对so文件加固的两篇文章通过节加密函数和通过hash段找到函数地址直接加密函数,感觉写的特别好,然后自己动手实践探索so加密,这里记录一下学习遇到的困难和所得吧,收获还是非常大的。
二、通过加密节的方式加密函数
1、加解密思路
加密:我们自己写一个Demo根据ELF文件格式,找到我们要加密的节,加密保存在ELF文件中
解密:这里有一个属性__attribute__((constructor)),这个属性使用的节优于main先执行,使我们解密有了可能。
2、实现流程
①编写我们的native代码,在native中将要加密的函数置于一个节中,并将解密函数赋予__attribute__((constructor))属性
a.在函数申明后面加上 __attribute__((section(".mytext"))) ,将函数定义在我们自己的section中
b.我们需要编写一个解密函数,属性用__attribute((constructor))申明,这样就可以在在so被加载的时候,在main之前将我们的节解密。
然后使用ndk-build将native代码编译成so文件
②编写加密程序(我这里使用VS2010)
a.解析so文件,找到.mytext段的起始地址和大小,这里是遍历所有节,根据其在字符串节中的名称,确定.mytext节
b.找到.mytext之后,进行加密,我们这里只是简单的异或,可以使用其他加密手段,最后写入文件
③将加密之后的so文件作为第三方库加载,注意这里不能直接编译后打包,要进行加密操作,android studio的加载方式可以参考我之前写的
Android Studio使用JNI中的使用第三方库加载,这里就不在多余的说明了。
3.代码实现
①Native代码,我们将要加密的函数置于一个新节中,利用__attribute__((section(".mytext")))属性
jint JNICALL native_Add(JNIEnv* env, jobject obj, jdouble num1, jdouble num2) __attribute__((section (".mytext"))); jint JNICALL native_Sub(JNIEnv *env, jobject obj, jdouble num1, jdouble num2) __attribute__((section (".mytext"))); jint JNICALL native_Mul(JNIEnv *env, jobject obj, jdouble num1, jdouble num2) __attribute__((section (".mytext"))); jint JNICALL native_Div(JNIEnv *env, jobject obj, jdouble num1, jdouble num2) __attribute__((section (".mytext")));
②Native代码,我们编写解密函数,我们给我们的解密函数__attribute__((constructor))属性,在so加载的时候优先执行
//此属性在so被加载时,优于main执行,开始解密 void init_native_Add() __attribute__((constructor)); unsigned long getLibAddr(); void init_native_Add(){ 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(); //在/proc/id/maps文件中找到我们的so文件,活动so文件地址 ehdr=(Elf32_Ehdr *)base; text_addr=ehdr->e_shoff+base;//加密节的地址 nblock=ehdr->e_entry >>16;//加密节的大小 nsize=ehdr->e_entry&0xffff;//加密节的大小 LOGD("nblock = 0x%d,nsize:%d", nblock,nsize); LOGD("base = 0x%x", text_addr); printf("nblock = %d\\n", nblock); //修改内存权限 if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){ puts("mem privilege change failed"); LOGD("mem privilege change failed"); } //进行解密,是针对加密算法的 for(i=0;i<nblock;i++){ char *addr=(char*)(text_addr+i); *addr=~(*addr); } if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC) != 0){ puts("mem privilege change failed"); } puts("Decrypt success"); } //获取到SO文件加载到内存中的起始地址,只有找到起始地址才能够进行解密; unsigned long getLibAddr(){ unsigned long ret=0; char name[]="libJniTest.so"; char buf[4096]; char *temp; int pid; FILE *fp; pid=getpid(); sprintf(buf,"/proc/%d/maps",pid); //这个文件中保存了进程映射的模块信息 cap /proc/id/maps 查看 fp=fopen(buf,"r"); if(fp==NULL){ LOGD("Error open maps file in progress %d",pid); puts("open failed"); goto _error; } while (fgets(buf,sizeof(buf),fp)){ if(strstr(buf,name)){ temp = strtok(buf, "-"); //分割字符串,返回 - 之前的字符 LOGD("Target so is %s\\r\\n",temp); ret = strtoul(temp, NULL, 16); //获取地址 LOGD("Target so address is %x",ret); break; } } _error: fclose(fp); return ret; }
解密函数的实现很简单,这里我们首先在getLibAddr函数中通过/proc/<pid>/maps文件获得加载的so文件路径,其中<pid>是该程序的id,maps文件中存放了加载的所有so文件的路径和基址,可通过shell命令 cat /proc/id/maps获得所有模块信息,也可以通过cat /proc/id/maps | grep libJniTest.so获得libJniTest.so模块的信息
然后我们通过ehdr->e_entry这个变量获取到被加密节的大小,ehdr->e_shoff获得加密节的地址偏移(加密的时候将加密节的信息写入这两个变量中,所以这里可以直接读取解密)。
然后在实现native中的注册代码,这里也不多说明了,可以看之前的Android Studio使用JNI,也可以看上传的代码。
③使用VS2010编写加密程序
这里需要熟悉ELF格式文件,找到我们自己定义的节.mytext,将节使用加密算法加密,将基地址和大小存入e_shoff和e_entry中
int _tmain(int argc, _TCHAR* argv[]) { char szSoPath[MAX_PATH] = "libJniTest.so"; char szSection[] = ".mytext"; char *shstr = NULL; char *content = NULL; int i; unsigned int base, length; unsigned short nblock; unsigned short nsize; unsigned char block_size = 16; char* szFileData = NULL; unsigned int ulLow; HANDLE hFile; ULONG ulHigh = 0; ULONG ulReturn = 0; //读取文件到内存 hFile = CreateFileA(szSoPath,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); if (hFile==INVALID_HANDLE_VALUE) { printf("打开的文件不存在!"); return -1; } ulLow = GetFileSize(hFile,&ulHigh); szFileData = new char[ulLow + 20]; printf("Read File at 0x%x\\r\\n",szFileData); if (ReadFile(hFile,szFileData,ulLow,&ulReturn,NULL)==0) { CloseHandle(hFile); delete szFileData; return FALSE; } Elf32_Ehdr* ehdr = (Elf32_Ehdr*)(szFileData); Elf32_Shdr* shdrstr = (Elf32_Shdr*)(szFileData + ehdr->e_shoff + sizeof(Elf32_Shdr) * ehdr->e_shstrndx); //字符串表的索引,偏移到字符串表 shstr = (char*)(szFileData + shdrstr->sh_offset);//偏移到字符串表 Elf32_Shdr* Shdr = (Elf32_Shdr*)(szFileData + ehdr->e_shoff); for(i = 0; i < ehdr->e_shnum; i++){ //根据字符串表的名称比较 if(strcmp(shstr + Shdr->sh_name, szSection) == 0){ base = Shdr->sh_offset; length = Shdr->sh_size; printf("Find section %s at 0x%x the size is 0x%x\\n", szSection,base,length); break; } Shdr++; } content= (char*)(szFileData + base); nblock = length / block_size; nsize = base / 4096 + (base % 4096 == 0 ? 0 : 1); printf("base = 0x%x, length = 0x%x\\n", base, length); printf("nblock = %d, nsize = %d\\n", nblock, nsize); //将节的地址和大小写入 ehdr->e_entry = (length << 16) + nsize; ehdr->e_shoff = base; //节的地址 printf("content is %x",content); //加密 for(i=0;i<length;i++){ content[i] = ~content[i]; } strcat(szSoPath,"_"); HANDLE hFile1 = CreateFileA(szSoPath,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL); if (hFile1==INVALID_HANDLE_VALUE) { printf("创建文件失败!"); return -1; } BOOL bRet = WriteFile(hFile1,szFileData,ulLow,&ulReturn,NULL); if(bRet) { printf("写入成功!\\r\\n"); } else { int a = GetLastError(); printf("写入失败:%d\\r\\n",a); } _error: delete(szFileData); CloseHandle(hFile); return 0; }
1)作为动态链接库,e_entry入口地址是无意义的,因为程序被加载时,设定的跳转地址是动态连接器的地址,这个字段是可以被作为数据填充的。
2)so装载时,与链接视图没有关系,即e_shoff、e_shentsize、e_shnum和e_shstrndx这些字段是可以任意修改的。被修改之后,使用readelf和ida等工具打开,会报各种错误,相信读者已经见识过了。
④运行结果
1)加密前
2)加密后
3)运行结果
(注:这里结果在原结果上加2)
4)使用grep命令查看加载模块
4.相关知识点
①将要加密的函数置于一个节中,解密函数使用__attribute__((constructor))属性优先执行,对于so动态链接库e_entry和e_shoff是可以修改存放我们加密节的大小和地址的,便于解密。
②掌握ELF文件格式知识,遍历节表,对应于字符串表中的节名,找到要加密的节,进行加密操作。
三、直接加密指定函数
1.原理
在so文件中,每个函数的结构描述是存放在.dynsym段中,每个函数名称保存在.dynstr段中,在ELF格式中有一个.hash段,由Elf32_Word对象组成的哈希表支持符号表访问。
bucket数组包含nbucket个项目,chain数组包含了nchain个项目,下标都是从0开始。
bucket和chain中都保存符号表索引,chain和符号表存在对应关系,符号表项的数目应该和nchain相等,所以符号表的索引也可用来选取chain表项,哈希函数能够接受符号名并且返回一个可以用来计算bucket的索引。
因此,如果哈希函数针对某个名字返回了数值X,则bucket[X%nbucket]给出了一个索引y,该索引可用于符号表,也可用于chain表。如果符号表不是所需要的,那么chain[y]则给出了具有相同哈希值的下一个符号表项。我们可以沿着chain链一直搜索,直到所选中的符号表项包含了所需要的符号,或者chain项中包含值STN_UNDEF。
上面的话有些复杂,简单来说就是用函数名称在hash函数中得到一个hash值,通过这个hash在chain中的位置就可以找到这个函数对应在.dynsym中对应的条目了。
hash函数如下
unsigned long elf_hash(const unsigned char* name){ unsigned long h = 0,g; while(*name) { h = (h<<4)+*name++; if(g = h & 0xf0000000) { h^=g>>24; h&=-g; } return h; } }
那么我们只用得到.hash段即可,但是我们怎么得到这个section呢?
由于so被加载到内存之后,就没有section了,对应的是segment了,而一个section包含多个section,相同的section可以被包含到不同的segment中。.dynamic段一般用于动态链接,所以.dynsym和.dynstr,.hash肯定包含在这里。我们可以解析了程序头信息之后,通过type获取到.dynamic程序头信息,然后获取到这个segment的偏移地址和大小,在进行解析成elf32_dyn结构。
2.实现方案
我们给函数加密,加密和解密都是基于装载视图实现,需要注意的是,被加密函数如果用static声明,那么函数是不会出现在.dynsym中,是无法在装载视图中通过函数名找到进行解密的。
①加密流程:
1)读取文件头,获取e_phoff、e_phentsize和e_phnum信息
2)通过Elf32_Phdr中的p_type字段,找到DYNAMIC。其实,DYNAMIC就是.dynamic section。从p_offset和p_filesz字段得到文件中的起始位置和长度。
3)遍历.dynamic,找到.dynsym、.dynstr、.hash section文件中的偏移和.dynstr的大小,
4)根据函数名称,计算hash值
5)根据hash值,找到下标hash%nbuckets的bucket;根据bucket中的值,读取.dynsym中的对应索引的Elf32_Sym符号,从符号的st_name索引找到在.dynstr中对应的字符串与函数名进行比较。若不等,则根据chain[hash%nbuckets]找下一个Elf32_Sym符号,直到找到或者chain终止为止。
6)找到函数对应的Elf32_Sym符号之后,即可以根据st_value和st_size字段找到函数的位置和大小
7)加密,写入文件
②解密流程为加密逆过程,找到函数地址的方式和加密流程中的方法是一致的,都是通过chain在dynsym中找到对应的函数项,然后在函数地址处进行解密。
3.代码实现
①使用VS2010编写加密代码
// Encrypting.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <stdio.h> #include <iostream> using namespace std; #include <Windows.h> #include "elf.h" typedef struct _funcInfo{ Elf32_Addr st_value; Elf32_Word st_size; }funcInfo; static Elf32_Off findTargetSectionAddr(char* szFileData, const char *szSection); static char getTargetFuncInfo(char* szFileData, const char *funcName, funcInfo *info); static unsigned elfhash(const char *_name); int _tmain(int argc, _TCHAR* argv[]) { char *shstr = NULL; funcInfo info; int i; char* szFileData = NULL; unsigned int ulLow; HANDLE hFile; ULONG ulHigh = 0; ULONG ulReturn = 0; char funcNameAdd[] = "native_Add"; char funcNameSub[] = "native_Sub"; char funcNameMul[] = "native_Mul"; char funcNameDiv[] = "native_Div"; char szSoPath[MAX_PATH] = "libJniTest.so"; char szSection[] = ".text"; Elf32_Off secOff; //读入文件在内存中 hFile = CreateFileA(szSoPath,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); if (hFile==INVALID_HANDLE_VALUE) { printf("打开的文件不存在!"); return -1; } ulLow = GetFileSize(hFile,&ulHigh); szFileData = new char[ulLow + 20]; if (ReadFile(hFile,szFileData,ulLow,&ulReturn,NULL)==0) { CloseHandle(hFile); delete szFileData; return FALSE; } //通过hash段中chain链获得的索引,获取在dynsym对应的条目 if(getTargetFuncInfo(szFileData, funcNameAdd, &info) == -1){ printf("Find function %s failed\\n", funcNameAdd); goto _error; } //得到函数地址 for(i=0;i<info.st_size-1;i++){ char *content = (char*)(szFileData + info.st_value -1 + i); *content = ~(*content); } //通过hash段中chain链获得的索引,获取在dynsym对应的条目 if(getTargetFuncInfo(szFileData, funcNameSub, &info) == -1){ printf("Find function %s failed\\n", funcNameSub); goto _error; } //得到函数地址 for(i=0;i<info.st_size-1;i++){ char *content = (char*)(szFileData + info.st_value -1 + i); *content = ~(*content); } //通过hash段中chain链获得的索引,获取在dynsym对应的条目 if(getTargetFuncInfo(szFileData, funcNameMul, &info) == -1){ printf("Find function %s failed\\n", funcNameMul); goto _error; } //得到函数地址 for(i=0;i<info.st_size-1;i++){ char *content = (char*)(szFileData + info.st_value -1 + i); *content = ~(*content); } //通过hash段中chain链获得的索引,获取在dynsym对应的条目 if(getTargetFuncInfo(szFileData, funcNameDiv, &info) == -1){ printf("Find function %s failed\\n", funcNameDiv); goto _error; } //得到函数地址 for(i=0;i<info.st_size-1;i++){ char *content = (char*)(szFileData + info.st_value -1 + i); *content = ~(*content); } //写入文件保存 strcat(szSoPath,"_"); HANDLE hFile1 = CreateFileA(szSoPath,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL); if (hFile1==INVALID_HANDLE_VALUE) { printf("创建文件失败!"); return -1; } BOOL bRet = WriteFile(hFile1,szFileData,ulLow,&ulReturn,NULL); if(bRet) { printf("写入成功!\\r\\n"); } else { int a = GetLastError(); printf("写入失败:%d\\r\\n",a); } _error: delete(szFileData); CloseHandle(hFile); return 0; } static unsigned elfhash(const char *_name) { const unsigned char *name = (const unsigned char *) _name; unsigned h = 0, g; while(*name) { h = (h << 4) + *name++; g = h & 0xf0000000; h ^= g; h ^= g >> 24; } return h; } static char getTargetFuncInfo(char* szFileData, const char *funcName, funcInfo *info){ char flag = -1; char *dynstr = NULL; int i; Elf32_Sym* funSym; Elf32_Phdr* phdr; Elf32_Off dyn_off; Elf32_Word dyn_size, dyn_strsz; Elf32_Dyn* dyn; Elf32_Addr dyn_symtab, dyn_strtab, dyn_hash; unsigned funHash, nbucket, nchain, funIndex; Elf32_Ehdr* ehdr = (Elf32_Ehdr*)szFileData; //视图模式 phdr = (Elf32_Phdr*)(szFileData + ehdr->e_phoff); for(i=0;i < ehdr->e_phnum; i++){ //获得动态链接节 if(phdr->p_type == PT_DYNAMIC){ dyn_size = phdr->p_filesz; dyn_off = phdr->p_offset; flag = 0; printf("Find section %s, size = 0x%x, addr = 0x%x\\n", ".dynamic", dyn_size, dyn_off); break; } phdr++; } if(flag){ puts("Find .dynamic failed"); goto _error; } flag = 0; printf("dyn_size:%d\\n",dyn_size); printf("count:%d\\n",(dyn_size/sizeof(Elf32_Dyn))); printf("off:%x\\n",dyn_off); dyn = (Elf32_Dyn*)(szFileData + dyn_off); for(i=0;i < dyn_size / sizeof(Elf32_Dyn); i++){ //符号表位置 if(dyn->d_tag == DT_SYMTAB){ dyn_symtab = dyn->d_un.d_ptr; flag += 1; printf("Find .dynsym, addr = 0x%x, val = 0x%x\\n", dyn_symtab, dyn->d_un.d_val); } //获得hash段 if(dyn->d_tag == DT_HASH){ dyn_hash = dyn->d_un.d_ptr; flag += 2; printf("Find .hash, addr = 0x%x\\n", dyn_hash); } //保存函数字符串的位置 if(dyn->d_tag == DT_STRTAB){ dyn_strtab = dyn->d_un.d_ptr; flag += 4; printf("Find .dynstr, addr = 0x%x\\n", dyn_strtab); } //字符串长度 if(dyn->d_tag == DT_STRSZ){ dyn_strsz = dyn->d_un.d_val; flag += 8; printf("Find .dynstr size, size = 0x%x\\n", dyn_strsz); } dyn++; } if((flag & 0x0f) != 0x0f){ puts("Find needed .section failed\\n"); goto _error; } dynstr = (char*) malloc(dyn_strsz); if(dynstr == NULL){ printf("Malloc .dynstr space failed"); goto _error; } memcpy(dynstr,szFileData + dyn_strtab,dyn_strsz); /* nbucket *----------------- * nchain *------------------ * bucket[0] * ... * bucket[nbucket-1] * ------------------ * chain[0] * ... * chain[nchain-1] */ funHash = elfhash(funcName); //获得函数名称经过hash运行后的值 printf("Function %s hashVal = 0x%x\\n", funcName, funHash); nbucket = *(int*)(szFileData + dyn_hash); //获得nbucket的值 printf("nbucket = %d\\n", nbucket); nchain = *(int*)(szFileData + dyn_hash + 4);//获得nchain的值 printf("nchain = %d\\n", nchain); funHash = funHash % nbucket; //bucket[X%nbucket]给出了一个索引y,该索引可用于符号表,也可用于chain表 printf("funHash mod nbucket = %d \\n", funHash); funIndex = *(int*)(szFileData + dyn_hash + 8 + funHash * 4);//y = bucket[X%nbucket]返回的索引y printf("funcIndex:%d\\n", funIndex); funSym = (Elf32_Sym*)(szFileData + dyn_symtab + funIndex*sizeof(Elf32_Sym));//该索引对应的符号表 if(strcmp(dynstr + funSym->st_name, funcName) != 0){ //如果索引y对应的符号表不是所需要的,那么chain[y]则给出了具有相同哈希值的下一个符号表项 while(1){ //我们可以沿着chain链一直搜索,直到所选中的符号表项包含了所需要的符号 printf("hash:%x,nbucket:%d,funIndex:%d\\n",dyn_hash,nbucket,funIndex); funIndex = *(int*)(szFileData + dyn_hash + 4*(2+nbucket+funIndex)); //搜索chain链 printf("funcIndex:%d\\n", funIndex); if(funIndex == 0){ puts("Cannot find funtion!\\n"); goto _error; } funSym = (Elf32_Sym*)(szFileData + dyn_symtab + funIndex*sizeof(Elf32_Sym)); //chain[]中对应的符号表 if(strcmp(dynstr + funSym->st_name, funcName) == 0){ break; } } } printf("Find: %s, offset = 0x%x, size = 0x%x\\n", funcName, funSym->st_value, funSym->st_size); info->st_value = funSym->st_value; info->st_size = funSym->st_size; free(dynstr); return 0; _error: free(dynstr); return -1; }
②Native代码
#include <jni.h> #include <stdio.h> //#include <assert.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <android/log.h> #include <elf.h> #include <sys/mman.h> #define LOG_TAG "Jiami" #define LOGD(fmt,args...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,fmt,##args) typedef struct _funcInfo{ Elf32_Addr st_value; Elf32_Word st_size; }funcInfo; JNIEXPORT jint JNICALL native_Add (JNIEnv *env, jobject obj, jdouble num1, jdouble num2) { return (jint)(num1 + num2 +3); } JNIEXPORT jint JNICALL native_Sub (JNIEnv *env, jobject obj, jdouble num1, jdouble num2) { return (jint)(num1 - num2 +3); } JNIEXPORT jint JNICALL native_Mul (JNIEnv *env, jobject obj, jdouble num1, jdouble num2) { return (jint)(num1 * num2 +3); } JNIEXPORT jint JNICALL native_Div (JNIEnv *env, jobject obj, jdouble num1, jdouble num2) { if以上是关于安卓加固之so文件加固的主要内容,如果未能解决你的问题,请参考以下文章