Java源文件通过Java编译器生成CLASS文件,再通过dx工具转换为classes.dex文件。
DEX文件从整体上来看是一个索引的结构,类名、方法名、字段名等信息都存储在常量池中,这样能够充分减少存储空间,相较于Java字节码文件更适合手机设备。
DEX文件的相关结构声明定义在/dalvik/libdex/DexFile.h文件中,下面我们先来看一下DEX文件中使用到的数据结构。
表1 dex文件使用到的数据结构
类型 | 含义 |
---|---|
u1 | 等同于uint8_t,表示1字节的无符号数 |
u2 | 等同于uint16_t,表示2字节的无符号数 |
u4 | 等同于uint32_t,表示4字节的无符号数 |
u8 | 等同于uint64_t,表示8字节的无符号数 |
sleb128 | 有符号LEB128,可变长度1~5字节 |
uleb128 | 无符号LEB128,可变长度1~5字节 |
uleb128p1 | 无符号LEB128加1,可变长度1~5字节 |
DEX文件的基本结构如下图所示:
header |
---|
string_ids |
type_ids |
proto_ids |
field_ids |
method_ids |
class_def |
data |
link_data |
图1 DEX文件结构
1. header(基本信息)
header是DEX文件头,包含magic字段、alder32校验值、SHA-1哈希值、string_ids的个数以及偏移地址等。DEX文件的头结构很固定,占用0x70个字节,具体定义代码如下所示(摘自DexFile.h):
/*
* Direct-mapped "header_item" struct.
*/
struct DexHeader {
u1 magic[8]; /* includes version number */
u4 checksum; /* adler32 checksum */
u1 signature[kSHA1DigestLen]; /* SHA-1 hash */
u4 fileSize; /* length of entire file */
u4 headerSize; /* offset to start of next section */
u4 endianTag;
u4 linkSize;
u4 linkOff;
u4 mapOff;
u4 stringIdsSize;
u4 stringIdsOff;
u4 typeIdsSize;
u4 typeIdsOff;
u4 protoIdsSize;
u4 protoIdsOff;
u4 fieldIdsSize;
u4 fieldIdsOff;
u4 methodIdsSize;
u4 methodIdsOff;
u4 classDefsSize;
u4 classDefsOff;
u4 dataSize;
u4 dataOff;
};
magic[8]:共8个字节。目前为固定值dex\n035。
checksum:文件校验码,使用alder32算法校验文件除去magic、checksum外余下的所有文件区域,用于检查文件错误。
signature:使用 SHA-1算法hash除去magic,checksum和signature外余下的所有文件区域 ,用于唯一识别本文件 。
fileSize:DEX文件的长度。
headerSize:header大小,一般固定为0x70字节。
endianTag:指定了DEX运行环境的cpu字节序,预设值ENDIAN_CONSTANT等于0x12345678,表示默认采用Little-Endian字节序。
linkSize和linkOff:指定链接段的大小与文件偏移,大多数情况下它们的值都为0。link_size:LinkSection大小,如果为0则表示该DEX文件不是静态链接。link_off用来表示LinkSection距离DEX头的偏移地址,如果LinkSize为0,此值也会为0。
mapOff:DexMapList结构的文件偏移。
stringIdsSize和stringIdsOff:DexStringId结构的数据段大小与文件偏移。
typeIdsSize和typeIdsOff:DexTypeId结构的数据段大小与文件偏移。
protoIdsSize和protoIdsSize:DexProtoId结构的数据段大小与文件偏移。
fieldIdsSize和fieldIdsSize:DexFieldId结构的数据段大小与文件偏移。
methodIdsSize和methodIdsSize:DexMethodId结构的数据段大小与文件偏移。
classDefsSize和classDefsOff:DexClassDef结构的数据段大小与文件偏移。
dataSize和dataOff:数据段的大小与文件偏移。
下面我们来看某apk中classes.dex的解析结果,确实与上面的结构一致:
2.DexMapList区段(大纲)
Dalvik虚拟机解析DEX文件的内容,最终将其映射成DexMapList数据结构,它实际上包含所有其他区段的结构大纲。DexHeader中的mapOff字段指明了DexMapList结构在DEX文件中的偏移。具体定义代码如下所示:
struct DexMapList {
u4 size; /* DexMapItem的个数 */
DexMapItem list[1]; /* DexMapItem的结构 */
};
struct DexMapItem {
u2 type; /* kDexType开头的类型 */
u2 unused; /* 未使用,用于字节对齐 */
u4 size; /* type指定类型的个数,它们在dex文件中连续存放 */
u4 offset; /* 指定类型数据的文件偏移 */
};
/* type字段为一个枚举常量,通过类型名称很容易判断它的具体类型。 */
/* map item type codes */
enum {
kDexTypeHeaderItem = 0x0000,
kDexTypeStringIdItem = 0x0001,
kDexTypeTypeIdItem = 0x0002,
kDexTypeProtoIdItem = 0x0003,
kDexTypeFieldIdItem = 0x0004,
kDexTypeMethodIdItem = 0x0005,
kDexTypeClassDefItem = 0x0006,
kDexTypeMapList = 0x1000,
kDexTypeTypeList = 0x1001,
kDexTypeAnnotationSetRefList = 0x1002,
kDexTypeAnnotationSetItem = 0x1003,
kDexTypeClassDataItem = 0x2000,
kDexTypeCodeItem = 0x2001,
kDexTypeStringDataItem = 0x2002,
kDexTypeDebugInfoItem = 0x2003,
kDexTypeAnnotationItem = 0x2004,
kDexTypeEncodedArrayItem = 0x2005,
kDexTypeAnnotationsDirectoryItem = 0x2006,
};
下面我们来看一下010Editor对某classes.dex文件的解析出的DexMapList结构。上面DexMapList结构中的size字段表示list数组的成员个数,即DexMapItem结构的数量:图中是11h,表示共有17个DexMapItem结构,与图中的list数组大小相符。
然后我们再来看下DexMapItem的结构。例如对于下图中的DexMapItem的第一项来说,type等于0说明其是kDexTypeHeaderItem类型的结构;unused一般都为0;size为1代表该结构仅有一个,即只有一个Dex文件头;offset为0代表Dex文件头从0h开始。
最后我们将所有DexMapItem结构整理成下表:
类型(type) | 个数(size) | 偏移(offset) |
---|---|---|
kDexTypeHeaderItem(0x0000) | 0x1 | 0x0 |
kDexTypeStringIdItem(0x0001) | 0xA115 | 0x70 |
kDexTypeTypeIdItem(0x0002) | 0x1D38 | 0x284C4 |
kDexTypeProtoIdItem(0x0003) | 0x2505 | 0x2F9A4 |
kDexTypeFieldIdItem(0x0004) | 0x9FB9 | 0x4B5E0 |
kDexTypeMethodIdItem(0x0005) | 0xC344 | 0x9B3A8 |
kDexTypeClassDefItem(0x0006) | 0x189D | 0xFCDC8 |
kDexTypeAnnotationSetItem(0x1003) | 0x10E0 | 0x12E168 |
kDexTypeCodeItem(0x2001) | 0x96DB | 0x138A34 |
kDexTypeAnnotationsDirectoryItem(0x2006) | 0xCE6 | 0x4A3EEC |
kDexTypeTypeList(0x1001) | 0x1620 | 0x4B9894 |
kDexTypeStringDataItem(0x2002) | 0xA115 | 0x4C74CA |
kDexTypeDebugInfoItem(0x2003) | 0x8FCC | 0x5C8544 |
kDexTypeAnnotationItem(0x2004) | 0x101C | 0x63FBC1 |
kDexTypeEncodedArrayItem(0x2005) | 0x10E | 0x653536 |
kDexTypeClassDataItem(0x2000) | 0x184F | 0x65B97A |
kDexTypeMapList(0x1000) | 0x1 | 0x6B8828 |
可以看出,其中区段的offset与header中的off是完全相等的。
3.DexStringId区段(字符串)
struct DexStringId {
u4 stringDataOff; /* 字符串数据偏移 */
}
DexStringId结构只有一个stringDataOff字段,直接指向字符串数据。这个区段中包含了DEX文件中用到的所有字符串。
4.DexTypeId区段(类名/类型名称字符串)
struct DexTypeId {
u4 descriptorIdx; /* 指向 DexStringId列表的索引 */
};
descriptorIdx为指向DexStringId列表的索引,它对应的字符串代表了具体类的类型(DEX文件中用到的所有基本数据类型和类的名称)。如下图中的第一项值为0xAEB,表示其是DexStringId中第0xAEB(2795)项;而第8项值为0x1969,表示其是DexStringId中第0x1969(6505)项。经过我们的验证,以上分析是正确的。
5.DexProtoId区段(方法声明=返回类型 + 参数列表)
struct DexProtoId {
u4 shortyIdx; /* 指向DexStringId列表的索引 */
u4 returnTypeIdx; /* 指向DexTypeId列表的索引 */
u4 parametersOff; /* 指向DexTypeList的偏移 */
}
struct DexTypeList {
u4 size; /* 接下来DexTypeItem的个数 */
DexTypeItem list[1]; /* DexTypeItem结构 */
};
struct DexTypeItem {
u2 typeIdx; /* 指向DexTypeId列表的索引 */
};
下面结合实例进行分析:
-
DexProtoId
shortyIdx:方法声明字符串,具体而言是由方法的返回类型与参数列表组成的一个字符串,并且返回类型位于参数列表的前面。如“III”“V”“VI”“VL”等。在下图的三个方法声明中分别为B、BL、DL。
returnTypeIdx:方法返回类型,指向DexTypeId列表。下图的分别为byte、byte、double。
parametersOff:指向一个DexTypeList结构体,存放了方法的参数类型。下图分别为0、0x4BA78C、0x4BA7BC。值为0表示参数为void。
-
DexTypeList
- size:DexTypeItem的个数,即参数的数量。下图分别为?、1、1,表示后两个方法都只有一个参数。
- list:指向size个DexTypeItem项,每一项代表方法的一个参数。
-
DexTypeItem
- typeIdx:指向DexTypeId列表,最终指向参数类型的字符串。如第三图61h(97)项在DexTypeId列表中正好指向”Landroid/content/Context;”类型字符串。
6.DexFieldId区段(字段)
DexFieldId结构中的数据全部是索引值,指明了字段所在的类、字段的类型以及字段名。
struct DexFieldId {
u2 classIdx; /* 类的类型,指向DexTypeId列表的索引 */
u2 typeIdx; /* 字段类型,指向DexTypeId列表的索引 */
u4 nameIdx; /* 字段名,指向DexStringId列表的索引 */
};
如下图,可以看到字段所属类名为MTT.ThirdAppInfoNew,字段类型为int,字段名为iCoreType。
7.DexMethodId区段(方法)
DexMethodId结构中的数据全部是索引值,指明了方法所在的类、方法的声明以及方法名。
struct DexMethodId {
u2 classIdx; /* 类的类型,指向DexTypeId列表的索引 */
u2 protoIdx; /* 声明类型,指向DexProtoId列表的索引 */
u4 nameIdx; /* 方法名,指向DexStringId列表的索引 */
};
如下图,可以看到方法所属类为MTT.ThirdAppInfoNew,方法声明为V,方法名为。
8.DexTypeClassDefItem(类定义)
struct DexClassDef {
u4 classIdx; /* 类的类型,指向DexTypeId列表的索引 */
u4 accessFlags; /* 访问标志 */
u4 superclassIdx; /* 父类类型,指向DexTypeId列表的索引 */
u4 interfacesOff; /* 接口,指向DexTypeList的偏移 */
u4 sourceFileIdx; /* 源文件名,指向DexStringId列表的索引 */
u4 annotationsOff; /* 注解,指向DexAnnotationsDirectoryItem结构 */
u4 classDataOff; /* 指向DexClassData结构的偏移 */
u4 staticValuesOff; /* 指向DexEncodedArray结构的偏移 */
};
struct DexClassData {
DexClassDataHeader header; /* 指定字段与方法的个数 */
DexField* staticFields; /* 静态字段,DexField结构 */
DexField* instanceFields; /* 实例字段,DexField结构 */
DexMethod* directMethods; /* 直接方法,DexMethod结构 */
DexMethod* virtualMethods; /* 虚方法,DexMethod结构 */
struct DexClassDataHeader {
u4 staticFieldsSize; /* 静态字段个数 */
u4 instanceFieldsSize; /* 实例字段个数 */
u4 directMethodsSize; /* 直接方法个数 */
u4 virtualMethodsSize; /* 虚方法个数 */
};
struct DexField {
u4 fieldIdx; /* 指向DexFieldId的索引 */
u4 accessFlags; /* 访问标志 */
};
struct DexMethod {
u4 methodIdx; /* 指向DexMethodId的索引 */
u4 accessFlags; /* 访问标志 */
u4 codeOff; /* 指向DexCode结构的偏移 */
};
struct DexCode {
u2 registersSize; /* 使用的寄存器个数 */
u2 insSize; /* 参数个数 */
u2 outsSize; /* 调用其他方法时使用的寄存器个数 */
u2 triesSize; /* Try/Catch个数 */
u4 debugInfoOff; /* 指向调试信息的偏移 */
u4 insnsSize; /*指令集个数,以2字节为单位 */
u2 insns[1]; /* 指令集 */
-
DexClassDef
- classIdx:索引值,表明类的类型。下图中值为0x6,指向类型MTT.ThirdAppInfoNew。
- accessFlags:类的访问标志,它是以ACC_开头的一个枚举值。具体定义可以参考这里。下图中值为0x11,表示同时具有ACC_PUBLIC和ACC_FINAL。
- superclassIdx:父类类型索引值。下图中值为0x1B13,指向java.lang.Object类。
- interfacesOff:如果类中含有接口声明或实现,interfaceOff会指向一个DexTypeList结构,否则这里的值为0。图中值为0x4B9894(4954260),指向的DexTypeList结构为java.lang.Cloneable。
- sourceFileIdx:字符串索引值,表示类所在的源文件名称。图中值为NO_INDEX(0xffffffff),表示该值丢失。
- annotationsOff:指向注解目录结构,根据类型不同会有注解类、注解方法、注解字段与注解参数,如果类中没有注解,这里的值则为0。图中值为0,表示类中没有注解。
- classDataOff:指向DexClassData结构,它是类的数据部分。图中为0x65B97A。
- staticValuesOff:指向DexEncodedArray结构,记录了类中的静态数据。图中为0,表示类中没有静态数据。
-
DexClassData
header:一个DexClassDataHeader结构,指定字段与方法的个数。如下图中的staticFieldsSize为0,表示没有静态字段;instanceFieldsSize为0xC,表示有12个实例字段;directMethodsSize为0x1,表示有一个直接方法;virtualMethodsSize为0x0,表示没有虚方法。
staticFields*:指向一个DexField结构,表示静态字段的类型与访问标志。由于本例没有静态字段,因此该结构无效。
-
directMethods*:指向一个DexField结构,表示实例字段的类型与访问标志。如下图本例中有一个实例字段。
directMethods*:指向一个DexMethod结构,表示直接方法的原型、名称、访问标志、代码数据块。。如下图本例中有12个实例字段。
virtualMethods*:指向一个DexMethod结构,表示虚方法的原型、名称、访问标志、代码数据块。由于本例没有静态字段,因此该结构无效。
-
DexField
- fieldIdx:指向DexFieldId的索引,表示字段的所属类、字段类型和字段名。
- accessFlags:访问标志。
-
DexMethod
- methodIdx:指向DexMethodId的索引,表示方法的所在类、方法的声明和方法名。
- accessFlags:访问标志。
- codeOff:指向DexCode结构的偏移,图中为0x138A34。
-
DexCode
- registersSize:该方法使用的寄存器个数。下图中为3。
- insSize:该方法的参数个数,对应smali语法中的”.register”指令。下图中为1。
- outsSize:该方法调用其他方法时,对应smali语法中的”.paramter”指令。例如现在有一个方法,使用了5个寄存器,其中有2个为参数,而该方法调用了另一个方法,后者使用了20个寄存器,那么Dalvik虚拟机在分配时,会在分配自身方法寄存器空间时加上那20个寄存器空间。下图中为1。
- triesSize:方法中Try/Catch的个数。
- debugInfoOff:如果dex文件保留了调试信息,debugInfoOff字段会指向它。
- insnsSize:指令个数,以2字节为单位。
- insns[1]:真正的指令部分。
参考文档
- 《Android软件安全与逆向分析》 非虫
- 《.dex — Dalvik Executable Format》 AOSP