当前位置:网站首页 > Haskell函数式编程 > 正文

安装信息是什么文件(安装信息是什么格式的)



本文来完成一个安卓下可执行文件格式的解析器,对有意向学习安卓软件安全,如Dex混淆、加固、病毒分析等的朋友可以起到一个抛砖引玉的作用。

01 准备一个Dex文件

在完成一个安卓可执行文件格式解析器前,首先需要准备一个Dex文件来进行分析。什么是Dex文件呢?它是安卓平台上的可执行文件。在安卓平台上,应用程序都以Java语言作为开发语言,但是它并没有使用JVM(Java虚拟机)作为运行环境来执行,安卓的设计者为它重新设计了一套虚拟机被称为Dalvik。Dalvik有别于JVM,首先它编译后不再使用.class作为扩展名,而是使用.dex为扩展名。由于虚拟机不同,生成的文件不同,因此,它也有一套属于自己的字节码。

在使用传统的Java编程时,需要安装JDK。对于安卓系统下应用的开发除了需要安装JDK以外,还需要其自身的PSDK,即安卓系统的平台开发包。对于Java语言而言,其编译生成文件的扩展名为.class,而对于安卓开发环境生成的可执行文件一般是.apk。APK文件并不是一个独立的文件,它是由多个文件打包而成的。在APK文件中,除了编译后的可执行文件以外,还包含了配置文件、资源文件等。APK中包含的可执行文件.dex文件就是这里要介绍的内容。

下面来说一下获得Dex文件的方法。

1. 从APK中提取Dex文件

APK文件是一个打包过的文件,其中包含了安卓程序运行所需要的配置文件、资源文件和Dex可执行文件等。看到这里,大家可以从网上下载一个APK文件,或者通过安卓的集成开发环境编译生成一个APK文件。将得到的APK文件的扩展名由.APK修改为.RAR(或者.ZIP也可以),这样就可通过解压缩工具(比如WinRar)打开,打开该.RAR文件后可以看到APK中打包的相关文件,如图1所示。

图1 通过解压缩工具打开APK文件

从图1中可以看到打开的压缩文件中有res目录,以及AndroidManifest.xml、classes.dex文件等,将classes.dex文件解压缩出来,就得到了稍后将要分析的文件。

2. 通过CLASS文件转换Dex文件

通过从APK文件中解压提取到的Dex文件体积通常比较大,对于初学者而言,较大的文件在进行格式分析的时候并不方便。在分析文件格式时,会从整体上了解文件格式的结构,然后去了解文件结构中的各个组成部分。而如果文件体积较大的话,在分析各个组成部分时比较耗费时间,从而导致不能快速地了解文件的整体结构。因此,为了在分析文件结构时,不仅能掌握文件格式的整体结构,也能快速地完成对各个组成部分的分析,就需要提交一个较小的Dex文件。

这里通过写一个简短的代码来编译生成一个体积较小的Dex文件,要自己编译生成一个Dex文件,一共分为3个步骤。

① 写一段简单的Java代码。

在任意文本编辑器中键入如下Java代码:
 
  
public class HelloWorld { public static void main(String[] args){ System.out.println("Hello World!"); } }
上面是一段简单的Java代码,将该代码保存为HelloWorld.java文件。

② 编译生成.class文件。

编译Java源码需要安装JDK,JDK的安装比较简单,下载JDK的安装包,然后将安装目录配置到系统的环境变量中就可以通过javac.exe文件来对Java的源码进行编译了。

在命令行下编译Java文件,编译方法如下:
 
  
d:\TestDex>javac -source 1.6 -target 1.6 HelloWorld.java
编译后会生成一个HelloWorld.class文件,运行该文件的方法如下:
 
  
d:\TestDex>java HelloWorld
③ 转换.class文件到.dex文件。

将Java的.class文件转换为.dex文件,需要安装安卓的开发环境和开发包,这里安装的是开发安卓的Eclipse。在安装目录下的\sdk\build-tools\android\有一个dx.bat文件,通过该文件即可完成.class文件到.dex文件的转换。

切换到\sdk\build-tools\android\目录下,执行如下方法:
 
  
dx --dex --output=HelloWorld.dex HelloWorld.class
这样就得到了一个体积非常小的.dex文件供学习分析使用。

02 DEX文件格式详解

前面介绍了如何得到一个体积较小的Dex文件,现在使用它来完成格式的分析。下面使用十六进制分析工具010Editor来分析Dex文件格式。

1. 010Editor模板

选择010Editor是因为它提供了很多二进制文件的解析模板,这些模板可以用来解析文件的格式,比如图片格式、音频格式、视频格式等,当然也包括解析Dex格式的模板。打开010Editor工具,如图2所示。
图2 010Editor主界面

选择010Editor的“Templates”菜单项,单击“Online Templates Repository”菜单项,将会打开一个在线的模板列表,如图3所示。
图3 010Editor模板列表

