DEX文件解析

DEX文件解析

对Dex进行分析之前,首先要知道它是什么,为什么值得我们去分析。由于是初学,对于其中部分内容描述可能并不是特别准确,仅建立在个人的理解基础上。如果有不同意见,欢迎在issues上讨论。

  • classes.dex文件中包含了apk的可执行代码。之前在对Apk文件进行简单的分析的时候,知道了java生成的代码,会编译生成.dex文件。
  • DEX文件中包含了许多的逻辑信息,通过对它的分析,我们可以尝试逆向得到编写的源码。

1 DEX文件整体结构

下图是一幅对Dex中各部分详细分析的图片,来源

img

简单区分的话,则可以划分为以下几个部分:

1584602765164

文件头,索引区,数据区,接下来就是对这几个部分的逐步分析过程

2 Dex解析过程

2.1 文件头 Dexheader

可以说理解了Dexheader,然后知道一点点编译原理,对于我们理解索引区可以有很大的帮助。

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
26
27
28
/*
* Direct-mapped "header_item" struct.
*/
struct DexHeader {
u1 magic[8]; //分析的文件版本以及类型
u4 checksum; //文件内容的校验和,不包括magic和自己,主要用于检查文件是否损坏
u1 signature[kSHA1DigestLen]; //签名信息,不包括 magic\checksum和自己
u4 fileSize; //整个文件的长度,单位为字节,包括所有的内容
u4 headerSize; //默认是0x70个字节
u4 endianTag; //大小端标签,标准.dex文件为小端,此项一般固定为0x12345678常量
u4 linkSize; //链接数据的大小
u4 linkOff; //链接数据的偏移值
u4 mapOff; //map item的偏移地址,该item属于data区里的内容,值要大于等于dataOff的大小
u4 stringIdsSize; //DEX中用到的所有字符串内容的大小*
u4 stringIdsOff; //DEX中用到的所有字符串内容的偏移量
u4 typeIdsSize; //DEX中类型数据结构的大小
u4 typeIdsOff; //DEX中类型数据结构的偏移值
u4 protoIdsSize; //DEX中的元数据信息数据结构的大小
u4 protoIdsOff; //DEX中的元数据信息数据结构的偏移值
u4 fieldIdsSize; //DEX中字段信息数据结构的大小
u4 fieldIdsOff; //DEX中字段信息数据结构的偏移值
u4 methodIdsSize; //DEX中方法信息数据结构的大小
u4 methodIdsOff; //DEX中方法信息数据结构的偏移值
u4 classDefsSize; //DEX中的类信息数据结构的大小
u4 classDefsOff; //DEX中的类信息数据结构的偏移值
u4 dataSize; //DEX中数据区域的结构信息的大小
u4 dataOff; //DEX中数据区域的结构信息的偏移值
};

以上是dexheader.hDexHeader结构体,可以看到具体有些什么信息。对于其中各部分的意义已经标注,下表则是我个人对于其中部分结构的理解:

名称 意义
magic 描述了文件的类型以及版本
checksum 整个文件检验和,除掉checksum和signature
signature 文件的签名信息,同样不包括checksum和signature
filesize 整个文件一共有多大
headerSize DexHeader有多大
endiantag 文件序标签,表明是大端存储还是小端存储
link 链接段
map 这是在数据区的内容,可能在内存中读取这一块完成对Dex文件的解析
stringid 是.dex文件中所有的字符串的索引
typeid 代码中的所有数据类型信息
proto 方法原型
field 字段(域)区
method 方法区
class 类区
data 数据区

