android逆向奇技淫巧十二:VMP解釋器原理及簡易模擬實現


  為了保護代碼、干擾靜態分析,android客戶端可以通過OLLVM干擾整個so層代碼執行的控制流,但不會改變函數調用的關系,所以抓住這點並不難破解OLLVM;另一個大家耳熟能詳的代碼保護方式就是VMP了!我之前介紹了windos下VMP代碼混淆的原理,其實在android下也類似:對原來的smail代碼做替換,然后用自己的解釋器“執行”替換后的代碼,實現的功能和原smail代碼一樣!但由於使用了VMP自己的代碼,並不是原生標准的smail代碼,jadx、jeb、GDA等反編譯工具大概率是會失效的,逆向人員靜態分析時大概率是看不懂的!

  VMP的全稱是虛擬機保護。從名字看,是通過虛擬機保護代碼的;在詳細闡述原理之前,先回顧一下與之對應的“物理機”的概念!

  物理CPU通過數據總線,從內存讀取指令,再解析和執行(由此誕生了三級流水線的概念:讀取、解析、執行)!本文的重點就是解析了:指令的格式都是opcode操作碼和操作數組成的!解析指令時先讀取操作碼,根據不同的操作碼確定指令的類型(加減乘除、mov等);然后再解析操作數,最后執行指令;以上所有的步驟都是CPU硬件完成的,不需要程序員編寫軟件去干預(所以才叫物理機的嘛)!這就是物理CPU執行代碼的基本流程和邏輯!虛擬機在執行指令的流程和原理上與物理CPU沒本質區別,完全一樣!那么虛擬機的讀取、解析和執行都是怎么模擬的了?

  只要是碼農,肯定都知道java、python、php等語言,這些語言的執行都是由各自對應的虛擬機解釋執行的;還有逆向人員很熟悉的unicorn、unidbg、bochs等虛擬機,原理也都是這樣的!既然市面上已經有這么多款虛擬機了,說明這種“虛擬化技術”已經非常成熟;為了一探究竟,這里以android下的art為例(畢竟是開源的嘛),說明這類虛擬化的執行原理!

  1、先用GDA打開一個apk,隨便選個函數,選擇“show ByteCode”選項,這事能看到smail的字節碼了,如下:

        

   art虛擬機會挨個讀取這些字節碼,然后自己去解析和執行;這里以8.0版本的art為例,在 http://androidxref.com/8.0.0_r4/xref/art/runtime/interpreter/interpreter_switch_impl.cc  這里能看到interpreter_switch_impl文件的源碼,里面有個非常長的do while循環,部分代碼截取如下(代碼太長了,沒法全部復制,感興趣的小伙伴建議自行打開鏈接查看):

