返回介绍

7.3 解析数据结构

发布于 2024-10-10 22:32:16 字数 17812 浏览 0 评论 0 收藏 0

下面按照图 7-1 的思路来一一讲解各个数据结构。

7.3.1 头部信息 Header 结构

dex 文件里的 header 除了描述.dex 文件的文件信息外,还有文件里其他各个区域的索引。header 对应为结构体类型,逻辑上的描述用结构体 header_item 来理解它。先给出结构体里面用到的数据类型 ubyte 和 uint 的解释,然后是结构体的描述,后面对各种结构描述的时候也是用这种方法。

代码定义如下:

查看 Hex 如下:

Header 的大小固定为 0x70,偏移地址从 0x00 到 0x70,提取信息如下:

用一张图来描述各个字段的长度:

里面以_size 和_off 为后缀的描述为:data_size 是以字节为单位描述 data 区的大小,其余的_size 都是描述该区里元素的个数;_off 描述相对于文件起始位置的偏移量。其余的 6 个是描述 dex 文件信息的,各项说明如下:

·magic value:这 8 个字节一般是常量,为了使 dex 文件能够被识别出来,它必须出现在 dex 文件的最开头的位置。数组的值可以转换为一个字符串如下:{0x64 0x65 0x78 0x0a 0x30 0x33 0x35 0x00}="dex\n035\0",中间是一个'\n'符号后面 035 是 dex 文件格式的版本。

·checksum 和 signature:checksum 是文件校验码,使用 alder32 算法校验文件除去 maigc、checksum 外余下的所有文件区域,用于检查文件错误。signature 使用 SHA-1 算法 hash 除去 magic、checksum 和 signature 外余下的所有文件区域,用于唯一识别本文件。

·file_size:dex 文件的大小。

·header_size:header 区域的大小,单位字节,一般固定为 0x70 常量。

·endian_tag:大小端标签,标准 dex 文件格式为小端,此项一般固定为 0x1234 5678 常量。

·link_size 和 link_off:这两个字段表示的是链接数据的大小和偏移值。

·map_off:map item 的偏移地址,该 item 属于 data 区里的内值要大于等于 data_off 的大小。

结构如 map_list 描述:

定义位置:data 区。

引用位置:header 区。

map_list 里先用一个 uint 描述后面有 size 个 map_item,后续就是对应的 size 个 map_item 描述。

map_item 结构有 4 个元素:type 表示该 map_item 的类型,本节能用到的描述如下,详细 DalvikExecutable Format 里 Type Code 的定义;size 表示再细分此 item,该类型的个数;offset 是第一个元素的针对文件初始位置的偏移量;unuse 是用对齐字节的,无实际用处。

结构定义如下:

header→map_off=0x0244,偏移为 0244 的位置值为 0x000d。每个 map_item 描述占用 12 字节,整个 map_list 占用 12*size+4 个字节。所以整个 map_list 占用空间为 12*13+4=160=0x00a0,占用空间为 0x 0244~0x 02E3。从文件内容上看,也是从 0x 0244 到文件结束的位置。

地址 0x0244 的一个 uinit 的值为 0x0000000d,map_list->size=0x0d=13,说明后续有 13 个 map_item。根据 map_item 的结构描述在 0x0248~0x02e3 里的值,整理出这段二进制所表示的 13 个 map_item 内容,汇成表格如下:

map_list→map_item 里的内容,有部分 item 跟 header 里面相应 item 的 offset 地址描述相同。但 map_list 描述的更为全面些,又包括了 HEADER_ITEM、TYPE_LIST,STRING_DATA_ITEM 等,最后还有它自己 TYPE_MAP_LIST。

至此,header 部分描述完毕,它包括描述 dex 文件的信息,其余各索引区和 data 区的偏移信息,一个 map_list 结构。map_list 里除了对索引区和数据区的偏移地址又一次描述,也有其他诸如 HEAD_ITEM、DEBUG_INFO_ITEM 等信息。

·string_ids_size 和 string_ids_off。这两个字段表示 dex 中用到的所有字符串内容的大小和偏移值,需要解析完这部分,然后用一个字符串池存起来,后面有其他的数据结构会用索引值来访问字符串池,这个池子也是非常重要的。

·type_ids_size 和 type_ids_off。这两个字段表示 dex 中的类型数据结构的大小和偏移值,比如类类型、基本类型等信息。

