好長時間沒有更新文章了,主要仍是工做上的事,連續加班一個月,沒有時間研究了,只有週末有時間,來看一下,不過我仍是延續以前的文章,繼續咱們的逆向之旅,今天咱們要來看一下如何經過對so加密,在介紹本篇文章以前的話,必定要先閱讀以前的文章:html
so文件格式詳解以及如何解析一個so文件java
http://blog.csdn.net/jiangwei0910410003/article/details/49336613
android
這個是咱們今天這篇文章的基礎,若是不瞭解so文件的格式的話,下面的知識點可能會看的很費勁
c++
下面就來介紹咱們今天的話題:對so中的section進行加密算法
加密:在以前的文章中咱們介紹了so中的格式,那麼對於找到一個section的base和size就能夠對這段section進行加密了shell
解密:由於咱們對section進行加密以後,確定須要解密的,否則的話,運行確定是報錯的,那麼這裏的重點是何時去進行解密,對於一個so文件,咱們load進程序以後,在運行程序以前咱們能夠從哪一個時間點來突破?這裏就須要一個知識點:安全
__attribute__((constructor));
微信
關於這個,屬性的用法這裏就不作介紹了,網上有相關資料,他的做用很簡單,就是優先於main方法以前執行,相似於Java中的構造函數,固然其實C++中的構造函數就是基於這個屬性實現的,咱們在以前介紹elf文件格式的時候,有兩個section會引發咱們的注意:app
對於這兩個section,其實就是用這個屬性實現的函數存在這裏,ionic
在動態連接器構造了進程映像,並執行了重定位之後,每一個共享的目標都得到執行 某些初始化代碼的機會。這些初始化函數的被調用順序是不必定的,不過全部共享目標 初始化都會在可執行文件獲得控制以前發生。
相似地,共享目標也包含終止函數,這些函數在進程完成終止動做序列時,經過 atexit() 機制執行。動態連接器對終止函數的調用順序是不肯定的。
共享目標經過動態結構中的 DT_INIT 和 DT_FINI 條目指定初始化/終止函數。一般 這些代碼放在.init 和.fini 節區中。
這個知識點很重要,咱們後面在進行動態調試so的時候,還會用到這個知識點,因此必定要理解。
因此,在這裏咱們找到了解密的時機,就是本身定義一個解密函數,而後用上面的這個屬性聲明就能夠了。
第1、咱們編寫一個簡單的native代碼,這裏咱們須要作兩件事:
一、將咱們核心的native函數定義在本身的一個section中,這裏會用到這個屬性:__attribute__((section (".mytext")));
其中.mytext就是咱們本身定義的section.
說到這裏,還記得咱們以前介紹的一篇文章中介紹了,動態的給so添加一個section:
http://blog.csdn.net/jiangwei0910410003/article/details/49361281
二、須要編寫咱們的解密函數,用屬性: __attribute__((constructor));聲明
這樣一個native程序就包含這兩個重要的函數,使用ndk編譯成so文件
第2、編寫加密程序,在加密程序中咱們須要作的是:
一、經過解析so文件,找到.mytext段的起始地址和大小,這裏的思路是:
找到全部的Section,而後獲取他的name字段,在結合String Section,遍歷找到.mytext字段
二、找到.mytext段以後,而後進行加密,最後在寫入到文件中。
前面介紹了原理和實現方案,下面就開始coding吧,
第1、咱們先來看看native程序
#include <jni.h> #include <stdio.h> #include <android/log.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <elf.h> #include <sys/mman.h> jstring getString(JNIEnv*) __attribute__((section (".mytext"))); jstring getString(JNIEnv* env){ return (*env)->NewStringUTF(env, "Native method return!"); }; void init_getString() __attribute__((constructor)); unsigned long getLibAddr(); void init_getString(){ 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; __android_log_print(ANDROID_LOG_INFO, "JNITag", "nblock = 0x%x,nsize:%d", nblock,nsize); __android_log_print(ANDROID_LOG_INFO, "JNITag", "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"); __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 *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC) != 0){ puts("mem privilege change failed"); } puts("Decrypt success"); } unsigned long getLibAddr(){ unsigned long ret = 0; char name[] = "libdemo.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; } JNIEXPORT jstring JNICALL Java_com_example_shelldemo_MainActivity_getString( JNIEnv* env, jobject thiz ) { #if defined(__arm__) #if defined(__ARM_ARCH_7A__) #if defined(__ARM_NEON__) #define ABI "armeabi-v7a/NEON" #else #define ABI "armeabi-v7a" #endif #else #define ABI "armeabi" #endif #elif defined(__i386__) #define ABI "x86" #elif defined(__mips__) #define ABI "mips" #else #define ABI "unknown" #endif return getString(env); }下面來分析一下代碼:
一、定義本身的段
jstring getString(JNIEnv*) __attribute__((section (".mytext"))); jstring getString(JNIEnv* env){ return (*env)->NewStringUTF(env, "Native method return!"); };這裏的getString返回一個字符串,提供給Android上層,而後將getString定義在.mytext段中。
unsigned long getLibAddr(){ unsigned long ret = 0; char name[] = "libdemo.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; }這裏的代碼其實就是讀取設備的 proc/<uid>/maps中的內容,由於這個maps中是程序運行的內存映像:
咱們只有獲取到so的起始地址,才能找到指定的Section而後進行解密。
三、解密函數
void init_getString(){ 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; //獲取so的起始地址 base = getLibAddr(); //獲取指定section的偏移值和size ehdr = (Elf32_Ehdr *)base; text_addr = ehdr->e_shoff + base; nblock = ehdr->e_entry >> 16; nsize = ehdr->e_entry & 0xffff; __android_log_print(ANDROID_LOG_INFO, "JNITag", "nblock = 0x%x,nsize:%d", nblock,nsize); __android_log_print(ANDROID_LOG_INFO, "JNITag", "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"); __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 *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC) != 0){ puts("mem privilege change failed"); } puts("Decrypt success"); }這裏咱們獲取到so文件的頭部,而後獲取指定section的偏移地址和size
//獲取so的起始地址 base = getLibAddr(); //獲取指定section的偏移值和size ehdr = (Elf32_Ehdr *)base; text_addr = ehdr->e_shoff + base; nblock = ehdr->e_entry >> 16; nsize = ehdr->e_entry & 0xffff;這裏可能會有困惑?爲何這裏是這麼獲取offset和size的,其實這裏咱們作了一點工做,就是咱們在加密的時候順便改寫了so的頭部信息,將offset和size值寫到了頭部中,這樣加大破解難度。後面在說到加密的時候在詳解。
text_addr是起始地址+偏移值,就是咱們的section在內存中的絕對地址
nsize是咱們的section佔用的頁數
而後修改這個section的內存操做權限
//修改內存的操做權限 if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){ puts("mem privilege change failed"); __android_log_print(ANDROID_LOG_INFO, "JNITag", "mem privilege change failed"); }這裏調用了一個系統函數:mprotect
第一個參數:須要修改內存的起始地址
必須須要頁面對齊,也就是必須是頁面PAGE_SIZE(0x1000=4096)的整數倍
第二個參數:須要修改的大小
佔用的頁數*PAGE_SIZE
第三個參數:權限值
最後讀取內存中的section內容,而後進行解密,在將內存權限修改回去。
而後使用ndk編譯成so便可,這裏咱們用到了系統的打印log信息,因此須要用到共享庫,看一下編譯腳本Android.mk
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := demo LOCAL_SRC_FILES := demo.c LOCAL_LDLIBS := -llog include $(BUILD_SHARED_LIBRARY)
關於如何使用ndk,這裏就不作介紹了,參考這篇文章:
http://blog.csdn.net/jiangwei0910410003/article/details/17710243
第2、加密程序
一、加密程序(Java版)
咱們獲取到上面的so文件,下面咱們就來看看如何進行加密的:
package com.jiangwei.encodesection; import com.jiangwei.encodesection.ElfType32.Elf32_Sym; import com.jiangwei.encodesection.ElfType32.elf32_phdr; import com.jiangwei.encodesection.ElfType32.elf32_shdr; public class EncodeSection { public static String encodeSectionName = ".mytext"; public static ElfType32 type_32 = new ElfType32(); public static void main(String[] args){ byte[] fileByteArys = Utils.readFile("so/libdemo.so"); if(fileByteArys == null){ System.out.println("read file byte failed..."); return; } /** * 先解析so文件 * 而後初始化AddSection中的一些信息 * 最後在AddSection */ parseSo(fileByteArys); encodeSection(fileByteArys); parseSo(fileByteArys); Utils.saveFile("so/libdemos.so", fileByteArys); } private static void encodeSection(byte[] fileByteArys){ //讀取String Section段 System.out.println(); int string_section_index = Utils.byte2Short(type_32.hdr.e_shstrndx); elf32_shdr shdr = type_32.shdrList.get(string_section_index); int size = Utils.byte2Int(shdr.sh_size); int offset = Utils.byte2Int(shdr.sh_offset); int mySectionOffset=0,mySectionSize=0; for(elf32_shdr temp : type_32.shdrList){ int sectionNameOffset = offset+Utils.byte2Int(temp.sh_name); if(Utils.isEqualByteAry(fileByteArys, sectionNameOffset, encodeSectionName)){ //這裏須要讀取section段而後進行數據加密 mySectionOffset = Utils.byte2Int(temp.sh_offset); mySectionSize = Utils.byte2Int(temp.sh_size); byte[] sectionAry = Utils.copyBytes(fileByteArys, mySectionOffset, mySectionSize); for(int i=0;i<sectionAry.length;i++){ sectionAry[i] = (byte)(sectionAry[i] ^ 0xFF); } Utils.replaceByteAry(fileByteArys, mySectionOffset, sectionAry); } } //修改Elf Header中的entry和offset值 int nSize = mySectionSize/4096 + (mySectionSize%4096 == 0 ? 0 : 1); byte[] entry = new byte[4]; entry = Utils.int2Byte((mySectionSize<<16) + nSize); Utils.replaceByteAry(fileByteArys, 24, entry); byte[] offsetAry = new byte[4]; offsetAry = Utils.int2Byte(mySectionOffset); Utils.replaceByteAry(fileByteArys, 32, offsetAry); } private static void parseSo(byte[] fileByteArys){ //讀取頭部內容 System.out.println("+++++++++++++++++++Elf Header+++++++++++++++++"); parseHeader(fileByteArys, 0); System.out.println("header:\n"+type_32.hdr); //讀取程序頭信息 //System.out.println(); //System.out.println("+++++++++++++++++++Program Header+++++++++++++++++"); int p_header_offset = Utils.byte2Int(type_32.hdr.e_phoff); parseProgramHeaderList(fileByteArys, p_header_offset); //type_32.printPhdrList(); //讀取段頭信息 //System.out.println(); //System.out.println("+++++++++++++++++++Section Header++++++++++++++++++"); int s_header_offset = Utils.byte2Int(type_32.hdr.e_shoff); parseSectionHeaderList(fileByteArys, s_header_offset); //type_32.printShdrList(); //這種方式獲取全部的Section的name /*byte[] names = Utils.copyBytes(fileByteArys, offset, size); String str = new String(names); byte NULL = 0;//字符串的結束符 StringTokenizer st = new StringTokenizer(str, new String(new byte[]{NULL})); System.out.println( "Token Total: " + st.countTokens() ); while(st.hasMoreElements()){ System.out.println(st.nextToken()); } System.out.println("");*/ /*//讀取符號表信息(Symbol Table) System.out.println(); System.out.println("+++++++++++++++++++Symbol Table++++++++++++++++++"); //這裏須要注意的是:在Elf表中沒有找到SymbolTable的數目,可是咱們仔細觀察Section中的Type=DYNSYM段的信息能夠獲得,這個段的大小和偏移地址,而SymbolTable的結構大小是固定的16個字節 //那麼這裏的數目=大小/結構大小 //首先在SectionHeader中查找到dynsym段的信息 int offset_sym = 0; int total_sym = 0; for(elf32_shdr shdr : type_32.shdrList){ if(Utils.byte2Int(shdr.sh_type) == ElfType32.SHT_DYNSYM){ total_sym = Utils.byte2Int(shdr.sh_size); offset_sym = Utils.byte2Int(shdr.sh_offset); break; } } int num_sym = total_sym / 16; System.out.println("sym num="+num_sym); parseSymbolTableList(fileByteArys, num_sym, offset_sym); type_32.printSymList(); //讀取字符串表信息(String Table) System.out.println(); System.out.println("+++++++++++++++++++Symbol Table++++++++++++++++++"); //這裏須要注意的是:在Elf表中沒有找到StringTable的數目,可是咱們仔細觀察Section中的Type=STRTAB段的信息,能夠獲得,這個段的大小和偏移地址,可是咱們這時候咱們不知道字符串的大小,因此就獲取不到數目了 //這裏咱們能夠查看Section結構中的name字段:表示偏移值,那麼咱們能夠經過這個值來獲取字符串的大小 //能夠這麼理解:當前段的name值 減去 上一段的name的值 = (上一段的name字符串的長度) //首先獲取每一個段的name的字符串大小 int prename_len = 0; int[] lens = new int[type_32.shdrList.size()]; int total = 0; for(int i=0;i<type_32.shdrList.size();i++){ if(Utils.byte2Int(type_32.shdrList.get(i).sh_type) == ElfType32.SHT_STRTAB){ int curname_offset = Utils.byte2Int(type_32.shdrList.get(i).sh_name); lens[i] = curname_offset - prename_len - 1; if(lens[i] < 0){ lens[i] = 0; } total += lens[i]; System.out.println("total:"+total); prename_len = curname_offset; //這裏須要注意的是,最後一個字符串的長度,須要用總長度減去前面的長度總和來獲取到 if(i == (lens.length - 1)){ System.out.println("size:"+Utils.byte2Int(type_32.shdrList.get(i).sh_size)); lens[i] = Utils.byte2Int(type_32.shdrList.get(i).sh_size) - total - 1; } } } for(int i=0;i<lens.length;i++){ System.out.println("len:"+lens[i]); } //上面的那個方法很差,咱們發現StringTable中的每一個字符串結束都會有一個00(傳說中的字符串結束符),那麼咱們只要知道StringTable的開始位置,而後就能夠讀取到每一個字符串的值了 */ } /** * 解析Elf的頭部信息 * @param header */ private static void parseHeader(byte[] header, int offset){ if(header == null){ System.out.println("header is null"); return; } /** * public byte[] e_ident = new byte[16]; public short e_type; public short e_machine; public int e_version; public int e_entry; public int e_phoff; public int e_shoff; public int e_flags; public short e_ehsize; public short e_phentsize; public short e_phnum; public short e_shentsize; public short e_shnum; public short e_shstrndx; */ type_32.hdr.e_ident = Utils.copyBytes(header, 0, 16);//魔數 type_32.hdr.e_type = Utils.copyBytes(header, 16, 2); type_32.hdr.e_machine = Utils.copyBytes(header, 18, 2); type_32.hdr.e_version = Utils.copyBytes(header, 20, 4); type_32.hdr.e_entry = Utils.copyBytes(header, 24, 4); type_32.hdr.e_phoff = Utils.copyBytes(header, 28, 4); type_32.hdr.e_shoff = Utils.copyBytes(header, 32, 4); type_32.hdr.e_flags = Utils.copyBytes(header, 36, 4); type_32.hdr.e_ehsize = Utils.copyBytes(header, 40, 2); type_32.hdr.e_phentsize = Utils.copyBytes(header, 42, 2); type_32.hdr.e_phnum = Utils.copyBytes(header, 44,2); type_32.hdr.e_shentsize = Utils.copyBytes(header, 46,2); type_32.hdr.e_shnum = Utils.copyBytes(header, 48, 2); type_32.hdr.e_shstrndx = Utils.copyBytes(header, 50, 2); } /** * 解析程序頭信息 * @param header */ public static void parseProgramHeaderList(byte[] header, int offset){ int header_size = 32;//32個字節 int header_count = Utils.byte2Short(type_32.hdr.e_phnum);//頭部的個數 byte[] des = new byte[header_size]; for(int i=0;i<header_count;i++){ System.arraycopy(header, i*header_size + offset, des, 0, header_size); type_32.phdrList.add(parseProgramHeader(des)); } } private static elf32_phdr parseProgramHeader(byte[] header){ /** * public int p_type; public int p_offset; public int p_vaddr; public int p_paddr; public int p_filesz; public int p_memsz; public int p_flags; public int p_align; */ ElfType32.elf32_phdr phdr = new ElfType32.elf32_phdr(); phdr.p_type = Utils.copyBytes(header, 0, 4); phdr.p_offset = Utils.copyBytes(header, 4, 4); phdr.p_vaddr = Utils.copyBytes(header, 8, 4); phdr.p_paddr = Utils.copyBytes(header, 12, 4); phdr.p_filesz = Utils.copyBytes(header, 16, 4); phdr.p_memsz = Utils.copyBytes(header, 20, 4); phdr.p_flags = Utils.copyBytes(header, 24, 4); phdr.p_align = Utils.copyBytes(header, 28, 4); return phdr; } /** * 解析段頭信息內容 */ public static void parseSectionHeaderList(byte[] header, int offset){ int header_size = 40;//40個字節 int header_count = Utils.byte2Short(type_32.hdr.e_shnum);//頭部的個數 byte[] des = new byte[header_size]; for(int i=0;i<header_count;i++){ System.arraycopy(header, i*header_size + offset, des, 0, header_size); type_32.shdrList.add(parseSectionHeader(des)); } } private static elf32_shdr parseSectionHeader(byte[] header){ ElfType32.elf32_shdr shdr = new ElfType32.elf32_shdr(); /** * public byte[] sh_name = new byte[4]; public byte[] sh_type = new byte[4]; public byte[] sh_flags = new byte[4]; public byte[] sh_addr = new byte[4]; public byte[] sh_offset = new byte[4]; public byte[] sh_size = new byte[4]; public byte[] sh_link = new byte[4]; public byte[] sh_info = new byte[4]; public byte[] sh_addralign = new byte[4]; public byte[] sh_entsize = new byte[4]; */ shdr.sh_name = Utils.copyBytes(header, 0, 4); shdr.sh_type = Utils.copyBytes(header, 4, 4); shdr.sh_flags = Utils.copyBytes(header, 8, 4); shdr.sh_addr = Utils.copyBytes(header, 12, 4); shdr.sh_offset = Utils.copyBytes(header, 16, 4); shdr.sh_size = Utils.copyBytes(header, 20, 4); shdr.sh_link = Utils.copyBytes(header, 24, 4); shdr.sh_info = Utils.copyBytes(header, 28, 4); shdr.sh_addralign = Utils.copyBytes(header, 32, 4); shdr.sh_entsize = Utils.copyBytes(header, 36, 4); return shdr; } /** * 解析Symbol Table內容 */ public static void parseSymbolTableList(byte[] header, int header_count, int offset){ int header_size = 16;//16個字節 byte[] des = new byte[header_size]; for(int i=0;i<header_count;i++){ System.arraycopy(header, i*header_size + offset, des, 0, header_size); type_32.symList.add(parseSymbolTable(des)); } } private static ElfType32.Elf32_Sym parseSymbolTable(byte[] header){ /** * public byte[] st_name = new byte[4]; public byte[] st_value = new byte[4]; public byte[] st_size = new byte[4]; public byte st_info; public byte st_other; public byte[] st_shndx = new byte[2]; */ Elf32_Sym sym = new Elf32_Sym(); sym.st_name = Utils.copyBytes(header, 0, 4); sym.st_value = Utils.copyBytes(header, 4, 4); sym.st_size = Utils.copyBytes(header, 8, 4); sym.st_info = header[12]; //FIXME 這裏有一個問題,就是這個字段讀出來的值始終是0 sym.st_other = header[13]; sym.st_shndx = Utils.copyBytes(header, 14, 2); return sym; } }在這裏,我須要解析so文件的頭部信息,程序頭信息,段頭信息
//讀取頭部內容 System.out.println("+++++++++++++++++++Elf Header+++++++++++++++++"); parseHeader(fileByteArys, 0); System.out.println("header:\n"+type_32.hdr); //讀取程序頭信息 //System.out.println(); //System.out.println("+++++++++++++++++++Program Header+++++++++++++++++"); int p_header_offset = Utils.byte2Int(type_32.hdr.e_phoff); parseProgramHeaderList(fileByteArys, p_header_offset); //type_32.printPhdrList(); //讀取段頭信息 //System.out.println(); //System.out.println("+++++++++++++++++++Section Header++++++++++++++++++"); int s_header_offset = Utils.byte2Int(type_32.hdr.e_shoff); parseSectionHeaderList(fileByteArys, s_header_offset); //type_32.printShdrList();
關於這個解析的工做說明這裏就不解析了,看以前解析elf文件的那篇文章。
獲取這些信息以後,下面就來開始尋找咱們的段了,只須要遍歷Section列表,找到名字是.mytext的section便可,而後獲取offset和size,對內容進行加密,回寫到文件中。下面來看看核心方法:
private static void encodeSection(byte[] fileByteArys){ //讀取String Section段 System.out.println(); int string_section_index = Utils.byte2Short(type_32.hdr.e_shstrndx); elf32_shdr shdr = type_32.shdrList.get(string_section_index); int size = Utils.byte2Int(shdr.sh_size); int offset = Utils.byte2Int(shdr.sh_offset); int mySectionOffset=0,mySectionSize=0; for(elf32_shdr temp : type_32.shdrList){ int sectionNameOffset = offset+Utils.byte2Int(temp.sh_name); if(Utils.isEqualByteAry(fileByteArys, sectionNameOffset, encodeSectionName)){ //這裏須要讀取section段而後進行數據加密 mySectionOffset = Utils.byte2Int(temp.sh_offset); mySectionSize = Utils.byte2Int(temp.sh_size); byte[] sectionAry = Utils.copyBytes(fileByteArys, mySectionOffset, mySectionSize); for(int i=0;i<sectionAry.length;i++){ sectionAry[i] = (byte)(sectionAry[i] ^ 0xFF); } Utils.replaceByteAry(fileByteArys, mySectionOffset, sectionAry); } } //修改Elf Header中的entry和offset值 int nSize = mySectionSize/4096 + (mySectionSize%4096 == 0 ? 0 : 1); byte[] entry = new byte[4]; entry = Utils.int2Byte((mySectionSize<<16) + nSize); Utils.replaceByteAry(fileByteArys, 24, entry); byte[] offsetAry = new byte[4]; offsetAry = Utils.int2Byte(mySectionOffset); Utils.replaceByteAry(fileByteArys, 32, offsetAry); }咱們知道Section中的sh_name字段的值是這個section段的name在StringSection中的索引值,這裏offset就是StringSection在文件中的偏移值。固然咱們須要知道的一個知識點就是:StringSection中的每一個name都是以\0結尾的,因此咱們只須要判斷字符串到結束符就能夠了,判斷方法是Utils.isEqualByteAry:
public static boolean isEqualByteAry(byte[] src, int start, String destStr){ if(destStr == null){ return false; } byte[] dest = destStr.getBytes(); if(src == null || dest == null){ return false; } if(dest.length == 0 || src.length == 0){ return false; } if(start >= src.length){ return false; } int len = 0; byte temp = src[start]; while(temp != 0){ len++; temp = src[start+len]; } byte[] sonAry = copyBytes(src, start, len); if(sonAry == null || sonAry.length == 0){ return false; } if(sonAry.length != dest.length){ return false; } String sonStr = new String(sonAry); if(destStr.equals(sonStr)){ return true; } return false; }這裏咱們加密的方法很簡單,加密完成以後,咱們須要作的是回寫到so文件中,固然這裏咱們還須要作一件事,就是將咱們加密的.mytext段的偏移值和pageSize保存到頭部信息中:
//修改Elf Header中的entry和offset值 int nSize = mySectionSize/4096 + (mySectionSize%4096 == 0 ? 0 : 1); byte[] entry = new byte[4]; entry = Utils.int2Byte((mySectionSize<<16) + nSize); Utils.replaceByteAry(fileByteArys, 24, entry);這裏又有一個知識點須要說明?你們可能會困惑,咱們這樣修改了so的頭部信息的話,在加載運行so文件的時候不會報錯嗎?這個就要看看Android底層是如何解析so文件,而後將so文件映射到內存中的了,下面咱們來看看系統是如何解析so文件的?
源代碼的位置:Android linker源碼:bionic\linker
在linker.h源碼中有一個重要的結構體soinfo,下面列出一些字段:
struct soinfo{ const char name[SOINFO_NAME_LEN]; //so全名 Elf32_Phdr *phdr; //Program header的地址 int phnum; //segment 數量 unsigned *dynamic; //指向.dynamic,在section和segment中相同的 //如下4個成員與.hash表有關 unsigned nbucket; unsigned nchain; unsigned *bucket; unsigned *chain; //這兩個成員只能會出如今可執行文件中 unsigned *preinit_array; unsigned preinit_array_count;
call_constructors -> call_array unsigned *init_array; unsigned init_array_count; void (*init_func)(void); //與init_array相似,只是在main結束以後執行 unsigned *fini_array; unsigned fini_array_count; void (*fini_func)(void); }
從上面咱們能夠知道,so中的有些信息在運行的時候是沒有用途的,有些東西是不能改的。
二、加密程序(C版)
上面說的是Java版本的,下面再來一個C版本的:
#include <stdio.h> #include <fcntl.h> #include "elf.h" #include <stdlib.h> #include <string.h> int main(int argc, char** argv){ char *encodeSoName = "libdemo.so"; char target_section[] = ".mytext"; char *shstr = NULL; char *content = NULL; Elf32_Ehdr ehdr; Elf32_Shdr shdr; int i; unsigned int base, length; unsigned short nblock; unsigned short nsize; unsigned char block_size = 16; int fd; fd = open(encodeSoName, O_RDWR); if(fd < 0){ printf("open %s failed\n", argv[1]); goto _error; } if(read(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)){ puts("Read ELF header error"); goto _error; } lseek(fd, ehdr.e_shoff + sizeof(Elf32_Shdr) * ehdr.e_shstrndx, SEEK_SET); if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){ puts("Read ELF section string table error"); goto _error; } if((shstr = (char *) malloc(shdr.sh_size)) == NULL){ puts("Malloc space for section string table failed"); goto _error; } lseek(fd, shdr.sh_offset, SEEK_SET); if(read(fd, shstr, shdr.sh_size) != shdr.sh_size){ puts("Read string table failed"); goto _error; } lseek(fd, ehdr.e_shoff, SEEK_SET); for(i = 0; i < ehdr.e_shnum; i++){ if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){ puts("Find section .text procedure failed"); goto _error; } if(strcmp(shstr + shdr.sh_name, target_section) == 0){ base = shdr.sh_offset; length = shdr.sh_size; printf("Find section %s\n", target_section); break; } } lseek(fd, base, SEEK_SET); content = (char*) malloc(length); if(content == NULL){ puts("Malloc space for content failed"); goto _error; } if(read(fd, content, length) != length){ puts("Read section .text failed"); goto _error; } nblock = length / block_size; nsize = length / 4096 + (length % 4096 == 0 ? 0 : 1); printf("base = %x, length = %x\n", base, length); printf("nblock = %d, nsize = %d\n", nblock, nsize); printf("entry:%x\n",((length << 16) + nsize)); ehdr.e_entry = (length << 16) + nsize; ehdr.e_shoff = base; for(i=0;i<length;i++){ content[i] = ~content[i]; } lseek(fd, 0, SEEK_SET); if(write(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)){ puts("Write ELFhead to .so failed"); goto _error; } lseek(fd, base, SEEK_SET); if(write(fd, content, length) != length){ puts("Write modified content to .so failed"); goto _error; } puts("Completed"); _error: free(content); free(shstr); close(fd); return 0; }
這裏就不作詳細解釋了
咱們在上面加密完成以後,咱們能夠驗證一下,使用readelf命令查看一下:
哈哈,加密成功,咱們在用IDA查看一下:
會有錯誤提示,可是咱們點擊OK,仍是成功打開了so文件,可是咱們ctrl+s查看段信息的時候:
也是沒有看到咱們的段信息,咱們能夠看一下咱們沒有加密前的效果:
既然加密成功了,那麼下面咱們得驗證一下可否運行成功
第3、Android測試demo
咱們在獲取加密以後的so文件以後,咱們用Android工程測試一下:
package com.example.shelldemo; import android.app.Activity; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.widget.TextView; public class MainActivity extends Activity { private TextView tv; private native String getString(); static{ System.loadLibrary("demo"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = (TextView) findViewById(R.id.tv); tv.setText(getString()); } }運行結果:
看到了,運行成功了。
案例下載地址:http://download.csdn.net/detail/jiangwei0910410003/9288051
一、Elf文件格式的深刻了解
二、兩個屬性的瞭解:__attribute__((constructor)); __attribute__((section (".mytext")));
三、程序的maps內存映像瞭解
四、修改內存屬性方法
五、Android系統如何解析so文件linker源碼
加密流程:
1) 從so文件頭讀取section偏移shoff、shnum和shstrtab
2) 讀取shstrtab中的字符串,存放在str空間中
3) 從shoff位置開始讀取section header, 存放在shdr
4) 經過shdr -> sh_name 在str字符串中索引,與.mytext進行字符串比較,若是不匹配,繼續讀取
5) 經過shdr -> sh_offset 和 shdr -> sh_size字段,將.mytext內容讀取並保存在content中。
6) 爲了便於理解,不使用複雜的加密算法。這裏,只將content的全部內容取反,即 *content = ~(*content);
7) 將content內容寫回so文件中
8) 爲了驗證第二節中關於section 字段能夠任意修改的結論,這裏,將shdr -> addr 寫入ELF頭e_shoff,將shdr -> sh_size 和 addr 所在內存塊寫入e_entry中,即ehdr.e_entry = (length << 16) + nsize。固然,這樣同時也簡化了解密流程,還有一個好處是:若是將so文件頭修正放回去,程序是不能運行的。
解密時,須要保證解密函數在so加載時被調用,那函數聲明爲:init_getString __attribute__((constructor))。(也可使用c++構造器實現, 其本質也是用attribute實現)
解密流程:
1) 動態連接器經過call_array調用init_getString
2) Init_getString首先調用getLibAddr方法,獲得so文件在內存中的起始地址
3) 讀取前52字節,即ELF頭。經過e_shoff得到.mytext內存加載地址,ehdr.e_entry獲取.mytext大小和所在內存塊
4) 修改.mytext所在內存塊的讀寫權限
5) 將[e_shoff, e_shoff + size]內存區域數據解密,即取反操做:*content = ~(*content);
6) 修改回內存區域的讀寫權限
(這裏是對代碼段的數據進行解密,須要寫權限。若是對數據段的數據解密,是不須要更改權限直接操做的)
這篇文章主要介紹瞭如何對so中的section進行加密,而後將咱們的native函數存到這個section中,從而達到對咱們函數的實現的加密,這樣對於後續的破解工做加大難度,可是仍是那句話,沒有絕對的安全,這種方式仍是很容易破解的,動態調試so,在init出下斷點,就能夠跟到咱們這裏的init_getString函數的實現了。關於動態調試的知識點你們不要着急,後續我會詳細講解的,因此說攻與防是永不停息的戰爭。下一篇我會繼續介紹如何對指定的函數進行加密,難度加大。。期待~~
PS: 關注微信,最新Android技術實時推送