对于这部分解析的代码如下:

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
self.header_data = {
'magic': self.data[0:8], # Dex 版本标识,8个字节
'checksum': unpack('<L', self.data[8:0xC])[0], # 校验和,占4个字节
'signature': self.data[0xC:0x20], # SHA-1 散列值 20个字节 0x14
'file_size': unpack('<L', self.data[0x20:0x24])[0], # 总文件大小。占4个字节
'header_size': unpack('<L', self.data[0x24:0x28])[0], # DexHeader大小,占4个字节,目前恒为0x70,112个字节
'endian_tag': unpack('<L', self.data[0x28:0x2C])[0], # 字节序标记,占4个字节,目前值为 0x12345678,表示小端存储
'link_size': unpack('<L', self.data[0x2C:0x30])[0], # 链接段个数,占4个字节
'link_off': unpack('<L', self.data[0x30:0x34])[0], # 链接段起始偏移,占4个字节
'map_off': unpack('<L', self.data[0x34:0x38])[0], # DexMapList起始偏移,占4个字节
'string_ids_size': unpack('<L', self.data[0x38:0x3C])[0], # DexStringId 个数,占4个字节
'string_ids_off': unpack('<L', self.data[0x3C:0x40])[0], # DexStringId 起始偏移
'type_ids_size': unpack('<L', self.data[0x40:0x44])[0], # DexTypeId 个数
'type_ids_off': unpack('<L', self.data[0x44:0x48])[0], # DexTypeId 起始偏移
'proto_ids_size': unpack('<L', self.data[0x48:0x4C])[0], # DexProtoId 个数
'proto_ids_off': unpack('<L', self.data[0x4C:0x50])[0], # DexProtoId 起始偏移
'field_ids_size': unpack('<L', self.data[0x50:0x54])[0], # DexFieldId 个数
'field_ids_off': unpack('<L', self.data[0x54:0x58])[0], # DexFieldId 起始偏移
'method_ids_size': unpack('<L', self.data[0x58:0x5C])[0], # DexMethodId 个数
'method_ids_off': unpack('<L', self.data[0x5C:0x60])[0], # DexMethodId 起始偏移
'class_defs_size': unpack('<L', self.data[0x60:0x64])[0], # DexClassDef 个数
'class_defs_off': unpack('<L', self.data[0x64:0x68])[0], # DexClassDef 起始偏移
'data_size': unpack('<L', self.data[0x68:0x6C])[0], # 数据段大小
'data_off': unpack('<L', self.data[0x6C:0x70])[0] # 数据段起始偏移
}

2.2 DexMapList

1
2
3
4
5
6
7
8
9
DexMaplist : {
size, u4
DexMapItem-->{
type, u2,类型
unused, u2,对齐
size, u4,类型个数
offset u4,类型起始偏移
}
}

首先是MapList的个数n,后面紧接着n个大小为12个DexMapItem结构体。

类型有以下几种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* map item type codes */
enum {
kDexTypeHeaderItem = 0x0000,
kDexTypeStringIdItem = 0x0001,
kDexTypeTypeIdItem = 0x0002,
kDexTypeProtoIdItem = 0x0003,
kDexTypeFieldIdItem = 0x0004,
kDexTypeMethodIdItem = 0x0005,
kDexTypeClassDefItem = 0x0006,
kDexTypeCallSiteIdItem = 0x0007,
kDexTypeMethodHandleItem = 0x0008,
kDexTypeMapList = 0x1000,
kDexTypeTypeList = 0x1001,
kDexTypeAnnotationSetRefList = 0x1002,
kDexTypeAnnotationSetItem = 0x1003,
kDexTypeClassDataItem = 0x2000,
kDexTypeCodeItem = 0x2001,
kDexTypeStringDataItem = 0x2002,
kDexTypeDebugInfoItem = 0x2003,
kDexTypeAnnotationItem = 0x2004,
kDexTypeEncodedArrayItem = 0x2005,
kDexTypeAnnotationsDirectoryItem = 0x2006,
};

根据map字段,我们可以知道每个类型字段的个数,起始偏移

2.3 DexStringId

1
2
3
4
5
6
7
DexStringId : {
stringDataOff 字符串数据的起始偏移,u4
}
stringData : {
size, 编码字符的个数,u1
string 编码字符
}

Dexhead中,得到DexStringId的起始偏移后和个数后,我们可以知道每一个string的数据区中起始偏移,而dex中的string使用的是MUTF-8数据结构。对于DexStringId的解析代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 对DexStringID的解析
# DexStringId -> StringDataOff -> StringData:{size , data}
def get_strings(self):
strings = []
strings_ids_size = self.header_data['string_ids_size']
strings_ids_off = self.header_data['string_ids_off'] # 起始偏移
for index in range(strings_ids_size):
# 每一个字符串的起始偏移,从strings_id_off 开始,有连续strings_id_size个字符串的起始地址,大小为 u4
offset = unpack('<L', self.data[strings_ids_off + (index * 4):strings_ids_off + (index * 4) + 4])[0]
# 每个StringData是由MUTF-8编码,第一个字节表示字符串长度n,后面跟着n个字符 + "00"作为字符串的结尾
c_size = self.data[offset]
c_char = self.data[offset + 1:offset + 1 + c_size]
strings.append(c_char)
return strings_ids_size, hex(strings_ids_off), strings

