Android逆向之旅---基於對so中的section加密技術實現so加固
一、前言
好長時間沒有更新文章了,主要還是工作上的事,連續加班一個月,沒有時間研究了,只有週末有時間,來看一下,不過我還是延續之前的文章,繼續我們的逆向之旅,今天我們要來看一下如何通過對so加密,在介紹本篇文章之前的話,一定要先閱讀之前的文章:
so檔案格式詳解以及如何解析一個so檔案
http://blog.csdn.net/jiangwei0910410003/article/details/49336613
這個是我們今天這篇文章的基礎,如果不瞭解so檔案的格式的話,下面的知識點可能會看的很費勁
下面就來介紹我們今天的話題:對so中的section進行加密
二、技術原理
加密:在之前的文章中我們介紹了so中的格式,那麼對於找到一個section的base和size就可以對這段section進行加密了
解密:因為我們對section進行加密之後,肯定需要解密的,不然的話,執行肯定是報錯的,那麼這裡的重點是什麼時候去進行解密,對於一個so檔案,我們load程式序之後,在執行程式之前我們可以從哪個時間點來突破?這裡就需要一個知識點:
__attribute__((constructor));
關於這個,屬性的用法這裡就不做介紹了,網上有相關資料,他的作用很簡單,就是優先於main方法之前執行,類似於Java中的建構函式,當然其實C++中的建構函式就是基於這個屬性實現的,我們在之前介紹elf檔案格式的時候,有兩個section會引起我們的注意:
對於這兩個section,其實就是用這個屬性實現的函式存在這裡,
在動態連結器構造了程式映像,並執行了重定位以後,每個共享的目標都獲得執行 某些初始化程式碼的機會。這些初始化函式的被呼叫順序是不一定的,不過所有共享目標 初始化都會在可執行檔案得到控制之前發生。
類似地,共享目標也包含終止函式,這些函式在程式完成終止動作序列時,通過 atexit() 機制執行。動態連結器對終止函式的呼叫順序是不確定的。
共享目標通過動態結構中的 DT_INIT 和 DT_FINI 條目指定初始化/終止函式。通常 這些程式碼放在.init 和.fini 節區中。
這個知識點很重要,我們後面在進行動態除錯so的時候,還會用到這個知識點,所以一定要理解。
所以,在這裡我們找到了解密的時機,就是自己定義一個解密函式,然後用上面的這個屬性宣告就可以了。
三、實現流程
第一、我們編寫一個簡單的native程式碼,這裡我們需要做兩件事:
1、將我們核心的native函式定義在自己的一個section中,這裡會用到這個屬性:__attribute__((section (".mytext")));
其中.mytext就是我們自己定義的section.
說到這裡,還記得我們之前介紹的一篇文章中介紹了,動態的給so新增一個section:
http://blog.csdn.net/jiangwei0910410003/article/details/49361281
2、需要編寫我們的解密函式,用屬性: __attribute__((constructor));宣告
這樣一個native程式就包含這兩個重要的函式,使用ndk編譯成so檔案
第二、編寫加密程式,在加密程式中我們需要做的是:
1、通過解析so檔案,找到.mytext段的起始地址和大小,這裡的思路是:
找到所有的Section,然後獲取他的name欄位,在結合String Section,遍歷找到.mytext欄位
2、找到.mytext段之後,然後進行加密,最後在寫入到檔案中。
四、技術實現
前面介紹了原理和實現方案,下面就開始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、解密函式
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
第二、加密程式
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段然後進行資料加密
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;
指向初始化程式碼,先於main函式之行,即在載入時被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結束之後執行
unsigned *fini_array;
unsigned fini_array_count;
void (*fini_func)(void);
}
另外,linker.c中也有許多地方可以佐證。其本質還是linker是基於裝載檢視解析的so檔案的。
基於上面的結論,再來分析下ELF頭的欄位。
1) e_ident[EI_NIDENT] 欄位包含魔數、位元組序、字長和版本,後面填充0。對於安卓的linker,通過verify_elf_object函式檢驗魔數,判定是否為.so檔案。那麼,我們可以向位置寫入資料,至少可以向後面的0填充位置寫入資料。遺憾的是,我在fedora 14下測試,是不能向0填充位置寫資料,連結器報非0填充錯誤。
2) 對於安卓的linker,對e_type、e_machine、e_version和e_flags欄位並不關心,是可以修改成其他資料的(僅分析,沒有實測)
3) 對於動態連結庫,e_entry 入口地址是無意義的,因為程式被載入時,設定的跳轉地址是動態聯結器的地址,這個欄位是可以被作為資料填充的。
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());
}
}
執行結果:
看到了,執行成功了。
案例下載地址:http://download.csdn.net/detail/jiangwei0910410003/9288051
五、技術總結
1、Elf檔案格式的深入瞭解
2、兩個屬性的瞭解:__attribute__((constructor)); __attribute__((section (".mytext")));
3、程式的maps記憶體映像瞭解
4、修改記憶體屬性方法
5、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函式的實現了。關於動態除錯的知識點大家不要著急,後續我會詳細講解的,所以說攻與防是永不停息的戰爭。下一篇我會繼續介紹如何對指定的函式進行加密,難度加大。。期待~~
相關文章
- Android對so體積優化的探索與實踐Android優化
- Android 關於 so 檔案的總結Android
- 某當網apk加固脫個soAPK
- 知物由學 | SO加固如何提升Android應用的安全性?Android
- Android Studio NDK:三、打包SOAndroid
- FFmpeg編譯Android使用的so庫編譯Android
- so easy 前端實現多語言前端
- Android JNI實現Java與C/C++互相呼叫,以及so庫的生成和呼叫(JNI方式呼叫美圖秀秀so)AndroidJavaC++
- Android so注入(inject)和Hook技術學習(二)——GAndroidHook
- Android 的 so 檔案載入機制Android
- Nginx+lua 實現呼叫.so檔案Nginx
- 為什麼說SO加固+無原始碼VMP是最佳的Android手遊安全保護方案?原始碼Android
- Android-ffmpeg編譯so檔案Android編譯
- so包
- Android native層動態載入so庫Android
- 鴻蒙手機版JNI實戰(JNI開發、SO庫生成、SO庫使用)鴻蒙
- Android so庫防客戶端破解的解決方案Android客戶端
- Android下檢視SO庫被依賴的情況Android
- 這個輪子讓SpringBoot實現api加密So EasySpring BootAPI加密
- Android逆向之旅--免Root實現微信訊息同步原理解析Android
- 搭建fast-whisper 環境時報錯 Unable to load any of {libcudnn_ops.so.9.1.0, libcudnn_ops.so.9.1, libcudnn_ops.so.9, libcudnn_ops.so}ASTDNN
- so-vits-svc實現歌聲轉換的一些提醒
- Android應用加固的簡單實現方案Android
- Is programming an Operating System so hard?
- 如何編譯openGauss對應版本的wal2json.so編譯JSON
- 現代加密技術加密
- Android中基於HTTP的網路技術AndroidHTTP
- 基於ARouter的Android元件化實現Android元件化
- Android基於MediaBroswerService的App實現概述AndroidROSAPP
- Android應用加固的簡單實現方案(二)Android
- HarmonyOS Next智慧家居系統安全加固:加解密技術的深度應用解密
- pdo_pgsql.so安裝SQL
- lua——alien庫實現lua呼叫C動態連結庫(dll、so)
- 基於CC的Android MVVM 元件化實現AndroidMVVM元件化
- 如何解出陣列中最大的子序列?花一分即可瞭解原理,just so so陣列
- JNI初步(五)jni ndk 一個.so檔案依賴另一個.so檔案的寫法
- Sublime text找不到.so檔案
- Linux 誤刪libc.so.6Linux
- 工具軟體便利世界 so be a builderUI