do {
125    dex_pc = inst->GetDexPc(insns);
126    shadow_frame.SetDexPC(dex_pc);
127    TraceExecution(shadow_frame, inst, dex_pc);
128    inst_data = inst->Fetch16(0);
129    switch (inst->Opcode(inst_data)) {
130      case Instruction::NOP:
131        PREAMBLE();
132        inst = inst->Next_1xx();
133        break;
134      case Instruction::MOVE:
135        PREAMBLE();
136        shadow_frame.SetVReg(inst->VRegA_12x(inst_data),
137                             shadow_frame.GetVReg(inst->VRegB_12x(inst_data)));
138        inst = inst->Next_1xx();
139        break;
140      case Instruction::MOVE_FROM16:
141        PREAMBLE();
142        shadow_frame.SetVReg(inst->VRegA_22x(inst_data),
143                             shadow_frame.GetVReg(inst->VRegB_22x()));
144        inst = inst->Next_2xx();
145        break;
146      case Instruction::MOVE_16:
147        PREAMBLE();
148        shadow_frame.SetVReg(inst->VRegA_32x(),
149                             shadow_frame.GetVReg(inst->VRegB_32x()));
150        inst = inst->Next_3xx();
151        break;
152      case Instruction::MOVE_WIDE:
153        PREAMBLE();
154        shadow_frame.SetVRegLong(inst->VRegA_12x(inst_data),
155                                 shadow_frame.GetVRegLong(inst->VRegB_12x(inst_data)));
156        inst = inst->Next_1xx();
157        break;

  這里說明一下:不同版本的art有不同的解釋smail代碼的方式,除了這種switch case形式,還有匯編、GotoTale等;因為swtich case是所有版本公用的形式,所以這里以swtich case舉例;

  大伙看到switch的條件了么?就是opcode,也就是操作碼;case分支就是根據不同的操作碼做不同的操作;比如第一個操作碼如果是NOP,那么指令直接向前加1,其他啥事也不干!第二個case的操作碼是MOVE指令,這時就要進一步解析操作數了,取出原寄存器、目標寄存器,然后把目標寄存器的值設置程原寄存器的值,最后照例把指令直接向前加1;由於opcode是8位的,一共有255總情況,所以case的分支也是有很多的(暫時沒用完);把libart.so用IDA打開反編譯,看到的就是如下效果:有點像OLLVM的控制流平坦化混淆;

         

   以上就是art解釋器的實現方式之一。怎么樣,都看懂了吧,原理其實並不復雜,不就是個swtich case嘛,自己都能動手做個簡單的!

  2、自己寫個簡單測試代碼,如下:

    public int add(int a, int b) {
        return a + b;
    }

    public int sub(int a, int b) {
        return a - b;
    }

    public int mul(int a, int b) {
        return a * b;
    }

    public int div(int a, int b) {
        return a / b;
    }

    public int compute(int a,int b){
         int c=a*a;
         int d=a*b;
         int e=a-b;
         int f=a/b;
         int result=c+d+e+f;
         return result;
    }

  最重要的就是最后的compute函數,比如很多時候網絡通信的sign簽名字段,就需要通過類似compute函數對傳的參數做簽名。目的地收到后用同樣的算法對參數做計算,看看兩個sign字段的值是不是一樣的。如果不是,說明參數被篡改了! 這里為了防止compute函數被篡改,可以先把compute編譯后的smail字節碼抽取藏起來,再通過自己的解釋器執行,達到用android原生art解釋器一樣的效果!上述代碼編譯后,看到的smail如下:

      

   這里只展示了smail代碼部分的字節碼,還有部分codeItem字節碼沒展示,完整的字節碼如下:注意看注釋,每個字段的含義都解釋清楚了;

const unsigned char Compute[] = { 0x08, 0x00,  //寄存器使用的個數
                                    0x03, 0x00,  //參數個數
                                    0x00, 0x00,  //調用其他方法時使用寄存器的個數
                                    0x00, 0x00,  //try catch個數
                                    0x6e, 0x77, 0x14,0x00,  //指令調試信息偏移
                                    0x0d, 0x00, 0x00, 0x00, //指令集個數,以2字節為單位;這里是d,那么指令總長度是13*2=26個字節,可以用這個確認函數結尾
                                    0x92, 0x00, 0x06, 0x06, //指令開始了;具體指令看上面的截圖
                                    0x92, 0x01, 0x06, 0x07,
                                    0x91, 0x02, 0x06, 0x07,
                                    0x93, 0x03, 0x06, 0x07,
                                    0x90,  0x04, 0x00, 0x01,
                                    0xb0, 0x24,
                                    0xb0, 0x34,
                                    0x0f, 0x04};

  現在的問題就簡單了,我們自己實現的解釋器核心兩個功能:

  •  解析上面的字節碼,按照codeItem的格式讀取每個字段
  •  讀取指令后模擬執行指令,得到正確的結果!

 (1)為了高效和安全,解釋器一般都是在so里面的,所以需要用C來實現。第一步,先從字節碼讀取需要用到的寄存器數量,分配對等長度的內存空間來模擬寄存器

   注意:這里有個很重要的結構體codeItem,描述了smail中每個函數的“元數據”,其成員見末尾代碼;在指令抽取加殼、dex2oat時都要用到,建議牢記!

   CodeItem *codeItem = (CodeItem *) Compute;
    int registersize = codeItem->registers_size_;
    int result = 0;
    int *VREG = reinterpret_cast<int *>(malloc(sizeof(int) * registersize));

 (2)讀取參數的個數,用寄存器總數減去參數個數,就是函數本身局部變量能使用的寄存器個數:

      int insNum = codeItem->ins_size_;//參數的個數
        int startIndex = registersize - insNum;//總的寄存器數量減去參數個數,剩下的才是解釋器能自由使用的寄存器個數
        VREG[startIndex] = 0;
        VREG[++startIndex] = a;
        VREG[++startIndex] = b;

   (3)找到指令開始的地址

unsigned long address = (unsigned long) Compute;
unsigned char *opOffset = reinterpret_cast<unsigned char *>(address + 16);//指令集的地址

  (4)最核心的開始了:讀取指令,解析出opcode,根據不同的opcode走不同的case處理分支,如下:

while (true) {
            unsigned char op = *opOffset;
            switch (op) {
                case 0x90: {//90040001 |0008: add-int v4, v0, v1
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] + VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0x91: {//91020607            |0004: sub-int v2, v6, v7
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] - VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0x92: {//92010607            |0002: mul-int v1, v6, v7
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] * VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0x93: {//93030607            |0006: div-int v3, v6, v7
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] / VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0xb0: {//b024                |000a: add-int/2addr v4, v2
                    unsigned char des = *(opOffset + 1);
                    int arg0 = des & 0x0F;
                    int arg1 = des >> 4;
                    VREG[arg0] = VREG[arg0] + VREG[arg1];
                    opOffset = opOffset + 2;
                    break;
                }
                case 0x0f: {//123cf4: 0f04                |000c: return v4*/
                    unsigned char des = *(opOffset + 1);
                    return VREG[des];
                }
            }
        }

  從上面的case分支看,這個函數所有的指令對應的處理方法都覆蓋到了;每次處理萬,結果都存放在了寄存器,實現原理和art虛擬機一摸一樣!至此,虛擬機本身的核心功能就完成了;這樣就能直接當成VMP來用了? 哈哈,如果這樣認為,就圖樣、圖森破了!VMP的本意是保護代碼,增加靜態分析的成本。然而截至目前,所有的工作都是圍繞解釋器執行smail代碼完成的,而smail代碼本身和編譯出來的一模一樣,沒任何變化!就算人為抽取出來,這段代碼以數組形式(本質就是連續內存)被執行,也容易在靜態分析的時候被發現,和果奔沒本質區別!這里又該怎么做了? VMP最重要的功能之二:代碼映射表!

 (5)映射的原理很簡單,相當於把原smail代碼加密,改成一套虛擬機自己能認識的代碼。比如上面的0x90,就是兩個數相加。為了迷惑反編譯器和逆向人員,可以把0x90換成其他的字節,比如0x20,這樣一來GDA、jadx、jeb等反編譯器要么不認識,要么反編譯出錯,逆向人員肯定看不懂啥意思,干擾靜態分析的目的就達到了!為了簡化說明原理,這里統一讓原加減乘除的opcode減去0x70,這樣一來新的smail代碼如下:

const unsigned char Compute[] = {0x08, 0x00,
                                 0x03, 0x00,
                                 0x00, 0x00,
                                 0x00, 0x00,
                                 0x6e, 0x77, 0x14,0x00,
                                 0x0d, 0x00, 0x00, 0x00,
                                 0x22, 0x00, 0x06, 0x06,
                                 0x22, 0x01,0x06, 0x07,
                                 0x21, 0x02, 0x06, 0x07,
                                 0x23, 0x03, 0x06, 0x07,
                                 0x20,0x04, 0x00, 0x01,
                                 0xb0, 0x24,
                                 0xb0, 0x34,
                                 0x0f, 0x04};

  感興趣的小伙伴可以自行反編譯試試,絕對和以前的smail代碼面目全非!為了執行這些混淆后的代碼,解釋器也要相應調整:

case 0x20: {//90040001 |0008: add-int v4, v0, v1
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] + VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0x21: {//91020607            |0004: sub-int v2, v6, v7
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] - VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0x22: {//92010607            |0002: mul-int v1, v6, v7
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] * VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0x23: {//93030607            |0006: div-int v3, v6, v7
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] / VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0xb0: {//b024                |000a: add-int/2addr v4, v2
                    unsigned char des = *(opOffset + 1);
                    int arg0 = des & 0x0F;
                    int arg1 = des >> 4;
                    VREG[arg0] = VREG[arg0] + VREG[arg1];
                    opOffset = opOffset + 2;
                    break;
                }
                case 0x0f: {//123cf4: 0f04                |000c: return v4*/
                    unsigned char des = *(opOffset + 1);
                    return VREG[des];
                }

  實際使用時,為了更加深層次地混淆,還可以混淆參數個數、寄存器的順序、操作數等,只要自己執行的時候參考映射表還原就行了!由此也很容易理解破解VMP混淆的兩個要點:

  •  找到映射表
  •  找到解釋器

  以上兩點我們后續再分享;這里解釋器的完整代碼如下:

#include <jni.h>
#include <string>

// Raw code_item.
struct CodeItem {
    uint16_t registers_size_;            // the number of registers used by this code
    //   (locals + parameters)
    uint16_t ins_size_;                  // the number of words of incoming arguments to the method
    //   that this code is for
    uint16_t outs_size_;                 // the number of words of outgoing argument space required
    //   by this code for method invocation
    uint16_t tries_size_;                // the number of try_items for this instance. If non-zero,
    //   then these appear as the tries array just after the
    //   insns in this instance.
    uint32_t debug_info_off_;            // file offset to debug info stream
    uint32_t insns_size_in_code_units_;  // size of the insns array, in 2 byte code units
    uint16_t insns_[1];                  // actual array of bytecode.
};
/*123cdc: 92000606            |0000: mul-int v0, v6, v6
123ce0: 92010607            |0002: mul-int v1, v6, v7
123ce4: 91020607            |0004: sub-int v2, v6, v7
123ce8: 93030607            |0006: div-int v3, v6, v7
123cec: 90040001            |0008: add-int v4, v0, v1
123cf0: b024                |000a: add-int/2addr v4, v2
123cf2: b034                |000b: add-int/2addr v4, v3
123cf4: 0f04                |000c: return v4*/
//90 20
//91 21
//92 22
//93 23
const unsigned char Compute[] = {0x08, 0x00,
                                 0x03, 0x00,
                                 0x00, 0x00,
                                 0x00, 0x00,
                                 0x6e, 0x77, 0x14,0x00,
                                 0x0d, 0x00, 0x00, 0x00,
                                 0x22, 0x00, 0x06, 0x06,
                                 0x22, 0x01,0x06, 0x07,
                                 0x21, 0x02, 0x06, 0x07,
                                 0x23, 0x03, 0x06, 0x07,
                                 0x20,0x04, 0x00, 0x01,
                                 0xb0, 0x24,
                                 0xb0, 0x34,
                                 0x0f, 0x04};
/*const unsigned char Compute[] = { 0x08, 0x00,  //寄存器使用的個數
                                    0x03, 0x00,  //參數個數
                                    0x00, 0x00,  //調用其他方法時使用寄存器的個數
                                    0x00, 0x00,  //try catch個數
                                    0x6e, 0x77, 0x14,0x00,  //指令調試信息偏移
                                    0x0d, 0x00, 0x00, 0x00, //指令集個數,以2字節為單位;這里是d,那么指令總長度是13*2=26個字節,可以用這個確認函數結尾
                                    0x92, 0x00, 0x06, 0x06, //指令開始了
                                    0x92, 0x01, 0x06, 0x07,
                                    0x91, 0x02, 0x06, 0x07,
                                    0x93, 0x03, 0x06, 0x07,
                                    0x90,  0x04, 0x00, 0x01,
                                    0xb0, 0x24,
                                    0xb0, 0x34,
                                    0x0f, 0x04};
*/
extern "C" JNIEXPORT jstring JNICALL
Java_com_kanxue_vmpprotect_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

int myinterpreter(JNIEnv *env, jobject obj, jint a, jint b) {
/*          .prologue
            .insnsSize 13 (16-bit)
            .registers 8 [ v0  v1  v2  v3  v4  v5  v6  v7  ]*/

    CodeItem *codeItem = (CodeItem *) Compute;
    int registersize = codeItem->registers_size_;
    int result = 0;
    int *VREG = reinterpret_cast<int *>(malloc(sizeof(int) * registersize));
    if (VREG != nullptr) {
        memset(VREG, 0, registersize * sizeof(int));
        int insNum = codeItem->ins_size_;//參數的個數
        int startIndex = registersize - insNum;//總的寄存器數量減去參數個數,剩下的才是解釋器能自由使用的寄存器個數
        VREG[startIndex] = 0;
        VREG[++startIndex] = a;
        VREG[++startIndex] = b;
        int pc = 0;
        unsigned long address = (unsigned long) Compute;
        unsigned char *opOffset = reinterpret_cast<unsigned char *>(address + 16);//指令集的地址
        while (true) {
            unsigned char op = *opOffset;
            switch (op) {
                /*case 0x90: {//90040001 |0008: add-int v4, v0, v1
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] + VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0x91: {//91020607            |0004: sub-int v2, v6, v7
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] - VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0x92: {//92010607            |0002: mul-int v1, v6, v7
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] * VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0x93: {//93030607            |0006: div-int v3, v6, v7
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] / VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }*/
                case 0x20: {//90040001 |0008: add-int v4, v0, v1
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] + VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0x21: {//91020607            |0004: sub-int v2, v6, v7
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] - VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0x22: {//92010607            |0002: mul-int v1, v6, v7
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] * VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0x23: {//93030607            |0006: div-int v3, v6, v7
                    unsigned char des = *(opOffset + 1);
                    unsigned char arg0 = *(opOffset + 2);
                    unsigned char arg1 = *(opOffset + 3);
                    VREG[des] = VREG[arg0] / VREG[arg1];
                    opOffset = opOffset + 4;
                    break;
                }
                case 0xb0: {//b024                |000a: add-int/2addr v4, v2
                    unsigned char des = *(opOffset + 1);
                    int arg0 = des & 0x0F;
                    int arg1 = des >> 4;
                    VREG[arg0] = VREG[arg0] + VREG[arg1];
                    opOffset = opOffset + 2;
                    break;
                }
                case 0x0f: {//123cf4: 0f04                |000c: return v4*/
                    unsigned char des = *(opOffset + 1);
                    return VREG[des];
                }

            }

        }
    }
}

extern "C" JNIEXPORT jint JNICALL
Java_com_vmpprotect_Compute_compute(JNIEnv *env, jobject obj, jint a, jint b) {
    int result = myinterpreter(env, obj, a, b);
    return result;
}

 

  

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM