1. 程式人生 > >安卓 dex 通用脫殼技術研究(一)

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

注:以下4篇博文中,部分圖片引用自DexHunter作者zyqqyz在slide.pptx中的圖片,版本歸原作者所有;

0x01 背景介紹

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

  1. JAVA/C程式碼混淆

  2. dex檔案加殼

  3. .so檔案加殼

  4. 反動態除錯技術

其中混淆和加殼是為了防止對應用的靜態分析;程式碼混淆會增加攻擊者的時間成本, 但並不能從根本上解決應用被逆向的問題;而加殼技術一旦被破解,其優勢更是蕩然無存;反除錯用來對抗對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。

在外部函式呼叫直譯器以後,直譯器執行的主要流程有以下幾個步驟。

  1. 初始化直譯器執行環境

  2. 根據系統引數,選擇使用Portable或Fast直譯器

  3. 跳轉到相應直譯器執行

  4. 取指及指令檢查

  5. 執行位元組碼對應程式段

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)];                                \
    }