作者:bigfog

原文链接:www.bigfog.info

前言

dex是安卓平台上Dalvik虚拟机的可执行文件,相当于windows平台的exe文件。java源文件通过java编译器生成class文件,再通过dx工具转换为classes.dex文件。整体是一个索引的结构,类名,方法名,字段名都存贮在常量池中,减少了存贮空间。为什么要学习dex文件格式呢?就像了解exe病毒分析时需要知道PE文件格式一样。现在越来越多的app都使用了加固技术以防止app被轻易反编译,当要对一个加固的恶意app分析就要了解dex文件格式。所以dex文件格式是基础中的基础。

文件基本结构

对于dex文件分析,最好用的工具就是010Editor了,因为他有模板可以分析相对应的结构。随意找一个apk,后缀改为zip后解压,将dex文件使用010Editor打开。然后使用模板进行分析,有些可能会报错(变量定义前使用local)。分析效果如下:

类型含义名称
文件头Header文件头
索引区String_ids字符串索引
索引区Type_ids类型的索引
索引区Proto_ids方法原型的索引
索引区Field_ids域的索引
索引区Method_ids方法的索引
数据区Class_dets类的定义区
数据区Data数据区
数据区Link_data链接数据区

这里再介绍一下dex使用到的数据结构

类型含义
u1uint8_t, 1字节的无符号数
u2uint16_t, 2字节的无符号数
u4uint32_t, 4字节的无符号数
u8uint64_t, 8字节的无符号数
sleb128有符号LEB128,可变长度1~5字节
uleb128无符号LEB128,可变长度1~5字节
uleb128p1无符号LEB128+1,可变长度1~5字节

Header文件头

header头记录了dex文件的一些基本信息,包括魔数字段,alder校验值,sha哈希值等,这三个字段在apk加壳中会用到。下面是详细解释:

字段名称描述
magic8个字节,dex 035 或者dex 036
checksum文件校验码(校验除去magic和checksum的所有文件区域)
signatureSHA-1哈希值uint32_t, 4字节的无符号数
filesizeDEX文件大小
headersizeheader大小,一般为0x70字节
endian tag指定运行环境的字节序,默认小端
linksize链接段的大小, 默认为0表示静态链接
linkoff链接段开始偏移
mapoffDexMapList的文件偏移
stringsize字符串列表中的字符串个数
stringidsoff字符串列表中的字符串偏移
typeIdsSize数据类型索引个数
typeIdsOff数据类型索引偏移
protoIdsSize方法原型个数
protoIdsOff方法原型位置偏移
fieldIdsSize字段列表中的个数
fieldIdsOff字段列表偏移
methodIdsSize方法列表中的个数
methodIdsOff方法列表偏移
classDefsSize类定义列表中的个数
classDefsOff类定义列表偏移
dataSize数据段的大小, 4字节对齐
dataOff数据段偏移

因为很多后半部分很多都是相对应的数据个数及地址偏移。这里就以StringSize和StringOff为例。从图上可以看到Stringsize的值为3672(十进制),也就是说有3672个字符串。打开String_ids索引区,可以看到:

而StringOff则保存着其偏移地址,以上图为例StringOff地址值为112(0x70)。我们来到0x70处,可以看到其保存的数据索引地址为0x2f9fc。那究竟0x2f9fc地址有没有保存数据呢?

正好和上文的匹配。其他的也是同样道理。

dex string ids索引段(字符串)

字符串索引区描述dex文件中所有的字符串信息

//Direct-mapped "string_id_item".
struct DexStringId 
{
    u4 stringDataOff;      //file offset to string_data_item
};

而stringDataOff是一个偏移地址,他指向数据结构item string data

struct string_data_item
{
uleb128 utf16_size;  这里用到了前面的LEB128 可变字节类型!
ubyte data;
}

