1. 关于Android dex文件

dex文件作为Android APK的组成部分,其主要是Android的Java代码经过编译生成class文件,再经过dx命令生成的。这里面包括了APK的源码,反编译时最主要就是对这个文件进行反编译。有人会问,知道了dex的文件结构有什么用呢?

在Android安全方面来说,dex是安全的重头戏,如果能够了解了dex文件的格式,那么对于dex文件的加固原理也就有简单的了解(关于加固应该不会写了)。

话不多说,写下dex文件解析过程吧!

2. dex文件格式

关于dex文件格式,官方的文件格式如下:

名称

格式

说明

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文件格式,可以分为三类:

头部信息 header

引用信息 string_ids(字符串索引)、type_ids(类型索引)、proto_ids(方法原型索引)、field_ids(方法原型索引)、method_ids(方法索引)、call_site_ids(根本没有找到)

数据信息 class_defs(类定义列表)、method_handles(方法句柄列表)、data(数据区,所有数据都在这里)、link_data(静态链接文件中使用的数据)

3. header信息

头部信息位于文件的头位置,用于表示文件的基本信息,比如文件大小、格式等等。dex文件的header格式如下:

名称

格式

说明

magic

ubyte[8] = DEX_FILE_MAGIC

魔法值。ubyte[8] DEX_FILE_MAGIC = { 0x64 0x65 0x78 0x0a 0x30 0x33 0x38 0x00 } = "dex\n038\0"

checksum

uint

文件剩余内容(除 magic 和此字段之外的所有内容)的 adler32 校验和;用于检测文件损坏情况

signature

ubyte[20]

文件剩余内容(除 magic、checksum 和此字段之外的所有内容)的 SHA-1 签名(哈希);用于对文件进行唯一标识

file_size

uint

整个文件(包括标头)的大小,以字节为单位

header_size

uint = 0x70

标头(整个区段)的大小,以字节为单位。这一项允许至少一定程度的向后/向前兼容性,而不必让格式失效。

endian_tag

uint = ENDIAN_CONSTANT

字节序标记。更多详情,请参阅上文中“ENDIAN_CONSTANT 和 REVERSE_ENDIAN_CONSTANT”下的讨论。

link_size

uint

链接区段的大小;如果此文件未进行静态链接,则该值为 0

link_off

uint

从文件开头到链接区段的偏移量;如果 link_size == 0,则该值为 0。该偏移量(如果为非零值)应该是到 link_data 区段的偏移量。本文档尚未指定此处所指的数据格式;此标头字段(和之前的字段)会被保留为钩子,以供运行时实现使用。

map_off

uint

从文件开头到映射项的偏移量。该偏移量(必须为非零)应该是到 data 区段的偏移量,而数据应采用下文中“map_list”指定的格式。

string_ids_size

uint

字符串标识符列表中的字符串数量

string_ids_off

uint

从文件开头到字符串标识符列表的偏移量;如果 string_ids_size == 0(不可否认是一种奇怪的极端情况),则该值为 0。该偏移量(如果为非零值)应该是到 string_ids 区段开头的偏移量。

type_ids_size

uint

类型标识符列表中的元素数量,最多为 65535

type_ids_off

uint

从文件开头到类型标识符列表的偏移量;如果 type_ids_size == 0(不可否认是一种奇怪的极端情况),则该值为 0。该偏移量(如果为非零值)应该是到 type_ids 区段开头的偏移量。

proto_ids_size

uint

原型标识符列表中的元素数量,最多为 65535

proto_ids_off

uint

从文件开头到原型标识符列表的偏移量;如果 proto_ids_size == 0(不可否认是一种奇怪的极端情况),则该值为 0。该偏移量(如果为非零值)应该是到 proto_ids 区段开头的偏移量。

field_ids_size

uint

字段标识符列表中的元素数量

field_ids_off

uint

从文件开头到字段标识符列表的偏移量;如果 field_ids_size == 0,则该值为 0。该偏移量(如果为非零值)应该是到 field_ids 区段开头的偏移量。

method_ids_size

uint

方法标识符列表中的元素数量

method_ids_off

uint

从文件开头到方法标识符列表的偏移量;如果 method_ids_size == 0,则该值为 0。该偏移量(如果为非零值)应该是到 method_ids 区段开头的偏移量。