·proto_ids_size 和 type_ids_off。这两个字段表示 dex 中元数据信息数据结构的大小和偏移值,描述方法的元数据信息,比如方法的返回类型,参数类型等信息。

·field_ids_size 和 field_ids_off。这两个字段表示 dex 中字段信息数据结构的大小和偏移值。

·method_ids_size 和 method_ids_off。这两个字段表示 dex 中方法信息数据结构的大小和偏移值。

·class_defs_size 和 class_defs_off。这两个字段表示 dex 中类信息数据结构的大小和偏移值,这个数据结构是整个 dex 中最复杂的,它内部层次很深,包含了很多其他的数据结构,所以解析起来也很麻烦,所以后面会着重讲解这个数据结构。

·data_size 和 data_off。这两个字段表示 dex 中数据区域的结构信息的大小和偏移值,这个结构中存放的是数据区域,比如定义的常量值等信息。

头部包含的信息还是很多的,主要分为两个部分:

·魔数+签名+文件大小等信息。

·后面的各个数据结构的大小和偏移值都是成对出现的。

7.3.2 string_ids 数据结构

string_ids 区索引了 dex 文件所有的字符串。这个区里的元素格式为 string_ids_item,可以使用结构体如下描述:

以_ids 结尾的各个段里放置的都是对应数据的偏移地址,只是为一个索引,所以才会在 dex 文件布局里把这些区归类为“索引区”。

string_data_off 只是一个偏移地址,它指向的数据结构为 string_data_item,代码定义如下:

这里涉及 LEB128(little endian base 128)格式,是基于 1 个字节的一种不定长度的编码方式。若第一个字节的最高位为 1,则表示还需要下一个字节来描述,直至最后一个字节的最高位为 0。每个字节的其余位用来表示数据。LEB128 这种数据类型的出现其实是为了解决一个问题,那就是减少内存的浪费,它表示整型类型的数值,但是整型类型四个字节有时候在使用的时候有点浪费,它的原理如下所示:

图中使用两个字节表示。编码的每个字节有效部分只有低 7 位,每个字节的最高位用来指示是否是最后一个字节:

·非最高字节的第 7 位为 0。

·最高字节的第 7 位为 1。

将 LEB128 编码的数字转换为可读数字的规则是:除去每个字节的第 7 位,将每个字节剩余的 7 个位拼接在一起,即为数字。

比如:LEB128 编码的 0x02b0 转换后的数字为 0x0130。

转换过程如下:

0x02b0=>0000 0010 1011 0000=>去除最高位=>000 0010 011 0000=>按 4 位重排=>00 0001 0011 0000=>0x130。

底层代码位于:android/dalvik/libdex/leb128.h。

Java 中也写了一个工具类:

这个方法是读取 dex 中 Uleb128 类型的数据,遇到一个字节最高位=0 就停止读下个字节的原理来实现即可,还有一个方法就是解码 Uleb128 类型的数据:

原理是去除每个字节的最高位,接着拼接剩下的 7 位,然后重新构造一个整型类型的数据,位不够就从低位开始左移。

通过上面的 Uleb128 的解释来看,其实 Uleb128 类型就是 1~5 个字节来回浮动,为什么是 5 呢?因为它要表示一个 4 个字节的整型类型,但是每个字节要去除最高位,那么肯定最多只需要 5 个字节就可以表示 4 个字节的整型类型数据了。

下面回归正题,继续来看 string_ids 数据结构。根据 string_ids_item 和 string_data_item 的描述,加上 header 里提供的入口位置 string_ids_size=0x0e 和 string_ids_off=0x70,可以整理出 string_ids 及其对应的数据如下:

string_ids_item 和 string_data_item 里提取出的对应数据表格如下:

string 里的各种标志符号,诸如 L、V、VL、[等在 dex 文件里有特殊的意思。

string_ids 的意思就是找到这些字符串。其实使用二进制编辑器打开 dex 文件时,一般工具默认翻译成 ASCII 码,总会有一大片熟悉的字符白生生地很是晃眼。刚才走过的分析流程,就是顺藤摸瓜找到它们是怎么来的。以后的一些 type-ids、method_ids 也会引用到这一片熟悉的字符串。

提示:后面的解析代码会看到,其实没必要用那么复杂地去解析 Uleb128 类型,因为会看到这个字符串和我们之前解析 XML 和 resource.arsc 格式一样,每个字符串的第一个字节表示字符串的长度,那么只要知道每个字符串的偏移地址就可以解析出字符串的内容了,而每个字符串的偏移地址是存放在 string_ids_item 中的。

到这里就解析完了 dex 中所有的字符串内容,用一个字符串池来进行存储即可。下面来继续看 type_ids 数据结构。

7.3.3 type_ids 数据结构

type_ids 数据结构中存放的数据主要是描述 dex 中所有的类型,比如类类型、基本类型等信息。type_ids 区索引了 dex 文件里的所有数据类型,包括 class 类型、数组类型(array types)和基本类型(primitive types)。本区域里的元素格式为 type_ids_item,结构描述如下:

type_ids_item 里面 descriptor_idx 值的意思是 string_ids 里的 index 序号,是用来描述此 type 的字符串。

根据 header 里 type_ids_size=0x07,type_ids_off=0xa8,找到对应的二进制描述区。00000a0:1a02,如下所示:

根据 type_id_item 的描述,整理出表格如下。

因为 type_id_item→descriptor_idx 里存放的是指向 string_ids 的 index 号,所以也能得到该 type 的字符串描述。这里出现了 3 个 type descriptor:

·L 表示 class 的详细描述,一般以分号表示 class 描述结束。

·V 表示 void 返回类型,只有在返回值的时候有效。

·[表示数组,[Ljava/lang/String;可以对应到 Java 语言里的 java.lang.String[]类型。

后面的其他数据结构也会使用到 type_ids 类型,所以这里解析完 type_ids 也是需要用一个池子来存放的,后面直接用索引 index 来访问即可。

7.3.4 proto_ids 数据结构

proto 的意思是 method prototype,代表 Java 语言里的一个 method 的原型。proto_ids 里的元素为 proto_id_item,结构如下:

其中:

·shorty_idx:跟 type_ids 一样,它的值是一个 string_ids 的 index 号,最终是一个简短的字符串描述,用来说明该 method 原型。

·return_type_idx:它的值是一个 type_ids 的 index 号,表示该 method 原型的返回值类型。

·parameters_off:后缀 off 是 offset,指向 method 原型的参数列表 type_list;若 method 没有参数,值为 0。参数列表的格式是 type_list,结构从逻辑上如下描述。size 表示参数的个数;type_idx 是对应参数的类型,它的值是一个 type_ids 的 index 号,跟 return_type_idx 是同类东西。

header 里 proto_ids_size=0x03,proto_ids_off=0xc4,它的二进制描述区如下:

根据 proto_id_item 和 type_list 的格式,对照这它们的二进制部分,整理出表格如下:

可以看出,有 3 个 method 原型,返回值都为 void,index=0 的没有参数传入,index=1 的传入一个。

String 参数,index=2 的传入一个 String[]类型的参数。

注意:在这里会看到很多带 idx 结尾的字段,这一般都是索引值,所以要注意,区分这个索引值到底是对应的哪张表格,是字符串池,还是类型池等信息,如果弄混淆的话,解析就会出现混乱了。后面其他数据结构都是需要注意。

7.3.5 field_ids 数据结构

filed_ids 区里面存放的是 dex 文件引用的所有的 field。这个区的元素格式是 field_id_item,逻辑结构描述如下所示:

其中:

·class_idx:表示本 field 所属的 class 类型,class_idx 的值是 type_ids 的一个 index,并且必须指向一个 class 类型。

·type_idx:表示本 field 的类型,它的值也是 type_ids 的一个 index。

·name_idx:表示本 field 的名称,它的值是 string_ids 的一个 index。

header 里 field_ids_size=1,field_ids_off=0xe8。说明本 dex 只有一个 field,这部分的二进制描述如下:

filed_ids 只有一些元素,比较简单。根据 filed_ids 的格式,整理出表格如下。它是 Java 最常用的 System.out 标准输出部分:

注意:这里的字段都是索引值,一定要区分是哪个池子的索引值。另外,这个数据结构后面也要使用到,所以需要用一个池子来存储。

7.3.6 method_ids 数据结构

method_ids 是索引区的最后一个条目,它索引了 dex 文件里的所有 method。method_ids 的元素格式是 method_id_item,结构跟 fields_ids 很相似,如下所示:

其中:

·class_idx:表示该 method 所属的 class 类型,class_idx 的值是 type_ids 的一个 index,并且必须指向一个 class 类型。

·name_idx:表示该 method 的名称,它的值是 string_ids 的一个 index。

·proto_idx:描述该 method 的原型,指向 proto_ids 的一个 index。

header 里 method_ids_size=0x04,method_ids_off=0xf0。本部分的二进制描述如下:

整理出表格如下:

对 dex 反汇编的时候,常用的 method 表示方法是如下这种形式:

将上述表格里的字符串再次整理下,method 的描述分别为:

至此,索引区的内容描述完毕,包括 string_ids、type_ids、proto_ids、field_ids、method_ids。每个索引区域里存放着指向具体数据的偏移地址(如 string_ids),或者存放的数据是其他索引区域里面的 index 号。

7.3.7 class_defs 数据结构

上面介绍了所有的索引区域,终于到了最后一个数据结构了,之所以放最后,是因为这个数据结构是最复杂的,层次太深了。下面我试着详细介绍一下。

1.class_def_item

从字面意思解释,class_defs 区域里存放着 class 的定义。它的结构较 dex 区都要复杂些,因为有些数据都直接指向了 data 区里面。

class_defs 的数据格式为 class_def_item,结构描述如下:

参数介绍如下:

·class_idx:描述具体的 class 类型,值是 type_ids 的一个 index。值必须是一个 class 类型,不能是数组类型或者基本类型。

·access_flags:描述 class 的访问类型,诸如 public、final、static 等。在 dex-format.html 里“access_flagsDefinitions”有具体的描述。

·superclass_idx:描述 supperclass 的类型,值的形式跟 class_idx 一样。

·interfaces_off:值为偏移地址,指向 class 的 interfaces,被指向的数据结构为 type_list。class 若没有 interfaces,值为 0。

·source_file_idx:表示源代码文件的信息,值是 string_ids 的一个 index。若此项信息缺失,此项值赋值为 NO_INDEX=0xffff ffff。

·annotions_off:值是一个偏移地址,指向的内容是该 class 的注释,位置在 data 区,格式为 annotations_direcotry_item。若没有此项内容,值为 0。

·class_data_off:值是一个偏移地址,指向的内容是该 class 的使用到的数据,位置在 data 区,格式为 class_data_item。若没有此项内容,值为 0。该结构里有很多内容,详细描述该 class 的 field、method、method 里的执行代码等信息,后面有一个比较大的篇幅来讲述 class_data_item。

·static_value_off:值是一个偏移地址,指向 data 区里的一个列表(list),格式为 encoded_array_item。若没有此项内容,值为 0。

header 里 class_defs_size=0x01,class_defs_off=0x 0110。则此段二进制描述为:

根据对数据结构 class_def_item 的描述,整理出表格如下:

其实最初被编译的源码只有几行,和 class_def_item 的表格对照下,一目了然。

2.class_def_item=>class_data_item

class_data_off 指向 data 区里的 class_data_item 结构,class_data_item 里存放着本 class 使用到的各种数据,下面是 class_data_item 的逻辑结构:

关于元素的格式 Uleb128 在 string_ids 里讲述过,不赘述。encoded_field 的结构如下:

encoded_method 的结构如下:

其中:

1)method_idx_diff:前缀 methd_idx 表示它的值是 method_ids 的一个 index,后缀_diff 表示它是于另外一个 method_idx 的一个差值,就是相对于 encodeed_method[]数组里上一个元素的 method_idx 的差值。其实 encoded_filed→field_idx_diff 表示的也是相同的意思,只是编译出来的 Hello.dex 文件里没有使用到 class filed 所以没有仔细讲,详细的参考 dex_format.html 的官网文档。

2)access_flags:访问权限,比如 public、private、static、final 等。

3)code_off:一个指向 data 区的偏移地址,目标是本 method 的代码实现。被指向的结构是:

·code_item,有近 10 项元素,后面再详细解释。

·class_def_item→class_data_off=0x 0234。

名称为 LHello;class 里只有 2 个 directive methods。directive_methods 里的值都是 Uleb128 的原始二进制值。按照 directive_methods 的格式 encoded_method 再整理一次这 2 个 method 描述,得到结果如下表格所描述:

method 一个是<init>,一个是 main,这里需要用 string_ids 那块介绍到的一个方法就是解码 uleb128 类型的方法得到正确的 value 值。

3.class_def_item=>class_data_item=>code_item

到这里,逻辑的描述有点深了。先回想一下是怎么走到这一步的,code_item 在 dex 里处于一个什么位置。

1)一个 dex 文件被分成了 9 个区,详细见图 7-1 所示。其中有一个索引区叫作 class_defs,索引了 dex 里面用到的 class,以及对这个 class 的描述。

2)class_defs 区,其实是 class_def_item 结构。这个结构里描述了 LHello;的各种信息,诸如名称、superclass、access flag、interface 等。class_def_item 里有一个元素 class_data_off,指向 data 区里的一个 class_data_item 结构,用来描述 class 使用到的各种数据。自此以后的结构都归于 data 区了。

3)class_data_item 结构,描述 class 里使用到的 static field,instance field,direct_method,和 virtual_method 的数目和描述。例子 Hello.dex 里,只有 2 个 direct_method,其余的 field 和 method 的数目都为 0。描述 direct_method 的结构叫作 encoded_method,是用来详细描述某个 method 的。

