感觉有必要在 Android 的 Dex 结构上好好补点功课,特地开篇文章补充一下自己的知识储备。
虽然 Android 在虚拟机上有 Dalvik 和 ART 的分别,在不同版本上应用的机制也不一样,但在 Dex 结构上的设计在已开始就是通用的,理解该数据结构有利于对加深对 Android 虚拟机的理解。
Dex 文件结构
最权威的当属官方文档:Dalvik 可执行文件格式
这里贴一下文件板式的说明:
名称 | 格式 | 说明 |
---|---|---|
header | header_item | 头文件 |
string_ids | string_id_item[] | 字符串标识符列表。这些是此文件使用的所有字符串的标识符,用于内部命名(例如类型描述符)或用作代码引用的常量对象。此列表必须使用 UTF-16 代码点值按字符串内容进行排序(不采用语言区域敏感方式),且不得包含任何重复条目。 |
type_ids | type_id_item[] | 类型标识符列表。这些是此文件引用的所有类型(类、数组或原始类型)的标识符(无论文件中是否已定义)。此列表必须按 string_id 索引进行排序,且不得包含任何重复条目。 |
proto_ids | proto_id_item[] | 方法原型标识符列表。这些是此文件引用的所有原型的标识符。此列表必须按返回类型(按 type_id 索引排序)主要顺序进行排序,然后按参数列表(按 type_id 索引排序的各个参数,采用字典排序方法)进行排序。该列表不得包含任何重复条目。 |
field_ids | field_id_item[] | 字段标识符列表。这些是此文件引用的所有字段的标识符(无论文件中是否已定义)。此列表必须进行排序,其中定义类型(按 type_id 索引排序)是主要顺序,字段名称(按 string_id 索引排序)是中间顺序,而类型(按 type_id 索引排序)是次要顺序。该列表不得包含任何重复条目。 |
method_ids | method_id_item[] | 方法标识符列表。这些是此文件引用的所有方法的标识符(无论文件中是否已定义)。此列表必须进行排序,其中定义类型(按 type_id 索引排序)是主要顺序,方法名称(按 string_id 索引排序)是中间顺序,而方法原型(按 proto_id 索引排序)是次要顺序。该列表不得包含任何重复条目。 |
class_defs | class_def_item[] | 类定义列表。这些类必须进行排序,以便所指定类的超类和已实现的接口比引用类更早出现在该列表中。此外,对于在该列表中多次出现的同名类,其定义是无效的。 |
call_site_ids | call_site_id_item[] | 调用站点标识符列表。这些是此文件引用的所有调用站点的标识符(无论文件中是否已定义)。此列表必须按 call_site_off 以升序进行排序。 |
method_handles | method_handle_item[] | 方法句柄列表。此文件引用的所有方法句柄的列表(无论文件中是否已定义)。此列表未进行排序,而且可能包含将在逻辑上对应于不同方法句柄实例的重复项。 |
data | ubyte[] | 数据区,包含上面所列表格的所有支持数据。不同的项有不同的对齐要求;如有必要,则在每个项之前插入填充字节,以实现所需的对齐效果。 |
link_data | ubyte[] | 静态链接文件中使用的数据。本文档尚未指定本区段中数据的格式。此区段在未链接文件中为空,而运行时实现可能会在适当的情况下使用这些数据。 |
单纯看文档可能无法理解,这里再补充一张图加深大家的理解:
Dex 较为复杂,推荐开启 010Editor 后自行调试,这样才能加深理解,这里我也拿贴一个调试的例子:
可以看到我选中的条目是 class_defs
段的第 59 个条目,类名为 public final android.support.v4.view.ViewCompat
,然后根据 class_data_off
的值,可以看到偏移恰好在 1766107(0x1AF2DB)
是该类的数据段,而后数据段内基本也是各种类型嵌套,比如说 encoded_method_list direct_methods
里定义了类里的所有方法,而 encoded_method method[4]
则是其中的一个具体方法,函数签名为 public static int android.support.v4.view.ViewCompat.getOverScrollMode(android.view.View)
,具体的函数体在 code_item code
内,而后函数体内的所有指令则是 ushort insns[7]
。
可以看到 Dex 文件实际上正是有这种嵌套+位置偏移的方式,巧妙地将信息存储在文件中。
练兵场 - 修改 FART 的修复脚本
FART 是看雪大牛 hanbingle 提出的一种基于主动调用的 Android 脱壳方案,在官网的 repo 中,作者提供了 fart.py 脚本,该脚本能合并提取出的指令和 Dex 文件,但只单纯以文本显示,因此我们的目标是在理解 Dex 结构的基础上,通过修改该脚本,得到还原的 Dex 文件。
看一下 FART 提取出的 ABCD_abcd.bin
文件,该文件由抽取而出的指令拼接而成,每个结构体如下图所示:
{
name:void com.SecShell.SecShell.ApplicationWrapper.<init>(),
method_idx:15,
offset:10536,
code_item_len:24,
ins:AQABAAEAAAAAAAAABAAAAHAQAAAAAA4A
};
其中 ins
字段内是 base64 编码的 DexCode 结构体,offset
字段是该结构体在对应 Dex 结构中的偏移,所以结合以上两点,能很方便地写出代码:
import base64
import os
import re
import sys
def parse_ins_bin(ins_path):
with open(ins_path, 'r') as f:
content = f.read()
ins_array=re.findall(r"{name:(.*?),method_idx:(.*?),offset:(.*?),code_item_len:(.*?),ins:(.*?)}", content) #(.*?)最短匹配
code_items = []
for ins in ins_array:
offset=(int)(ins[2])
code_item = base64.b64decode(ins[4])
code_items.append((offset, code_item))
return code_items
def parse_all_ins_bin(ins_dir, keyword):
ins_bins = os.listdir(ins_dir)
ins_bins = [i for i in ins_bins if '.bin' in i and keyword in i]
code_items = []
for ins_bin in ins_bins:
code_items += parse_ins_bin('{}/{}'.format(ins_dir, ins_bin))
return code_items
if __name__ == "__main__":
dex_path = sys.argv[1]
ins_dir = sys.argv[2]
keyword = dex_path.split('/')[-1].split('_')[0]
with open(dex_path, 'rb') as f:
dex = bytearray(f.read())
code_items = parse_all_ins_bin(ins_dir, keyword)
for offset, code_item in code_items:
size = len(code_item)
dex[offset:offset+size] = code_item
output_path = dex_path.replace('.dex', '_repair.dex')
with open(output_path, 'wb') as f:
f.write(dex)
结语
前前后后看了几天资料,最后结合 010Editor 一个个结构看下来才焕然大悟,果然实践出真知 →_→
没写很详细的说明,想理解细节的建议阅读下方的参考链接👇