class_defs_size

uint

类定义列表中的元素数量

class_defs_off

uint

从文件开头到类定义列表的偏移量;如果 class_defs_size == 0(不可否认是一种奇怪的极端情况),则该值为 0。该偏移量(如果为非零值)应该是到 class_defs 区段开头的偏移量。

data_size

uint

data 区段的大小(以字节为单位)。该数值必须是 sizeof(uint) 的偶数倍。

data_off

uint

从文件开头到 data 区段开头的偏移量。

可以看到文件格式非常之多,这可比之前解析的resources文件庞大的多了,我们先定义下对应的实体:

class header_item(

var magic: Array,

var checksum: uint32_t, //文件剩余内容(除 magic 和此字段之外的所有内容)的 adler32 校验和;用于检测文件损坏情况

var signature: ByteArray, //文件剩余内容(除 magic、checksum 和此字段之外的所有内容)的 SHA-1 签名(哈希);用于对文件进行唯一标识

var file_size: uint32_t, // 整个文件(包括标头)的大小,以字节为单位

var header_size: uint32_t, // 标头(整个区段)的大小,以字节为单位。这一项允许至少一定程度的向后/向前兼容性,而不必让格式失效。

var endian_tag: uint32_t, // ENDIAN_CONSTANT 字节序标记。更多详情,请参阅上文中“ENDIAN_CONSTANT 和 REVERSE_ENDIAN_CONSTANT”下的讨论。

var link_size: uint32_t, // 链接区段的大小;如果此文件未进行静态链接,则该值为 0

var link_off: uint32_t, // 从文件开头到链接区段的偏移量;如果 link_size == 0,则该值为 0。该偏移量(如果为非零值)应该是到 link_data 区段的偏移量。本文档尚未指定此处所指的数据格式;此标头字段(和之前的字段)会被保留为钩子,以供运行时实现使用。

var map_off: uint32_t, // 从文件开头到映射项的偏移量。该偏移量(必须为非零)应该是到 data 区段的偏移量,而数据应采用下文中“map_list”指定的格式。

var string_ids_size: uint32_t, // 字符串标识符列表中的字符串数量

var string_ids_off: uint32_t, // 从文件开头到字符串标识符列表的偏移量;如果 string_ids_size == 0(不可否认是一种奇怪的极端情况),则该值为 0。该偏移量(如果为非零值)应该是到 string_ids 区段开头的偏移量。

var type_ids_size: uint32_t, // 类型标识符列表中的元素数量,最多为 65535

var type_ids_off: uint32_t, // 从文件开头到类型标识符列表的偏移量;如果 type_ids_size == 0(不可否认是一种奇怪的极端情况),则该值为 0。该偏移量(如果为非零值)应该是到 type_ids 区段开头的偏移量。

var proto_ids_size: uint32_t, // 原型标识符列表中的元素数量,最多为 65535

var proto_ids_off: uint32_t, // 从文件开头到原型标识符列表的偏移量;如果 proto_ids_size == 0(不可否认是一种奇怪的极端情况),则该值为 0。该偏移量(如果为非零值)应该是到 proto_ids 区段开头的偏移量。

var field_ids_size: uint32_t, // 字段标识符列表中的元素数量

var field_ids_off: uint32_t, // 从文件开头到字段标识符列表的偏移量;如果 field_ids_size == 0,则该值为 0。该偏移量(如果为非零值)应该是到 field_ids 区段开头的偏移量。

var method_ids_size: uint32_t, // 方法标识符列表中的元素数量

var method_ids_off: uint32_t, // 从文件开头到方法标识符列表的偏移量;如果 method_ids_size == 0,则该值为 0。该偏移量(如果为非零值)应该是到 method_ids 区段开头的偏移量。

var class_defs_size: uint32_t, // 类定义列表中的元素数量

var class_defs_off: uint32_t, // 从文件开头到类定义列表的偏移量;如果 class_defs_size == 0(不可否认是一种奇怪的极端情况),则该值为 0。该偏移量(如果为非零值)应该是到 class_defs 区段开头的偏移量。

var data_size: uint32_t, // data 区段的大小(以字节为单位)。该数值必须是 sizeof(uint) 的偶数倍。

var data_off: uint32_t //从文件开头到 data 区段开头的偏移量。

) : base_item() {

override fun toString(): String {

super.toString()

val b = ByteArray(magic.size, { i -> magic[i].getValue() })

val string = java.lang.String(b)

return "magic = $string\n" +

"checksum = ${getHexValue(checksum)},value = ${getUUnitValue(checksum)}\n" +

"checksum = ${getHexValue(checksum)},value = ${getUUnitValue(checksum)}\n" +

"file_size = ${getHexValue(file_size)},value = ${getUUnitValue(file_size)}\n" +

"header_size = ${getHexValue(header_size)},value = ${getUUnitValue(header_size)}\n" +

"endian_tag = ${getHexValue(endian_tag)},value = ${getUUnitValue(endian_tag)}\n" +

"link_size = ${getHexValue(link_size)},value = ${getUUnitValue(link_size)}\n" +

"link_off = ${getHexValue(link_off)},value = ${getUUnitValue(link_off)}\n" +

"map_off = ${getHexValue(map_off)},value = ${getUUnitValue(map_off)}\n" +

"string_ids_size = ${getHexValue(string_ids_size)},value = ${getUUnitValue(string_ids_size)}\n" +

"string_ids_off = ${getHexValue(string_ids_off)},value = ${getUUnitValue(string_ids_off)}\n" +

"type_ids_size = ${getHexValue(type_ids_size)},value = ${getUUnitValue(type_ids_size)}\n" +

"type_ids_off = ${getHexValue(type_ids_off)},value = ${getUUnitValue(type_ids_off)}\n" +

"proto_ids_size = ${getHexValue(proto_ids_size)},value = ${getUUnitValue(proto_ids_size)}\n" +

"proto_ids_off = ${getHexValue(proto_ids_off)},value = ${getUUnitValue(proto_ids_off)}\n" +

"field_ids_size = ${getHexValue(field_ids_size)},value = ${getUUnitValue(field_ids_size)}\n" +

"field_ids_off = ${getHexValue(field_ids_off)},value = ${getUUnitValue(field_ids_off)}\n" +

"method_ids_size = ${getHexValue(method_ids_size)},value = ${getUUnitValue(method_ids_size)}\n" +

"method_ids_off = ${getHexValue(method_ids_off)},value = ${getUUnitValue(method_ids_off)}\n" +

"class_defs_size = ${getHexValue(class_defs_size)},value = ${getUUnitValue(class_defs_size)}\n" +

"class_defs_off = ${getHexValue(class_defs_off)},value = ${getUUnitValue(class_defs_off)}\n" +

"data_size = ${getHexValue(data_size)},value = ${getUUnitValue(data_size)}\n" +

"data_off = ${getHexValue(data_off)},value = ${getUUnitValue(data_off)}\n"

}

}