在图3中找到BinaryTemplates,然后单击鼠标右键选择“另存为”,将其保存至010Editor安装目录下的“010 Editor_602\010 Editor\Data”路径下。然后启动010Editor,打开生成的TestDex.dex文件,010Editor会自动匹配模板对Dex文件进行解析,并将解析的结果显示出来,如果没有显示则按下快捷键Alt + 4打开“Template Results”窗口,即可看到解析结果,如图4所示。
图4 Template Results窗口

在一开始学习Dex文件格式解析的时候,能有一个参考和对照还是很有必要的。就好比在学习Windows下PE文件格式时,能使用LordPE等解析工具去对照学习会方便很多。010Editor既可以直接查看十六进制数据,也可以查看解析后的数据内容,当然是首选的分析工具了。

2. 整体认识DEX文件结构

DEX文件格式的结构大体分为3部分,第一部分是Dex头部,第二部分是Dex的数据索引,第三部分是Dex的数据内容。如图5所示。
图5 Dex文件整体结构

在Dex头部会给出一些基本的信息,比如文件的字节序、校验和、签名等。在Dex头部中还会给出数据索引所在的偏移和索引包含的数据量。比如在header部分会给出STRING_IDS在文件的偏移位置和文件中包含多少个STRING_ID。

Dex数据索引给出了具体资源数据所在文件的偏移位置。比如,STRING_IDS中保存了具体STRING数据在文件中的偏移位置。

Dex头部、Dex数据索引和Dex数据的关系大致如图6所示。
图6 Dex文件索引结构

如果图5是Dex文件格式的平面结构,那么图6就是Dex文件结构通过索引的方式而展现的立体结构了。两幅图的对应关系也可以很明显地看出来。

Dex文件结构的定义如下:
 
  
struct DexFile { /* 直接映射的"opt"头部 */ const DexOptHeader* pOptHeader; /* 基础 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; /* * 这些不映射到"auxillary"部分,不包含在该文件中 * */ const DexClassLookup* pClassLookup; const void* pRegisterMapPool; // RegisterMapClassPool /* 指向 DEX 文件开头的指针 */ const u1* baseAddr; /* 跟踪辅助结构的内存开销 */ int overhead; /* 其他与 DEX 相关联的数据结构 */ //void* auxData; };
对于前面生成的Dex文件而言,DexFile结构体的定义有所不同,文件定义如下:
 
  
struct DexFile { /* 基础 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; };
这样看,Dex文件的结构体定义就与前面介绍的基本相同了,可以看到有DexHeader(Dex头部)、DexStringId、DexTypeId等结构体。在后面的介绍中,就以该结构体为准来介绍Dex文件的结构。

DexFile结构体定义在安卓系统源码的dalvik/libdex/DexFile.h中。如何找到这份文件呢?打开androidxref网站,如图7所示。
图7 AndroidXRef页面

然后在AndroidXRef页面上选择“JellyBean-4.1.1”(这里选择的是4.1.1的源码进行学习),打开如图8所示的页面。在图8的页面的列表中选择“dalvik”选项,并双击进入对应的源码结构中,如图9所示。在图9中选择“libdex”目录进入源码列表页面,在页面中选择“DexFile.h”即可打开DexFile结构所在的头文件。
图8 选择dalvik选项

图9 选择libdex目录

上面给出了Dex文件的整体结构,并给出了查看安卓系统源码的方法。大家可以通过此方法查找自己想要了解和熟悉的安卓系统的源码,从而深入了解安卓操作系统的源码。

3. Dex文件中的数据类型

安卓系统的内核也都是用C++语言实现的,但是在其源代码中,将C++语言中原本的一些数据类型进行了重新定义。从DexFile.h随便找一个数据结构的定义来进行查看,具体如下:
 
  
/* * Direct-mapped "map_item". */ struct DexMapItem { u2 type; u2 unused; u4 size; u4 offset; };
在DexMapItem数据结构中有类似u2、u4这种数据类型是在C++语言中没有的,这些数据类型就是被重新进行宏定义过的数据类型。在DexFile.h头文件的最上面,有一条文件包含的语句,具体如下:
 
  
#include "vm/Common.h"
直接单击Common.h就可以查看到一个搜索的列表,如图10所示。
图10 Common.h文件

图10是关于“vm common . h”搜索结果的列表,这里只有一个搜索结果。直接单击“Common.h”打开该文件。在该文件中就可以看到u1、u2等数据类型的定义,定义如下:
 
  
/* * 以下类型匹配 VM 规范中的定义 */ typedef uint8_t u1; typedef uint16_t u2; typedef uint32_t u4; typedef uint64_t u8; typedef int8_t s1; typedef int16_t s2; typedef int32_t s4; typedef int64_t s8;
可以看到u1、u2、u4和u8这些数据类型只是无符号的整型,宽度分别是1字节、2字节、4字节和8字节,s1、s2、s4和s8是有符号的整型,宽度也分别是1字节、2字节、4字节和8字节。

在解析Dex文件结构的时候,还有一种数据类型是LEB128(Little Endian Base 128),它也分为有符号类型的和无符号类型的,分别是sleb128、uleb128和uleb128p1。LEB128是由1~5个字节组成为一个32位的数据。每个32位的数据是由一个字节表示还是由两个字节组成后进行表示,取决于第一个字节的最高位,如果第一个字节的最高位为1,那么就需要第二个字节。同理,如果第二个字节的最高位也是1,那么就需要第三位共同构成后进行表示。对于LEB128类型的数据,可以使用安卓系统中提供的算法进行读取,该代码在/dalvik/libdex/Leb128.h文件中。对于读取sleb128和uleb128数据类型的函数有两个不同的函数,分别是readSignedLeb128()和readUnsignedLeb128()。

readSignedLeb128()函数的实现如下:
 
  
/* * 读取无符号的 LEB128 值,更新指向已读取值未尾的指针,该函数第 5 种编码类型中的非零高阶位 */ DEX_INLINE int readSignedLeb128(const u1 pStream) { const u1* ptr = *pStream; int result = *(ptr++); if (result <= 0x7f) { result = (result << 25) >> 25; } else { int cur = *(ptr++); result = (result & 0x7f) | ((cur & 0x7f) << 7); if (cur <= 0x7f) { result = (result << 18) >> 18; } else { cur = *(ptr++); result |= (cur & 0x7f) << 14; if (cur <= 0x7f) { result = (result << 11) >> 11; } else { cur = *(ptr++); result |= (cur & 0x7f) << 21; if (cur <= 0x7f) { result = (result << 4) >> 4; } else { /* * 注意,不检查 cur 是否越界,这意味着可接受高 4 阶位中的垃圾 */ cur = *(ptr++); result |= cur << 28; } } } } *pStream = ptr; return result; }
对于readUnsignedLeb128()函数的实现如下:
 
  
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 是否越界,这意味着可接受高 4 阶位中的垃圾 */ cur = *(ptr++); result |= cur << 28; } } } } *pStream = ptr; return result; }
对于在分析Dex文件格式时,遇到LEB128类型的数据时,直接调用readSignedLeb128()或readUnsignedLeb128()函数进行数据的读取即可。具体的实现也不复杂,如果阅读有不明白的地方,那么就单步调试来仔细观察每行代码的执行。

