Android so加固的簡單脫殼

本文博客地址: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


發佈了144 篇原創文章 · 獲贊 183 · 訪問量 71萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章