基于對so中的section加密技術實現(xiàn)so加固

一、前言

so文件格式詳解以及如何解析一個so文件。這個是我們今天這篇文章的基礎,如果不了解so文件的格式的話,下面的知識點可能會看的很費勁。

二、技術原理

加密:找到一個section的base和size就可以對這段section進行加密了
解密:因為我們對section進行加密之后,肯定需要解密的,不然的話,運行肯定是報錯的,那么這里的重點是什么時候去進行解密,對于一個so文件,我們load進程序之后,在運行程序之前我們可以從哪個時間點來突破?這里就需要一個知識點:attribute((constructor));

關于這個,屬性的用法這里就不做介紹了,網上有相關資料,他的作用很簡單,就是優(yōu)先于main方法之前執(zhí)行,類似于Java中的構造函數(shù),當然其實C++中的構造函數(shù)就是基于這個屬性實現(xiàn)的,我們在之前介紹elf文件格式的時候,有兩個section會引起我們的注意:

對于這兩個section,其實就是用這個屬性實現(xiàn)的函數(shù)存在這里,在動態(tài)鏈接器構造了進程映像,并執(zhí)行了重定位以后,每個共享的目標都獲得執(zhí)行 某些初始化代碼的機會。這些初始化函數(shù)的被調用順序是不一定的,不過所有共享目標 初始化都會在可執(zhí)行文件得到控制之前發(fā)生。
類似地,共享目標也包含終止函數(shù),這些函數(shù)在進程完成終止動作序列時,通過 atexit() 機制執(zhí)行。動態(tài)鏈接器對終止函數(shù)的調用順序是不確定的。
共享目標通過動態(tài)結構中的 DT_INIT 和 DT_FINI 條目指定初始化/終止函數(shù)。通常這些代碼放在.init 和.fini 節(jié)區(qū)中。

這個知識點很重要,我們后面在進行動態(tài)調試so的時候,還會用到這個知識點,所以一定要理解。

所以,在這里我們找到了解密的時機,就是自己定義一個解密函數(shù),然后用上面的這個屬性聲明就可以了。

三、實現(xiàn)流程

第一、我們編寫一個簡單的native代碼,這里我們需要做兩件事:

1、將我們核心的native函數(shù)定義在自己的一個section中,這里會用到這個屬性:attribute((section (".mytext")));

其中.mytext就是我們自己定義的section.

2、需要編寫我們的解密函數(shù),用屬性: attribute((constructor));聲明這樣一個native程序就包含這兩個重要的函數(shù),使用ndk編譯成so文件

第二、編寫加密程序,在加密程序中我們需要做的是:

1、通過解析so文件,找到.mytext段的起始地址和大小,這里的思路是:

找到所有的Section,然后獲取他的name字段,在結合String Section,遍歷找到.mytext字段

2、找到.mytext段之后,然后進行加密,最后在寫入到文件中。

四、技術實現(xiàn)

前面介紹了原理和實現(xiàn)方案,下面就開始coding吧,

第一、我們先來看看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);  
}  

下面來分析一下代碼:

1、定義自己的段

jstring getString(JNIEnv*) __attribute__((section (".mytext")));  
jstring getString(JNIEnv* env){  
    return (*env)->NewStringUTF(env, "Native method return!");  
}; 

這里的getString返回一個字符串,提供給Android上層,然后將getString定義在.mytext段中。

2、獲取so加載到內存中的起始地址

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然后進行解密。

3、解密函數(shù)

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占用的頁數(shù)
然后修改這個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");  
}  

這里調用了一個系統(tǒng)函數(shù):mprotect
第一個參數(shù):需要修改內存的起始地址
必須需要頁面對齊,也就是必須是頁面PAGE_SIZE(0x1000=4096)的整數(shù)倍

第二個參數(shù):需要修改的大小
占用的頁數(shù)*PAGE_SIZE