解析结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
strings_id个数: 16 	
起始偏移: 0x70
内容:[
b'<init>',
b'Hello.java',
b'I',
b'III',
b'LHello;',
b'Ljava/io/PrintStream;',
b'Ljava/lang/Object;',
b'Ljava/lang/System;',
b'V',
b'VI',
b'VL',
b'[Ljava/lang/String;',
b'foo',
b'main',
b'out',
b'println']

2.4 DexTypeID

1
2
3
DexTypeId : {
DescriptorIdx DexTypeID 在String ID 中的索引
}

解析代码:

1
2
3
4
5
6
7
8
9
10
# 对DexTypeId 的解析
# DexTypeId -> DescriptorIdx:存储了DexType在DexStringId种的索引
def get_types(self):
type_ids = []
type_ids_size = self.header_data['type_ids_size']
type_ids_off = self.header_data['type_ids_off']
for index in range(type_ids_size):
types_idx = unpack('<L', self.data[type_ids_off + (index * 4):type_ids_off + (index * 4) + 4])[0]
type_ids.append(types_idx)
return type_ids_size, hex(type_ids_off), type_ids

结果:

1
2
3
type_id个数: 7 	 
起始偏移: 0xb0
在strings_id中的索引: [2, 4, 5, 6, 7, 8, 11]

2.5 DexProtoID

1
2
3
4
5
6
7
8
9
10
11
12
DexProtoID : {
shortyIdx, u4,指向DexStringId的索引
returnTypeIdx, u4,指向DexTypeId的索引,返回类型
parametersOff u4,指向DexTypeList的索引
}

parametersOff --> DexTypeList : {
size, u4, 参数个数
DexTypeItem : {
idx u2,指向DexTypeId 的索引,参数类型
}
}

解析代码如下:

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
26
27
28
29
30
31
32
33
# 对DexProtoId的解析
# DexProtoId -> {shortyIdx, returnTypeIDx, parametersOff -> {size, DexTypeItem -> type_idx} }
def get_proto_ids(self):
proto_ids = []
proto_ids_size = self.header_data['proto_ids_size']
proto_ids_off = self.header_data['proto_ids_off']
for index in range(proto_ids_size):
# 每一个DexProtoId 大小都是12字节
# 指向DexStringId列表索引
shorty_idx = unpack('<L', self.data[proto_ids_off + (index * 12):proto_ids_off + (index * 12) + 4])[0]
# 指向DexTypeId列表索引
return_type_id = unpack('<L', self.data[proto_ids_off + 4 + (index * 12):proto_ids_off + 8 + (index * 12)])[
0]
# 指向DexTypeList的偏移量
parameters_off = unpack('<L', self.data[proto_ids_off + 8 + (index * 12):proto_ids_off + 12 + (index * 12)])[0]
type_ids = []
dex_type_size = 0
if parameters_off != 0:
# parameters_off指向 DexTypeList结构
# 前四个字节是DexTypeItem结构的个数
dex_type_size = unpack('<L', self.data[parameters_off:parameters_off + 4])[0]
# 后面有dex_type_size个DexTypeItem
for i in range(dex_type_size):
type_idx = unpack('<H', self.data[parameters_off+4+(i*2):parameters_off+6+(i*2)])[0]
type_ids.append(type_idx)
proto_ids.append({'shorty_idx': shorty_idx,
'return_type_id': return_type_id,
'parameters': {'parameters_off': hex(parameters_off),
'dex_type_size': dex_type_size,
'type_ids': type_ids
}
})
return proto_ids_size, hex(proto_ids_off), proto_ids

结果:

