Android Dex文件格式(一)

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)当前例子中字段值字段描述
magic0x00x8dex 035dex魔术字, 固定信息: dex\n035
checksum0x80x40x0F828C9Calder32算法, 去除了magic和checksum
字段之外的所有内容的校验码
signature0xc0x1458339636BED8A6CC826E
A09B77D5C3A620262CD
sha-1签名, 去除了magic、checksum和
signature字段之外的所有内容的签名
fileSize0x200x40x0000043C整个dex的文件大小
headerSize0x240x40x00000070整个dex文件头的大小 (固定大小为0x70)
endianTag0x280x40x12345678字节序 (大尾方式、小尾方式)
默认为小尾方式 <--> 0x12345678
linkSize0x2c0x40x00000000链接段的大小, 默认为0表示静态链接
linkOff0x300x40x00000000链接段开始偏移
mapOff0x340x40x0000039Cmap_item偏移
stringIdsSize0x380x40x00000019字符串列表中的字符串个数
stringIdsOff0x3c0x40x00000070字符串列表偏移
typeIdsSize0x400x40x00000009类型列表中的类型个数
typeIdsOff0x440x40x000000D4类型列表偏移
protoIdsSize0x480x40x00000006方法声明列表中的个数
protoIdsOff0x4c0x40x000000F8方法声明列表偏移
fieldIdsSize0x500x40x00000001字段列表中的个数
fieldIdsOff0x540x40x00000140字段列表偏移
methodIdsSize0x580x40x00000009方法列表中的个数
methodIdsOff0x5c0x40x00000148方法列表偏移
classDefsSize0x600x40x00000001类定义列表中的个数
classDefsOff0x640x40x00000190类定义列表偏移
dataSize0x680x40x0000028C数据段的大小, 4字节对齐
dataOff0x6c0x40x000001B0数据段偏移
第二块: 索引区
        索引区中索引了整个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:
indexstringDataOffutf16_sizedatastring
00x2520x020x0D, 0x0A , 0x00回车换行
10x2560x060x3C, 0x69, 0x6E, 0x69, 0x74, 0x3E, 0x00<init>
20x25E0x0C0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x00Hello World!
30x26C0x0A0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2E, 0x6A, 0x61, 0x76, 0x61, 0x00Hello.java
40x2780x010x4C, 0x00L
50x27B0x070x4C, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x3B, 0x00LHello;
60x2840x020x4C, 0x4C, 0x00LL
70x2880x030x4C, 0x4C, 0x4C, 0x00LLL
80x28D0x150x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x69, 0x6F, 0x2F, 0x50, 0x72, 0x69, 0x6E, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x3B, 0x00Ljava/io/PrintStream;
90x2A40x120x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x4F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x3B, 0x00Ljava/lang/Object;
100x2B80x120x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x3B, 0x00Ljava/lang/String;
110x2CC0x190x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x42, 0x75, 0x69, 0x6C, 0x64, 0x65, 0x72, 0x3B, 0x00Ljava/lang/StringBuilder;
120x2E70x120x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6D, 0x3B, 0x00Ljava/lang/System;
130x2FB0x070x4D, 0x79, 0x50, 0x72, 0x69, 0x6E, 0x74, 0x00MyPrint
140x3040x010x56, 0x00V
150x3070x020x56, 0x4C, 0x00VL
160x30B0x130x5B, 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x4F, 0x62, 0x6A, 0x65, 0x63, 0x74, 0x3B, 0x00  [Ljava/lang/Object;
170x3200x130x5B, 0x4C, 0x6A, 0x61, 0x76, 0x61, 0x2F, 0x6C, 0x61, 0x6E, 0x67, 0x2F, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x3B, 0x00  [Ljava/lang/String;
180x3350x060x61, 0x70, 0x70, 0x65, 0x6E, 0x64, 0x00append
190x33D0x040x6D, 0x61, 0x69, 0x6E, 0x00main
200x3430x0D0x6E, 0x69, 0x68, 0x61, 0x6F, 0x2C, 0x20, 0x73, 0x68, 0x69, 0x6A, 0x69, 0x65, 0x00nihao, shijie
210x3520x030x6F, 0x75, 0x74, 0x00out
220x3570x060x70, 0x72, 0x69, 0x6E, 0x74, 0x66, 0x00printf
230x35F0x070x70, 0x72, 0x69, 0x6E, 0x74, 0x6C, 0x6E, 0x00println
240x3680x080x74, 0x6F, 0x53, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x00toString
通过源码和字符串列表中的对比可以发现, 我们定义的类的类名, 成员函数名, 函数的参数类型, 字符串, 以及调用的系统函数的名和源码的文件名在字符串列表中都有对应的值
包括在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:
indexdescriptorIdxstring
00x05LHello;
10x08Ljava/io/PrintStream;
20x09Ljava/lang/Object;
30x0ALjava/lang/String;
40x0BLjava/lang/StringBuilder;
50x0CLjava/lang/System;
60x0EV
70x10[Ljava/lang/Object;
80x11[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:
indexshortyIdxreturnTypeIdxparametersOffshortyIdx_stringreturnTypeIdx_string
00x070x010x23CLLLLjava/io/PrintStream;
10x040x030x0LLjava/lang/String;
20x060x040x244LLLjava/lang/StringBuilder;
30x0E0x060x0VV
40x0F0x060x244VLV
50x0F0x060x24CVLV
type_list:
parametersOfftypeIdxstring
0x23C0x03Ljava/lang/String;
0x23C0x07[Ljava/lang/Object;
0x2440x03Ljava/lang/String;
0x24C0x08[Ljava/lang/String;
 
4. 字段索引区, 描述dex文件中所有的字段声明, 这个结构中的数据全部都是索引值, 指明了字段所在的类、字段的类型以及字段名称
 //Direct-mapped "field_id_item".
        struct DexFieldId {
            u2  classIdx;       类的类型, DexTypeId中的索引下标
            u2  typeIdx;        字段类型, DexTypeId中的索引下标      
            u4  nameIdx;        字段名称, DexStringId中的索引下标
        };

indexclassIdxtypeIdxnameIdxclassIdx_stringtypeIdx_stringnameIdx_string
00x050x010x15Ljava/lang/System;Ljava/io/PrintStream;out
 
5. 方法索引区, 描述Dex文件中所有的方法, 指明了方法所在的类、方法的声明以及方法名字
  //Direct-mapped "method_id_item".
        struct DexMethodId{
            u2  classIdx;           类的类型, DexTypeId中的索引下标
            u2  protoIdx;            声明类型, DexProtoId中的索引下标
            u4  nameIdx;            方法名, DexStringId中的索引下标
        };

indexclassIdprotoIdxnameIdxclassIdx_stringprotoIdx_stringnameIdx_string
00x000x030x01LHello;void()<init>
10x000x040x0DLHello;void(Ljava/lang/String;)MyPrint
20x000x050x13LHello;void([Ljava/lang/String;)main
30x0x10x000x16Ljava/io/PrintStream;Ljava/io/PrintStream;
(Ljava/lang/String;,
[Ljava/lang/Object;)
printf
40x010x040x17Ljava/io/PrintStream;void(Ljava/lang/String;)println
50x020x030x01Ljava/lang/Object;void()<init>
60x040x030x04Ljava/lang/StringBuilder;void()<init>
70x040x020x12Ljava/lang/StringBuilder;Ljava/lang/StringBuilder;
(Ljava/lang/String;)  
append
80x040x010x18Ljava/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,
};
复制代码

indextypeunusedsizeoffsettype_string
00x000x000x010x00kDexTypeHeaderItem
10x010x000x190x70kDexTypeStringIdItem
20x020x000x090xD4kDexTypeTypeIdItem
30x030x000x060xF8kDexTypeProtoIdItem
40x040x000x010x140kDexTypeFieldIdItem
50x050x000x090x148kDexTypeMethodIdItem
60x060x000x010x190kDexTypeClassDefItem
70x20010x000x030x1B0kDexTypeCodeItem
80x10010x000x030x23CkDexTypeTypeList
90x20020x000x190x252kDexTypeStringDataItem
100x20030x000x030x372kDexTypeDebugInfoItem
110x20000x000x010x388kDexTypeClassDataItem
120x10000x000x010x39CkDexTypeMapList
可以看到Dex文件头中的项与在DexMapList中存在的项的描述信息(个数和偏移)是一致的
当Android系统加载dex文件时,如果比较文件头类型个数与map里类型不一致时,就会停止使用这个dex文件
 
由于第三块数据区的内容比较多, 所以将Dex文件格式分为(一)(二)两个部分, 在第(二)部分中将对数据区进行详细的解析
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值