一、前言
今天我們繼續來介紹so加固方式,在前面一篇文章中我們介紹了對so中指定的段(section)進行加密來實現對so加固
http://blog.csdn.net/jiangwei0910410003/article/details/49962173
這篇文章我們延續之前的這篇文章來介紹一下如何對函數進行加密來實現加固,當然這篇文章和前篇文章有很多類似的地方,這里就不做太多的解釋了,所以還請閱讀這篇文章之前先去了解前一篇文章。
二、技術原理
這篇和之前的那篇文章唯一的不同點就是如何找到指定的函數的偏移地址和大小
那么我們先來了解一下so中函數的表現形式:
在so文件中,每個函數的結構描述是存放在.dynsym段中的。每個函數的名稱保存在.dynstr段中的,類似於之前說過的每個section的名稱都保存在.shstrtab段中,所以在前面的文章中我們找到指定段的時候,就是通過每個段的sh_name字段到.shstrtab中尋找名字即可,而且我們知道.shstrtab這個段在頭文件中是有一個index的,就是在所有段列表中的索引值,所以很好定位.shstrtab.
但是在這篇文章我們可能遇到一個問題,就是不能按照這種方式去查找指定函數名了:

可能有的人意識到一個方法,就是我們可以通過section的type來獲取.dynsym和.dynstr。我們看到上圖中.dynsym類型是:DYNSYM,
.dynstr類型是STRTAB,但是這種方法是不行的,因為這個type不是唯一的,也就說不同的section,type可能相同,我們沒辦法區分,比如.shstrtab和.dynstr的type都是STRTAB.其實從這里我們就知道這兩個段的區別了:
.shstrtab值存儲段的名稱,.dynstr是存儲so中的所有符號名稱。
那么我們該怎么辦呢?這時候我們再去看一下elf的說明文檔:
http://download.csdn.net/detail/jiangwei0910410003/9204051
我們看到有一個.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值,然后再做一些計算就可以得到這個函數在.dynsym段中這個函數對應的條目了。關於這個hash函數,是公用的,我們在Android中的bonic/linker.c源碼中也是可以找到的:
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中呢?elf中並沒有對這個段進行數據結構的描述,有人可能想到了我們在上圖看到.hash段的type是HASH,那么我們再通過這個type來獲取?但是之前說了,這個type不是唯一的,通過他來獲取section是不靠譜的?那么我們該怎么辦呢?這時候我們就要看一下程序頭信息了:

我們知道程序頭信息是最后so被加載到內存中的映像描述,這里我們看到有一個.dynamic段。我們再看看so文件的裝載視圖和鏈接視圖:

這個我們在之前也說過,在so被加載到內存之后,就沒有section了,對應的是segment了,也就是程序頭中描述的結構,而且一個segment可以包含多個section,相同的section可以被包含到不同的segment中。.dynamic段一般用於動態鏈接的,所以.dynsym和.dynstr,.hash肯定包含在這里。我們可以解析了程序頭信息之后,通過type獲取到.dynamic程序頭信息,然后獲取到這個segment的偏移地址和大小,在進行解析成elf32_dyn結構。下面兩種圖就是程序頭的type類型和dyn結構描述,可以在elf.h中找到:
/**
* typedef struct dynamic{
Elf32_Sword d_tag;
union{
Elf32_Sword d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
*/
public static class elf32_dyn{
public byte[] d_tag = new byte[4];
public byte[] d_val = new byte[4];
public byte[] d_ptr = new byte[4];
/*public static class d_un{
public static byte[] d_val = new byte[4];
public static byte[] d_ptr = new byte[4];
}*/
@Override
public String toString(){
return "d_tag:"+Utils.bytes2HexString(d_tag)+";d_un_d_val:"+Utils.bytes2HexString(d_val)+";d_un_d_ptr:"+Utils.bytes2HexString(d_ptr);
}
}這里,需要注意的是,C語言中的union聯合體結構,所以我們在Java解析的時候需要注意,后面會詳細介紹。
這里的三個字段很好理解:
d_tag:標示,標示這個dyn是什么類型的,是.dynsym還是.dynstr等
d_val:這個section的大小
d_ptr:這個section的偏移地址
細心的同學可能會發現一個問題,就是在這里尋找.dynamic也是通過類型的,然后再找到對應的section.這種方式和之前說的通過type來尋找section,有兩個不同:
第一、在程序頭信息中,type標示.dynamic段是唯一的,所以可以通過type來進行尋找
第二、我們看到上面的鏈接視圖和裝載視圖發現,我們這種通過程序頭中的信息來查找.dysym等section靠譜點,因為當so被加載到內存中,就不存在了section了,只有segment了。
三、實現方案
編寫native程序,只是native直接返回字符串給UI。需要做的是對Java_com_example_shelldemo2_MainActivity_getString函數進行加密。加密和解密都是基於裝載視圖實現。需要注意的是,被加密函數如果用static聲明,那么函數是不會出現在.dynsym中,是無法在裝載視圖中通過函數名找到進行解密的。當然,也可以采用取巧方式,類似上節,把地址和長度信息寫入so頭中實現。Java_com_example_shelldemo2_MainActivity_getString需要被調用,那么一定是能在.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的大小。在我的測試環境下,fedora 14和windows7 Cygwin x64中elf.h定義.hash的d_tag標示是:DT_GNU_HASH;而安卓源碼中的是:DT_HASH。
4) 根據函數名稱,計算hash值
5) 根據hash值,找到下標hash % nbuckets的bucket;根據bucket中的值,讀取.dynsym中的對應索引的Elf32_Sym符號;從符號的st_name所以找到在.dynstr中對應的字符串與函數名進行比較。若不等,則根據chain[hash % nbuckets]找下一個Elf32_Sym符號,直到找到或者chain終止為止。這里敘述得有些復雜,直接上代碼。
for(i = bucket[funHash % nbucket]; i != 0; i = chain[i]){
if(strcmp(dynstr + (funSym + i)->st_name, funcName) == 0){
flag = 0;
break;
}
}6) 找到函數對應的Elf32_Sym符號后,即可根據st_value和st_size字段找到函數的位置和大小
7) 后面的步驟就和上節相同了,這里就不贅述
解密流程為加密逆過程,大體相同,只有一些細微的區別,具體如下:
1) 找到so文件在內存中的起始地址
2) 也是通過so文件頭找到Phdr;從Phdr找到PT_DYNAMIC后,需取p_vaddr和p_filesz字段,並非p_offset,這里需要注意。
3) 后續操作就加密類似,就不贅述。對內存區域數據的解密,也需要注意讀寫權限問題。
上面就介紹了完了,下面我們就可以來開始coding了。
四、代碼實現
第一、native程序
#include <jni.h>
#include <android/log.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <elf.h>
#include <sys/mman.h>
#define DEBUG
typedef struct _funcInfo{
Elf32_Addr st_value;
Elf32_Word st_size;
}funcInfo;
void init_getString() __attribute__((constructor));
static void print_debug(const char *msg){
#ifdef DEBUG
__android_log_print(ANDROID_LOG_INFO, "JNITag", "%s", msg);
#endif
}
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 unsigned int getLibAddr(){
unsigned int 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;
}
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);
// __android_log_print(ANDROID_LOG_INFO, "JNITag", "phdr = 0x%p, size = 0x%x\n", phdr, ehdr->e_phnum);
for (i = 0; i < ehdr->e_phnum; ++i) {
// __android_log_print(ANDROID_LOG_INFO, "JNITag", "phdr = 0x%p\n", phdr);
if(phdr->p_type == PT_DYNAMIC){
flag = 0;
print_debug("Find .dynamic segment");
break;
}
phdr ++;
}
if(flag)
goto _error;
dyn_vaddr = phdr->p_vaddr + base;
dyn_size = phdr->p_filesz;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "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;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find .dynsym section, addr = 0x%x\n", dyn_symtab);
}
if(dyn->d_tag == DT_HASH){
dyn_hash = (dyn->d_un).d_ptr;
flag += 2;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find .hash section, addr = 0x%x\n", dyn_hash);
}
if(dyn->d_tag == DT_STRTAB){
dyn_strtab = (dyn->d_un).d_ptr;
flag += 4;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find .dynstr section, addr = 0x%x\n", dyn_strtab);
}
if(dyn->d_tag == DT_STRSZ){
dyn_strsz = (dyn->d_un).d_val;
flag += 8;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find strsz size = 0x%x\n", dyn_strsz);
}
}
if((flag & 0x0f) != 0x0f){
print_debug("Find needed .section failed\n");
goto _error;
}
dyn_symtab += base;
dyn_hash += base;
dyn_strtab += base;
dyn_strsz += base;
funHash = elfhash(funcName);
funSym = (Elf32_Sym *) dyn_symtab;
dynstr = (char*) dyn_strtab;
nbucket = *((int *) dyn_hash);
bucket = (int *)(dyn_hash + 8);
chain = (unsigned int *)(dyn_hash + 4 * (2 + nbucket));
flag = -1;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "hash = 0x%x, nbucket = 0x%x\n", funHash, nbucket);
int mod = (funHash % nbucket);
__android_log_print(ANDROID_LOG_INFO, "JNITag", "mod = %d\n", mod);
__android_log_print(ANDROID_LOG_INFO, "JNITag", "i = 0x%d\n", bucket[mod]);
for(i = bucket[mod]; i != 0; i = chain[i]){
__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find index = %d\n", i);
if(strcmp(dynstr + (funSym + i)->st_name, funcName) == 0){
flag = 0;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "Find %s\n", funcName);
break;
}
}
if(flag) goto _error;
info->st_value = (funSym + i)->st_value;
info->st_size = (funSym + i)->st_size;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "st_value = %d, st_size = %d", info->st_value, info->st_size);
return 0;
_error:
return -1;
}
void init_getString(){
const char target_fun[] = "Java_com_example_shelldemo2_MainActivity_getString";
funcInfo info;
int i;
unsigned int npage, base = getLibAddr();
__android_log_print(ANDROID_LOG_INFO, "JNITag", "base addr = 0x%x", base);
if(getTargetFuncInfo(base, target_fun, &info) == -1){
print_debug("Find Java_com_example_shelldemo2_MainActivity_getString failed");
return ;
}
npage = info.st_size / PAGE_SIZE + ((info.st_size % PAGE_SIZE == 0) ? 0 : 1);
__android_log_print(ANDROID_LOG_INFO, "JNITag", "npage = 0x%d", npage);
__android_log_print(ANDROID_LOG_INFO, "JNITag", "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){
print_debug("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){
print_debug("mem privilege change failed");
}
}
JNIEXPORT jstring JNICALL
Java_com_example_shelldemo2_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 (*env)->NewStringUTF(env, "Native method return!");
}
這里就不想做太多解釋了,代碼邏輯和之前文章中的加密section中的代碼類似,只有在尋找函數的地方有點不同,這個也不再這里說明了,在加密的代碼中我在說明一下。
第二、加密程序
1、Java版本加密程序
private static void encodeFunc(byte[] fileByteArys){
//尋找Dynamic段的偏移值和大小
int dy_offset = 0,dy_size = 0;
for(elf32_phdr phdr : type_32.phdrList){
if(Utils.byte2Int(phdr.p_type) == ElfType32.PT_DYNAMIC){
dy_offset = Utils.byte2Int(phdr.p_offset);
dy_size = Utils.byte2Int(phdr.p_filesz);
}
}
System.out.println("dy_size:"+dy_size);
int dynSize = 8;
int size = dy_size / dynSize;
System.out.println("size:"+size);
byte[] dest = new byte[dynSize];
for(int i=0;i<size;i++){
System.arraycopy(fileByteArys, i*dynSize + dy_offset, dest, 0, dynSize);
type_32.dynList.add(parseDynamic(dest));
}
//type_32.printDynList();
byte[] symbolStr = null;
int strSize=0,strOffset=0;
int symbolOffset = 0;
int dynHashOffset = 0;
int funcIndex = 0;
int symbolSize = 16;
for(elf32_dyn dyn : type_32.dynList){
if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_HASH){
dynHashOffset = Utils.byte2Int(dyn.d_ptr);
}else if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_STRTAB){
System.out.println("strtab:"+dyn);
strOffset = Utils.byte2Int(dyn.d_ptr);
}else if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_SYMTAB){
System.out.println("systab:"+dyn);
symbolOffset = Utils.byte2Int(dyn.d_ptr);
}else if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_STRSZ){
System.out.println("strsz:"+dyn);
strSize = Utils.byte2Int(dyn.d_val);
}
}
symbolStr = Utils.copyBytes(fileByteArys, strOffset, strSize);
//打印所有的Symbol Name,注意用0來進行分割,C中的字符串都是用0做結尾的
/*String[] strAry = new String(symbolStr).split(new String(new byte[]{0}));
for(String str : strAry){
System.out.println(str);
}*/
for(elf32_dyn dyn : type_32.dynList){
if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_HASH){
//這里的邏輯有點繞
/**
* 根據hash值,找到下標hash % nbuckets的bucket;根據bucket中的值,讀取.dynsym中的對應索引的Elf32_Sym符號;
* 從符號的st_name所以找到在.dynstr中對應的字符串與函數名進行比較。若不等,則根據chain[hash % nbuckets]找下一個Elf32_Sym符號,
* 直到找到或者chain終止為止。這里敘述得有些復雜,直接上代碼。
for(i = bucket[funHash % nbucket]; i != 0; i = chain[i]){
if(strcmp(dynstr + (funSym + i)->st_name, funcName) == 0){
flag = 0;
break;
}
}
*/
int nbucket = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset, 4));
int nchian = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+4, 4));
int hash = (int)elfhash(funcName.getBytes());
hash = (hash % nbucket);
//這里的8是讀取nbucket和nchian的兩個值
funcIndex = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+hash*4 + 8, 4));
System.out.println("nbucket:"+nbucket+",hash:"+hash+",funcIndex:"+funcIndex+",chian:"+nchian);
System.out.println("sym:"+Utils.bytes2HexString(Utils.int2Byte(symbolOffset)));
System.out.println("hash:"+Utils.bytes2HexString(Utils.int2Byte(dynHashOffset)));
byte[] des = new byte[symbolSize];
System.arraycopy(fileByteArys, symbolOffset+funcIndex*symbolSize, des, 0, symbolSize);
Elf32_Sym sym = parseSymbolTable(des);
System.out.println("sym:"+sym);
boolean isFindFunc = Utils.isEqualByteAry(symbolStr, Utils.byte2Int(sym.st_name), funcName);
if(isFindFunc){
System.out.println("find func....");
return;
}
while(true){
/**
* lseek(fd, dyn_hash + 4 * (2 + nbucket + funIndex), SEEK_SET);
if(read(fd, &funIndex, 4) != 4){
puts("Read funIndex failed\n");
goto _error;
}
*/
//System.out.println("dyHash:"+Utils.bytes2HexString(Utils.int2Byte(dynHashOffset))+",nbucket:"+nbucket+",funIndex:"+funcIndex);
funcIndex = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+4*(2+nbucket+funcIndex), 4));
System.out.println("funcIndex:"+funcIndex);
System.arraycopy(fileByteArys, symbolOffset+funcIndex*symbolSize, des, 0, symbolSize);
sym = parseSymbolTable(des);
isFindFunc = Utils.isEqualByteAry(symbolStr, Utils.byte2Int(sym.st_name), funcName);
if(isFindFunc){
System.out.println("find func...");
int funcSize = Utils.byte2Int(sym.st_size);
int funcOffset = Utils.byte2Int(sym.st_value);
System.out.println("size:"+funcSize+",funcOffset:"+funcOffset);
//進行目標函數代碼部分進行加密
//這里需要注意的是從funcOffset-1的位置開始
byte[] funcAry = Utils.copyBytes(fileByteArys, funcOffset-1, funcSize);
for(int i=0;i<funcAry.length-1;i++){
funcAry[i] = (byte)(funcAry[i] ^ 0xFF);
}
Utils.replaceByteAry(fileByteArys, funcOffset-1, funcAry);
break;
}
}
break;
}
}
}這里的解密程序,需要說明一下。
1)、定位到.dynamic的segment,解析成elf32_dyn結構信息
//尋找Dynamic段的偏移值和大小
int dy_offset = 0,dy_size = 0;
for(elf32_phdr phdr : type_32.phdrList){
if(Utils.byte2Int(phdr.p_type) == ElfType32.PT_DYNAMIC){
dy_offset = Utils.byte2Int(phdr.p_offset);
dy_size = Utils.byte2Int(phdr.p_filesz);
}
}
System.out.println("dy_size:"+dy_size);
int dynSize = 8;
int size = dy_size / dynSize;
System.out.println("size:"+size);
byte[] dest = new byte[dynSize];
for(int i=0;i<size;i++){
System.arraycopy(fileByteArys, i*dynSize + dy_offset, dest, 0, dynSize);
type_32.dynList.add(parseDynamic(dest));
}這里有一個解析elf32_dyn結構:
private static elf32_dyn parseDynamic(byte[] src){
elf32_dyn dyn = new elf32_dyn();
dyn.d_tag = Utils.copyBytes(src, 0, 4);
dyn.d_ptr = Utils.copyBytes(src, 4, 4);
dyn.d_val = Utils.copyBytes(src, 4, 4);
return dyn;
}這里需要注意的是,elf32_dyn中用到了聯合體union結構,Java中是不存在這個類型的,所以我們需要了解這個聯合體的含義,這里雖然是三個字段,但是大小是8個字節,而不是12字節,這個需要注意的。dyn.d_val和dyn.d_val是在一個聯合體中的。
2)、計算目標函數的hash值,得到函數的偏移值和大小
for(elf32_dyn dyn : type_32.dynList){
if(Utils.byte2Int(dyn.d_tag) == ElfType32.DT_HASH){
//這里的邏輯有點繞
/**
* 根據hash值,找到下標hash % nbuckets的bucket;根據bucket中的值,讀取.dynsym中的對應索引的Elf32_Sym符號;
* 從符號的st_name所以找到在.dynstr中對應的字符串與函數名進行比較。若不等,則根據chain[hash % nbuckets]找下一個Elf32_Sym符號,
* 直到找到或者chain終止為止。這里敘述得有些復雜,直接上代碼。
for(i = bucket[funHash % nbucket]; i != 0; i = chain[i]){
if(strcmp(dynstr + (funSym + i)->st_name, funcName) == 0){
flag = 0;
break;
}
}
*/
int nbucket = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset, 4));
int nchian = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+4, 4));
int hash = (int)elfhash(funcName.getBytes());
hash = (hash % nbucket);
//這里的8是讀取nbucket和nchian的兩個值
funcIndex = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+hash*4 + 8, 4));
System.out.println("nbucket:"+nbucket+",hash:"+hash+",funcIndex:"+funcIndex+",chian:"+nchian);
System.out.println("sym:"+Utils.bytes2HexString(Utils.int2Byte(symbolOffset)));
System.out.println("hash:"+Utils.bytes2HexString(Utils.int2Byte(dynHashOffset)));
byte[] des = new byte[symbolSize];
System.arraycopy(fileByteArys, symbolOffset+funcIndex*symbolSize, des, 0, symbolSize);
Elf32_Sym sym = parseSymbolTable(des);
System.out.println("sym:"+sym);
boolean isFindFunc = Utils.isEqualByteAry(symbolStr, Utils.byte2Int(sym.st_name), funcName);
if(isFindFunc){
System.out.println("find func....");
return;
}
while(true){
/**
* lseek(fd, dyn_hash + 4 * (2 + nbucket + funIndex), SEEK_SET);
if(read(fd, &funIndex, 4) != 4){
puts("Read funIndex failed\n");
goto _error;
}
*/
//System.out.println("dyHash:"+Utils.bytes2HexString(Utils.int2Byte(dynHashOffset))+",nbucket:"+nbucket+",funIndex:"+funcIndex);
funcIndex = Utils.byte2Int(Utils.copyBytes(fileByteArys, dynHashOffset+4*(2+nbucket+funcIndex), 4));
System.out.println("funcIndex:"+funcIndex);
System.arraycopy(fileByteArys, symbolOffset+funcIndex*symbolSize, des, 0, symbolSize);
sym = parseSymbolTable(des);
isFindFunc = Utils.isEqualByteAry(symbolStr, Utils.byte2Int(sym.st_name), funcName);
if(isFindFunc){
System.out.println("find func...");
int funcSize = Utils.byte2Int(sym.st_size);
int funcOffset = Utils.byte2Int(sym.st_value);
System.out.println("size:"+funcSize+",funcOffset:"+funcOffset);
//進行目標函數代碼部分進行加密
//這里需要注意的是從funcOffset-1的位置開始
byte[] funcAry = Utils.copyBytes(fileByteArys, funcOffset-1, funcSize);
for(int i=0;i<funcAry.length-1;i++){
funcAry[i] = (byte)(funcAry[i] ^ 0xFF);
}
Utils.replaceByteAry(fileByteArys, funcOffset-1, funcAry);
break;
}
}
break;
}
}這里的尋找邏輯有點饒人,但是我們知道了解原理即可:

結合上面的這張圖就可以理解了。其中nbucket和nchain,bucket[i]和chain[i]都是4個字節。他們的值就是目標函數在.dynsym中的位置。
2、C版加密程序
#include <stdio.h>
#include <fcntl.h>
#include "elf.h"
#include <stdlib.h>
#include <string.h>
typedef struct _funcInfo{
Elf32_Addr st_value;
Elf32_Word st_size;
}funcInfo;
Elf32_Ehdr ehdr;
//For Test
static void print_all(char *str, int len){
int i;
for(i=0;i<len;i++)
{
if(str[i] == 0)
puts("");
else
printf("%c", str[i]);
}
}
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 Elf32_Off findTargetSectionAddr(const int fd, const char *secName){
Elf32_Shdr shdr;
char *shstr = NULL;
int i;
lseek(fd, 0, SEEK_SET);
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(shstr);
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, secName) == 0){
printf("Find section %s, addr = 0x%x\n", secName, shdr.sh_offset);
break;
}
}
free(shstr);
return shdr.sh_offset;
_error:
return -1;
}
static char getTargetFuncInfo(int fd, const char *funcName, funcInfo *info){
char flag = -1, *dynstr;
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;
lseek(fd, ehdr.e_phoff, SEEK_SET);
for(i=0;i < ehdr.e_phnum; i++){
if(read(fd, &phdr, sizeof(Elf32_Phdr)) != sizeof(Elf32_Phdr)){
puts("Read segment failed");
goto _error;
}
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;
}
}
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);
lseek(fd, dyn_off, SEEK_SET);
for(i=0;i < dyn_size / sizeof(Elf32_Dyn); i++){
int sizes = read(fd, &dyn, sizeof(Elf32_Dyn));
if(sizes != sizeof(Elf32_Dyn)){
puts("Read .dynamic information failed");
//goto _error;
break;
}
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);
}
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);
}
}
if((flag & 0x0f) != 0x0f){
puts("Find needed .section failed\n");
goto _error;
}
dynstr = (char*) malloc(dyn_strsz);
if(dynstr == NULL){
puts("Malloc .dynstr space failed");
goto _error;
}
lseek(fd, dyn_strtab, SEEK_SET);
if(read(fd, dynstr, dyn_strsz) != dyn_strsz){
puts("Read .dynstr failed");
goto _error;
}
funHash = elfhash(funcName);
printf("Function %s hashVal = 0x%x\n", funcName, funHash);
lseek(fd, dyn_hash, SEEK_SET);
if(read(fd, &nbucket, 4) != 4){
puts("Read hash nbucket failed\n");
goto _error;
}
printf("nbucket = %d\n", nbucket);
if(read(fd, &nchain, 4) != 4){
puts("Read hash nchain failed\n");
goto _error;
}
printf("nchain = %d\n", nchain);
funHash = funHash % nbucket;
printf("funHash mod nbucket = %d \n", funHash);
lseek(fd, funHash * 4, SEEK_CUR);
if(read(fd, &funIndex, 4) != 4){
puts("Read funIndex failed\n");
goto _error;
}
printf("funcIndex:%d\n", funIndex);
lseek(fd, dyn_symtab + funIndex * sizeof(Elf32_Sym), SEEK_SET);
if(read(fd, &funSym, sizeof(Elf32_Sym)) != sizeof(Elf32_Sym)){
puts("Read funSym failed");
goto _error;
}
if(strcmp(dynstr + funSym.st_name, funcName) != 0){
while(1){
printf("hash:%x,nbucket:%d,funIndex:%d\n",dyn_hash,nbucket,funIndex);
lseek(fd, dyn_hash + 4 * (2 + nbucket + funIndex), SEEK_SET);
if(read(fd, &funIndex, 4) != 4){
puts("Read funIndex failed\n");
goto _error;
}
printf("funcIndex:%d\n", funIndex);
if(funIndex == 0){
puts("Cannot find funtion!\n");
goto _error;
}
lseek(fd, dyn_symtab + funIndex * sizeof(Elf32_Sym), SEEK_SET);
if(read(fd, &funSym, sizeof(Elf32_Sym)) != sizeof(Elf32_Sym)){
puts("In FOR loop, Read funSym failed");
goto _error;
}
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;
}
int main(int argc, char **argv){
char secName[] = ".text";
char funcName[] = "Java_com_example_shelldemo2_MainActivity_getString";
char *soName = "libdemo.so";
char *content = NULL;
int fd, i;
Elf32_Off secOff;
funcInfo info;
unsigned a = elfhash(funcName);
printf("a:%d\n", a);
fd = open(soName, O_RDWR);
if(fd < 0){
printf("open %s failed\n", argv[1]);
goto _error;
}
secOff = findTargetSectionAddr(fd, secName);
if(secOff == -1){
printf("Find section %s failed\n", secName);
goto _error;
}
if(getTargetFuncInfo(fd, funcName, &info) == -1){
printf("Find function %s failed\n", funcName);
goto _error;
}
content = (char*) malloc(info.st_size);
if(content == NULL){
puts("Malloc space failed");
goto _error;
}
lseek(fd, info.st_value - 1, SEEK_SET);
if(read(fd, content, info.st_size) != info.st_size){
puts("Malloc space failed");
goto _error;
}
for(i=0;i<info.st_size -1;i++){
content[i] = ~content[i];
}
lseek(fd, info.st_value-1, SEEK_SET);
if(write(fd, content, info.st_size) != info.st_size){
puts("Write modified content to .so failed");
goto _error;
}
puts("Complete!");
_error:
free(content);
close(fd);
return 0;
}這里就不做介紹了。
上面對so中的函數加密成功了,那么下面我們來驗證加密,我們使用IDA進行查看:


看到我們加密的函數內容已經面目全非了,看不到信息了。
比較加密前的:


哈哈,加密成功了~~
案例下載地址:http://download.csdn.net/detail/jiangwei0910410003/9289009
第三、測試Android項目
我們用加密之后的so文件來測試一下:
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());
}
}
運行結果:
運行成功啦。
五、總結
這篇文章是延續之前的加密section文章繼續講述了加密函數來實現so加固,這個和加密section唯一的區別就是如何找到加密函數的偏移地址和大小,其他都是類似的,那么對於so加固的知識點就用這兩篇文章來介紹了,當然這兩種方式都是有缺點的,就是如果我們在init_getString函數下斷點,然后動態調試一下,就可以很輕易的破解了,而且通過dump出內存中運行的dex也是可以做到的,所以,沒有絕對的安全,只有相對的攻防~~
PS: 關注微信,最新Android技術實時推送
