- 对本书的赞誉
- 前言
- 基础篇
- 第 1 章 Android 中锁屏密码加密算法分析
- 第 2 章 Android 中 NDK 开发
- 第 3 章 Android 中开发与逆向常用命令总结
- 第 4 章 so 文件格式解析
- 第 5 章 AndroidManifest.xml 文件格式解析
- 第 6 章 resource.arsc 文件格式解析
- 第 7 章 dex 文件格式解析
- 防护篇
- 第 8 章 Android 应用安全防护的基本策略
- 第 9 章 Android 中常用权限分析
- 第 10 章 Android 中的 run-as 命令
- 第 11 章 Android 中的 allowBackup 属性
- 第 12 章 Android 中的签名机制
- 第 13 章 Android 应用加固原理
- 第 14 章 Android 中的 so 加固原理
- 工具篇
- 第 15 章 Android 逆向分析基础
- 第 16 章 反编译神器 apktool 和 Jadx
- 第 17 章 Hook 神器 Xposed
- 第 18 章 脱壳神器 ZjDroid
- 第 19 章 Native 层 Hook 神器 Cydia Substrate
- 操作篇
- 第 20 章 静态方式逆向应用
- 第 21 章 动态调试 smali 源码
- 第 22 章 IDA 工具调试 so 源码
- 第 23 章 逆向加固应用
- 第 24 章 逆向应用经典案例分析
- 第 25 章 Android 中常见漏洞分析
- 第 26 章 文件加密病毒 Wannacry 样本分析
7.3 解析数据结构
下面按照图 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论