android逆向奇技淫巧十二:dexVMP解释器原理及简易模拟实现

  为了保护代码、干扰静态分析,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;
}

  (6)在分析so源码时:cmp非常重要,因为在switch结构中需要根据不同的指令做不同的动作,所以cmp也可以成为逆向vmp的突破口之一!比如trace某app时遇到的这种:

       

   (7)真实的art虚拟机:核心就是switch case结构解析从0x00~0xff的opcode(理论上最多支持255种opcode),不同的opcode对应不同的处理方法,整个switch case函数有2300+多行,代码在这:http://aospxref.com/android-8.1.0_r81/xref/art/runtime/interpreter/interpreter_switch_impl.cc,部分代码如下:

do {
173      dex_pc = inst->GetDexPc(insns);
174      shadow_frame.SetDexPC(dex_pc);
175      TraceExecution(shadow_frame, inst, dex_pc);
176      inst_data = inst->Fetch16(0);
177      switch (inst->Opcode(inst_data)) {
178        case Instruction::NOP:
179          PREAMBLE();
180          inst = inst->Next_1xx();
181          break;
182        case Instruction::MOVE:
183          PREAMBLE();
184          shadow_frame.SetVReg(inst->VRegA_12x(inst_data),
185                               shadow_frame.GetVReg(inst->VRegB_12x(inst_data)));
186          inst = inst->Next_1xx();
187          break;
188        case Instruction::MOVE_FROM16:
189          PREAMBLE();
190          shadow_frame.SetVReg(inst->VRegA_22x(inst_data),
191                               shadow_frame.GetVReg(inst->VRegB_22x()));
192          inst = inst->Next_2xx();
193          break;
194        case Instruction::MOVE_16:
195          PREAMBLE();
196          shadow_frame.SetVReg(inst->VRegA_32x(),
197                               shadow_frame.GetVReg(inst->VRegB_32x()));
198          inst = inst->Next_3xx();
199          break;
200        case Instruction::MOVE_WIDE:
201          PREAMBLE();
202          shadow_frame.SetVRegLong(inst->VRegA_12x(inst_data),
203                                   shadow_frame.GetVRegLong(inst->VRegB_12x(inst_data)));
204          inst = inst->Next_1xx();
205          break;

  看吧,就是一个死循环,不停的根据不同的opcode做不同的action;其实VMP和jvm这种解释器没本质区别,只不过运行的地方不同:vmp运行在客户端,解释器运行在操作系统

   (8) 除了大家耳熟能详的dexVMP,还有dex2C,就是分析smali语法,然后转成同等功效的C代码,最后编译成native代码,项目地址在:https://github.com/amimo/dcc ;由于每个java函数都要“翻译”成C函数,所以每个函数都没啥规律可言(vmp因为有解释器,函数的结构看起来很类似),所以dex2C隐蔽性更好!但是部分使用人员反馈dex2c会导致app卡顿,可能这也是其没有大规模铺开的根本原因!

  

posted @ 2021-06-26 17:46  第七子007  阅读(4317)  评论(3编辑  收藏  举报