一、前言
最近在學習安卓加固方面的知識,看到了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 (num2 == 0) return 0; return (jint)(num1 / num2 +3); } //Java和JNI函數的綁定表 static JNINativeMethod gMethods[] = { {"Add", "(DD)I", (void *)native_Add}, {"Sub", "(DD)I", (void *)native_Sub}, {"Mul", "(DD)I", (void *)native_Mul}, {"Div", "(DD)I", (void *)native_Div}, }; //注冊native方法到java中 static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods, int numMethods) { jclass clazz; clazz = (*env)->FindClass(env, className); if (clazz == NULL) { return JNI_FALSE; } if ((*env)->RegisterNatives(env, clazz, gMethods,numMethods) < 0){ return JNI_FALSE; } return JNI_TRUE; } int register_ndk_load(JNIEnv *env) { return registerNativeMethods(env, "com/example/caculate/MainActivity", gMethods,sizeof(gMethods) / sizeof(gMethods[0])); //NELEM(gMethods)); } JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env = NULL; jint result = -1; if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) { return result; } register_ndk_load(env); // 返回jni的版本 return JNI_VERSION_1_4; } //此屬性在so被加載時,優於main執行,開始解密 void init_native_Add() __attribute__((constructor)); void init_native_Sub(); void init_native_Mul(); void init_native_Div(); unsigned long getLibAddr(); static char getTargetFuncInfo(unsigned long base, const char *funcName, funcInfo *info); 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; } void init_native_Add(){ const char target_fun[] = "native_Add"; funcInfo info; int i; unsigned int npage, base = getLibAddr(); LOGD("base addr is 0x%x",base); if(getTargetFuncInfo(base, target_fun, &info) == -1){ LOGD("Find native_Add failed"); return ; } npage = info.st_size / PAGE_SIZE + ((info.st_size % PAGE_SIZE == 0) ? 0 : 1); LOGD("npage = 0x%d", npage); LOGD("npage = 0x%d", PAGE_SIZE); if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), 4096*npage, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){ LOGD("mem privilege change failed"); } for(i=0;i< info.st_size - 1; i++){ char *addr = (char*)(base + info.st_value -1 + i); *addr = ~(*addr); } if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), 4096*npage, PROT_READ | PROT_EXEC) != 0){ LOGD("mem privilege change failed"); } init_native_Sub(); init_native_Mul(); init_native_Div(); } void init_native_Sub(){ const char target_fun[] = "native_Sub"; funcInfo info; int i; unsigned int npage, base = getLibAddr(); LOGD("base addr is 0x%x",base); if(getTargetFuncInfo(base, target_fun, &info) == -1){ LOGD("Find native_Sub failed"); return ; } npage = info.st_size / PAGE_SIZE + ((info.st_size % PAGE_SIZE == 0) ? 0 : 1); LOGD("npage = 0x%d", npage); LOGD("npage = 0x%d", PAGE_SIZE); if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), 4096*npage, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){ LOGD("mem privilege change failed"); } for(i=0;i< info.st_size - 1; i++){ char *addr = (char*)(base + info.st_value -1 + i); *addr = ~(*addr); } if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), 4096*npage, PROT_READ | PROT_EXEC) != 0){ LOGD("mem privilege change failed"); } } void init_native_Div(){ const char target_fun[] = "native_Div"; funcInfo info; int i; unsigned int npage, base = getLibAddr(); LOGD("base addr is 0x%x",base); if(getTargetFuncInfo(base, target_fun, &info) == -1){ LOGD("Find native_Div failed"); return ; } npage = info.st_size / PAGE_SIZE + ((info.st_size % PAGE_SIZE == 0) ? 0 : 1); LOGD("npage = 0x%d", npage); LOGD("npage = 0x%d", PAGE_SIZE); if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), 4096*npage, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){ LOGD("mem privilege change failed"); } for(i=0;i< info.st_size - 1; i++){ char *addr = (char*)(base + info.st_value -1 + i); *addr = ~(*addr); } if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), 4096*npage, PROT_READ | PROT_EXEC) != 0){ LOGD("mem privilege change failed"); } } void init_native_Mul(){ const char target_fun[] = "native_Mul"; funcInfo info; int i; unsigned int npage, base = getLibAddr(); LOGD("base addr is 0x%x",base); if(getTargetFuncInfo(base, target_fun, &info) == -1){ LOGD("Find native_Mul failed"); return ; } npage = info.st_size / PAGE_SIZE + ((info.st_size % PAGE_SIZE == 0) ? 0 : 1); LOGD("npage = 0x%d", npage); LOGD("npage = 0x%d", PAGE_SIZE); if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), 4096*npage, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){ LOGD("mem privilege change failed"); } for(i=0;i< info.st_size - 1; i++){ char *addr = (char*)(base + info.st_value -1 + i); *addr = ~(*addr); } if(mprotect((void *) ((base + info.st_value) / PAGE_SIZE * PAGE_SIZE), 4096*npage, PROT_READ | PROT_EXEC) != 0){ LOGD("mem privilege change failed"); } } //獲取到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; } static char getTargetFuncInfo(unsigned long base, const char *funcName, funcInfo *info){ char flag = -1, *dynstr; int i; Elf32_Ehdr *ehdr; Elf32_Phdr *phdr; Elf32_Off dyn_vaddr; Elf32_Word dyn_size, dyn_strsz; Elf32_Dyn *dyn; Elf32_Addr dyn_symtab, dyn_strtab, dyn_hash; Elf32_Sym *funSym; unsigned funHash, nbucket; unsigned *bucket, *chain; ehdr = (Elf32_Ehdr *)base; phdr = (Elf32_Phdr *)(base + ehdr->e_phoff);//視圖模式 LOGD("[+]phdr = 0x%p, size = 0x%x\n", phdr, ehdr->e_phnum); for (i = 0; i < ehdr->e_phnum; ++i) { LOGD("[+]phdr = 0x%p\n", phdr); //獲得動態鏈接節 if(phdr->p_type == PT_DYNAMIC){ flag = 0; LOGD("Find .dynamic segment"); break; } phdr ++; } if(flag) goto _error; dyn_vaddr = phdr->p_vaddr + base; dyn_size = phdr->p_filesz; LOGD("[+]dyn_vadd = 0x%x, dyn_size = 0x%x", dyn_vaddr, dyn_size); flag = 0; for (i = 0; i < dyn_size / sizeof(Elf32_Dyn); ++i) { dyn = (Elf32_Dyn *)(dyn_vaddr + i * sizeof(Elf32_Dyn)); //符號表位置 if(dyn->d_tag == DT_SYMTAB){ dyn_symtab = (dyn->d_un).d_ptr; flag += 1; LOGD("[+]Find .dynsym section, addr = 0x%x\n", dyn_symtab); } //獲得hash段 if(dyn->d_tag == DT_HASH){ dyn_hash = (dyn->d_un).d_ptr; flag += 2; LOGD("[+]Find .hash section, addr = 0x%x\n", dyn_hash); } //保存函數字符串的位置 if(dyn->d_tag == DT_STRTAB){ dyn_strtab = (dyn->d_un).d_ptr; flag += 4; LOGD("[+]Find .dynstr section, addr = 0x%x\n", dyn_strtab); } //字符串長度 if(dyn->d_tag == DT_STRSZ){ dyn_strsz = (dyn->d_un).d_val; flag += 8; LOGD("[+]Find strsz size = 0x%x\n", dyn_strsz); } } if((flag & 0x0f) != 0x0f){ LOGD("Find needed .section failed\n"); goto _error; } dyn_symtab += base; dyn_hash += base; dyn_strtab += base; dyn_strsz += base; /* nbucket *----------------- * nchain *------------------ * bucket[0] * ... * bucket[nbucket-1] * ------------------ * chain[0] * ... * chain[nchain-1] */ funHash = elfhash(funcName);//獲得函數名稱經過hash運行后的值 funSym = (Elf32_Sym *) dyn_symtab; dynstr = (char*) dyn_strtab; nbucket = *((int *) dyn_hash);//獲得nbucket的值 bucket = (int *)(dyn_hash + 8);//bucket鏈 chain = (unsigned int *)(dyn_hash + 4 * (2 + nbucket));//越過bucket鏈,到達chain鏈 flag = -1; LOGD("[+]hash = 0x%x, nbucket = 0x%x\n", funHash, nbucket); //bucket[X%nbucket]給出了一個索引y,該索引可用於符號表,也可用於chain表 int mod = (funHash % nbucket); LOGD("[+]mod = %d\n", mod); LOGD("[+]i = 0x%d\n", bucket[mod]); //i = mod = bucket[funHash%nbucket],通過遍歷i = chain[i]表,找到funSym對應的符號表 for(i = bucket[mod]; i != 0; i = chain[i]){ LOGD("[+]Find index = %d\n", i); if(strcmp(dynstr + ((Elf32_Sym*)((char*)funSym + i* sizeof(Elf32_Sym)))->st_name, funcName) == 0){ flag = 0; LOGD("[+]Find %s\n", funcName); break; } } if(flag) goto _error; info->st_value = ((Elf32_Sym*)((char*)funSym + i* sizeof(Elf32_Sym)))->st_value;//函數對應符號表中保存函數的地址 info->st_size =((Elf32_Sym*)((char*)funSym + i* sizeof(Elf32_Sym)))->st_size;//函數符號表中保存函數的大小 LOGD("[+]st_value = %d,st_size = %d",info->st_value,info->st_size); return 0; _error: return -1; }
Android.mk文件
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog LOCAL_PRELINK_MODULE := false LOCAL_MODULE := JniTest LOCAL_SRC_FILES := MyJniCalc.c LOCAL_SHARED_LIBRARIES := libandroid_runtime include $(BUILD_SHARED_LIBRARY)
4.執行結果
1)加密之前
2)加密之后
3)運行結果
(注:這里結果為原結果加3)
四、總結
這里的so文件加固相對於windows平台還是比較簡單的,沒有復雜的跳轉,學習起來比較容易的,只需熟悉ELF格式,找到對應的位置進行加解密。這篇屬於拿來主義,不過自己在實踐學習的過程中也學習到特別多的知識,比如安卓模擬器使用grep命令,加載第三方庫的方法,也犯了很多小錯誤也一一解決了,期待以后更加深入的學習和分享~