注:以下4篇博文中,部分圖片引用自DexHunter作者zyqqyz在slide.pptx中的圖片,版本歸原作者所有;
0x01 背景介紹
安卓 APP 的保護一般分為下列幾個方面:
-
JAVA/C代碼混淆
-
dex文件加殼
-
.so文件加殼
-
反動態調試技術
其中混淆和加殼是為了防止對應用的靜態分析;代碼混淆會增加攻擊者的時間成本, 但並不能從根本上解決應用被逆向的問題;而加殼技術一旦被破解,其優勢更是盪然無存;反調試用來對抗對APP的動態分析;
昨天看雪zyqqyz同學發了一個Android應用程序通用自動脫殼方法:DexHunter,詳見Github;通過定制Dalvik虛擬機實現,並對目前國內6款主流加固產品的進行了測試,效果良好;下面我們會講解Dalvik解釋器實現原理,並分析DexHunter代碼實現;
目前來看,要對抗這種脫殼方法,最好的辦法應該是APP啟動時hook被修改的幾處函數,並還原之;對開發者來說,應當將涉及資產、創新的關鍵代碼放入Native層實現,並通過.so加殼和反調試進行保護;
上次與梆梆的交流,他們提到梆梆3.0正在研發類似PC上的VMP保護殼技術,實現APK保護;但個人認為要在兼容性方面做的工作實在太多,短時間內估計很難實現了;
下面開始,分3個方面介紹通過定制Dalvik的通用脫殼方案:1.Dalvik 解釋器原理分析,2.DexHunter代碼分析,3.測試
0x02 Dalvik 解釋器原理分析
解釋器是Dalvik虛擬機的執行引擎,它負責解釋執行Dalvik字節碼。在字節碼加載完畢后,Dalvik虛擬機調用解釋器開始取指解釋字節碼,解釋器跳轉到解釋程序處執行。目前安卓解釋器有兩種,Portable和Fast解釋器,分別使用C和匯編實現;優勢分別是兼容性和性能,具體使用哪個可以自己來指定,因此本着簡單的原則,我們分析並使用Portable解釋器;
獲取字節碼並分析與解釋執行是Dalvik虛擬機解釋器的主要工作。Dalvik虛擬機的入口函數是vm/interp下的dvmInterpret函數;外部通過調用dvmInterpret函數進入解釋器執行,其流程為dvmCallMethod->dvmCallMethodV->dvmInterpret。
在外部函數調用解釋器以后,解釋器執行的主要流程有以下幾個步驟。
-
初始化解釋器執行環境
-
根據系統參數,選擇使用Portable或Fast解釋器
-
跳轉到相應解釋器執行
-
取指及指令檢查
-
執行字節碼對應程序段
dvmInterpret函數作為解釋器的入口函數,主要完成整個流程的前三部分,執行流程如下:
由於前三部分與我們定制Dalvik無關,這里不做詳細介紹;
根據解釋器的功能,可以想像的到,最簡單的模型就是一個大的switch語句,對每條指令進行判斷,然后case到相應的代碼進行解釋,解釋完成后又回到switch頂部,如下:
while (insn) { switch (insn) { case NOP: break; case MOV: do something; break; ... case OP: do something; break; default: break; } 取指; }
然而當解釋完成一條指令后,再重新判斷指令類型是個昂貴的開銷。因為對於每條指令,都將從switch頂部開始判斷,也就是從NOP指令開始判斷,直到找到相應的指令為止,這使得解釋器的執行效率十分低下。
這類問題的解決方法就是空間換時間,Dalvik就采用了這個思路;它為每條指令分配一個對應的標簽(Label),標簽標示的是該指令解釋程序的開始,每條指令的解釋程序末尾,有取指動作,可以取下一條要執行指令;Dalvik具體使用GCC的Threaded Code技術來實現,它使用了一個靜態的標簽數組,用來存儲各個字節碼解釋程序對應的標簽地址,其具體以一個宏來定義:
dalvik/libdex/DexOpcodes.h
/* * Macro used to generate a computed goto table for use in implementing * an interpreter in C. */ #define DEFINE_GOTO_TABLE(_name) \ static const void* _name[kNumPackedOpcodes] = { \ /* BEGIN(libdex-goto-table); GENERATED AUTOMATICALLY BY opcode-gen */ \ H(OP_NOP), \ H(OP_MOVE), \ H(OP_MOVE_FROM16), \ H(OP_MOVE_16), \ H(OP_MOVE_WIDE), \ ... }
下面看下H宏實現:
#define H(_op) &&op_##_op
那如何根據指令得到相應的Label地址呢?Dalvik中使用了索引號:
enum Opcode { // BEGIN(libdex-opcode-enum); GENERATED AUTOMATICALLY BY opcode-gen OP_NOP = 0x00, OP_MOVE = 0x01, OP_MOVE_FROM16 = 0x02, OP_MOVE_16 = 0x03, OP_MOVE_WIDE = 0x04, OP_MOVE_WIDE_FROM16 = 0x05, OP_MOVE_WIDE_16 = 0x06, OP_MOVE_OBJECT = 0x07, .... }
因此整個執行流程就是:取指令->取索引號->取Label得到解釋程序地址->執行指令,並取下一條指令。
上面分析了解釋器的基本模型,下面看Dalvik Portable的執行流程。其解析流程如圖:
首先進行相關變量的聲明,保存當前正在解釋的方法curMethod、程序計數器pc、棧楨指針fp、當前指令inst、指令譯碼的相關部分包括保存寄存器值vsrc1,vsrc2,vdst、設置方法調用指針methodToCall等。
通過DEFINE_GOTO_TABLE(handlerTable)宏進行GOTO Label的綁定,獲取並拷貝self->interpSave里保存的當前狀態,包括方法method、程序計數器pc、堆棧幀curFrame、返回值retval、要分析的Dex文件的類對象信息curMethod->clazz->pDvmDex等已聲明的變量。其代碼如下:
dalvik/vm/mterp/out/InterpC-portable.cpp
/* copy state in */ curMethod = self->interpSave.method; pc = self->interpSave.pc; fp = self->interpSave.curFrame; retval = self->interpSave.retval; /* only need for kInterpEntryReturn? */ methodClassDex = curMethod->clazz->pDvmDex;
最后通過FINISH(0)來取得第一條指令開始執行字節碼解析。
在Dalvik Portable中,解釋程序是由一系列宏控制,以對應的Label來表示,以NOP操作為例,其定義如下:
dalvik/vm/mterp/out/InterpC-portable.cpp
/*--- start of opcodes ---*/ /* File: c/OP_NOP.cpp */ HANDLE_OPCODE(OP_NOP) FINISH(1); OP_END /* File: c/OP_MOVE.cpp */ HANDLE_OPCODE(OP_MOVE /*vA, vB*/) vdst = INST_A(inst); vsrc1 = INST_B(inst); ILOGV("|move%s v%d,v%d %s(v%d=0x%08x)", (INST_INST(inst) == OP_MOVE) ? "" : "-object", vdst, vsrc1, kSpacing, vdst, GET_REGISTER(vsrc1)); SET_REGISTER(vdst, GET_REGISTER(vsrc1)); FINISH(1); OP_END /* File: c/OP_MOVE_FROM16.cpp */ HANDLE_OPCODE(OP_MOVE_FROM16 /*vAA, vBBBB*/) vdst = INST_AA(inst); vsrc1 = FETCH(1); ILOGV("|move%s/from16 v%d,v%d %s(v%d=0x%08x)", (INST_INST(inst) == OP_MOVE_FROM16) ? "" : "-object", vdst, vsrc1, kSpacing, vdst, GET_REGISTER(vsrc1)); SET_REGISTER(vdst, GET_REGISTER(vsrc1)); FINISH(2); OP_END
HANDLE_OPCODE(OP_NOP)表示對應的是OP_NOP操作,緊接其后的是解釋程序的具體實現。到OP_END結束。而在Portable中,所以有解釋程序都由C語言編寫。NOP操作中的HANDLE_OPCODE、FINISH和OP_END都是宏定義。其中HANDLE_OPCODE和OP_END是成對出現的,OP_END什么也不做:
#define OP_END
所以
HANDLE_OPCODE(OP_NOP) FINISH(1); OP_END
可以翻譯為:
op_OP_NOP: FINISH(1);
對於NOP指令,其完成的工作就是什么也不做。因此,對應的解釋程序就是直接取下一條將要執行的指令,也就是FINISH(1)所完成的工作。在FINISH()宏里,虛擬機獲取下一條指令,並從指令中提取操作碼號,根據該操作碼號到指令解釋程序查找表中得到相應的標簽,然后跳轉到該處理程序執行。其定義如下:
# define FINISH(_offset) { \ ADJUST_PC(_offset); \ inst = FETCH(0); \ if (self->interpBreak.ctl.subMode) { \ dvmCheckBefore(pc, fp, self); \ } \ goto *handlerTable[INST_INST(inst)]; \ }