这里对LEB128做一个简单介绍:LEB128是Dex中特有的用来存贮最大32位数的数据类型,其特点是由1-5个字节组成。每个字节的第一位用来表示是否用到下个字节,剩下的7位是有效位,所以第五个字节的首位要为0。有符号LEB128的符号由最后字节的有效位的最高位决定,即最后字节的第二位。

实现过程:

  1. 将一个整数用的二进制表示。
  2. 以四个为一组,在头部添加零,将其扩展为7的整数倍。
  3. 然后按照7位一组,从最低有效位到最高有效位方向,在每组的头部添加一位构成一个字节,最高有效位添加0。
  4. 然后将这组顺序进行反转,得到这一整数的LEB128编码。

举例如下:

0x98765=10011000011101100101 把它补成7的倍数010011000011101100101然后按7位分组0100110 0001110 1100101然后在每组前加一位1,第一组加0得出 00100110 10001110 11100101 也就是0x268ee5

dex type ids索引区(类名/类型名称字符串)

包括class类型,数组类型,基本类型等。其结构体描述如下:

struct type_ids_item
{
    uint descriptor_idx;
}

dex proto ids索引区(方法声明)

方法声明=返回类型+参数列表

struct DexProtoId 
{
    u4 shortyIdx;   /* 指向DexStringId列表的索引 */
    u4 returnTypeIdx;   /* 指向DexTypeId列表的索引 */
    u4 parametersOff;   /* 指向DexTypeList的偏移 */
}

shortyldx:方法声明字符串,由方法的返回类型与参数列表组成的一个字符串,且返回值类型位于参数列表之前。

returnTypeIdx:方法返回类型,指向DexTypeId列表。
parametersOff:指向一个DexTypeList结构体,存放了方法的参数类型。

struct DexTypeList {
    u4 size;             /* 接下来DexTypeItem的个数 */
    DexTypeItem list[1]; /* DexTypeItem结构 */
}

size:DexTypeItem结构的个数,参数数量。

list:指向size个方法的参数。

struct DexTypeItem {
    u2 typeIdx;    /* 指向DexTypeId列表的索引 */
}

typeIdx:指向DexTypeId列表,最终指向参数类型的字符串。如第三图61h(97)项在DexTypeId列表中正好指向”Landroid/content/Context;”类型字符串。

dex field ids索引区

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

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

classIdx:表示field所属的class类型

typeIdx:表示本field的类型
nameIdx:表示本field的名称

dex method ids索引区(方法)

该段索引了dex文件引用的所有方法。

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

classIdx:表示方法所属类的类型

protoIdx:表示方法的原型
nameIdx:表示方法的名称

dex class defs数据区(定义)

这个数据区结构比较复杂,有些数据直接指向了data区。

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结构的偏移 */
};

classIdx:表示类的类型。

accessFlags:类的访问标志,它是以ACC_开头的一个枚举值。
superclassIdx:父类类型索引值。
interfacesOff:如果类中含有接口声明或实现,interfaceOff会指向一个DexTypeList结构,否则这里的值为0。
sourceFileIdx:字符串索引值,表示类所在的源文件名称。
annotationsOff:指向注解目录结构,根据类型不同会有注解类、注解方法、注解字段与注解参数,如果类中没有注解,这里的值则为0。
classDataOff:指向DexClassData结构,它是类的数据部分。
staticValuesOff:指向DexEncodedArray结构,记录了类中的静态数据。
item class data结构体

struct DexClassDataHeader {
    u4 staticFieldsSize;  /* 静态字段个数 */
    u4 instanceFieldsSize; /* 实例字段个数 */
    u4 directMethodsSize;  /* 直接方法个数 */
    u4 virtualMethodsSize; /* 虚方法个数 */
};

总结

对于dex的学习还不够深,对最后一部分的分析不彻底。看了很多大佬们的博客,都写得很好。以后有时间再对dex文件格式进行研究学习。

个人拙见,如有错误可指正,欢迎留言交流。

参考链接:

https://blog.csdn.net/feglass/article/details/51761902
https://www.cnblogs.com/Cnforce/p/3817911.html