第三個參數(shù):權限值
最后讀取內存中的section內容,然后進行解密,在將內存權限修改回去。

然后使用ndk編譯成so即可,這里我們用到了系統(tǒng)的打印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)  

第二、加密程序

1、加密程序(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段然后進行數(shù)據(jù)加密  
                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的數(shù)目,但是我們仔細觀察Section中的Type=DYNSYM段的信息可以得到,這個段的大小和偏移地址,而SymbolTable的結構大小是固定的16個字節(jié) 
        //那么這里的數(shù)目=大小/結構大小 
        //首先在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的數(shù)目,但是我們仔細觀察Section中的Type=STRTAB段的信息,可以得到,這個段的大小和偏移地址,但是我們這時候我們不知道字符串的大小,所以就獲取不到數(shù)目了 
        //這里我們可以查看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]); 
        } 
        //上面的那個方法不好,我們發(fā)現(xiàn)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);//魔數(shù)  
        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個字節(jié)  
        int header_count = Utils.byte2Short(type_32.hdr.e_phnum);//頭部的個數(shù)  
        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個字節(jié)  
        int header_count = Utils.byte2Short(type_32.hdr.e_shnum);//頭部的個數(shù)  
        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個字節(jié)  
        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段然后進行數(shù)據(jù)加密  
            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文件映射到內存中的了,下面我們來看看系統(tǒng)是如何解析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 數(shù)量  
unsigned *dynamic; //指向.dynamic,在section和segment中相同的  
//以下4個成員與.hash表有關  
unsigned nbucket;  
unsigned nchain;  
unsigned *bucket;  
unsigned *chain;  
//這兩個成員只能會出現(xiàn)在可執(zhí)行文件中  
unsigned *preinit_array;  
unsigned preinit_array_count;  

指向初始化代碼,先于main函數(shù)之行,即在加載時被linker所調用,在linker.c可以看到:__linker_init -> link_image ->

call_constructors -> call_array  
unsigned *init_array;  
unsigned init_array_count;  
void (*init_func)(void);  
//與init_array類似,只是在main結束之后執(zhí)行  
unsigned *fini_array;  
unsigned fini_array_count;  
void (*fini_func)(void);  
}  

另外,linker.c中也有許多地方可以佐證。其本質還是linker是基于裝載視圖解析的so文件的。
基于上面的結論,再來分析下ELF頭的字段。

  1. e_ident[EI_NIDENT] 字段包含魔數(shù)、字節(jié)序、字長和版本,后面填充0。對于安卓的linker,通過verify_elf_object函數(shù)檢驗魔數(shù),判定是否為.so文件。那么,我們可以向位置寫入數(shù)據(jù),至少可以向后面的0填充位置寫入數(shù)據(jù)。遺憾的是,我在fedora 14下測試,是不能向0填充位置寫數(shù)據(jù),鏈接器報非0填充錯誤。
  2. 對于安卓的linker,對e_type、e_machine、e_version和e_flags字段并不關心,是可以修改成其他數(shù)據(jù)的(僅分析,沒有實測)
  3. 對于動態(tài)鏈接庫,e_entry 入口地址是無意義的,因為程序被加載時,設定的跳轉地址是動態(tài)連接器的地址,這個字段是可以被作為數(shù)據(jù)填充的。
  4. so裝載時,與鏈接視圖沒有關系,即e_shoff、e_shentsize、e_shnum和e_shstrndx這些字段是可以任意修改的。被修改之后,使用readelf和ida等工具打開,會報各種錯誤,相信讀者已經見識過了。
  5. 既然so裝載與裝載視圖緊密相關,自然e_phoff、e_phentsize和e_phnum這些字段是不能動的。
    從上面我們可以知道,so中的有些信息在運行的時候是沒有用途的,有些東西是不能改的。

2、加密程序(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查看段信息的時候:

也是沒有看到我們的段信息,我們可以看一下我們沒有加密前的效果:

既然加密成功了,那么下面我們得驗證一下能否運行成功

第三、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());  
    }  
}  