接着我们先解析一波,这里要清楚下源码中都是采用的小端顺序(自行百度):

val stream = File(File("").absolutePath + "//..//dexAnalyzer//src//com//nick//classes.dex").inputStream()

val os = ByteArrayOutputStream()

var bytes = ByteArray(1024)

var len = stream.read(bytes)

while (len != -1) {

os.write(bytes, 0, len)

len = stream.read(bytes)

}

// 将文件转成二进制字节数组操作

bytes = os.toByteArray()

// 创建header_item

val header_item = header_item(

Array(8, { i -> read_uint8_t(bytes, i) }), // 1

read_uint32_t(bytes, 8), // 4

kotlin.ByteArray(20, { // 20

b ->

bytes[b + 12]

}),

read_uint32_t(bytes, 32), // 4

read_uint32_t(bytes, 32 + 4), // 4

read_uint32_t(bytes, 32 + 4 * 2), // 4

read_uint32_t(bytes, 32 + 4 * 3), // 4

read_uint32_t(bytes, 32 + 4 * 4), // 4

read_uint32_t(bytes, 32 + 4 * 5), // 4

read_uint32_t(bytes, 32 + 4 * 6), // 4

read_uint32_t(bytes, 32 + 4 * 7), // 4

read_uint32_t(bytes, 32 + 4 * 8), // 4

read_uint32_t(bytes, 32 + 4 * 9), // 4

read_uint32_t(bytes, 32 + 4 * 10), // 4

read_uint32_t(bytes, 32 + 4 * 11), // 4

read_uint32_t(bytes, 32 + 4 * 12), // 4

read_uint32_t(bytes, 32 + 4 * 13), // 4

read_uint32_t(bytes, 32 + 4 * 14), // 4

read_uint32_t(bytes, 32 + 4 * 15), // 4

read_uint32_t(bytes, 32 + 4 * 16), // 4

read_uint32_t(bytes, 32 + 4 * 17), // 4

read_uint32_t(bytes, 32 + 4 * 18), // 4

read_uint32_t(bytes, 32 + 4 * 19) // 4

)

