• 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;
    }

      

      

  • 相关阅读:
    SlideShowExtender制作相册
    Response.Redirect(),Server.Transfer(),Server.Execute()的区别
    虚方法,抽象类,多态性
    gridview获取当前行索引的方法
    AutoQueryTextBox(AjaxPro.dll)非常值得研究的javascript代码
    abstract & virtual & override & new比较(转)
    Asp.net技巧:gridview获取当前行索引的方法
    js 获取浏览器高度和宽度值
    深入理解abstract class和interface
    c++ 静态数据成员和静态成员函数
  • 原文地址:https://www.cnblogs.com/theseventhson/p/14933920.html
Copyright © 2020-2023  润新知