注:以下4篇博文中,部分图片引用自DexHunter作者zyqqyz在slide.pptx中的图片,版本归原作者所有;
安卓 APP 的保护一般分为下列几个方面:
JAVA/C代码混淆
dex文件加壳
.so文件加壳
反动态调试技术
其中混淆和加壳是为了防止对应用的静态分析;代码混淆会增加攻击者的时间成本, 但并不能从根本上解决应用被逆向的问题;而加壳技术一旦被破解,其优势更是荡然无存;反调试用来对抗对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.测试
解释器是Dalvik虚拟机的执行引擎,它负责解释执行Dalvik字节码。在字节码加载完毕后,Dalvik虚拟机调用解释器开始取指解释字节码,解释器跳转到解释程序处执行。目前安卓解释器有两种,Portable和Fast解释器,分别使用C和汇编实现;优势分别是兼容性和性能,具体使用哪个可以自己来指定,因此本着简单的原则,我们分析并使用Portable解释器;
获取字节码并分析与解释执行是Dalvik虚拟机解释器的主要工作。Dalvik虚拟机的入口函数是vm/interp下的dvmInterpret函数;外部通过调用dvmInterpret函数进入解释器执行,其流程为dvmCallMethod->dvmCallMethodV->dvmInterpret。
在外部函数调用解释器以后,解释器执行的主要流程有以下几个步骤。
初始化解释器执行环境
根据系统参数,选择使用Portable或Fast解释器
跳转到相应解释器执行
取指及指令检查
执行字节码对应程序段
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)]; \ }