1
2
3
4
5
6
7
proto_id个数为: 4	 
起始偏移为: 0xcc
内容为: [
{'shorty_idx': 3, 'return_type_id': 0, 'parameters': {'parameters_off': '0x1b4', 'dex_type_size': 2, 'type_ids': [0, 0]}},
{'shorty_idx': 8, 'return_type_id': 5, 'parameters': {'parameters_off': '0x0', 'dex_type_size': 0, 'type_ids': []}},
{'shorty_idx': 9, 'return_type_id': 5, 'parameters': {'parameters_off': '0x1bc', 'dex_type_size': 1, 'type_ids': [0]}},
{'shorty_idx': 10, 'return_type_id': 5, 'parameters': {'parameters_off': '0x1c4', 'dex_type_size': 1, 'type_ids': [6]}}]

2.6 DexFieldID

1
2
3
4
5
DexFieldID : {
classIdx, u2, 类的类型,指向TypeID
typeIdx, u2, 字段类型,指向TypeID
nameIdx, u4, 名称,指向StringID
}

解析代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 对 DexFieldId的解析,字段
# DexFieldId -> {classIdx, tyedIdx, nameIdx }
def get_field_ids(self):
field_ids = []
field_ids_size = self.header_data['field_ids_size']
field_ids_off = self.header_data['field_ids_off']

for index in range(field_ids_size):
# 类的类型,指向 DexTypeId 列表的索引
class_idx = unpack('<H', self.data[field_ids_off + (index * 8):field_ids_off + (index * 8) + 2])[0]
# 字段类型,指向 DexTypeId 列表的索引
type_idx = unpack('<H', self.data[field_ids_off + (index * 8) + 2:field_ids_off + (index * 8) + 4])[0]
# 字段名,指向 DexString 列表的索引
name_idx = unpack('<L', self.data[field_ids_off + (index * 8) + 4:field_ids_off + (index * 8) + 8])[0]
field_ids.append({'class_idx': class_idx, 'type_idx': type_idx, 'name_idx': name_idx})
return field_ids_size, hex(field_ids_off), field_ids

结果:

1
2
3
Field_id个数为: 1	 
起始偏移为: 0xfc
索引字段为: [{'class_idx': 4, 'type_idx': 2, 'name_idx': 14}]

2.7 DexMethodID

1
2
3
4
5
DexMethodId : {
classIDx, u2,指向TypeId,类声明
protoIdx, u2,指向TypeId,原型声明
nameIdx u4,指向StringId,名字
}

解析代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 对 DexMethodId的解析
# DexMethodIId -> {classIdx, protoIdx, nameIdx}
def get_method_ids(self):
methods = []
method_ids_size = self.header_data['method_ids_size']
method_ids_off = self.header_data['method_ids_off']

for index in range(method_ids_size):
class_idx = unpack('<L', self.data[method_ids_off+(index*12):method_ids_off+(index*12)+4])[0]
proto_idx = unpack('<L', self.data[method_ids_off+(index*12)+4:method_ids_off+(index*12)+8])[0]
name_idx = unpack('<L', self.data[method_ids_off+(index*12)+8:method_ids_off+(index*12)+12])[0]
methods.append({'class_idx': class_idx, 'proto_idx': proto_idx, 'name_idx': name_idx})
return method_ids_size, hex(method_ids_off), methods

结果:

1
2
3
4
5
6
7
8
Method 个数为5	 
起始偏移为:0x104
索引字段为: [
{'class_idx': 65537, 'proto_idx': 0, 'name_idx': 1},
{'class_idx': 12, 'proto_idx': 196609, 'name_idx': 13},
{'class_idx': 131074, 'proto_idx': 15, 'name_idx': 65539},
{'class_idx': 0, 'proto_idx': 1, 'name_idx': 1},
{'class_idx': 3, 'proto_idx': 0, 'name_idx': 1}]

