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了)
如果在编译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

 
进入正题, 先来看一张dex文件结构图, 来了解一个大概:

整个dex文件被分成了三个大块
 
第一块: 文件头
    文件头记录了dex文件的一些基本信息, 以及大致的数据分布. dex文件头部总长度是固定的0x70
dex_header:
字段名称 偏移量 长度(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)中可以看到对该结构的描述

字符串列表中的字符串并非普通的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:
string_data_item:
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
通过源码和字符串列表中的对比可以发现, 我们定义的类的类名, 成员函数名, 函数的参数类型, 字符串, 以及调用的系统函数的名和源码的文件名在字符串列表中都有对应的值
包括在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:
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;

源码中的类类型、返回值类型在类型列表中都有对应的值, 在做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:
type_list:
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中的索引下标
        };

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中的索引下标
        };

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,
};

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文件格式分为(一)(二)两个部分, 在第(二)部分中将对数据区进行详细的解析

Android Dex文件格式(一)的更多相关文章

  1. Android Dex文件格式(二)

    第三块: 数据区         索引区中的最终数据偏移以及文件头中描述的map_off偏移都指向数据区, 还包括了即将要解析的class_def_item, 这个结构非常重要,下面就开始解析   c ...

  2. Android Dex文件格式解析

    Dex文件是Android虚拟机下的可执行文件,包含了应用程序所用到所有操作指令和运行时数据.在程序编译过程中,java源文件先被编译成class文件,然后通过dx工具将多个class文件整合为一个d ...

  3. dex文件格式一

    一.生成dex文件 我们可以通过java文件来生成一个简单的dex文件 编译过程: 首先编写java代码如下: (1) 编译成 java class 文件 执行命令 : javac Hello.jav ...

  4. dex文件格式学习

    一.dex文件的生成 我们可以通过java文件来生成一个简单的dex文件 编译过程: 首先编写java代码如下: (1) 编译成 java class 文件 执行命令 : javac Hello.ja ...

  5. [Android Security] DEX文件格式分析

    copy from : https://segmentfault.com/a/1190000007652937 0x00 前言 分析 dex 文件格式最好的方式是找个介绍文档,自己再写一个简单的 de ...

  6. Android逆向之旅---解析编译之后的Dex文件格式

    一.前言 新的一年又开始了,大家是否还记得去年年末的时候,我们还有一件事没有做,那就是解析Android中编译之后的classes.dex文件格式,我们在去年的时候已经介绍了: 如何解析编译之后的xm ...

  7. dex文件格式二

    一. dex文件头 (1) magic value 在DexFile.c   dexFileParse函数中 会先检查magic opt 啥是magic opt呢? 我们刚刚从cache目录拷贝出来的 ...

  8. Android学习笔记----解决“com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536”问题

    同时在工程中引入了多个第三方jar包,导致调用的方法数超过了android设定的65536个(DEX 64K problem),进而导致dex无法生成,也就无法生成APK文件. 解决办法如下: 1.谷 ...

  9. 解决“com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536”问题(l转)

    同时在工程中引入了多个第三方jar包,导致调用的方法数超过了android设定的65536个(DEX 64K problem),进而导致dex无法生成,也就无法生成APK文件. 解决办法如下: 1.谷 ...

随机推荐

  1. Linux学习笔记之——安装虚拟机后,如何启用网卡

    版本:CentOS-6.5-i386-minimal 虚拟机:vmware 11.1.2   安装完之后是看不到网卡信息的,如下:         我们编辑网卡etho的配置信息:         将 ...

  2. Django RedirectView

    RedirectView作用是重定向一个指定,给定的Url.这个给定的Url可能包含有字典风格的字符串,因为关键字(词)会被改变,所以从这个Url中捕获的参数可能也会被修改,例如,Url中的“%”应该 ...

  3. memset 的实现分析

    memset 是 msvcrt 中的一个函数,其作用和用途是显而易见的,通常是对一段内存进行填充,就其作用本身不具有任何歧义性.但就有人一定要纠结对数组的初始化一定要写成如下形式: int a[... ...

  4. WebStorm注册码

    WebStorm注册码User Name:EMBRACE License Key:===== LICENSE BEGIN =====24718-1204201000001h6wzKLpfo3gmjJ8 ...

  5. 如何成功运行一个最简单的servlet

    好吧,又是一个简单到不能再简单的问题~~ 由于各种原因,这次就不上图了,直接步骤和代码了. 1.前期准备 jdk.tomcat.EditPlus(eclipse)安装成功并且设置好环境变量. 2.由于 ...

  6. 使用Struts2搭建登录注册示例

    使用Struts2来搭建mvc网站框架还是比较容易的,Struts2提供了各项辅助功能,保证了web开发的快速方便.下面使用struts2来搭建一个登录注册示例. 0 项目结构截图 1 搭建Strut ...

  7. UML Sequence sample: if-else

    if (balance >= amount) { ... } else { ... }

  8. 剑指offer-二叉树的深度

    题目: 输入一棵二叉树,求该树的深度.从根结点到叶结点依次经过的结点(含根.叶结点)形成树的一条路径,最长路径的长度为树的深度. 链接: http://www.nowcoder.com/practic ...

  9. backup3

    private void changLayoutTemp2(IActiveView activeView, IPageLayout pageLayout, IPageLayout pTempPageL ...

  10. GitHub的.gitignore文件设置

    用Eclipse连接GitHub 在本地仓库(最上层文件夹)建立.gitignore文件后,所有子文件夹下对应文件或者文件夹在submit的时候就会被忽略. 我将Eclipse的workspace当作 ...