Android Dex文件格式(一)

时间:2023-02-04 20:27:50

dex是Android平台上(Dalvik虚拟机)的可执行文件, 相当于Windows平台中的exe文件, 每个Apk安装包中都有dex文件, 里面包含了该app的所有源码, 通过反编译工具可以获取到相应的java源码。

       为什么需要学习dex文件格式? 最主要的一个原因: 由于通过反编译dex文件可以直接看到java源码, 越来越多的app(包括恶意病毒app)都使用了加固技术以防止app被轻易反编译, 当需要对一个加固的恶意病毒app进行分析或对一个app进行破解时, 就需要了解dex文件格式, 将加固的dex文件还原后(脱壳)再进行反编译获取java源码, 所以要做Android安全方面的深入, dex文件格式是基础中的基础。
通过一个构建简单的dex文件, 来学习和了解dex文件的相关格式, 首先编写一段java代码:
public class Hello
{
    public static void MyPrint(String str)
    {
        System.out.printf(str + "\r\n");
    }

    public static void main(String[] argc)
    {
        MyPrint("nihao, shijie");
        System.out.println("Hello World!");
    }
}
将.java文件编译成.class文件:
  1. javac Hello.java
将.class文件编译成.dex文件:
  1. dx --dex --output=Hello.dex Hello.class
dx是Android SDK中继承的工具(dx.bat), 在SDK目录下 AndroidSDK\build-tools\19.1.0中(选择自己的安装版本, 这里就用19.1.0了)
Android Dex文件格式(一)
Android Dex文件格式(一)
如果在编译dex时, 出现图上的错误提示, 说明编译.class文件时使用的JDK版本太高了, 使用1.6版本的JDK就可以了, 重新生成.class文件, 然后再使用dx工具生成.dex文件即可:
javac -source 1.6 -target 1.6 Hello.java
可以将生成的dex放到Android的虚拟机中运行测试:
adb push Hello.dex /mnt/sdcard/
adb shell dalvikvm -cp /mnt/sdcard/Hello.dex Hello
Android Dex文件格式(一)
Android Dex文件格式(一)
进入正题, 先来看一张dex文件结构图, 来了解一个大概:
Android Dex文件格式(一)
Android Dex文件格式(一)
整个dex文件被分成了三个大块
第一块: 文件头
    文件头记录了dex文件的一些基本信息, 以及大致的数据分布. dex文件头部总长度是固定的0x70
dex_header:
Android Dex文件格式(一)
Android Dex文件格式(一)
字段名称 偏移量 长度(byte) 当前例子中字段值 字段描述
magic 0x0 0x8 dex 035 dex魔术字, 固定信息: dex\n035
checksum 0x8 0x4 0x0F828C9C alder32算法, 去除了magic和checksum
字段之外的所有内容的校验码
signature 0xc 0x14 58339636BED8A6CC826E
A09B77D5C3A620262CD
sha-1签名, 去除了magic、checksum和
signature字段之外的所有内容的签名
fileSize 0x20 0x4 0x0000043C 整个dex的文件大小
headerSize 0x24 0x4 0x00000070 整个dex文件头的大小 (固定大小为0x70)
endianTag 0x28 0x4 0x12345678 字节序 (大尾方式、小尾方式)
默认为小尾方式 <--> 0x12345678
linkSize 0x2c 0x4 0x00000000 链接段的大小, 默认为0表示静态链接
linkOff 0x30 0x4 0x00000000 链接段开始偏移
mapOff 0x34 0x4 0x0000039C map_item偏移
stringIdsSize 0x38 0x4 0x00000019 字符串列表中的字符串个数
stringIdsOff 0x3c 0x4 0x00000070 字符串列表偏移
typeIdsSize 0x40 0x4 0x00000009 类型列表中的类型个数
typeIdsOff 0x44 0x4 0x000000D4 类型列表偏移
protoIdsSize 0x48 0x4 0x00000006 方法声明列表中的个数
protoIdsOff 0x4c 0x4 0x000000F8 方法声明列表偏移
fieldIdsSize 0x50 0x4 0x00000001 字段列表中的个数
fieldIdsOff 0x54 0x4 0x00000140 字段列表偏移
methodIdsSize 0x58 0x4 0x00000009 方法列表中的个数
methodIdsOff 0x5c 0x4 0x00000148 方法列表偏移
classDefsSize 0x60 0x4 0x00000001 类定义列表中的个数
classDefsOff 0x64 0x4 0x00000190 类定义列表偏移
dataSize 0x68 0x4 0x0000028C 数据段的大小, 4字节对齐
dataOff 0x6c 0x4 0x000001B0 数据段偏移
第二块: 索引区
        索引区中索引了整个dex中的字符串、类型、方法声明、字段以及方法的信息, 其结构体的开始位置和个数均来自dex文件头中的记录(或通过map_list也可以索引到记录)
