安卓 dex 通用脫殼技術研究(一)

http://my.oschina.net/cve2015/blog/508604


摘要 本文剖析安卓 Dalvik 解釋器 Portable 的原理與實現,並通過修改 Dalvik 虛擬機實現 dex 文件的通用脫殼方案;本方法同樣適應於 ART 虛擬機;

0x01 背景介紹

安卓 APP 的保護一般分爲下列幾個方面:

  1. JAVA/C代碼混淆

  2. dex文件加殼

  3. .so文件加殼

  4. 反動態調試技術

其中混淆和加殼是爲了防止對應用的靜態分析;代碼混淆會增加攻擊者的時間成本, 但並不能從根本上解決應用被逆向的問題;而加殼技術一旦被破解,其優勢更是蕩然無存;反調試用來對抗對APP的動態分析;

7月份的烏雲安全峯會上,來自上交的楊文博在"Android應用程序通用自動脫殼方法研究"中分享了他們開發的通用脫殼工具,通過定製Dalvik實現,並對目前國內6款主流加固產品的進行了測試,效果良好;

然而他並沒有公開源碼;昨天看雪zyqqyz同學發了一個自己的實現:DexHunter,詳見Github;下面我們會講解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。

在外部函數調用解釋器以後,解釋器執行的主要流程有以下幾個步驟。

  1. 初始化解釋器執行環境

  2. 根據系統參數,選擇使用Portable或Fast解釋器

  3. 跳轉到相應解釋器執行

  4. 取指及指令檢查

  5. 執行字節碼對應程序段

dvmInterpret函數作爲解釋器的入口函數,主要完成整個流程的前三部分,執行流程如下:

由於前三部分與我們定製Dalvik無關,這裏不做詳細介紹;

根據解釋器的功能,可以想像的到,最簡單的模型就是一個大的switch語句,對每條指令進行判斷,然後case到相應的代碼進行解釋,解釋完成後又回到switch頂部,如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
 * 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宏實現:

?
1
#define    H(_op)    &&op_##_op

那如何根據指令得到相應的Label地址呢?Dalvik中使用了索引號:

?
1
2
3
4
5
6
7
8
9
10
11
12
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

?
1
2
3
4
5
6
    /* 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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*--- 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什麼也不做:

?
1
#define    OP_END

所以

?
1
2
3
HANDLE_OPCODE(OP_NOP)
    FINISH(1);
OP_END

可以翻譯爲:

?
1
2
op_OP_NOP:
    FINISH(1);

對於NOP指令,其完成的工作就是什麼也不做。因此,對應的解釋程序就是直接取下一條將要執行的指令,也就是FINISH(1)所完成的工作。在FINISH()宏裏,虛擬機獲取下一條指令,並從指令中提取操作碼號,根據該操作碼號到指令解釋程序查找表中得到相應的標籤,然後跳轉到該處理程序執行。其定義如下:

?
1
2
3
4
5
6
7
8
# define FINISH(_offset) {                                                  \
        ADJUST_PC(_offset);                                                 \
        inst = FETCH(0);                                                    \
        if (self->interpBreak.ctl.subMode) {                                \
            dvmCheckBefore(pc, fp, self);                                   \
        }                                                                   \
        goto *handlerTable[INST_INST(inst)];                                \
    }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章