2.8 DexClassID

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
DexClassId : {
classIdx, 类的类型,u4,指向TypeId
accessFlags, 访问标志,u4,见下图
superclassIdx, 父类类型,u4,指向TypeId
interfaceOff, 接口偏移,u4,指向DexTypeList偏移
sourceFileIdx, 源文件名,u4,指向stringId
annotationsOff, 注解区偏移,u4,指向 DexAnnotationsDirectoryItem
classDataOff, 类数据偏移,u4,指向 Dexclassdata
staticValuesOff 静态数据,u4,指向 DexEncodeArray
}
------------------------------------------------------------------------
interfaceOff --> DexTypeList : {
size, u4, 参数个数
DexTypeItem : {
idx u2,指向DexTypeId 的索引,接口类型
}
}
------------------------------------------------------------------------
##TODO 注解类的解析(实在是太多了)
------------------------------------------------------------------------
classDataOff --> DexClassData {
DexClassDataHeader header;
DexField* staticFields;
DexField* instanceFields;
DexMethod* directMethods;
DexMethod* virtualMethods;
};
header --> struct DexClassDataHeader {
u4 staticFieldsSize;
u4 instanceFieldsSize;
u4 directMethodsSize;
u4 virtualMethodsSize;
};
/* expanded form of encoded_field */
staticFields | instanceFields -->
struct DexField {
u4 fieldIdx; /* index to a field_id_item */
u4 accessFlags;
};
/* expanded form of encoded_method */
directMethods | virtualMethods
struct DexMethod {
u4 methodIdx; /* index to a method_id_item */
u4 accessFlags;
u4 codeOff; /* file offset to a code_item */
};

codeOff --> struct DexCode {
u2 registersSize;
u2 insSize;
u2 outsSize;
u2 triesSize;
u4 debugInfoOff; /* file offset to debug info stream */
u4 insnsSize; /* size of the insns array, in u2 units */
u2 insns[1];
/* followed by optional u2 padding */
/* followed by try_item[triesSize] */
/* followed by uleb128 handlersSize */
/* followed by catch_handler_item[handlersSize] */
};

解析代码:

仅仅解析到了每个字段的偏移,后面的内容以后有机会在详细整理,整理上结构梳理过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def get_class_defs(self):
class_defs = []
class_defs_size = self.header_data['class_defs_size']
class_defs_off = self.header_data['class_defs_off']
for index in range(class_defs_size):
class_idx = unpack('<L',self.data[class_defs_off+(index*32):class_defs_off+(index*32)+4])[0]
access_flags = unpack('<L',self.data[class_defs_off+(index*32)+4:class_defs_off+(index*32)+8])[0]
superclass_idx = unpack('<L',self.data[class_defs_off+(index*32)+8:class_defs_off+(index*32)+12])[0]
interfaces_off = unpack('<L',self.data[class_defs_off+(index*32)+12:class_defs_off+(index*32)+16])[0]
source_file_idx = unpack('<L',self.data[class_defs_off+(index*32)+16:class_defs_off+(index*32)+20])[0]
annotations_off = unpack('<L',self.data[class_defs_off+(index*32)+20:class_defs_off+(index*32)+24])[0]
class_data_off = unpack('<L',self.data[class_defs_off+(index*32)+24:class_defs_off+(index*32)+28])[0]
static_values_off = unpack('<L',self.data[class_defs_off+(index*32)+28:class_defs_off+(index*32)+32])[0]
class_defs.append({
'class_idx': class_idx,
'access': access_flags,
'superclass_idx': superclass_idx,
'interfaces_off': interfaces_off,
'source_file_idx': source_file_idx,
'annotation_off': annotations_off,
'class_data_off': class_data_off,
'static_values_off': static_values_off
})
return class_defs_size,hex(class_defs_off), class_defs

解析结果:

1
2
3
class_def_id个数为: 1	 
起始偏移为: 0x12c
内容为:[{'class_idx': 1, 'access': 1, 'superclass_idx': 3, 'interfaces_off': '0x0', 'source_file_idx': 1, 'annotation_off': '0x0', 'class_data_off': '0x27b', 'static_values_off': '0x0'}]

3 总结

花了几天,重新梳理了一遍Dex文件,大概是清楚了中间的一个流程,但是在后续学习中还有许多地方值得继续深入学习的,比如:

  • 为什么需要DexMapList数据结构来做一个map
  • classDef后面的数据区开始的位置存储的是什么
  • 签名验证的整个过程是什么样的
  • MultiDex的怎么分析
  • opcode解析过程理解

参考资料

[1] Android 软件安全权威指南

[2] 官方文档

[3] Dex文件结构学习,顺便还写了一个解析Dex文件的小程序.

[3] 51CTO上一篇关于dexfile相关源码的分析

[4] github上一个python写的dex文件解析器

[5] 浅谈 Android Dex 文件