1. 字符串索引区, 描述dex文件中所有的字符串信息
    //Direct-mapped "string_id_item".
        struct DexStringId {
            u4 stringDataOff;      //file offset to string_data_item
        };
描述字符串索引的结构体为DexStringId, 里面只有一个成员是指向string_id_item结构的偏移, 在dalvik源码的doc文档(dex-format.html)中可以看到对该结构的描述
Android Dex文件格式(一)
Android Dex文件格式(一)
字符串列表中的字符串并非普通的ascii字符串, 它们是由MUTF-8编码表示的
MUTF-8为Modified UTF-8, 即经过修改的UTF-8编码, 有以下特点:
①. MUTF-8使用1~3字节编码长度
②. 大于16位的Unicode编码 U+10000~U+10ffff使用3字节来编码
③. U+0000采用2字节来编码
④. 采用类似于C语言中的空字符null作为字符串的结尾
string_id_item:
Android Dex文件格式(一)
Android Dex文件格式(一)
string_data_item:
Android Dex文件格式(一)
Android Dex文件格式(一)
index stringDataOff utf16_size data string
0 0x252 0x02 0x0D, 0x0A , 0x00 回车换行
1 0x256 0x06 0x3C, 0x69, 0x6E, 0x69, 0x74, 0x3E, 0x00 <init>
2 0x25E 0x0C 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x00 Hello World!
3 0x26C 0x0A 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2E, 0x6A, 0x61, 0x76, 0x61, 0x00 Hello.java
4 0x278 0x01 0x4C, 0x00 L
5 0x27B 0x07 0x4C, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x3B, 0x00 LHello;
6 0x284 0x02 0x4C, 0x4C, 0x00 LL
7 0x288 0x03 0x4C, 0x4C, 0x4C, 0x00 LLL
8 0x28D 0x15 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x69, 0x6F, 0x2F, 0x50, 0x72, 0x69, 0x6E, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x3B, 0x00 Ljava/io/PrintStream;
9 0x2A4 0x12 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x4F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x3B, 0x00 Ljava/lang/Object;
10 0x2B8 0x12 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x3B, 0x00 Ljava/lang/String;
11 0x2CC 0x19 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x42, 0x75, 0x69, 0x6C, 0x64, 0x65, 0x72, 0x3B, 0x00 Ljava/lang/StringBuilder;
12 0x2E7 0x12 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6D, 0x3B, 0x00 Ljava/lang/System;
13 0x2FB 0x07 0x4D, 0x79, 0x50, 0x72, 0x69, 0x6E, 0x74, 0x00 MyPrint
14 0x304 0x01 0x56, 0x00 V
15 0x307 0x02 0x56, 0x4C, 0x00 VL
16 0x30B 0x13 0x5B, 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x4F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x3B, 0x00   [Ljava/lang/Object;
17 0x320 0x13 0x5B, 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x3B, 0x00   [Ljava/lang/String;
18 0x335 0x06 0x61, 0x70, 0x70, 0x65, 0x6E, 0x64, 0x00 append
19 0x33D 0x04 0x6D, 0x61, 0x69, 0x6E, 0x00 main
20 0x343 0x0D 0x6E, 0x69, 0x68, 0x61, 0x6F, 0x2C, 0x20, 0x73, 0x68, 0x69, 0x6A, 0x69, 0x65, 0x00 nihao, shijie
21 0x352 0x03 0x6F, 0x75, 0x74, 0x00 out
22 0x357 0x06 0x70, 0x72, 0x69, 0x6E, 0x74, 0x66, 0x00 printf
23 0x35F 0x07 0x70, 0x72, 0x69, 0x6E, 0x74, 0x6C, 0x6E, 0x00 println
24 0x368 0x08 0x74, 0x6F, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x00 toString
Android Dex文件格式(一)
Android Dex文件格式(一)
通过源码和字符串列表中的对比可以发现, 我们定义的类的类名, 成员函数名, 函数的参数类型, 字符串, 以及调用的系统函数的名和源码的文件名在字符串列表中都有对应的值
包括在MyPrint函数中printf的参数 str + "\r\n", 实际被转换为StringBuilder.append的形式在字符串列表中也有所体现
了解了字符串列表中的信息后, 其实就可以实现一个dex的字符串混淆器了, 把当前有意义的字符串名称替换成像a b c这样无意义的名称
当前目前只依靠字符串列表就实现混淆是不够的, 因为里面包含了系统函数的名称(System.out.print、main等),像这样的系统函数是不能被混淆的,所以还需要借助其他索引区的信息将一些不能被混淆的字符串排除掉
2. 类型索引区, 描述dex文件中所有的类型, 如类类型、基本类型、返回值类型等
  //Direct-mapped "type_id_item".
        struct DexTypeId {
            u4  descriptorIdx;      //DexStringId中的索引下标
        };
描述类型索引的结构体为DexTypeId, 里面只有一个成员是指向字符串索引区的下标, 基本上结构体成员中以Idx结尾的都是某个索引列表的下标
type_id_item:
Android Dex文件格式(一)
Android Dex文件格式(一)
index descriptorIdx string
0 0x05 LHello;
1 0x08 Ljava/io/PrintStream;
2 0x09 Ljava/lang/Object;
3 0x0A Ljava/lang/String;
4 0x0B Ljava/lang/StringBuilder;
5 0x0C Ljava/lang/System;
6 0x0E V
7 0x10 [Ljava/lang/Object;
8 0x11 [Ljava/lang/String;

Android Dex文件格式(一)

Android Dex文件格式(一)
源码中的类类型、返回值类型在类型列表中都有对应的值, 在做dex字符串混淆的时间, 可以通过类型索引区过滤掉描述系统类类型、返回值类型的字符串,当然这还是不够的, 还需要借助其他索引区进行相应的排除

3. 方法声明索引区, 描述dex文件中所有的方法声明

      //Direct-mapped "proto_id_item".
        struct DexProtoId {
            u4  shortyIdx;          //DexStringId中的索引下标
            u4  returnTypeIdx;      //DexTypeId中的索引下标
            u4  parametersOff;      //DexTypeList的偏移
        };
shortyIdx为方法声明字符串
returnTypeIdx为方法返回类型字符串
parametersOff指向一个DexTypeList结构体, 存放了方法的参数列表, 如果方法没有参数值为0
      //Direct-mapped "type_item".
        struct DexTypeItem {
            u2  typeIdx;            //DexTypeId中的索引下标
        };
        //rect-mapped "type_list".
        struct DexTypeList {
            u4  size;               //DexTypeItem的个数
            DexTypeItem list[1];    //DexTypeItem变长数组
        };
proto_id_item:
Android Dex文件格式(一)
Android Dex文件格式(一)
type_list:
Android Dex文件格式(一)
Android Dex文件格式(一)
proto_it_item:
index shortyIdx returnTypeIdx parametersOff shortyIdx_string returnTypeIdx_string
0 0x07 0x01 0x23C LLL Ljava/io/PrintStream;
1 0x04 0x03 0x0 L Ljava/lang/String;
2 0x06 0x04 0x244 LL Ljava/lang/StringBuilder;
3 0x0E 0x06 0x0 V V
4 0x0F 0x06 0x244 VL V
5 0x0F 0x06 0x24C VL V

type_list:

parametersOff typeIdx string
0x23C 0x03 Ljava/lang/String;
0x23C 0x07 [Ljava/lang/Object;
0x244 0x03 Ljava/lang/String;
0x24C 0x08 [Ljava/lang/String;
4. 字段索引区, 描述dex文件中所有的字段声明, 这个结构中的数据全部都是索引值, 指明了字段所在的类、字段的类型以及字段名称
 //Direct-mapped "field_id_item".
        struct DexFieldId {
            u2  classIdx;       类的类型, DexTypeId中的索引下标
            u2  typeIdx;        字段类型, DexTypeId中的索引下标
            u4  nameIdx;        字段名称, DexStringId中的索引下标
        };

Android Dex文件格式(一)

Android Dex文件格式(一)
index classIdx typeIdx nameIdx classIdx_string typeIdx_string nameIdx_string
0 0x05 0x01 0x15 Ljava/lang/System; Ljava/io/PrintStream; out
5. 方法索引区, 描述Dex文件中所有的方法, 指明了方法所在的类、方法的声明以及方法名字
  //Direct-mapped "method_id_item".
        struct DexMethodId{
            u2  classIdx;           类的类型, DexTypeId中的索引下标
            u2  protoIdx;            声明类型, DexProtoId中的索引下标
            u4  nameIdx;            方法名, DexStringId中的索引下标
        };

Android Dex文件格式(一)

Android Dex文件格式(一)
index classId protoIdx nameIdx classIdx_string protoIdx_string nameIdx_string
0 0x00 0x03 0x01 LHello; void() <init>
1 0x00 0x04 0x0D LHello; void(Ljava/lang/String;) MyPrint
2 0x00 0x05 0x13 LHello; void([Ljava/lang/String;) main
3 0x0x1 0x00 0x16 Ljava/io/PrintStream; Ljava/io/PrintStream;
(Ljava/lang/String;,
[Ljava/lang/Object;)
printf
4 0x01 0x04 0x17 Ljava/io/PrintStream; void(Ljava/lang/String;) println
5 0x02 0x03 0x01 Ljava/lang/Object; void() <init>
6 0x04 0x03 0x04 Ljava/lang/StringBuilder; void() <init>
7 0x04 0x02 0x12 Ljava/lang/StringBuilder; Ljava/lang/StringBuilder;
(Ljava/lang/String;)  
append
8 0x04 0x01 0x18 Ljava/lang/StringBuilder; Ljava/lang/String;() toString
到此第二块索引区就解析完了, 可以看到解析的步骤非常简单,在解析第三块数据区之前, 补上一个MapList的解析,就是在dex文件头中map_off所指向的位置
这个DexMapList描述Dex文件中可能出现的所有类型, map_list和dex文件头中的有些数据是重复的, 但比dex文件头中要多, 完全是为了检验作用而存在的
 //Direct-mapped "map_list".
        struct DexMapList {
            u4  size;                       //DexMapItem的个数
            DexMapItem list[1];             //变长数组
        };
  struct DexMapItem {
            u2 type;                        //kDexType开头的类型
            u2 unused;                      //未使用, 用于字节对齐
            u4 size;                        //指定类型的个数
            u4 offset;                      //指定类型数据的文件偏移
        };
/* 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,
};

Android Dex文件格式(一)

Android Dex文件格式(一)
index type unused size offset type_string
0 0x00 0x00 0x01 0x00 kDexTypeHeaderItem
1 0x01 0x00 0x19 0x70 kDexTypeStringIdItem
2 0x02 0x00 0x09 0xD4 kDexTypeTypeIdItem
3 0x03 0x00 0x06 0xF8 kDexTypeProtoIdItem
4 0x04 0x00 0x01 0x140 kDexTypeFieldIdItem
5 0x05 0x00 0x09 0x148 kDexTypeMethodIdItem
6 0x06 0x00 0x01 0x190 kDexTypeClassDefItem
7 0x2001 0x00 0x03 0x1B0 kDexTypeCodeItem
8 0x1001 0x00 0x03 0x23C kDexTypeTypeList
9 0x2002 0x00 0x19 0x252 kDexTypeStringDataItem
10 0x2003 0x00 0x03 0x372 kDexTypeDebugInfoItem
11 0x2000 0x00 0x01 0x388 kDexTypeClassDataItem
12 0x1000 0x00 0x01 0x39C kDexTypeMapList
可以看到Dex文件头中的项与在DexMapList中存在的项的描述信息(个数和偏移)是一致的
当Android系统加载dex文件时,如果比较文件头类型个数与map里类型不一致时,就会停止使用这个dex文件
由于第三块数据区的内容比较多, 所以将Dex文件格式分为(一)(二)两个部分, 在第(二)部分中将对数据区进行详细的解析