首页
论坛
课程
招聘
[原创]dex起步探索
2021-7-14 21:49 15180

[原创]dex起步探索

2021-7-14 21:49
15180

参考

部分内容摘自:android软件安全权威指南:丰生强

从根源上搞懂基础的原理是很有必要的,这样有助于我们更方便的利用它的特性,达到我们的目的。

我把内容主要分为二个部分。原理探索、案例分析

原理探索

dex文件

我们在正向开发app编译时,编写的java代码,会编译成java字节码保存在.class后缀的文件中。然后再用dx工具将java字节码转换成dex文件(Dalvik字节码)。在转换的过程中,会将所有java字节码中的所有冗余信息组成一个常量池。例如多个class文件中都存在的字符串"hello world"。转换后将单独存放在一个地方,并且所有类共享。包括方法的签名也会组成常量池。我们将编译好的apk文件解压后就能拿到classes.dex文件。

dex文件格式

1、DexFile结构

上面拿到的classes.dex文件包含了apk的可执行代码。Dalvik虚拟机会解析加载文件并执行代码。只要我们了解这个文件格式的组成,那么就可以自己解析这个文件获取到想要的数据。

 

首先是安卓源码中的dalvik/libdex/DexFile.h这里可以找到dex文件的数据结构。下面贴上源码部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct DexFile {
    /* odex的头 */
    const DexOptHeader* pOptHeader;
 
    /* dex文件头,指定了dex文件的一些数据,记录了其他数据结构在dex文件中的物理偏移 */
    const DexHeader*    pHeader;
      /* 索引结构区 */
    const DexStringId*  pStringIds;
    const DexTypeId*    pTypeIds;
    const DexFieldId*   pFieldIds;
    const DexMethodId*  pMethodIds;
    const DexProtoId*   pProtoIds;
      /* 真实的数据存放 */
    const DexClassDef*  pClassDefs;
      /* 静态链接数据区 */
    const DexLink*      pLinkData;
    /*
     * These are mapped out of the "auxillary" section, and may not be
     * included in the file.
     */
    const DexClassLookup* pClassLookup;
    const void*         pRegisterMapPool;       // RegisterMapClassPool
 
    /* points to start of DEX file data */
    const u1*           baseAddr;
 
    /* track memory overhead for auxillary structures */
    int                 overhead;
 
    /* additional app-specific data structures associated with the DEX */
    //void*               auxData;
};

为了更好的理解。dex文件格式,我们可以用010编辑器打开一个dex文件对照这个结构体来观察一下。

 

 

可以看到,这个dex文件由这8个部分组成。

 

dex_header:dex文件头,指定了dex文件的一些数据,记录了其他数据结构在dex文件中的物理偏移

 

string_ids:字符串列表(前面说的去掉冗余信息组成的常量池,全局共享使用的)

 

type_ids:类型签名列表(去掉冗余信息组成的常量池)

 

proto_ids:方法声明列表(去掉冗余信息组成的常量池)

 

field_ids:字段列表(去掉冗余信息组成的常量池)

 

method_ids:方法列表(去掉冗余信息组成的常量池)

 

class_def:类型结构体列表(去掉冗余信息组成的常量池)

 

map_list:这里记录了前面7个部分的偏移和大小。

 

然后我们开始逐个的看各个部分的结构。

2、dex_header

先是贴上源码看看这个部分的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct DexHeader {
    u1  magic[8];           /* 表示是一个有效的dex文件。值一般固定为64 65 78 0A 30 33 35 00(dex.035*/
    u4  checksum;           /* adler32 checksum dex文件的校验和,用来判断文件是否已经损坏或者篡改 */
    u1  signature[kSHA1DigestLen]; /* SHA-1 hash 用来识别未经dexopt优化的dex文件*/
    u4  fileSize;           /* length of entire file 记录了包括dexHeader在内的整个dex文件的大小*/
    u4  headerSize;         /* offset to start of next section  dexHeader占用的字节数,一般都是0x70*/
    u4  endianTag;                    /* 指定dex运行环境的cpu字节序。预设是ENDIAN_CONSTANT等于0x12345678,也就是默认小端字节序 */
    u4  linkSize;                        /* 链接段的大小 */
    u4  linkOff;                        /* 链接段的偏移 */
    u4  mapOff;                            /* DexMapList结构的文件偏移 */
    u4  stringIdsSize;            /* 下面都是数据段的大小和文件偏移 */
    u4  stringIdsOff;
    u4  typeIdsSize;
    u4  typeIdsOff;
    u4  protoIdsSize;
    u4  protoIdsOff;
    u4  fieldIdsSize;
    u4  fieldIdsOff;
    u4  methodIdsSize;
    u4  methodIdsOff;
    u4  classDefsSize;
    u4  classDefsOff;
    u4  dataSize;
    u4  dataOff;
};