println(header_item)

这里说明下read_uint32_t()方法是用于读取四个字节的byte,并将其转为uint32_t格式,其实也就是int。看下打印结果:

头部信息结果

二进制文件

我们可以对比下,结果完全是OK的!

4 string_ids信息

上面看完了头部的信息,下面看下很重要的一部分信息string_ids信息,还是先看下格式:

string_id_item

名称

格式

说明

string_data_off

uint

从文件开头到此项的字符串数据的偏移量。该偏移量应该是到 data 区段中某个位置的偏移量,而数据应采用下文中“string_data_item”指定的格式。没有偏移量对齐要求。

string_data_item

名称

格式

说明

utf16_size

uleb128

此字符串的大小;以 UTF-16 代码单元(在许多系统中为“字符串长度”)为单位。也就是说,这是该字符串的解码长度(编码长度隐含在 0 字节的位置)。

data

ubyte[]

一系列 MUTF-8 代码单元(又称八位字节),后跟一个值为 0 的字节。

这里又有问题了:

uleb128格式如何获取?

从Android源码中获取

/**

* Gets the number of bytes in the unsigned LEB128 encoding of the

* given value.

*

* @param value the value in question

* @return its write size, in bytes

*/

public static int unsignedLeb128Size(int value) {

// TODO: This could be much cleverer.

int remaining = value >> 7;

int count = 0;

while (remaining != 0) {

remaining >>= 7;

count++;

}

return count + 1;

}

/**

* Gets the number of bytes in the signed LEB128 encoding of the

* given value.

*

* @param value the value in question

* @return its write size, in bytes

*/

public static int signedLeb128Size(int value) {

// TODO: This could be much cleverer.

int remaining = value >> 7;

int count = 0;

boolean hasMore = true;

int end = ((value & Integer.MIN_VALUE) == 0) ? 0 : -1;

while (hasMore) {

hasMore = (remaining != end)

|| ((remaining & 1) != ((value >> 6) & 1));

value = remaining;

remaining >>= 7;

count++;

}

return count;

}

MUTF-8又是怎样的?

可以看下这篇博客,解释的很清楚。

格式分析完了,下面来解析吧:

// 偏移量

var offset = header_item.header_size.getValue()

// 字符串偏移量的数组

string_ids = Array(header_item.string_ids_size.getValue(), {

i ->

string_id_item(read_uint32_t(bytes, offset + i * 4))

})

// 字符串数组数组

string_ids?.forEach {

val value = it.string_data_off.getValue()

val unsignedLeb128 = readUnsignedLeb128(bytes, value)

val string_data_item = string_data_item(it.string_data_off, uleb128(unsignedLeb128.second), coypByte(bytes, unsignedLeb128.first, unsignedLeb128.second))

list.add(string_data_item)

println(string_data_item)

}

部分结果如下:

截取部分

5 已解析部分

header

string_ids

type_ids

proto_ids

field_ids

method_ids

class_defs

6 未解析部分

call_site_ids

method_handles

link_data

源码就在这里

7 总结

解析这个dex文件大概花了一个星期的时间,从Android官网看文档、查资料。一点点从零到大部分解析完成,感觉还是挺有趣的。