運行成功了

五、技術總結

1、Elf文件格式的深入了解
2、兩個屬性的了解:attribute((constructor)); attribute((section (".mytext")));
3、程序的maps內存映像了解
4、修改內存屬性方法
5、Android系統(tǒng)如何解析so文件linker源碼

六、梳理流程步驟

加密流程:

  1. 從so文件頭讀取section偏移shoff、shnum和shstrtab
  2. 讀取shstrtab中的字符串,存放在str空間中
  3. 從shoff位置開始讀取section header, 存放在shdr
  4. 通過shdr -> sh_name 在str字符串中索引,與.mytext進行字符串比較,如果不匹配,繼續(xù)讀取
  5. 通過shdr -> sh_offset 和 shdr -> sh_size字段,將.mytext內容讀取并保存在content中。
  6. 為了便于理解,不使用復雜的加密算法。這里,只將content的所有內容取反,即 content = ~(content);
  7. 將content內容寫回so文件中
  8. 為了驗證第二節(jié)中關于section 字段可以任意修改的結論,這里,將shdr -> addr 寫入ELF頭e_shoff,將shdr -> sh_size 和 addr 所在內存塊寫入e_entry中,即ehdr.e_entry = (length << 16) + nsize。當然,這樣同時也簡化了解密流程,還有一個好處是:如果將so文件頭修正放回去,程序是不能運行的。

解密時,需要保證解密函數(shù)在so加載時被調用,那函數(shù)聲明為:init_getString attribute((constructor))。(也可以使用c++構造器實現(xiàn), 其本質也是用attribute實現(xiàn))

解密流程:

  1. 動態(tài)鏈接器通過call_array調用init_getString
  2. Init_getString首先調用getLibAddr方法,得到so文件在內存中的起始地址
  3. 讀取前52字節(jié),即ELF頭。通過e_shoff獲得.mytext內存加載地址,ehdr.e_entry獲取.mytext大小和所在內存塊
  4. 修改.mytext所在內存塊的讀寫權限
  5. 將[e_shoff, e_shoff + size]內存區(qū)域數(shù)據(jù)解密,即取反操作:content = ~(content);
  6. 修改回內存區(qū)域的讀寫權限
    (這里是對代碼段的數(shù)據(jù)進行解密,需要寫權限。如果對數(shù)據(jù)段的數(shù)據(jù)解密,是不需要更改權限直接操作的)

六、總結

這篇文章主要介紹了如何對so中的section進行加密,然后將我們的native函數(shù)存到這個section中,從而達到對我們函數(shù)的實現(xiàn)的加密,這樣對于后續(xù)的破解工作加大難度,但是還是那句話,沒有絕對的安全,這種方式還是很容易破解的,動態(tài)調試so,在init出下斷點,就可以跟到我們這里的init_getString函數(shù)的實現(xiàn)了。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 本文參考自:Android逆向之旅—基于對so中的section加密技術實現(xiàn)so加固,增加了自己的實踐過程,以及一...
    difcareer閱讀 6,191評論 6 4
  • 1. 背景 在我們的日常工作中經常會遇到一些BUG,而且這些BUG發(fā)生在native層,也就是在我們的so共...
    2baf611355d8閱讀 17,495評論 3 41
  • SO文件中函數(shù)的加密和解密 簡介 原理上來說,找到so文件函數(shù)的位置,對其二進制進行一定加密操作后即加密了,解密也...
    jackzhoud閱讀 8,102評論 0 4
  • 久違的晴天,家長會。 家長大會開好到教室時,離放學已經沒多少時間了。班主任說已經安排了三個家長分享經驗。 放學鈴聲...
    飄雪兒5閱讀 7,868評論 16 22
  • 今天感恩節(jié)哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開了第一次的黨會,身份的轉變要...
    余生動聽閱讀 10,916評論 0 11

友情鏈接更多精彩內容