Android Dex文件结构解析

时间:2021-08-19 17:16:31

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字节序。


linkSizelinkOff:指定链接段的大小与文件偏移,大多数情况下它们的值都为0。link_size:LinkSection大小,如果为0则表示该DEX文件不是静态链接。link_off用来表示LinkSection距离DEX头的偏移地址,如果LinkSize为0,此值也会为0。

mapOff:DexMapList结构的文件偏移。

stringIdsSizestringIdsOff:DexStringId结构的数据段大小与文件偏移。

typeIdsSizetypeIdsOff:DexTypeId结构的数据段大小与文件偏移。

protoIdsSizeprotoIdsSize:DexProtoId结构的数据段大小与文件偏移。

fieldIdsSizefieldIdsSize:DexFieldId结构的数据段大小与文件偏移。

methodIdsSizemethodIdsSize:DexMethodId结构的数据段大小与文件偏移。

classDefsSizeclassDefsOff:DexClassDef结构的数据段大小与文件偏移。

dataSizedataOff:数据段的大小与文件偏移。


下面我们来看某apk中classes.dex的解析结果,确实与上面的结构一致:

Android 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数组大小相符。

Android Dex文件结构解析

然后我们再来看下DexMapItem的结构。例如对于下图中的DexMapItem的第一项来说,type等于0说明其是kDexTypeHeaderItem类型的结构;unused一般都为0;size为1代表该结构仅有一个,即只有一个Dex文件头;offset为0代表Dex文件头从0h开始。

Android Dex文件结构解析

最后我们将所有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)项。经过我们的验证,以上分析是正确的。

Android Dex文件结构解析

Android Dex文件结构解析

Android Dex文件结构解析

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;”类型字符串。

Android Dex文件结构解析

Android Dex文件结构解析

Android Dex文件结构解析

Android Dex文件结构解析

6.DexFieldId区段(字段)

DexFieldId结构中的数据全部是索引值,指明了字段所在的类、字段的类型以及字段名。

struct DexFieldId {
u2 classIdx; /* 类的类型,指向DexTypeId列表的索引 */
u2 typeIdx; /* 字段类型,指向DexTypeId列表的索引 */
u4 nameIdx; /* 字段名,指向DexStringId列表的索引 */
};

如下图,可以看到字段所属类名为MTT.ThirdAppInfoNew,字段类型为int,字段名为iCoreType。

Android Dex文件结构解析

7.DexMethodId区段(方法)

DexMethodId结构中的数据全部是索引值,指明了方法所在的类、方法的声明以及方法名。

struct DexMethodId {
u2 classIdx; /* 类的类型,指向DexTypeId列表的索引 */
u2 protoIdx; /* 声明类型,指向DexProtoId列表的索引 */
u4 nameIdx; /* 方法名,指向DexStringId列表的索引 */
};

如下图,可以看到方法所属类为MTT.ThirdAppInfoNew,方法声明为V,方法名为。

Android Dex文件结构解析

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,表示类中没有静态数据。

Android Dex文件结构解析

  • DexClassData

    • header:一个DexClassDataHeader结构,指定字段与方法的个数。如下图中的staticFieldsSize为0,表示没有静态字段;instanceFieldsSize为0xC,表示有12个实例字段;directMethodsSize为0x1,表示有一个直接方法;virtualMethodsSize为0x0,表示没有虚方法。
      Android Dex文件结构解析

    • staticFields*:指向一个DexField结构,表示静态字段的类型与访问标志。由于本例没有静态字段,因此该结构无效。

    • directMethods*:指向一个DexField结构,表示实例字段的类型与访问标志。如下图本例中有一个实例字段。
      Android Dex文件结构解析
    • directMethods*:指向一个DexMethod结构,表示直接方法的原型、名称、访问标志、代码数据块。。如下图本例中有12个实例字段。 Android Dex文件结构解析

    • 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文件结构解析

参考文档

  1. 《Android软件安全与逆向分析》 非虫
  2. 《.dex — Dalvik Executable Format》 AOSP