4)encoded_method 结构,描述某个 method 的 method 类型,access flags 和一个指向 code_item 的偏移地址,里面存放的是该 method 的具体实现。

5)code_item,一层又一层,简要地说,code_item 结构里描述着某个 method 的具体实现。它的结构如下描述:

末尾的 3 项见图 7-2 所示,标志为 optional,表示可能有,也可能没有,根据具体的代码来:

·registers_size:该段代码使用到的寄存器数目。

·ins_size:method 传入参数的数目。

·outs_size:该段代码调用其他 method 时需要的参数个数。

·tries_size:try_item 结构的个数。

·debug_off:偏移地址,指向该段代码 debug 信息存放位置,是一个 debug_info_item 结构。

·insns_size:指令列表的大小,以 16bit 为单位。insns 是 instructions 的缩写。

·padding:值为 0,用于对齐字节。

·tries 和 handlers:用于处理 Java 中的 exception,常见的语法有 try catch。

4.分析 main method 的执行代码并与 smali 反编译的结果比较

在 7.2 节里有 2 个方法,因为 main 里的执行代码是自己写的,分析它会熟悉很多。偏移地址是 directive_method[1]→code_off=0x0148,二进制描述如下:

根据 code_item 的结构整理表格如下:

insns 数组里的 8 个二进制原始数据,对这些数据的解析,需要参考官网的文档“Dalvik VM InstructionFormat”和“Bytecode for Dalvik VM”。

分析思路整理如下:

1)“Dalvik VM Instruction Format”里操作符 op 都是位于首个 16 位数据的低 8 位,起始的是 op=0x62。

2)在“Bytecode for Dalvik VM”里找到对应的 Syntax 和 format。

3)在“Dalvik VM Instruction Format”里查找 21c,得知 op=0x62 的指令占据 2 个 16 位数据,格式是 AA|op BBBB,解释为 op vAA,type@BBBB。因此这 8 组 16 位数据里,前 2 个是一组。对比数据得 AA=0x00,BBBB=0x0000。

4)返回“Bytecode for Dalvik VM”里查阅对 sget_object 的解释,AA 的值表示 Value Register,即 0 号寄存器;BBBB 表示 static field 的 index,就是之前分析的 field_ids 区里 Index=0 指向的那个东西,当时的 fields_ids 的分析结果如下:

对 field 常用的表述是:包含 field 的类型→field 名称:field 类型。此次指向的就是 Ljava/lang/System;→out:Ljava/io/printStream。

5)综上所述,前 2 个 16 位数据 0x 0062 0000 解释为:

其余的 6 个 16 位数据分析思路与这个一样,依次整理如下:

6)最后再整理下 main method,用容易理解的方式表示出来就是:

看起来很像 smali 格式语言,不妨使用 smali 反编译 Hello.dex,看看 smali 生成的代码跟方才推导出来的有什么差异,结果如下:

从内容上看,二者形式上有些差异,但表述的是同一个 method。这说明刚才分析的路子是没有跑偏的。另外一个 method 是<init>,若是分析的话,思路和流程跟 main 一样。走到这里,心里很踏实了。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文