4. 分析Dex各结构体数据结构

通过前面的内容已经从整体上认识了Dex文件格式,接下来使用前面我们自行编译生成的Dex文件来进行Dex文件格式的分析。

(1)DexHeader结构体

在安卓系统源码的/dalvik/libdex/DexFile.h文件中,给出了DexHeader结构体的定义,该结构体中描述了Dex文件的基本信息,并给出了各种数据索引的数量和偏移地址。该结构体的定义如下:
 
  
/* * Direct-mapped "header_item" struct. */ struct DexHeader { u1 magic[8]; u4 checksum; u1 signature[kSHA1DigestLen]; u4 fileSize; u4 headerSize; u4 endianTag; u4 linkSize; u4 linkOff; u4 mapOff; 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结构体在宏观上面描述了Dex文件的信息,下面对该结构体中的字段进行描述。

magic:表示 Dex 文件的标识与版本号。

checksum:表示 Dex 文件的文件校验和,它使用 alder32 算法校验文件。

signature:表示 Dex 文件的签名,该签名使用的是 SHA-1 哈希算法。

fileSize:表示文件的大小。

headerSize:表示 Dex 文件头部的大小。

endianTag:表示文件的字节序。

其他部分基本都是以Size和Off命名的字段变量,以Size命名的变量表示相关数据的数量,以Off命名的变量表示相关数据索引的在文件中的偏移地址。比如,stringIdsSize表示string相关数据的数量,stringIdsOff表示string相关数据索引在文件中的偏移地址。注意,这里给出的并不是string相关数据在文件中的偏移地址,而是string相关数据索引在文件中的偏移地址。如果对这个索引的概念模糊了,那么再到前面看一下图6。

用任意十六进制编辑器(推荐使用010Editor)打开前面我们自行编译生成的Dex文件,用具体数据来对照DexHeader结构体的定义进行分析。用010Editor打开Dex文件后,DexHeader对应的数据部分如图11所示,010Editor对数据按照Dex文件格式解析后如图12所示。
图11 DexHeader对应的数据

图12 010Editor对DexHeader数据的解析

图12是010Editor对DexHeader数据的解析,在解析中一共有6列数据,分别是Name(字段名称)、Value(字段值)、Start(开始位置)、Size(长度)、Color(颜色,该字段忽略掉)和Comment(注释、备注)。

按照DexHeader结构体的字段与数据进行整理,整理如表1所示。
表1 DexHeader数据字段整理

表1给出了DexHeader各个字段的值,下面对一些字段的值进行一下解释。

magic:该字段的值是一个字符串,为“dex 035”。

在源代码中给出了该字段的定义,不过在源代码中对该定义分成了两部分,定义如下:
 
  
/* DEX file magic number */ #define DEX_MAGIC "dex\n" /* * 旧的仍然识别的版本(对应于 Android API 级别 13 或更早的版本) */ #define DEX_MAGIC_VERS_API_13 "035\0"
以上就是关于DexHeader的介绍。在这里再说明一点,有很多朋友会问,将这样的数据整理成表格是否有意义呢?答案是肯定的,对于第一次接触文件格式而言,将数据整理成这样的表格学习是非常直观的,而且在遇到了问题的时候,可以通过查看表格的方式进行回顾和分析。对于学习和掌握文件格式而言,能通过观察二进制文件直接解析文件格式,或直接通过十六进制编辑器来自己完成具体的文件是一种好的学习实践方法。

(2)DexMapItem结构体

在DexHeader结构体中的mapOff是DexMapList的偏移地址,Dalvik虚拟机在解析DEX文件后按照MapItem将其映射。

DexMapList结构体定义如下:
 
  
/* * 直接映射的"map_list" */ struct DexMapList { u4 size; /* 列表中的条目 */ DexMapItem list[1]; /* 条目 */ };
size: 表示 DexMapList 中共有多少个 DexMapItem 结构体。

list: 表示它是一个 DexMapItem 数组,这里定义了数组的大小是 1,由于 C 语言和 C++ 语言并不对数组进行越界检查,因此将其数组的大小定义为 1,然后通过越界访问来达到访问任意多个数组元素的目的。这种设计思路在 C 语言和 C++语言中经常见到,比如 Windows 的 PE 文件结构,导入函数名称表时就遇到过。

DexMapItem是具体每一部分的类型、偏移的一个描述,DexMapItem结构体定义如下:
 
  
/* * 直接定义的"map_item". */ struct DexMapItem { u2 type; u2 unused; u4 size; u4 offset; };
DexMapItem是映射后每一部分的描述,或索引。它与DexHeader中的xxxSize和xxxOff类似。在DexMapItem保存的是数据的类型、size和Off。

type:类型编码,该编码在安卓系统的源代码中定义了一套枚举值,枚举值都以 kDexType 开头;

unused:没有被使用,为了结构体对齐;

size:所指向类型数据的数量,同 DexHeader 中 xxxSize;

offset:所指向类型数据在文件内的偏移,同 DexHeader 中的 xxxOff。

对于类型编码type枚举值的定义如下:
 
  
/* 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, };
按照DexMapList和DexMapItem来对数据进行解析。

首先,需要从DexHeader中查看DexMapList所在的偏移,该值在DexHeader中的MapOff字段中保存,按照表1可以看到MapOff的值为0x248,根据该值将得到DexMapList的数据。

然后,在文件偏移位置0x248处,查看DexMapList中的size字段,来确定有多少个DexMapItem数据,如图13所示。
图13 DexMapList的size字段

从图13中可以看出,文件偏移0x248的值为0xD,也就是DexMapList的Size字段的值为13,表示该值的后面有13个DexMapItem结构体。也就是图13中,文件偏移0x248后面的数据都是DexMapItem的数据。在010Editor中对DexMapItem数据的解析如图14所示。
图14 010Editor对DexMapItem数据的解析

最后,通过图13来对各个DexMapItem的数据进行整理。整理后如表2所示。
表2 DexMapItem数据字段整理

在DexMapItem结构体中,给出了DexHeader的位置,给出了一些数据的索引的偏移,比如String、Type、Proto等,这些在DexHeader的头部和索引位置也有给出,但是DexMapItem给出得更为全面一些。

从DexHeader结构体和DexMapItem结构体数组都可以去解析DEX文件,如果只是要解析相对重要的结构体,那么按照DexHeader结构体进行解析即可,如果需要解析得更为全面一些,那么可以通过DexMapItem结构体数组进行解析。这个在解析的时候大家可以自行选择。

(3)DexStringItem结构体

DexStringItem结构体在DEX文件中也是一个数组,该数组中保存了DEX文件中所有需要的字符串。DexStringItem结构体被DexStringId进行索引,而该索引由DexHeader中的stringIdsOff给出,也可以通过遍历DexMapItem数组中类型为kDexTypeStringIdItem的元素中的offset来得到DexStringId的位置。

这里通过DexHeader的stringIdsOff来得到其位置,该stringIdsOff的值为0x70。通过DexHeader的stringIdsSize可以知道DexStringId的数量为14个。DexStringId的数据如图15所示。
图15 DexStringId在010Editor中的数据

这里给出DexStringId结构体的定义:
 
  
/* * Direct-mapped "string_id_item". */ struct DexStringId { u4 stringDataOff; };
从结构体的注释中可以看出,该结构体只有一个字段是stringDataOff,就是指向了字符串数据在文件中的偏移。

下面给出DexStringItem结构体的定义:
 
  
Struct DexStringItem { uleb128 size; ubyte data; };
uleb128:表示字符串的长度,在前面的内容中已经介绍过 uleb128 类型了;

data:表示字符串,字符串以 C 语言或 C++的 NULL(\0)结尾,该字符串并不是传统 的 ASCII 字符串,它是一种被称作 MUTF-8 编码的字符串,它类似 UTF-8 编码但是稍有不同, 这里不做过多的解释了。

该结构体并不是从DexFile.h中给出的,而是自己定义的。

Dex文件中的字符串资源是通过DexStringId进行索引的,一个索引对应一个字符串,即对应一个DexStringItem,那么有多少个DexStringId,就有多少个DexStringItem。在010Editor中查看DexStringId和DexStringItem相关的数据,如图16、图17和图18所示。
图16 DexStringId数组在010Editor中的数据

图17 DexStringItem数组在010Editor中的数据

图18 010Editor对DexStringItem数组的解析

将DexStringItem进行整理,整理如表3所示。
表3 DexStringItem数据整理

从表3中的“字符串”列可以看到很多熟悉的字符串。在对Windows程序进行逆向的时候,可以通过字符串作为分析的入手特征,或者通过Windows API函数作为入手的特征。在表3中的字符串中可以看到DEX程序中的数据字符串,比如“HelloWorld”,也可以看到DEX程序中调用的函数,比如“println”。因此,掌握了DexStringItem数组以后,即使靠猜测,也对程序已经有了一个大致的了解了。

在表3中的第一列是序号列或索引列,因为在后面的数据结构中对字符串引用时使用的就是该表的序号。

(4)DexTypeId结构体

按照DexHeader来看,在DexStringItem后面是DexTypeId的索引,它们由typeIdsSize和typeIdsOff给出。TypeIds给出了程序中所使用的所有的数据类型。该数据结构的定义如下:
 
  
/* * Direct-mapped "type_id_item". */ struct DexTypeId { u4 descriptorIdx; };
通过DexHeader的typeIdsSize得到TypeIds的数量是7,它在文件内的偏移位置是0xA8。DexTypeId结构体中只有一个字段,该字段的值是在DexStringItem中的索引值,先来看一下010Editor中的数据和010Editor对数据的解析,如图19和图20所示。
图19 DexTypeId数组在010Editor中的数据

图20 010Editor对DexTypeId数组的解析

从图19中可以看到,第一个DexTypeId的数据是0x00000003,这个值是在DexStringItem数组中的索引,索引的下标从0开始。那么用0x00000003这个索引值在表3中进行查找,对应的字符串是“LHelloWorld;”。

将DexTypeId的数据进行整理,如表4所示。
表4 DexTypeId数据整理

在表4中的第一列是序号列或索引列,因为在后面的数据结构中对类型引用时使用的就是该表的序号。

在表4中出现了3种类型,分别是L类型、V类型和[类型。在Dalvik中数据类型可以分为原始数据类型和引用数据类型。在安卓系统的文档中给出了类型的描述,如表5所示。
表5 类型描述

(5)DexProtoId结构体

接着DexHeader结构体来看,在DexTypeId下面接着是DexProtoId结构体,它由protoIdsSize和protoIdsOff给出。它表示了Java语言里的方法原型。

DexProtoId结构体的定义如下:
 
  
/* * 直接映射的"proto_id_item" */ struct DexProtoId { u4 shortyIdx; u4 returnTypeIdx; u4 parametersOff; };
在DexProtoId结构体中一共有3个字段,下面分别介绍。

shortyIdx:指向 DexStringIds 列表的索引。

returnTypeIdx:指向 DexTypeIds 列表的索引,它是方法返回值的类型。

parametersOff:指向 DexTypeList 结构体的偏移。

从parametersOff参数可以看出,对于方法原型来说,无法通过DexProtoId一个结构体完整的描述,从而通过parametersOff字段引入了另外的一个结构体,即DexTypeList。对于方法,如果有参数,那么parametersOff将是参数的列表,如果没有参数,那么parametersOff的值为0。下面查看DexTypeList结构体的定义:
 
  
/* * 直接映射的"type_list" */ struct DexTypeList { u4 size; DexTypeItem list[1]; };
DexTypeList结构体中有两个字段,下面分别介绍。

size:表示参数的数量。

list:表示参数列表,参数列表仍然是一个结构体 DexTypeItem。

查看DexTypeItem结构体的定义如下:
 
  
/* * 直接映射的"type_item" */ struct DexTypeItem { u2 typeIdx; };
DexTypeItem结构体只有一个参数,该参数是typeIds的索引。

到此,对于DexProtoId相关的结构体就介绍完了。对于方法原型而言,在Dex文件中,一个方法的原型需要使用3个结构体才能进行完整的描述。结合实例,来完整的对这部分数据进行解析。先来看一下010Editor工具解析后的情况,如图21所示。
图21 010Editor对DexProtoId数组的解析

解析DexProtoId相关的数据,仍然从protoIdsSize和protoIdsOff开始,从DexHeader结构体中可以看到protoIdsSize的值是3,表示有3个DexProtoIds;protoIdsOff给出了DexProtoId所在的文件偏移,其偏移位置在0xC4的位置处。

在文件偏移0xC4位置处的数据如图22所示。
图22 DexProtoId数组在010Editor中的数据

按照图22中选中的数据,来对该部分内容进行解析,如表6所示。
表6 DexProtoId解析表

表6只是对 DexProtoId 进行了解析,但是还没有解析相应的参数,也就是根据 parameterOff 值来找到相关参数的描述。对于索引为 0 的 DexProtoId 是没有参数的,因此只需要看索引 1 和索引 2 的数据,如图23所示。
图23 DexTypeList 在 010Editor 中的数据

根据图23对索引 1 和索引 2 进行参数的整理,如表7所示。
表7 DexProtoId参数解析

进行到这一步的时候,已经可以看到很多更为具体的内容了,用索引 1 来进行介绍。原型声明 VL 表示,方法返回值类型是 V,方法的参数的类型是 L(V、L 参考表 10-5);返回值类型 V 表示返回值的类型是 V(V 参考表 10-5);参数 Ljava/lang/String 表示参数的具体类型。

由上面的分析可以构造出索引 1 方法的原型伪代码如下。
 
  
void (Ljava/lang/String) { return void; }
由此可以看出 DexProtoId 已经基本可以得到了 Dex 文件中所有方法的原型。

(6)DexFieldId 结构体

在DexFieldIds 中会给出 Dex 文件中所有的 field。DexFieldId 是由 DexHeader 结构体的 fieldIdsSize 和 fieldIdsOff 给出,同样它们给出了 DexFieldId 的数量和偏移。

首先来查看 DexFieldId 结构体的定义,定义如下:
 
  
/* * Direct-mapped "field_id_item". */ struct DexFieldId { u2 classIdx; u2 typeIdx; u4 nameIdx; };
DexFieldId 结构体只有 3 个字段,下面分别进行介绍。

classIdx:表示该 field 所属的 class,该值是 DexTypeIds 中的一个索引;

typeIdx:表示该 field 的类型,该值也是 DexTypeIds 中的一个索引;

nameIdx:表示该 field 的名称,该值是 DexStringIds 中的一个索引。

查看在 010Editor 中 DexFieldId 的数据,以及对数据的解析,分别如图24、图25 所示。
图24 DexFieldId 在 010Editor 中的数据

图25 010Editor 对 DexFieldId 数组的解析

解析 DexFieldId 同样从 fieldIdsSize 和 fieldIdsOff 开始,它们的值分别是 1 和 0xE8。也 就是说,该 Dex 文件中只有一个 DexFieldId,且它在文件中的偏移位置在 0xE8 处。解析后 如表8所示。
表8 DexFieldId解析

(7)DexMethodId结构体

在DexFieldId下面是DexMethodId结构体,该结构体索引自DEX文件中的所有Method,该结构体的定义如下:
 
  
/* * Direct-mapped "method_id_item". */ struct DexMethodId { u2 classIdx; u2 protoIdx; u4 nameIdx; };
该结构体的字段有3个,下面分别进行介绍。

classIdx:表示该方法所属的类,它是 DexTypeIds 的索引;

protoIdx:表示该方法的原型,它是 DexProtoIds 的索引;

nameIdx:表示该方法的名称,它是 DexStringIds 的索引。

查看在010Editor中DexMethodId的数据,以及对数据的解析,分别如图26、图27所示。
图26 DexMethodId在010Editor中的数据

图27 010Editor对DexMethodId数组的解析

解析DexMethodId同样从methodIdsSize和methodIdsOff开始,它们的值分别是4和0xF0。也就是说,该Dex文件中有4个DexMethodIds,且它在文件中的偏移位置在0xF0处。解析后如表9所示。
表9 DexMethodId的解析

按照表9整理相应的方法,如表10所示。
表10 整理后的Method

到此,所有的索引就介绍完了,字符串、类型、方法原型、字段、方法这些关键的信息都已经按照从0开始为索引建立好相应的表格了。从这些建立的表格可以感觉出,通过建立表格的过程会逐步地熟悉Dex文件的格式,但是它也是一个繁琐的过程。在刚开始学习的时候,就需要这样逐个字节地去“抠”,搞清楚它表示什么,它的作用是什么。这样才能把基础的知识掌握好,掌握好基础以后使用工具时就游刃有余了。

(8)DexClassDef结构体

DexClassDef是DexHeader中给出的最后一个重要的结构体了,它通过classDefsSize和classDefsOff给出。看一下DexClassDef结构体的定义,该定义如下:
 
  
/* * 直接映射的"class_def_item" */ struct DexClassDef { u4 classIdx; u4 accessFlags; u4 superclassIdx; u4 interfacesOff; u4 sourceFileIdx; u4 annotationsOff; u4 classDataOff; u4 staticValuesOff; };
该结构体的字段中引入了其他的结构体,下面分别进行介绍。

classIdx:表示 class 的类型,只能是类,不能是数组或基本类型,它是一个 DexTypeIds 的索引值。

accessFlags:表示类的访问类型,它是以 ACC_开头的枚举值,在 DexFile.h 中定义如下。
 
  
/* * 访问标记和 mask,标准的标记和 mask 都小于或等于 0x4000 * * 注意 ClassFlags 枚举中 vm/oo/Object.h 里相关的声明 ClassFlags */ enum { ACC_PUBLIC = 0x00000001, ACC_PRIVATE = 0x00000002, ACC_PROTECTED = 0x00000004, ACC_STATIC = 0x00000008, ACC_FINAL = 0x00000010, ACC_SYNCHRONIZED = 0x00000020, ACC_SUPER = 0x00000020, ACC_VOLATILE = 0x00000040, ACC_BRIDGE = 0x00000040, ACC_TRANSIENT = 0x00000080, ACC_VARARGS = 0x00000080, ACC_NATIVE = 0x00000100, ACC_INTERFACE = 0x00000200, ACC_ABSTRACT = 0x00000400, ACC_STRICT = 0x00000800, ACC_SYNTHETIC = 0x00001000, ACC_ANNOTATION = 0x00002000, ACC_ENUM = 0x00004000, ACC_CONSTRUCTOR = 0x00010000, ACC_DECLARED_SYNCHRONIZED = 0x00020000, ACC_CLASS_MASK = (ACC_PUBLIC | ACC_FINAL | ACC_INTERFACE | ACC_ABSTRACT | ACC_SYNTHETIC | ACC_ANNOTATION | ACC_ENUM), ACC_INNER_CLASS_MASK = (ACC_CLASS_MASK | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC), ACC_FIELD_MASK = (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC | ACC_FINAL | ACC_VOLATILE | ACC_TRANSIENT | ACC_SYNTHETIC | ACC_ENUM), ACC_METHOD_MASK = (ACC_PUBLIC | ACC_PRIVATE | ACC_PROTECTED | ACC_STATIC | ACC_FINAL | ACC_SYNCHRONIZED | ACC_BRIDGE | ACC_VARARGS | ACC_NATIVE | ACC_ABSTRACT | ACC_STRICT | ACC_SYNTHETIC | ACC_CONSTRUCTOR | ACC_DECLARED_SYNCHRONIZED), };
superclassIdx:表示 superclass 的类型,即父类的类型,它是一个 DexTypeIds 的索 引值。

interfaceOff:表示 interface 的偏移地址,即接口的偏移地址,该偏移处的值是 DexTypeList 类型的结构体。

sourceFileIdx:表示源代码的字符串,该值是 DexStringIds 的索引值。

annotationsOff :表示该 class 的注释,该值是在文件中的偏移值,该偏移处是 DexAnnotationsDirectoryItem 结构体,该结构体的定义如下。
 
  
/* * Direct-mapped "annotations_directory_item". */ struct DexAnnotationsDirectoryItem { u4 classAnnotationsOff; u4 fieldsSize; u4 methodsSize; u4 parametersSize; /* 后面是 DexFieldAnnotationsItem[fieldsSize] */ /* 后面是 DexMethodAnnotationsItem[methodsSize] */ /* 后面是 DexParameterAnnotationsItem[parametersSize] */ };
classDataOff:表示class的数据,该值是在文件中的偏移值,该偏移处是DexClassData结构体(该结构体定义在DexClass.h文件中,并没有定义在DexFile.h中),该结构体的定义如下。
 
  
/* class_data_item 的扩展形式,注意,如果特定项不存在 * (如没有静态字段),那么 * is set to NULL. */ struct DexClassData { DexClassDataHeader header; DexField* staticFields; DexField* instanceFields; DexMethod* directMethods; DexMethod* virtualMethods; };
在DexClassData中有5个字段,下面分别进行介绍。

Header:表示其后4个字段的数量,它是DexClassDataHeader结构体,该结构体的定义如下。
 
  
/* class_data_item header 的扩展形式 */ struct DexClassDataHeader { u4 staticFieldsSize; u4 instanceFieldsSize; u4 directMethodsSize; u4 virtualMethodsSize; };
该结构体中的数量是以ULEB128进行存储的,通过源码中可以看出,代码如下。
 
  
/* 不经验证就读取 class_data_item 的头部,这会更新指向已读取数据未尾的指针 */ DEX_INLINE void dexReadClassDataHeader(const u1 pData, DexClassDataHeader *pHeader) { pHeader->staticFieldsSize = readUnsignedLeb128(pData); pHeader->instanceFieldsSize = readUnsignedLeb128(pData); pHeader->directMethodsSize = readUnsignedLeb128(pData); pHeader->virtualMethodsSize = readUnsignedLeb128(pData); }
staticFields:表示静态字段,它是一个 DexField 结构体。

instanceFields:表示实例字段,它是一个 DexField 结构体。

directMethods:表示直接方法,它是一个 DexMethod 结构体。

virtualMethods:表示虚方法,它是一个 DexMethod 结构体。

下面分别看一下DexField结构体的定义和DexMethod结构体的定义,DexField结构体的定义如下。
 
  
/* expanded form of encoded_field */ struct DexField { u4 fieldIdx; u4 accessFlags; };
fieldIdx:表示在 DexFieldIds 列表中的索引。

accessFlags:表示字段的访问标识。

DexMethod 结构体定义如下。
 
  
/* expanded form of encoded_method */ struct DexMethod { u4 methodIdx; u4 accessFlags; u4 codeOff; };
methodIdx:表示在 DexMethodIds 列表中的索引。

accessFlags:表示方法的访问标识。

codeOff:表示指向 DexCode 结构的偏移位置。

DexCode结构体是具体类方法中的信息,包括寄存器的个数、参数个数、指令个数,以及指令等信息,DexCode结构体的定义如下。
 
  
/* * Direct-mapped "code_item". * * 当抛出异常时使用"catches"表,当显示异常栈跟踪或调试信息时,显示"debugInfo",偏移量是零 * 表示没有条目 */ struct DexCode { u2 registersSize; u2 insSize; u2 outsSize; u2 triesSize; u4 debugInfoOff; u4 insnsSize; u2 insns[1]; /* 后面是可选的 u2 padding */ /* 后面是 try_item[triesSize] */ /* 后面是 uleb128 handlersSize */ /* 后面是 catch_handler_item[handlersSize] */ };
TegistersSize:表示使用寄存器的个数。

insSize:表示参数的个数。

outSize:表示调用其他方法时使用的寄存器的个数。

triesSize:表示异常处理的个数,即 try/catch 的个数。

debugInfoOff:表示调试信息在文件中的偏移。

insnsSize:表示该方法中包含指令的数量,以字为单位。

insns:表示该方法中的指令。

staticValuesOff:表示该 class 中静态数据,该值是在文件中的偏移值,该偏移处是 DexEncodedArray 结构体,该结构体的定义如下。
 
  
/* * 直接映射的"encoded_array" * * 注意,该结构是按字节对齐的 */ struct DexEncodedArray { u1 array[1]; };
关于DexClassDef结构体就介绍完了,DexClassDef结构体中最重要的部分就是DexClassData结构体,由DexClassData结构体引出了DexClassDataHeader结构体、DexField结构体、DexMethod结构体和DexCode结构体。

下面通过010Editor查看对DexClassDef结构体相关数据的解析,如图28和图29所示。
图28 010Editor对DexClassDef结构体的解析

图29 010Editor对DexClassData结构体的解析

解析数据同样从得到classDefsOff和classDefSize开始,classDefsOff的值为0x110,classDefSize的值为1,说明在该Dex文件中只有1个DexClassDef结构体,它在文件中的偏移地址为0x110。

那么,同样按照前面的方式,将Dex中的数据建立成一张一张的表格来进行分析。

从表11中可以看出,类的名字为HelloWorld,类的访问类型是public,类的父类是java.lang.Object,类所属的文件是HelloWorld.java。类数据在文件中的偏移地址为0x23A,其他的字段都为0。
表11 DexClassDef结构体解析

按照类数据所在文件中的偏移地址0x23A来解析DexClassData。在解析之前先来完整的查看一下DexClassDef相关的几个结构体的关系,如图30所示。此图是各个数据结构之间的关系,并非数据组织的方法。根据图30,来解析DexClassData相关的数据,DexClassData结构体相关的结构体有3个(这里介绍了3个),因此按照它相关的结构体来整理相关的表格。
图30 DexClassDef各数据结构之间的关系

首先解析的表格是DexClassDataHeader结构体,如表12所示。
表12 DexClassDataHeader相关数据结构解析

从表12中可以看出,除了directMethodsSize字段为2以外,其余的字段都为0。那么这就说明,这里只需要解析directMethods对应的DexMethod结构体即可,如表13所示。
表13 directMethods对应的DexMethod结构体解析

在解析DexMethod结构体时需要注意,最后的codeOff结构体的偏移是ULEB类型的,并不是直接给出的。这里的值按照010Editor的值进行对照即可,如果想手动去转换的话,请参考前面关于ULEB读取的代码,这里不进行介绍了。

最后是对DexCode结构体进行介绍了,解析后如表14所示。
表14 DexCode结构体解析

到此,关于Dex文件的格式就介绍完了。总体感觉并没有PE文件结构那么复杂,请大家自行解析一遍,以便加深印象。

来源:网信鹤岗

到此这篇安装信息是什么文件(安装信息是什么格式的)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!

版权声明


相关文章:

  • 计算机零基础编程入门的书(计算机编程零基础教程)2024-12-06 23:27:08
  • 支付方式图片搞笑(支付方式图片表情包)2024-12-06 23:27:08
  • 支付方式图片国际结算(支付结算的支付方式)2024-12-06 23:27:08
  • pivot函数表格用法(pivot_table函数)2024-12-06 23:27:08
  • 字符串转码技术介绍(字符串转换编码格式)2024-12-06 23:27:08
  • ip报文格式分析题(ipv4报文格式解析)2024-12-06 23:27:08
  • ifstream get函数(if(getchar()=='\\n'))2024-12-06 23:27:08
  • mysql窗口函数执行顺序(mysql窗口函数从哪个版本开始有)2024-12-06 23:27:08
  • 鸿蒙软件后缀格式(鸿蒙系统软件的后缀)2024-12-06 23:27:08
  • 报文格式(报文格式错误怎么解决)2024-12-06 23:27:08
  • 全屏图片