本文博客地址:http://blog.csdn.net/qq1084283172/article/details/78077603
Android應用的so庫文件的加固一直存在,也比較常見,特地花時間整理了一下Android so庫文件加固方面的知識。本篇文章主要是對看雪論壇《簡單的so脫殼器》這篇文章的思路和代碼的分析,很久之前就閱讀過這篇文章但是一直沒有時間來詳細分析它的代碼,最近比較有空來分析一下這篇文章中提到的Android so脫殼器的代碼udog,github下載地址爲:https://github.com/devilogic/udog,Android so加固的一般手法就是去除掉外殼Android so庫文件的 ELF 鏈接視圖 相關的信息,例如區節頭表的偏移、區節頭表的項數、區節頭表名稱字符串表的序號等,這樣處理以後將Android so加固的外殼so庫文件拖到IDA中去分析的時候,直接提示區節頭表無效的錯誤,IDA工具不能對Android so加固的外殼so庫文件進行分析,達到抗IDA工具靜態分析的目的。
Android so加固中被保護的Android so庫文件是由外殼Android so庫文件在.init段或者.init_array段的構造函數裏自定linker進行內存加載和解密的,被保護的Android so庫文件自定義內存加載、映射完成以後將外殼Android so庫文件的soinfo*(dlopen函數返回的)修改爲被保護Android so庫文件的soinfo*,這樣被保護的Android so庫文件的內存加載就成功了並且就可以被調用了。儘管被加固保護的Android so庫文件被加密保護起來了,但是該Android so庫文件還是會在內存中進行解密出來,因此我們可以在被加固保護的Android so庫文件內存解密時進行內存dump處理,然後對dump出來的Android so庫文件進行ELF文件格式的調整和修復以及section區節頭表的重建,就可以實現被保護的Android so庫文件的脫殼了。
簡單so脫殼器這篇文章中提到的so脫殼器udog的代碼比較簡單,之前以爲udog的代碼比較複雜,後來整理了一下作者玩命的代碼發現很多代碼都是廢棄的,核心關鍵的代碼部分不是很複雜但是對於學習Android so加固的脫殼很有作用,也是Android so加固脫殼和內存dump後修復的第一步,玩命版主主要實現了被加固Android so庫文件的內存dump和ELF文件格式部分參數的修復處理,對於脫殼後ELF文件的section區節頭表等的重建並沒有實現。
udog代碼的入口 main函數 在linker.cpp文件中如下圖所示:
在linker.cpp文件中,main函數工作流程是: 先對用戶輸入的命令行參數進行解析處理得到 用戶參數解析結果描述結構體options_t,然後根據用戶輸入的命令參數解析的結果options_t 進行Android so的脫殼相關的操作。udog脫殼器中幫助命令行在文件options.cpp中實現,如下圖所示:
用戶輸入的命令行參數解析結果保存結構體options_t中,如下圖代碼所示:
// 保存用戶輸入命令行參數的解析結果
struct options_t {
bool call_dt_init;
bool call_dt_init_array;
bool call_dt_finit;
bool call_dt_finit_array;
bool load_pre_libs;
bool load_needed_libs;
bool load;
bool not_relocal; /* 不對重定位表進行修復 */
bool make_sectabs; /* 重建elf文件的區節頭表 */
bool dump;
bool help;
bool version;
bool debug;
bool check;
bool clear_entry;
int debuglevel;
unsigned xct_offset;
unsigned xct_size;
char dump_file[128];
char target_file[128];
};
對用戶輸入的命令行參數進行解析處理的操作在函數handle_arguments中實現,如下圖代碼所示:
// main函數在linker.cpp類
int main(int argc, char* argv[]) {
// 解析用戶輸入的命令行參數
g_opts = handle_arguments(argc, argv);
// 解析用戶輸入的命令行參數
struct options_t* handle_arguments(int argc, char* argv[]) {
// 保存命令行參數解析後的結果
static struct options_t opts;
// 清零
memset(&opts, 0, sizeof(opts));
// 默認參數的設置
opts.call_dt_init = true;
opts.call_dt_init_array = true;
opts.call_dt_finit = true;
opts.call_dt_finit_array = true;
opts.load_pre_libs = true;
opts.load_needed_libs = true;
int opt;
int longidx;
int dump = 0, help = 0, version = 0,
debug = 0, check = 0, xcto = 0,
xcts = 0, clear_entry = 0;
if (argc == 1) {
return NULL;
}
// 輸入參數選項的解析順序和規則
// 該數據結構包括了所有要定義的短選項,每一個選項都只用單個字母表示。
// 如果該選項需要參數,則其後跟一個冒號
const char* short_opts = ":hvcd:";
// 解析參數的長選項
struct option long_opts[] = {
// 1--選項需要參數
{ "dump", 1, &dump, 1 },
// 0--選項無參數
{ "help", 0, &help, 2 },
{ "version", 0, &version, 3 },
{ "debug", 1, &debug, 4 },
{ "check", 0, &check, 5 },
{ "xcto", 1, &xcto, 6 },
{ "xcts", 1, &xcts, 7 },
{ "clear-entry",0, &clear_entry, 8 },
// 2--選項參數可選
{ 0, 0, 0, 0 }
};
// 對輸入的命令行參數進行解析,longidx爲解析參數長選項中的序號數
// 參考:https://baike.baidu.com/item/getopt_long/5634851?fr=aladdin
// 參考:http://blog.csdn.net/ast_224/article/details/3861625
while ((opt = getopt_long(argc, argv, short_opts, long_opts, &longidx)) != -1) {
switch (opt) {
case 0:
// 進行so庫文件的dump處理
if (dump == 1) {
opts.dump = true;
// 暫時不對dump的so庫文件的重定位表進行修復
opts.not_relocal = false;
// 對dump的so庫文件的區節頭表進行重建
opts.make_sectabs = true;
// 當處理一個帶參數的選項時,全局變量optarg會指向它的參數
// optarg爲目標so庫文件dump後的文件保存路徑
strcpy(opts.dump_file, optarg);
// 加載dump的so庫文件
opts.load = true;
dump = 0;
} else if (help == 2) {
opts.help = true;
help = 0;
} else if (version == 3) {
opts.version = true;
version = 0;
} else if (debug == 4) {
opts.debug = true;
opts.debuglevel = atoi(optarg);
debug = 0;
} else if (check == 5) {
opts.check = true;
check = 0;
} else if (xcto == 6) {
opts.xct_offset = strtol(optarg, NULL, 16);
xcto = 0;
} else if (xcts == 7) {
opts.xct_size = strtol(optarg, NULL, 16);
xcts = 0;
} else if (clear_entry == 8) {
opts.clear_entry = true;
clear_entry = 0;
} else {
//printf("unknow options: %c\n", optopt);
return NULL;
}
break;
case 'c':
opts.check = true;
break;
case 'h':
opts.help = true;
break;
case 'v':
opts.version = true;
break;
case 'd':
opts.dump = true;
opts.not_relocal = false;
opts.make_sectabs = true;
strcpy(opts.dump_file, optarg);
opts.load = true;
break;
case '?':
//printf("unknow options: %c\n", optopt);
return NULL;
break;
case ':':
//printf("option need a option\n");
return NULL;
break;
}/* end switch */
}/* end while */
/* 無文件 */
if (optind == argc) {
return NULL;
}
// 當函數分析完所有參數時,全局變量optind(into argv)會指向第一個‘非選項’的位置
// 需要被dump的so庫文件的文件路徑
strcpy(opts.target_file, argv[optind]);
// 返回的引用
return &opts;
}
根據對用戶輸入命令行參數的解析結果options_t,進行Android so加固脫殼相關的操作,這裏主要關注的是被加固的Android so庫文件的內存dump相關的處理部分,如下圖所示:
// 加載需要dump的so庫文件
if (g_opts->load) {
// 清零處理
memset(&g_infos, 0, sizeof(g_infos));
// 構建libdl.so庫文件系統符號表symtab的填充
fill_libdl_symtab();
// 構建libdl_info結構體的填充
fill_libdl_info();
// unsigned ret = __linker_init((unsigned **)(argv-1));
// if (ret == 0) return ret;
// 獲取到需要被dump的so庫文件的文件路徑
char* fname = g_opts->target_file;
// 動態加載需要被dump的so庫文件返回信息描述結構體soinfo指針
soinfo* lib = (soinfo*)dlopen(fname, 0);
if (lib == NULL) {
// 動態庫加載失敗的情況
return -1;
}
//void* handle = dlsym(lib, "prepare_key");
//if (handle) {
// printf("%x\n", *(unsigned*)handle);
//}
// 從加載後的外殼so庫文件中dump出解密後的被保護的so庫文件
if (g_opts->dump) {
// dump出被保護的so庫文件
if (dump_file(lib) != 0) {
return -1;
}
// 對dump出來被保護的so庫文件進行區節頭表的重建(玩命版主沒有處理)
// if (g_opts->make_sectabs) {
//
// if (make_sectables(g_opts->dump_file) != 0) {
// return -1;
// }
// }
} else {
/* 打印代碼CRC */
if (g_opts->check) {
checkcode_by_x((unsigned char*)(lib->base),
"code text crc32",
g_opts->xct_offset,
g_opts->xct_size);
}
}
// 卸載外殼so庫文件的加載
dlclose(lib);
// 不加載外殼so庫文件的處理,crc32的校驗
} else {
/* 未加載的功能 */
if (g_opts->check) {
checkcode(g_opts->target_file, "code text crc32",
g_opts->xct_offset,
g_opts->xct_size);
}
}
在進行被加固Android so庫文件的內存dump處理之前,還需要了解一下Android so庫文件內存加載相關的知識。一般情況下,調用 dlopen函數 實現對Android so庫文件的內存加載,調用dlopen函數成功以後返回Android so庫文件內存加載後的模塊句柄,其實該句柄就是 soinfo* (soinfo結構體的指針),Android so庫文件內存加載成功的內存鏡像就是由結構體soinfo來描述的,soinfo結構體在進程內存中比較完整的描述了ELF文件的執行視圖相關的信息。
/* so信息結構 */
struct soinfo
{
char name[SOINFO_NAME_LEN]; /* so庫文件的文件路徑 */
const Elf32_Phdr *phdr; /* 指向程序段頭表 */
int phnum; /* 程序段頭表的數量 */
unsigned entry; /* so庫文件的代碼執行入口地址 */
unsigned base; /* so庫文件內存加載後的基地址 */
unsigned size; /* so庫文件所有可加載段的長度 */
int unused; // DO NOT USE, maintained for compatibility.
unsigned *dynamic; /* .dynamic段描述結構體所在的起始地址*/
unsigned unused2; // DO NOT USE, maintained for compatibility
unsigned unused3; // DO NOT USE, maintained for compatibility
soinfo *next;
unsigned flags;
const char *strtab; /* .strtab段所在的內存地址 */
Elf32_Sym *symtab; /* .symtab段所在的內存地址 */
unsigned nbucket;
unsigned nchain;
unsigned *bucket;
unsigned *chain;
unsigned *plt_got;
Elf32_Rel *plt_rel;
unsigned plt_rel_count;
Elf32_Rel *rel;
unsigned rel_count;
unsigned *preinit_array;
unsigned preinit_array_count;
unsigned *init_array;
unsigned init_array_count;
unsigned *fini_array;
unsigned fini_array_count;
void (*init_func)(void);
void (*fini_func)(void);
#if defined(ANDROID_ARM_LINKER)
/* ARM EABI section used for stack unwinding. */
unsigned *ARM_exidx;
unsigned ARM_exidx_count;
#elif defined(ANDROID_MIPS_LINKER)
#if 0
/* not yet */
unsigned *mips_pltgot
#endif
unsigned mips_symtabno;
unsigned mips_local_gotno;
unsigned mips_gotsym;
#endif /* ANDROID_*_LINKER */
unsigned refcount;
struct link_map linkmap;
int constructors_called; /* 構造函數已經被調用 */
/* When you read a virtual address from the ELF file, add this
* value to get the corresponding address in the process' address space */
Elf32_Addr load_bias;
int has_text_relocations;
/* 表明是否是從主程序中調用 */
//int loader_is_main;
};
被加固Android so庫文件脫殼操作的流程: 調用dlopen函數加載外殼Android so庫文件,dlopen函數成功返回(即被加固保護的Android so庫文件在內存中解密、自定義加載成功得到soinfo*,替換掉外殼Android so庫文件返回的soinfo結構體指針 soinfo* 爲被加固保護的Android so庫文件加載成功後得到的 soinfo* ,實現被加固保護的Android so與外殼Android so的無縫銜接)得到被加固保護的Android
so庫文件的soinfo*(被加固保護Android so庫文件內存鏡像的描述結構體),然後對被加固保護的Android so庫文件的內存soinfo結構體指針進行解析,獲取到被加固保護的Android so庫文件ELF文件格式執行視圖的描述信息,並根據獲取到的這些信息對被加固Android so庫文件進行內存dump處理。
被加固保護Android so庫文件的內存dump操作在 函數dump_file 中實現,代碼如下圖所示:
// 從外殼so動態加載返回的soinfo中dump出被加固so庫文件
// 參考的源碼文件: /bionic/linker/linker.h
int dump_file(soinfo* lib) {
// 創建新文件,用以保存dump出來的so庫文件
FILE* fp = fopen(g_opts->dump_file, "w");
if (NULL ==fp) {
printf("create new file: %s error !", g_opts->dump_file);
return -1;
}
// 修改外殼so庫文件的整個內存加載區域爲可讀可寫可執行
int ret = mprotect((void*)lib->base, lib->size, 7 /**全部權限打開**/);
// 打印外殼so庫文件內存加載返回的soinfo中的信息
printf("--------------------------------------------------\n");
// so庫文件的內存加載地址
printf("base = 0x%x\n", lib->base);
// so庫文件的內存加載映射大小
printf("size = 0x%x\n", lib->size);
// so庫文件的代碼指令的入口地址
printf("entry = 0x%x\n", lib->entry);
// so庫文件的程序段頭表的數量
printf("program header count = %d\n", lib->phnum);
printf("--------------------------------------------------\n");
// dump出來的so庫文件的大小
unsigned dump_size = lib->size;
unsigned buf_size = dump_size + 0x10;
// 申請內存空間
unsigned char* buf = new unsigned char [buf_size];
if (NULL == buf) {
printf("size = alloc memery err !\n");
return -1;
}
// 將soinfo描述的so庫文件的內存數據拷貝到申請的內存空間中
memcpy(buf, (void*)lib->base, lib->size);
// 定位到soinfo描述的so庫文件(ELF)的文件頭Elf32_Ehdr
Elf32_Ehdr* elfhdr = (Elf32_Ehdr*)(void*)buf;
// 修改區節頭表的數量爲0
elfhdr->e_shnum = 0;
// 修改該elf文件的區節數據的文件偏移爲0
elfhdr->e_shoff = 0;
// 修改該elf文件區節表名稱字符串所在的區節頭表的序號爲0
elfhdr->e_shstrndx = 0;
// 獲取到該elf文件的程序段頭表的文件偏移
unsigned phoff = elfhdr->e_phoff;
// 定位到該elf文件的程序段頭表的位置
Elf32_Phdr* phdr = (Elf32_Phdr*)(void*)(buf + phoff);
// 遍歷該elf文件的程序段頭表
for (int i = 0; i < lib->phnum; i++, phdr++) {
// 獲取該程序段頭描述的程序段所在的相對虛擬內存地址
unsigned v = phdr->p_vaddr;
// 修正該程序段的文件偏移地址爲虛擬內存地址
phdr->p_offset = v;
// 獲取該程序段頭描述的程序段的內存對齊後的數據長度大小
unsigned s = phdr->p_memsz;
// 修正該程序段的文件數據長度大小爲內存對齊後的數據長度大小
phdr->p_filesz = s;
}
/* 是否清除DT_INIT入口點 */
if (g_opts->clear_entry)
fix_entry(buf, lib);
// 將該soinfo描述的so庫文件修正後的內存數據寫入到新創建的g_opts->dump_file文件中
ret = fwrite((void*)buf, 1, dump_size, fp);
// 刷新文件流
fflush(fp);
// 資源的清理
if (buf) delete [] buf;
// 關閉文件
fclose(fp);
printf("Dump so Successful\n");
return 0;
}
根據被加固保護Android so庫文件內存鏡像描述結構體 soinfo* 從進程內存中dump出so庫文件的初步操作流程梳理如下:
1. 根據 傳入參數soinfo* 獲取到被加固Android so庫文件的內存加載基地址和內存所有段的長度並修改該so庫文件所在內存區域的內存屬性爲可讀可寫可執行。
2. 定位到soinfo描述的so庫文件(ELF)的文件頭Elf32_Ehdr,由於在Android so庫文件內存加載時是基於ELF文件的可執行視圖,因此該so庫文件的鏈接視圖的信息都會被去掉。爲了避免so庫文件內存dump後被IDA分析出錯一般會將Android so庫文件中seciton區節頭表相關描述信息的參數設置爲0,注意:Android 7.0版本的linker在進行Android so庫文件的加載時會進行seciton區節頭表相關描述信息參數的檢查。
3. 由於ELF文件加載內存時需要進行內存對齊的處理,因此內存中的Android so庫的程序段的文件偏移和文件數據長度的大小需要進行修正處理。(從Android so庫文件比較完整修復的角度來考慮,這一步不是必須甚至是應該去掉的,參考《ELF section修復的一些思考》。)
// elf32文件的程序段的描述頭結構
typedef struct elf32_phdr{
Elf32_Word p_type; // 程序段的屬性值
Elf32_Off p_offset; // 程序段的文件偏移
Elf32_Addr p_vaddr; // 程序段的相對虛擬地址RVA
Elf32_Addr p_paddr; // 程序段的物理地址
Elf32_Word p_filesz; // 程序段的文件數據長度大小
Elf32_Word p_memsz; // 程序段的文件數據內存對齊處理後的長度大小
Elf32_Word p_flags; // 程序段加載到內存後的可讀可寫可執行等內存屬性值
Elf32_Word p_align; // 程序需要內存對齊的數值
} Elf32_Phdr;
4. Android so庫文件的 .init段構造函數地址是否清除 的處理。
void fix_entry(unsigned char* buf, soinfo* lib) {
// 獲取.dynamic段所在的內存地址
unsigned* d = lib->dynamic;
// 遍歷.dynamic段的描述結構體
while (*d) {
// 獲取到so庫文件的初始化代碼地址的描述結構體
if (*d == DT_INIT) {
// 獲取到so庫文件的初始化代碼地址所在的文件偏移
unsigned offset = (unsigned)(d+1) - lib->base;
// 設置so庫文件的初始化代碼地址的相對虛擬地址爲0
*(unsigned*)(void*)(buf + offset) = 0;
break;
}
d += 2;
}
}
5. Android so庫文件脫殼的修復還缺少的步驟:對比較簡單的Android so脫殼和修復來說,上面的這些脫殼步驟已經可以了,IDA已經能夠比較正常的分析了,但是基於Android so脫殼和修復進一步處理而言,上面的步驟3應該去掉。udog作者進行了Android so庫文件的內存dump和初步的ELF文件修復處理,關於ELF文件的section區節頭表相關信息的重建還沒有完成,留給讀者自己來完成,但是作者玩命已經給出了大致的修復思路。關於Android
so庫文件內存dump後修復的詳細處理思路可以參考文章《ELF section修復的一些思考》、《從零打造簡單的SODUMP工具》、《基於init_array加密的SO的脫殼》、《安卓so文件脫殼思路(java版)附源代碼》、《ELF文件格式學習,section修復》,後面有時間我也會對這幾篇文章進行學習和分析。
6. Android so庫文件內存dump處理操作的說明:自我感覺文章中,對於 dump_file函數 處理的Android so庫文件的描述不是很準確(這裏提到的被內存dump處理的Android so庫文件不一定是被加固保護的Android so庫文件的,因爲 dlopen函數返回的soinfo* 也可能是外殼so加載器的Android so庫文件的,具體以實際操作得到的結果爲準,與Android so加固的對抗思路有一定的關係,不好準確描述。)
完整註釋版udog代碼下載地址:http://download.csdn.net/download/qq1084283172/9997375