Android Dex文件格式解析

时间:2024-04-03 09:59:53

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代码:
Android Dex文件格式解析
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!");
    }
}
Android Dex文件格式解析
将.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
Android Dex文件格式解析
      //Direct-mapped "type_item".
        struct DexTypeItem {
            u2  typeIdx;            //DexTypeId中的索引下标
        };
        //rect-mapped "type_list".
        struct DexTypeList {
            u4  size;               //DexTypeItem的个数
            DexTypeItem list[1];    //DexTypeItem变长数组
        };
Android Dex文件格式解析
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;                      //指定类型数据的文件偏移
        };
Android Dex文件格式解析
/* 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文件格式解析

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


------------------2-----------------------------

一段Dalvik汇编代码由一系列Dalvik指令组成,指令语法由指令的位描述与指令格式 标识来决定。位描述约定如下:

每16位的字采用空格分隔开来
每个字母表示四位,每个字母按顺序从高字节开始,排列到低字节。每四位之间可 能使用竖线“丨”来表示不同的内容
顺序采用A〜Z的单个大写字母作为一个4位的操作码,op表示一个8位的操作码
¢来表示这字段所有位为0值

以指令格式“A|G|op BBBB F|E|D|C”为例:
指令中间有两个空格,每个分开的部分大小为16位,所以这条指令由三个16位的字组 成。第一个16位是“A|G|op”,高8位由A与G组成,低字节由操作码op组成。第二个16 位由BBBB组成,它表示一个16位的偏移值。第三个16位分别由F、E、D、C共四个4 字节组成,在这里它们表示寄存器参数
单独使用位标识还无法确定一条指令,必须通过指令格式标识来指定指令的格式编码。 它的约定如下:
指令格式标识大多由三个字符组成,前两个是数字,最后一个是字母
第一个数字是表示指令有多少个16位的字组成
第二个数字是表示指令最多使用寄存器的个数。特殊标记“r”标识使用一定范围内的寄存器
第三个字母为类型码,表示指令用到的额外数据的类型

 

Android Dex文件格式解析

例子:22x
第一个数字2表示指令有两个16位字组成,第二个数字2表示指令使用到2个寄存器, 第三个字母x表示没有使用到额外的数据

Dalvik指令对语法约定
每条指令从操作码开始,后面紧跟参数,参数个数不定,每个参数之间采用逗号分开
每条指令的参数从指令第一部分开始,op位于低8位,高8位可以是一个8位的 参数,也可以是两个4位的参数,还可以为空,如果指令超过16位,则后面部分 依次作为参数
如果参数采用“vX”的方式表示,表明它是一个寄存器,如v0、v1等。这里采用v而不用r是为了避免与基于该虚拟机架构本身的寄存器命名产生冲突,如ARM 架构寄存器命名采用I开头
如果参数采用“#+X”的方式表示,表明它是一个常量数字
如果参数采用“+X”的方式表示,表明它是一个相对指令的地址偏移
如果参数采用“[email protected]”的方式表示,表明它是一个常量池索引值。其中kind表 示常量池类型,它可以是“string”(字符串常量池索引)、“type”(类型常量池索引)、“field”(字段常量池索引)或者“meth”(方法常量池索引)

例子:op vAA, [email protected]
指令用到了 1个寄存器参数vAA,并且还附加了一个字符串常量池索引[email protected]

Android 源码 Dalvik/docs 目录下提供了一份文档 insmction-formats.html,里面详细列举了Dalvik指令的所有格式
在Android Dex文件格式(二)中有详细的指令格式解析, 通过解析Hello.dex的指令格式还原出smali代码


------------------------------------3 ------------------------------------

第三块: 数据区
        索引区中的最终数据偏移以及文件头中描述的map_off偏移都指向数据区, 还包括了即将要解析的class_def_item, 这个结构非常重要,下面就开始解析
 
class_def_item:
        这个结构由dex文件头中的classDefsSize和classDefsOff所指向, 描述Dex文件中所有类定义信息, 每一个DexClassDef中包含一个DexClassData的结构(classDataOff),
        每一个DexClassData中包含了一个Class的数据, Class数据中包含了所有的方法, 方法中包含了该方法中的所有指令
Android Dex文件格式解析
   //Direct-mapped "class_def_item".
        struct DexClassDef {
            u4  classIdx;               //类的类型, DexTypeId中的索引下标
            u4  accessFlags;            //访问标志
            u4  superclassIdx;          //父类类型, DexTypeId中的索引下标
            u4  interfacesOff;          //接口偏移, 指向DexTypeList的结构
            u4  sourceFileIdx;          //源文件名, DexStringId中的索引下标
            u4  annotationsOff;         //注解偏移, 指向DexAnnotationsDirectoryItem的结构 
            u4  classDataOff;           //类数据偏移, 指向DexClassData的结构
            u4  staticValuesOff;        //类静态数据偏移, 指向DexEncodedArray的结构
        };
        
        struct DexClassData {
            DexClassDataHeader header;          //指定字段与方法的个数
            DexField*          staticFields;    //静态字段
            DexField*          instanceFields;  //实例字段
            DexMethod*         directMethods;   //直接方法
            DexMethod*         virtualMethods;  //虚方法
        };
        struct DexClassDataHeader {
            uleb128 staticFieldsSize;            //静态字段个数
            uleb128 instanceFieldsSize;          //实例字段个数
            uleb128 directMethodsSize;           //直接方法个数
            uleb128 virtualMethodsSize;          //虚方法个数
        };
        struct DexMethod {
            uleb128 methodIdx;                   //指向DexMethodId的索引
            uleb128 accessFlags;                 //访问标志
            uleb128 codeOff;                     //指向DexCode结构的偏移
        };
        struct DexCode {
            u2  registersSize;      使用的寄存器个数
            u2  insSize;            参数个数
            u2  outsSize;           调用其他方法时使用的寄存器个数
            u2  triesSize;          Try/Catch个数
            u4  debugInfoOff;       指向调试信息的偏移
            u4  insnsSize;          指令集个数, 以2字节为单位
            u2  insns[1];           指令集
            //followed by optional u2 padding
            //followed by try_item[triesSize]
            //followed by uleb128 handlersSize
            //followed by catch_handler_item[handlersSize]
        };
Android Dex文件格式解析
class_def_item:
Android Dex文件格式解析
index classIdx accessFlags superclassIdx interfacesOff sourceFileIdx annotationsOff classDataOff staticValuesOff
0 0x00 0x01 0x02 0x00 0x03 0x00 0x388 0x00
0_string LHello; public Ljava/lang/Object; no interfaces Hello.java no annotations    
DexClassData:
Android Dex文件格式解析
DexClassData的定义在源码的DexClass.h中, 在DexClass.h中的u4类型是uleb128类型, 在DexFile.h中的u4类型是unsigned int
所以这里DexClassDataHeader中的u4其实都是uleb128类型
  1. 在程序中,一般使用32位比特位来表示一个整型的数值。不过,一般能够使用到的整数值都不会太大,使用32比特位来表示就有点太浪费了。
  2. 对于普通计算机来说,这没什么问题,毕竟存储空间那么大。但是,对于移动设备来说,存储空间和内存空间都非常宝贵,不能浪费,能省就省。
每个leb128由1~5字节组成, 所有字节组合在一起表示一个32位的数据, 每个字节只有7位有效, 如果第1个字节的最高位为1, 表示leb128需要使用到第2个字节, 如果第2个字节的最高位为1, 表示需要使用到第3个字节, 以此类推直到最后的字节最高位为0, 当然, leb128最多只会使用到5个字节, 如果读取5个字节后下一个字节最高位仍为1, 则表示该dex无效
Android Dex文件格式解析
这张图表示了,只使用两个字节进行编码的情况。可以看到,第一个字节的最高位为1,代表还要用到接着的下一个字节。并且,第一个字节存放的是整型值的最低7位。而第二个字节的最高位为0,代表编码到此结束,剩下的7个比特位存放了整型值的高7位数据。
例如uleb128编码的数据:  c0 83 92 25
拆分为二进制
11000000 10000011 10010010 00100101
读取第1个字节0xc0(11000000), 其最高位为1, 表示需要继续读取第2个字节 (余下的7位: 1000000)
读取第2个字节0x83(10000011), 其最高位为1, 表示需要继续读取第3个字节 (余下的7位: 0000011)
读取第3个字节0x92(10010010), 其最高位为1, 表示需要继续读取第4个字节 (余下的7位: 0010010)
读取第4个字节0x25(00100101), 其最高位为0, 表示读取结束 (余下的7位: 0100101)
读取结束后, 按照读取的字节顺序将剩余的7位数据从右向左依次拼接起来即可:
Android Dex文件格式解析
最终拼得二进制数据: 0100101001001000000111000000
将其转换为十六进制数据: 0x4A481C0 即可得到当前uleb128编码的数据所表示的最终32位数据
在dalvik的源码中也有对uleb128数据读取的代码:
Android Dex文件格式解析
DEX_INLINE int readUnsignedLeb128(const u1** pStream) {  
    const u1* ptr = *pStream;  
    int result = *(ptr++);  
  
    if (result > 0x7f) {  
        int cur = *(ptr++);  
        result = (result & 0x7f) | ((cur & 0x7f) << 7);  
        if (cur > 0x7f) {  
            cur = *(ptr++);  
            result |= (cur & 0x7f) << 14;  
            if (cur > 0x7f) {  
                cur = *(ptr++);  
                result |= (cur & 0x7f) << 21;  
                if (cur > 0x7f) {  
                    cur = *(ptr++);  
                    result |= cur << 28;  
                }  
            }  
        }  
    }  
  
    *pStream = ptr;  
    return result;  
}  
Android Dex文件格式解析
index staticFieldsSize instanceFieldsSize directMethodsSize virtualMethodsSize
0 0 0 3 0
通过对DexClassDataHeader的解析, 得到有3个直接方法, 后面紧跟的就是DexMethod结构,该结构中的u4也是uleb128编码的
DexMethod:
Android Dex文件格式解析
index methodIdx accessFlags codeOff
0 0x0 0x10001 0x1B0
0_string void LHello;-><init>() public|constructor  
1 0x01 0x09 0x1C8
1_string void LHello;->MyPrint(Ljava/lang/String;) public|static  
2 0x01(看010Editor解析此处也是0x01) 0x09 0x210
2_string 但是字符串描述也是下表为0x02的信息 public|static  
DexMethod表中下标0中成员methonIdx所指向的Method信息:
Android Dex文件格式解析
DexMethod表中下标1中成员methonIdx所指向的Method信息:
Android Dex文件格式解析
DexMethod中的codeOff指向的是DexCode的结构,描述了方法更详细的信息以及方法中指令的内容, 在这个结构中会涉及到Dalvik指令格式的解析
可以先了解一下Dalvik指令格式: http://www.cnblogs.com/dacainiao/p/6035298.html
DexMethod_index registersSize insSize outsSize triesSize debugInfoOff insnsSize insns
0 0x01 0x01 0x01 0x00 0x372 0x04 0x70, 0x10, 0x05, 0x00, 0x00, 0x00, 0x0E, 0x00
1 0x04 0x01 0x03 0x00 0x377 0x1C 0x62, 0x00, 0x00, 0x00, 0x22, 0x01, 0x04, 0x00, 0x70, 0x10, 0x06, 0x00, 0x01, 0x00, 0x6E, 0x20, 0x07, 0x00, 0x31, 0x00, 0x0C, 0x01, 0x1A, 0x02, 0x00, 0x00, 0x6E, 0x20, 0x07, 0x00, 0x21, 0x00, 0x0C, 0x01, 0x6E, 0x10, 0x08, 0x00, 0x01, 0x00, 0x0C, 0x01, 0x12, 0x02, 0x23, 0x22, 0x07, 0x00, 0x6E, 0x30, 0x03, 0x00, 0x10, 0x02, 0x0E, 0x00
2 0x03 0x01 0x02 0x00 0x380 0x0D 0x1A, 0x00, 0x14, 0x00, 0x71, 0x10, 0x01, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x1A, 0x01, 0x02, 0x00, 0x6E, 0x20, 0x04, 0x00, 0x10, 0x00, 0x0E, 0x00
下面就开始解析这3个方法中每个方法的指令格式:
第一个方法 DexMethod_index: 0,  void LHello;-><init>()
其指令集为: 0x70, 0x10, 0x05, 0x00, 0x00, 0x00, 0x0E, 0x00
根据Dalvik指令格式介绍, 每16位的字采用空格分隔开来, 每个字母表示四位, 每条指令的参数从指令第一部分开始,op位于低8位,高8位可以是一个8位的 参数,也可以是两个4位的参数,还可以为空,如果指令超过16位,则后面部分 依次作为参数
所以上述指令格式可以划分为: 0x70, 0x10 | 0x05, 0x00 | 0x00, 0x00 | 0x0E, 0x00
指令集格式采用小尾方式, 第一部分的低8位是 0x70 (op)
拿到op位后,查看Dalvik的官方文档, 在dalvik\docs目录下  dalvik-bytecode.html 和 instruction-formats.html
查看dalvik-bytecode.html 对于0x70的定义
Android Dex文件格式解析
Android Dex文件格式解析
可以看到0x70表示为 invoke-direct, 而指令的格式编码为35c
Android Dex文件格式解析
Android Dex文件格式解析
  1. 单独使用位标识还无法确定一条指令,必须通过指令格式标识来指定指令的格式编码。 它的约定如下:
  2. 指令格式标识大多由三个字符组成,前两个是数字,最后一个是字母
  3. 第一个数字是表示指令有多少个16位的字组成
  4. 第二个数字是表示指令最多使用寄存器的个数。特殊标记“r”标识使用一定范围内的寄存器
  5. 第三个字母为类型码,表示指令用到的额外数据的类型
所以当前的指令格式编码为35c表示
该条指令占用了3个16位
该条指令最多使用5个寄存器
该条指令有常量池索引
既然占用了3个16位,所以先将该条指令的3个16位拷贝过来
0x70, 0x10 | 0x05, 0x00 | 0x00, 0x00
 
查看instruction-formats.html对于类型码35c的定义
Android Dex文件格式解析
Android Dex文件格式解析
 由于每个字母占用4位, 所以按照类型码35c的格式编码解析当前指令为
Android Dex文件格式解析
Android Dex文件格式解析
B = 1, A = 0, C = 5, D = 0, E = 0, F = 0, G = 0
可以看到后面[B = 1]对应的是 [B=1] op {vD}, [email protected]
由于D = 0, 所以 {vD) == {v0}
Android Dex文件格式解析
Android Dex文件格式解析
 Android Dex文件格式解析
Android Dex文件格式解析
C = 5, 在dalvik-bytecode.html 中对于0x70定义处有提到, C是方法列表的下标,占16位
所以[email protected] == void Ljava/lang/Object-->init()
最终该条指令解析为:
invoke-direct {v0},  Ljava/lang/Object-><init>V
然后继续解析剩下的指令: 0x0E, 0x00
还是一样的方法:
0x0E为低8位,所以为op位,查看dalvik-bytecode.html 关于0x0E的定义
Android Dex文件格式解析
Android Dex文件格式解析
 指令的格式编码为10x, 表示
该条指令占用了1个16位
该条指令最多使用0个寄存器
该条指令无额外数据
最终该条指令解析为:
return-void
所以,第一个方法,DexMethod_index: 0,  void LHello;-><init>(),的方法实现为:
invoke-direct {v0},  Ljava/lang/Object-><init>V
return-void
对照反编译工具的结果:
Android Dex文件格式解析
 Android Dex文件格式解析
 
第二个方法 DexMethod_index: 1,  void LHello;->MyPrint(Ljava/lang/String;)
其指令集为: 0x62, 0x00, 0x00, 0x00, 0x22, 0x01, 0x04, 0x00, 0x70, 0x10, 0x06, 0x00, 0x01, 0x00, 0x6E, 0x20, 0x07, 0x00, 0x31, 0x00, 0x0C, 0x01, 0x1A, 0x02, 0x00, 0x00, 0x6E, 0x20, 0x07, 0x00, 0x21, 0x00, 0x0C, 0x01, 0x6E, 0x10, 0x08, 0x00, 0x01, 0x00, 0x0C, 0x01, 0x12, 0x02, 0x23, 0x22, 0x07, 0x00, 0x6E, 0x30, 0x03, 0x00, 0x10, 0x02, 0x0E, 0x00
这里就不再赘述了, 都是按照上面的方法来解析的
0x62为op,62: sget-object   格式编码为 21c
2个16位(0x62, 0x00, 0x00, 0x00),最多1个寄存器, 常量表索引
Android Dex文件格式解析
Android Dex文件格式解析
 a = 0, b = 0, B: static field reference index (16 bits)
b是field中的下标
Android Dex文件格式解析
Android Dex文件格式解析
最终解释为:
sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream;
紧接着0x22为op,  22: new-instance vAA, [email protected], 格式编码为 21c
2个16位(0x22, 0x01, 0x04, 0x00),最多1个寄存器, 常量表索引
Android Dex文件格式解析
Android Dex文件格式解析
a = 1, b = 4
A: destination register (8 bits)
B: type index
Android Dex文件格式解析
Android Dex文件格式解析
最终解释为:
new-instance v1, Ljava/lang/StringBuilder;
紧接着0x70为op, 70: invoke-direct 格式编码为 35c
3个16位(0x70, 0x10, 0x06, 0x00, 0x01, 0x00), 最多5个寄存器,常量池索引
Android Dex文件格式解析
Android Dex文件格式解析
b = 1, a = 0, c = 6, f = 0, g = 0, e = 0, d =1
C: method index (16 bits)
Android Dex文件格式解析
[B=1] op {vD}, [email protected]
Android Dex文件格式解析
最终解析为:
invoke-direct {v1}, Ljava/lang/StringBuilder-><init>V
紧接着0x6E为op, 6e: invoke-virtual 格式编码为 35c
3个16位(0x6E, 0x20, 0x07, 0x00, 0x31, 0x00), 最多5个寄存器,常量池索引
Android Dex文件格式解析
Android Dex文件格式解析
b = 2, a = 0, c = 7, g = 0, f = 0, e = 3, d = 1
[B=2] op {vD, vE}, [email protected]
Android Dex文件格式解析
Android Dex文件格式解析
最终解析为:
invoke-virtual {v1, v3} Ljava/lang/StringBuilder->append( Ljava/lang/String;)Ljava/lang/StringBuilder
紧接着0c为op,  0c: move-result-object vAA  格式编码为11x
1个16位(0x0C, 0x01), 最多1个寄存器, 无额外数据
Android Dex文件格式解析
Android Dex文件格式解析
 b = 0, a = 1
如果参数采用“+X”的方式表示,表明它是一个相对指令的地址偏移
最终解析为:
move-result-object v1
 
紧接着1a为op, 1a: const-string vAA, [email protected]  格式编码为21c
2个16位(0x1A, 0x02, 0x00, 0x00),最多1个寄存器,常量池索引
a = 2, b = 0
A: destination register (8 bits)
B: string index
Android Dex文件格式解析
Android Dex文件格式解析
最终解析为:
const-string v2, "\r\n"
紧接着0x6E为op, 6e: invoke-virtual 格式编码为 35c
3个16位(0x6E, 0x20, 0x07, 0x00, 0x21, 0x00), 最多5个寄存器,常量池索引
Android Dex文件格式解析
Android Dex文件格式解析
b = 2, a = 0, c = 7, g = 0, f = 0, e = 2, d = 1
[B=2] op {vD, vE}, [email protected]
最终解析为:
invoke-virtual {v1, v2} Ljava/lang/StringBuilder->append( Ljava/lang/String;)Ljava/lang/StringBuilder
 
Dalvik指令解析方法大致就是这样,后面以此类推就可以,就不再继续解析了,解析这玩意太累。。。。
 
  1. 解析完dex之后我们有很多事都可以做了
  2. 1、我们可以检测一个apk中是否包含了指定系统的api(当然这些api没有被混淆),同样也可以检测这个apk是否包含了广告,以前我们可以通过解析AndroidManifest.xml文件中的serviceactivityreceivermeta等信息来判断,因为现在的广告sdk都需要添加这些东西,如果我们可以解析dex的话,那么我们可以得到他的所有字符串内容,就是string_ids池,这样就可以判断调用了哪些api。那么就可以判断这个apk的一些行为了,当然这里还有一个问题,假如dex加密了我们就蛋疼了。好吧,那就牵涉出第二件事了。
  3. 2、我们在之前说过如何对apk进行加固,其实就是加密apk/dex文件内容,那么这时候我们必须要了解dex的文件结构信息,因为我们先加密dex,然后在动态加载dex进行解密即可
  4. 3、我们可以更好的逆向工作,其实说到这里,我们看看apktool源码也知道,他内部的反编译原理就是这些,只是他会将指令翻译成smail代码,这个网上是有相对应的jarapi的,所以我们知道了dex的数据结构,那么原理肯定就知道了,同样还有一个dex2jar工具原理也是类似的
  5. 4、 .....等等等等
 
 

Android Dex文件格式学习笔记pdf版下载:

链接: http://pan.baidu.com/s/1eR745Rs

密码: qqqk

 

原文链接:https://www.cnblogs.com/dacainiao/p/6035274.html