java 解析修改dex_Android dex文件解析相关推荐

  1. 2020-10-27(dex文件解析)

    一张图搞懂dex 大图这里 当然也可以通过下面的图12 DexFile的文件格式,了解更清楚. DEX文件详解 什么是dex文件? 如何生成一个dex文件 dex文件的作用 dex文件格式详解 什么是 ...

  2. java解析五元组_pcap文件解析,并且按照五元组分类

    [实例简介] pcap文件解析,并按照五元组分包,全部用java语言实现. [实例截图] [核心代码] PcapTestZZ ├── PcapTestZ │   ├── 111.206.37.1930 ...

  3. java怎么xml文件解析_Java对Xml文件解析

    JAVA 解析 XML 通常有两种方式,DOM 和 SAX. DOM 虽然是 W3C 的标准,提供了标准的解析方式,但它的解析效率一直不尽如人意,因为使用DOM解析XML时,解析器读入整个文档并构建一 ...

  4. 【Android 安全】DEX 加密 ( Java 工具开发 | 生成 dex 文件 | Java 命令行执行 )

    文章目录 一.生成 dex 文件 二.生成 dex 文件代码示例 三.生成 dex 结果 参考博客 : [Android 安全]DEX 加密 ( 常用 Android 反编译工具 | apktool ...

  5. awx文件解析_Android so(ELF)文件解析

    一.前言 so文件是啥?so文件是elf文件,elf文件后缀名是.so,所以也被chang常称之为so文件,elf文件是linux底下二进制文件,可以理解为windows下的PE文件,在Android ...

  6. 哪些服务器曾被发现文件解析漏洞,常见的文件解析漏洞总结

    常见的文件解析漏洞总结 iis解析漏洞 解析漏洞主要是说一些特殊文件被iis,apache,nginx等web容器在特殊情况下被解释成脚本文件格式 ==iis5.x/6.0解析漏洞:== 1,目录解析 ...

  7. java整合groove实战—xml文件解析

    背景:在JAVA中执行groovy有多种方式,个人觉得最舒服的莫过于使用IDE插件创建groovy工程,这样可以直接在java代码中引用groovy的类.因groovy的IDE插件会自动对groovy ...

  8. Java进度条(excel文件解析)的实现

    文件上传页面 <!doctype html> <html> <head> <meta charset="utf-8"> <ti ...

  9. java 对.EML格式邮件文件解析

    使用 Apache James Mime4J <dependency><groupId>org.apache.james</groupId><artifact ...

最新文章

  1. 区块链笔记-Hash算法
  2. laravel-admin配置安装完新手使用
  3. NetBeans eclipse比較
  4. angularJS+requireJS实现controller及directive的按需加载
  5. 信息学奥赛C++语言:小玉家的电费
  6. 十分钟学会用Go编写Web中间件
  7. 微信 html关闭当前页
  8. REST another WebService???
  9. OpenCV-特征提取与检测(04、亚像素级别角点检测)
  10. 求两个字符串的最长的连续公共子串
  11. 凤凰刷机找不到手机设备的解决方法
  12. mappedBy reference an unknown target entity property解决方法
  13. 百度地图迁徙大数据_百度地图迁徙大数据:除武汉外多地出行趋势回升
  14. iOS判断 英文 数字 汉字等
  15. 任务并行库(Task Parellel Library)parallel.for parallel.foreach、List、ConcurrentBag 并行集合、线程安全结合
  16. matplotlib图表多曲线多纵轴绘制工具方法
  17. 125家单位联合完成微生物组实验手册(Microbiome Protocol eBook)第1版
  18. 【C语言】exit(0)与exit(1)有什么区别
  19. php反转图片颜色,PHP 图片处理类(水印、透明度、缩放、相框、锐化、旋转、翻转、剪切、反色)...
  20. hathitrust 下载工具 (大量英文原版名著扫描免费下载)

热门文章

  1. Poj 1451 JAVA 个人解题报告
  2. 签字板 JAVA_JS canvas实现画板和签字板功能
  3. Python3发送带图邮件
  4. 河南固始计算机学校哪个好,信阳市最好的5所高级中学,每一所都实力雄厚,都出过高考状元...
  5. 从王者荣耀聊聊游戏的帧同步
  6. 【湃哒星说安全】攻防演练中数据库信息收集方法记录
  7. 开发板搭建pppd客户端
  8. JS转换金额大写方法
  9. 基于Hadoop技术实现的离线电商分析平台(Flume、Hadoop、Hbase、SpringMVC、highcharts)- 驴妈妈旅游项目
  10. 【基础教程】模拟退火算法【007期】