安卓 dex 通用脱壳技术研究(一)

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


摘要  本文剖析安卓 Dalvik 解释器 Portable 的原理与实现,并通过修改 Dalvik 虚拟机实现 dex 文件的通用脱壳方案;本方法同样适应于 ART 虚拟机;

目录[-]

  • 0x01 背景介绍
  • 0x02 Dalvik 解释器原理分析
  • 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函数作为解释器的入口函数,主要完成整个流程的前三部分,执行流程如下:

    安卓 dex 通用脱壳技术研究(一)_第1张图片

    由于前三部分与我们定制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得到解释程序地址->执行指令,并取下一条指令。

    安卓 dex 通用脱壳技术研究(一)_第2张图片

    上面分析了解释器的基本模型,下面看Dalvik Portable的执行流程。其解析流程如图:

    安卓 dex 通用脱壳技术研究(一)_第3张图片

    首先进行相关变量的声明,保存当前正在解释的方法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)];                                \
         }

    你可能感兴趣的:(Android)