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; // 虚方法个数
};
DexField 结构描述了字段的类型与访问标志,它的结构声明如下
struct DexField {
u4 fieldIdx; /* 指向 DexFieldId 的索引 */
u4 accessFlags;
};
DexMethod 结构描述方法的原型、名称、访问标志以及代码数据块,它的结构声明如下
struct DexMethod
{
u4 methodIdx; /* 指向 DexMethodId 的索引 */
u4 accessFlags; /* 访问标志 */
u4 codeOff; /* 指向 DexCode 结构的偏移 */
};
其中的 codeOff 字段指向了一个 DexCode 结构体,DexCode 结构体描述了方法更详细的信息以及方法中指令的内容,该结构体声明如下
struct DexCode {
u2 registersSize; // 使用的寄存器个数
u2 insSize; // 参数个数
u2 outsSize; // 调用其他方法时使用的寄存器个数
u2 triesSize; // try/catch 语句个数
u4 debugInfoOff; // 指向调试信息的偏移
u4 insnsSize; // 指令集的个数,以2字节为单位
u2 insns[1]; // 指令集
/* 2字节空间用于结构体对齐 */
/* try_item[triesSize] DexTry 结构 */
/* Try/Catch 中 handler 的个数 */
/* catch_handler_item[handlersSize],DexCatchHandler结构 */
};
这里也就是说来到有关代码指令的部分了,而那些反编译的软件,分析的也应该是这部分
这里表示该类有1个静态字段,3个直接方法
定位到主函数这里,使用的寄存器个数是 3 个。参数个数是 1 个,就是 main() 方法中的 String[] args。调用外部方法时使用的寄存器个数为 2 个。指令个数是 8 。
这里即是main() 方法的指令集
然后是官方文档,有关于字节码和助记符的对应关系
Dalvik 字节码格式 | Android Open Source Project
第一个指令 62 00 01 00,查询文档 62 对应指令为 sget-object vAA, field@BBBB,AA 对应 00 , 表示 v0 寄存器。BBBB 对应 01 00 ,表示 field_ids 中索引为 1 的字段,根据前面的解析结果该字段为 Ljava/lang/System;->out;Ljava/io/PrintStream,整理一下,62 00 01 00 表示的就是:
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
接着是 62 01 00 00。还是 sget-object vAA, field@BBBB, AA 对应 01 ,BBBB 对应 0000, 使用的是 v1 寄存器,field 位 field_ids 中索引为 0 的字段,即 LHello;->HELLO_WORLD;Ljava/lang/String,该句完整指令为:
sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;
接着是 6E 20 03 00, 查看文档 6E 指令为 invoke-virtual {vC, vD, vE, vF, vG}, meth@BBBB。6E 后面一个十六位 2 表示调用方法是两个参数,那么 BBBB 就是 03 00,指向 method_ids 中索引为 3 方法。根据前面的解析结果,该方法就是 Ljava/io/PrintStream;->println(Ljava/lang/String;)V。完整指令为:
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
最后的是return,main()方法就结束了
62 00 01 00 : sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
62 01 00 00 : sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;
6E 20 03 00 : invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
OE OO : return-void