这里可以看到,如果在DexHeader中,可以找到其他部分的偏移和大小,以及整个文件的大小,解析出这块数据,其他部分的任意数据,我们都可以获取到。然后再使用对应的结构体来解析。另外留意这里DexHeader的结构体的大小是固定0x70字节的。所以有的脱壳工具中会将70 00 00 00来作为特征在内存中查找dex进行脱壳(比如FRIDA-DEXDump的深度检索)

 

然后我们贴一下真实classes.dex文件的DexHeader数据是什么样的。

 

3、string_ids

先看看字符串列表的结构体,非常简单,就是字符串的偏移,但是并不是普通的ascii字符串,而是MUTF-8编码的。这个是一个经过修改的UTF-8编码。和传统的UTF-8相似

1
2
3
struct DexStringId {
    u4 stringDataOff;      /* 字符串数据偏移 */
};

4、type_ids

类型签名列表的结构体也是非常简单,和上面字符串列表差不多

1
2
3
struct DexTypeId {
    u4  descriptorIdx;      /* index into stringIds list for type descriptor */
};

真实数据图如下,可以看到值类型签名都在前面,后面都是引用类型签名。

 

5、proto_ids

方法声明的列表的结构体较为复杂,因为方法签名必然是有几点信息构成:返回值类型、参数类型列表(就是每个参数是什么类型)。方法声明的结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
struct DexTypeList {
    u4  size;               /* dexTypeItem的个数 */
    DexTypeItem list[1];    /* entries */
};
struct DexTypeItem {
    u2  typeIdx;            /* DexTypeId的索引 */
};
struct DexProtoId {
    u4  shortyIdx;          /* DexStringId列表的索引,方法签名字符串,由返回值和参数类型列表组合*/
    u4  returnTypeIdx;      /* DexTypeId的索引,返回值的类型 */
    u4  parametersOff;      /* 指向DexTypeList的偏移,参数类型列表 */
};

同样看看这个结构的真实数据

 

6、field_ids

字段描述的结构体,我们可以先想象一下,要找一个字段,我们需要些什么:字段所属的类,字段的类型,字段名称。有这些信息,就可以找到各自对应的字段了。接下来看看定义的结构体

1
2
3
4
5
struct DexFieldId {
    u2  classIdx;           /* 类的类型,指向DexTypeId的索引,字段所属的类 */
    u2  typeIdx;            /* 字段类型,指向DexTypeId的索引,字段的类型 */
    u4  nameIdx;            /* 字段名,指向DexStringId的索引,字段的名称 */
};

然后看一段真实数据

 

7、method_ids

方法描述的结构体,同样先了解找一个方法的几个必须项:方法所属的类,方法的签名(签名中有方法的返回值和方法的参数,也就是上面的proto_ids中记录的),方法的名称。然后下面看结构体

1
2
3
4
5
struct DexMethodId {
    u2  classIdx;           /* 类的类型,指向DexTypeId的索引,方法所属的类 */
    u2  protoIdx;           /* 声明类型,指向DexProtoId的索引,方法的签名 */
    u4  nameIdx;            /* 方法名,指向DexStringId索引,方法的名称 */
};

看看一组方法的真实数据

 

8、class_def

类定义的结构体,这个比较复杂。直接贴上结构体和原文的说明。这里大致可以看出来,和上面的原理差不多,通过这个结构体来描述类的内容。

