http://my.oschina.net/cve2015/blog/508604
目录[-]
0x01 背景介绍
安卓 APP 的保护一般分为下列几个方面:
-
JAVA/C代码混淆
-
dex文件加壳
-
.so文件加壳
-
反动态调试技术
其中混淆和加壳是为了防止对应用的静态分析;代码混淆会增加攻击者的时间成本, 但并不能从根本上解决应用被逆向的问题;而加壳技术一旦被破解,其优势更是荡然无存;反调试用来对抗对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。
在外部函数调用解释器以后,解释器执行的主要流程有以下几个步骤。
-
初始化解释器执行环境
-
根据系统参数,选择使用Portable或Fast解释器
-
跳转到相应解释器执行
-
取指及指令检查
-
执行字节码对应程序段
dvmInterpret函数作为解释器的入口函数,主要完成整个流程的前三部分,执行流程如下:
由于前三部分与我们定制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得到解释程序地址->执行指令,并取下一条指令。
上面分析了解释器的基本模型,下面看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
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)]; \
}
|