Dex文件是 Android 系统中特有的一种可执行文件格式,全称为 Dalvik Executable。它是 Android 应用程序中的 Java 字节码文件在打包过程中经过编译和优化后的产物,供 Android 虚拟机(Dalvik 或 ART)运行使用。
参考文章
Android逆向笔记 —— DEX 文件格式解析 – 知乎
Dalvik 可执行文件格式 | Android Open Source Project
Dex文件生成与运行
public class Hello {
private static String HELLO_WORLD = "Hello World!";
public static void main(String[] args) {
System.out.println(HELLO_WORLD);
}
}
首先 javac
编译成 Hello.class
文件,然后利用 Sdk 自带的 dx
工具生成 DEX 文件,dx 工具位于 Sdk 的 build-tools 目录下这里。
直接编译再利用dx生成Dex文件报错了,似乎是编译成class文件用的jdk版本太高的原因。参考这篇文章的方法
javac -target 1.6 -source 1.6 Hello.java
如果是低版本的Sdk是dx,但是高版本的Sdk的build-tools找不到dx了,好像变成d8了,不过d8我咋弄都报错。
可以利用adb运行Dex文件。
dalvikvm -cp ./Hello.dex Hello
Dex文件结构
这是网上广为流传的一图流
然后010 Editor有模板方便学习和查看结构
先给Dex文件分个层,分层图片如下
我能找到的源码的结构体如下,因为网上说的源码网站我上不去,在这个网站看的
https://android.googlesource.com/platform/dalvik/+/android-4.4.2_r2/libdex/DexFile.h
struct DexFile {
/* 直接映射的“opt”头部 */
const DexOptHeader* pOptHeader;
/* 指向基础 DEX 中直接映射的结构体和数组的指针 */
const DexHeader* pHeader; // DEX 文件头
const DexStringId* pStringIds; // 字符串标识符数组
const DexTypeId* pTypeIds; // 类型标识符数组
const DexFieldId* pFieldIds; // 字段标识符数组
const DexMethodId* pMethodIds; // 方法标识符数组
const DexProtoId* pProtoIds; // 原型标识符数组
const DexClassDef* pClassDefs; // 类定义数组
const DexLink* pLinkData; // 链接数据
/*
* 这些来自“辅助”部分(auxillary section),可能不会包含在 DEX 文件中。
*/
const DexClassLookup* pClassLookup; // 类查找表
const void* pRegisterMapPool; // 寄存器映射池(RegisterMapClassPool)
/* 指向 DEX 文件数据的起始位置 */
const u1* baseAddr;
/* 用于追踪辅助结构体的内存开销 */
int overhead;
/* 与该 DEX 关联的额外应用程序特定的数据结构 */
//void* auxData;
};
header
结构体如下
struct DexHeader{
u1 magic[8]; /*魔数字段,格式如"dex\n035\x00",其中035表示结构的版本*/
u4 checksum; /*校验码, 是小端序*/
u1 signature[kSHA1DigertLen]; /*SHA-1签名,长度20*/
u4 file_Size; /*Dex文件总长度*/
u4 header_Size; /*文件头长度,009版本=0x5C,035版本=0x70*/
u4 endian_Tag; /*标识字节顺序的常量,根据这个常量可以判断是否交换了字节顺序,缺省情况下=0x78563412*/
u4 link_Size; /*连接段大小,0表示静态链接*/
u4 link_Offset; /*连接段开始位置,从本文件头开始算起。 上为0此也为0*/
u4 map_Offset; /*map数据基地址*/
u4 string_ids_size; /*字符串列表字符串个数*/
u4 string_ids_off; /**/
u4 type_ids_size; /*类型列表里类型个数*/
u4 type_ids_offset; /**/
u4 proto_ids_size; /*原型列表里原型个数*/
u4 proto_ids_offset; /**/
u4 field_ids_size; /*字段列表里字段个数*/
u4 field_ids_offset; /**/
u4 method_ids_size; /*方法列表里方法个数*/
u4 method_ids_offset; /**/
u4 class_defs_size; /*类定义列表里类的个数*/
u4 class_defs_offset; /**/
u4 data_size; /*数据段大小,必须以4自己对齐*/
u4 data_offset; /**/
}
其中的 u 表示无符号数,u1 就是 8 位无符号数,u4 就是 32 位无符号数。
具体介绍一下一些比较复杂、重要的成员。
checksum 是对去除 magic 、 checksum 以外的文件部分作 alder32 算法得到的校验值,用于判断 DEX 文件是否被篡改。
signature 是对除去 magic 、 checksum 、 signature 以外的文件部分作 sha1 得到的文件哈希值,即去掉头0x20个字节。
file_Size指明整个dex文件的大小
DexHeader的大小固定为70个字节,即header_Size的值。
endianTag 用于标记 DEX 文件是大端表示还是小端表示。由于 DEX 文件是运行在 Android 系统中的,所以一般都是小端表示,这个值也是恒定值 0x12345678。
其余部分分别标记了 DEX 文件中其他各个数据结构的个数和其在数据区的偏移量。根据偏移量我们就可以轻松的获得各个数据结构的内容
string_ids
struct DexStringId {
u4 stringDataOff;
};
string_ids 是一个偏移量数组,stringDataOff 表示每个字符串在 data 区的偏移量。根据偏移量在 data 区拿到的数据中,第一个字节表示的是字符串长度,后面跟着的才是字符串数据。
指向的字符串中包含了变量名,方法名,文件名等等。
如图,这里字符串“HELLO_WORLD”的偏移为0x01BC
偏移指向的数据第一个是字符串的长度,字符串的结尾也同样有 ‘\0’
type_ids
struct DexTypeId {
u4 descriptorIdx;
};
type_ids 表示的是类型信息,descriptorIdx 的值为字符串索引,指向 string_ids 中的内容,通过字符串索引得到的值在 data 池中得到字符串,最后根据所得字符串解析对应类型。
这里举个例子,通过 descriptorIdx 的值 0x8 指向 string_ids 中的 string_id[8],通过 string_id[8] 的值找到字符串 ‘V’,字符串 ‘V’ 对应类型就是 void。
proto_ids
struct DexProtoId
{
u4 shortyIdx; //指向 DexStringId 列表的索引
u4 returnTypeIdx; //指向 DexTypeId 列表的索引
u4 parametersOff; //指向 DexTypeList 的偏移
};
proto_ids 表示方法声明信息,它包含以下三个变量:
- shortyIdx : 指向 string_ids ,表示方法声明的字符串
- returnTypeIdx : 指向 type_ids ,表示方法的返回类型的字符串
- parametersOff : 方法参数列表的偏移量,0代表没有参数
方法参数列表的数据结构在 DexFile.h 中用 DexTypeList 来表示:
struct DexTypeList {
u4 size; /* #of entries in list */
DexTypeItem list[1]; /* entries */
};
struct DexTypeItem {
u2 typeIdx; /* index into typeIds */
};
如图,得到获得 DexStringId、DexTypeId 列表的索引值 0xB、0x5 以及 DexTypeList 的偏移
所以方法声明为VL,返回值类型为void,参数列表为1个参数,类型为java.lang.String
fieId_ids
method_ids
由 DexMethodId 结构体对象组成,结构中的数据也均为索引值,指明了方法所在的类、方法的声明以及方法名,结构体定义如下:
struct DexMethodId
{
u2 classIdx; /* 类的类型,指向 DexTypeId 列表的索引 */
u2 protoIdx; /* 声明类型,指向 DexProtoId 列表的索引 */
u4 nameIdx; /* 方法名,指向 DexStringId 列表的索引 */
};
属性含义如下
- classIdx : 指向 type_ids ,表示类的类型
- protoIdx : 指向 type_ids ,表示方法声明
- nameIdx : 指向 string_ids ,表示方法名
class_def
由 DexClassDef 结构体组成,该结构体的声明如下
struct DexClassDef
{
u4 classIdx; /* 类的类型,指向 DexTypeId 列表的索引 */
u4 accessFlags; /* 访问标志 */
u4 superclassIdx; /* 父类类型,指向 DexTypeId 列表的索引 */
u4 interfacesOff; /* 接口,指向 DexTypeList 的偏移 */
u4 sourceFileIdx; /* 源文件名,指向 DexTypeStringId 列表的索引 */
u4 annotationsOff; /* 注解,指向 DexAnnotationsDirectoryItem 结构 */
u4 classDataOff; /* 指向 DexClassData 结构的偏移 */
u4 staticValuesOff; /* 指向 DexEncodedArray 结构的偏移 */
};
class_def 是 DEX 文件结构中最复杂也是最核心的部分,它表示了类的所有信息,对应 DexFile.h 中的 DexClassDef :
- classIdx : 指向 type_ids ,表示类信息
- accessFlags : 访问标识符
- superclassIdx : 指向 type_ids ,表示父类信息
- interfacesOff : 指向 DexTypeList 的偏移量,表示接口信息
- sourceFileIdx : 指向 string_ids ,表示源文件名称
- annotationOff : 注解信息
- classDataOff : 指向 DexClassData 的偏移量,表示类的数据部分
- staticValueOff :指向 DexEncodedArray 的偏移量,表示类的静态数据
accessFlags的定义可以在参考文章的官方网站中看
其中的 DexClassData 结构声明在 DexClass.h 文件中,声明如下
struct DexClassData
{
DexClassDataHeader header; // 指定字段与方法的个数
DexField* staticFields; // 静态字段,DexField 结构
DexField* instanceFields; // 实例字段,DexField 结构
DexMethod* directMethods; // 直接方法,DexMethod 结构
DexMethod* virtualMethods; // 虚方法,DexMethod 结构
};
//其中的 DexClassDataHeader 结构记录了当前类中字段与方法的数目,在 DexClass.h 文件中声明,声明如下
struct DexClassDataHeader
{
u4 staticFieldsSize; // 静态字段个数
u4 instanceFieldsSize; // 实例字段个数
u4 directMethodsSize; // 直接方法个数
u4 virtualMethodsSize; // 虚方法个数
};