1
2
3
4
5
6
7
8
9
10
struct DexClassDef {
    u4  classIdx;           /* 类的类型,指向DexTypeId的索引 */
    u4  accessFlags;                /* 访问标志 */
    u4  superclassIdx;      /* 父类的类型,指向DexTypeId的索引 */
    u4  interfacesOff;      /* 接口,指向DexTypeList的偏移,如果没有接口的声明和实现,值为0 */
    u4  sourceFileIdx;      /* 类所在的源文件名,指向DexStringId的索引 */
    u4  annotationsOff;     /* 注释,根据类型不同会有注解类,注解字段,注解方法,注解参数,没有注解值就是0,指向DexAnnotationsDirectoryItem的结构体 */
    u4  classDataOff;       /* 类的数据部分,指向DexClassData结构的偏移 */
    u4  staticValuesOff;    /* 类中的静态数据,指向DexEncodeArray结构的偏移 */
};

下面同样展示一组真实数据

 

 

上面数据看到里面的class_data也是一个结构体,然后继续看这个类数据的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* expanded form of a class_data_item header */
struct DexClassDataHeader {
    u4 staticFieldsSize;                /* 静态字段的个数 */
    u4 instanceFieldsSize;            /* 实例字段的个数 */
    u4 directMethodsSize;                /* 直接方法的个数 */
    u4 virtualMethodsSize;            /* 虚方法的个数 */
};
 
/* expanded form of encoded_field */
struct DexField {
    u4 fieldIdx;    /* 指向DexFieldId的索引 */
    u4 accessFlags;    /* 访问标志 */
};
 
/* expanded form of encoded_method */
struct DexMethod {
    u4 methodIdx;    /* 指向DexMethodId的索引 */
    u4 accessFlags;     /* 访问标志 */
    u4 codeOff;      /* 指向DexCode结构的偏移 */
};
 
struct DexClassData {
    DexClassDataHeader header;                            /* 指定字段和方法的个数 */
    DexField*          staticFields;                /* 静态字段 */
    DexField*          instanceFields;            /* 实例字段 */
    DexMethod*         directMethods;                /* 直接方法 */
    DexMethod*         virtualMethods;            /* 虚方法 */
};

到这里我们基本看到了在开发中,一个类的所有特征。完整的描述出了一个类的所有信息。

 

9、DexCode上面最后看到方法的代码是通过上面的DexCode结构体来找到的。最后看下这个结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
struct DexCode {
    u2  registersSize;            /* 使用寄存器的个数 */
    u2  insSize;                        /* 参数的个数 */
    u2  outsSize;                        /* 调用其他方法时使用的寄存器个数 */
    u2  triesSize;                    /* try/catch语句的个数 */
    u4  debugInfoOff;       /* 指向调试信息的偏移 */
    u4  insnsSize;          /* 指令集的个数,以2字节为单位 */
    u2  insns[1];                        /* 指令集 */
    /* 2字节空间用于对齐 */
    /* followed by try_item[triesSize] DexTry结构体 */
    /* followed by uleb128 handlersSize */
    /* followed by catch_handler_item[handlersSize] DexCatchHandler结构体 */
};

到了这里,存储的就是执行的指令集了。通过执行指令来跑这个方法。下面看一组真实数据

 

 

这里看到这个类的具体描述字段和函数,还有访问标志等等信息。然后我们继续看里面函数的执行代码部分。看下面一组数据

 

观察的函数是MainActivity类的DoTcp函数。DexCode(也就是code_item,也叫codeOff)的偏移地址是0x127ec。

 

下面观察到DexCode结构体的偏移到指令集insns字段的偏移是codeOff+2+2+2+2+4+4=0x127fc(这里+的2和4是看下面结构体insns前面的字段占了多少个字节计算的,可以当做固定+16个字节)。指令集的长度是0x93。

 

 

最后看看指令集的开始数据是0x62、0xe26、0x11a、0x232。但是我们要注意前面有说明,这里是两字节空间对齐。所以,这里的值我们应该前面填充一下。

 

前面四个字节我们要看做0x0062、0x0e26、0x011a、0x0232。但是我们还要注意,还有个端序问题会影响字节的顺序,这里是小端序,所以我们再调整下

 

前面四个字节我们要看做0x6200、0x260e、0x1a01、0x3202。把这段指令集的数据看明白后,我们用gda打开这个dex文件。然后找到对应的方法,查看一下。

 

然后发现数据对上了。这里存储的果然就是我们dex分析方法的字节码了。

 

9、map_list

在书中的意思是,Dalvik虚拟机解析Dex后,将其映射成DexMapList的数据结构,然后在里面可以找到前面8个