Android逆向工程:Dex 文件格式解析详解

Android DEX 文件格式解析详解

本文档基于 l0neman 的博客 和 Android 9.0.0_r3 系统源码整理 & 一些个人理解

前言

Java 代码文件在经过 javac 编译器编译后会产出 .class 格式的 Java 虚拟机可执行的字节码文件.dex,而 DEX 文件则是 Android SDK 编译 Java 代码后的产物(Android SDK 使用 dx 或 d8 编译器将 .class 文件编译为 .dex 文件)。

了解 DEX 文件结构是理解 Android 虚拟机原理的基础,同时也是学习 Android 逆向工程的基础。

DEX 文件的文件后缀为 .dex,是 Android 虚拟机的可执行文件。

基本数据类型

首先说明组成 DEX 文件结构中的基本类型,包含原生类型和 uleb128 类型:

名称 说明
byte 8 位有符号整数
ubyte 8 位无符号整数
short 16 位有符号整数,小端字节序
ushort 16 位无符号整数,小端字节序
int 32 位有符号整数,小端字节序
uint 32 位无符号整数,小端字节序
long 64 位有符号整数,小端字节序
ulong 64 位无符号整数,小端字节序
sleb128 有符号 LEB128,可变长度
uleb128 无符号 LEB128,可变长度
uleb128p1 无符号 LEB128 + 1,可变长度

简单记忆:
dex中, byte 、short、int、long 和ubyte\uint\ushort\ulong ,带u的是无符号整数,不带u的是有符号整数,(无符号的表示正数和0,有符号的表示正负数)刚好相反

在系统源码 DexFile.h 的对应类型别名定义如下:

// DexFile.h
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;

LEB128 类型

LEB128 类型是 DEX 文件中的基础类型之一,该类型格式借鉴了 DWARF3 规范。在 DEX 文件中,LEB128 仅用于对 32 位数字进行编码。

LEB128 全称为"Little-Endian Base 128",表示任意有符号或无符号整数的可变长度编码。

每个 LEB128 编码值由 1-5 个字节组成,共同表示一个 32 位的值,设计 LEB128 的目的是为了节省内存。

表示方法是:每个字节的首位为标志位,为 0 说明这个字节是 LEB128 类型的最后一个字节,为 1 则表示后面还有若干字节,然后将每个字节去除标志位后的 7 位组合成一个 32 位数字。

uleb128

对于无符号的 uleb128 类型,使用两个字节表示十进制数字 4176 的示例如下:

11010000  00100000

去除标志位后:

1010000   0100000

组合后使用便于阅读的大端格式表示为:

00010000  01010000

1000001010000 转化为十进制的值为 4176。

sleb128

对于有符号的 sleb128 类型,最后一个字节的除了标志位的最高位将为符号位,将进行符号扩展。

例如下面使用两个字节的 sleb128 表示有符号数:

11010000  01100000

去除标志位后:

1010000   1100000

组合后使用便于阅读的大端格式表示为:

00110000  01010000

由于符号位为 1,说明表示负数,则使用补码计算:

首先求反码:00001111  10101111
然后 +1 ->  00001111  10110000

111110110000 的十进制结果为 4016,添加负号为 -4016

uleb128p1

uleb128p1 用于表示一个有符号值,它的编码方法为 uleb128 的值加 1,那么解码时首先以 uleb128 格式解析值,然后减去 1,主要是为了表示 -1 这个常用数而节省空间。

LEB128 类型的一些典型值:

Binary Hex sleb128 uleb128 uleb128p1
00000000 00 0 0 -1
00000001 01 1 1 0
01111111 7f -1 127 126
10000000 01111111 80 7f -128 16256 16255

DEX 文件结构图

DEX 文件的总体结构可以用以下示意图表示:

+------------------------+
|      header_item       |  文件头 (0x70 字节)
+------------------------+
|      string_ids        |  字符串ID列表
+------------------------+
|       type_ids         |  类型ID列表
+------------------------+
|       proto_ids        |  方法原型ID列表
+------------------------+
|       field_ids        |  字段ID列表
+------------------------+
|      method_ids        |  方法ID列表
+------------------------+
|      class_defs        |  类定义列表
+------------------------+
|      call_site_ids     |  调用站点ID列表(可选)
+------------------------+
|    method_handle_ids   |  方法句柄ID列表(可选)
+------------------------+
|         data           |  数据区域
|                        |  ├─ string_data_item
|                        |  ├─ type_list
|                        |  ├─ code_item
|                        |  ├─ class_data_item
|                        |  ├─ map_list
|                        |  └─ debug_info_item
+------------------------+

DEX 文件结构

DEX 文件的总体结构包含以下主要部分:

  1. header_item - 文件头部,包含文件的基本信息和各个段的偏移量
  2. string_ids - 字符串标识符列表
  3. type_ids - 类型标识符列表
  4. proto_ids - 方法原型标识符列表
  5. field_ids - 字段标识符列表
  6. method_ids - 方法标识符列表
  7. class_defs - 类定义列表
  8. map_list - 数据区段映射列表
  9. code_item - 代码项
  10. debug_info_item - 调试信息项

DEX 结构详细说明

header_item

DEX 文件的头部具有一个 header_item 结构,它存放了整个 DEX 文件的元信息,描述了一个 DEX 文件的概要结构。

struct header_item {
   
   
    /* 魔数 */
    u1 magic[8];
    
    /* 文件剩余内容(除 magic 和此字段之外的所有内容)的 adler32 校验和 */
    u4 checksum;
    
    /* 文件剩余内容的 SHA-1 签名(哈希),用于对文件进行唯一标识 */
    u1 signature[20];
    
    /* 整个文件(包括标头)的大小,以字节为单位 */
    u4 file_size;
    
    /* 标头(整个区段)的大小,以字节为单位 */
    u4 header_size;
    
    /* 字节序标记 */
    u4 endian_tag;
    
    /* 链接区段的大小 */
    u4 link_size;
    
    /* 从文件开头到链接区段的偏移量 */
    u4 link_off;
    
    /* 映射列表的偏移量 */
    u4 map_off;
    
    /* string_id_item 的个数 */
    u4 string_ids_size;
    
    /* string_id_item 列表的偏移量 */
    u4 string_ids_off;
    
    /* type_id_item 的个数 */
    u4 type_ids_size;
    
    /* type_id_item 列表的偏移量 */
    u4 type_ids_off;
    
    /* proto_id_item 的个数 */
    u4 proto_ids_size;
    
    /* proto_id_item 列表的偏移量 */
    u4 proto_ids_off;
    
    /* field_id_item 的个数 */
    u4 field_ids_size;
    
    /* field_id_item 列表的偏移量 */
    u4 field_ids_off;
    
    /* method_id_item 的个数 */
    u4 method_ids_size;
    
    /* method_id_item 列表的偏移量 */
    u4 method_ids_off;
    
    /* class_def_item 的个数 */
    u4 class_defs_size;
    
    /* class_def_item 列表的偏移量 */
    u4 class_defs_off;
    
    /* 数据区段的大小,以字节为单位 */
    u4 data_size;
    
    /* 数据区段开始的偏移量 */
    u4 data_off;
};

string_ids

字符串标识符列表,存储所有字符串的索引信息。每个 string_id_item 结构包含:

struct string_id_item {
   
   
    u4 string_data_off;  // 字符串数据的偏移量
};

对应的字符串数据结构为:

struct string_data_item {
   
   
    uleb128 utf16_size;     // 字符串的UTF-16代码单元数量
    ubyte data[utf16_size]; // 字符串数据,以MUTF-8格式编码,以0x00结尾
};

MUTF-8(Modified UTF-8)编码规则:

  • 空字符(U+0000)编码为 0xC0 0x80
  • 其他字符按标准 UTF-8 编码
  • 字符串以 NULL 字节(0x00)结尾

type_ids

类型标识符列表,存储所有类型的索引信息。每个 type_id_item 结构包含:

struct type_id_item {
   
   
    u4 descriptor_idx;  // 指向 string_ids 列表的索引,表示类型描述符
};

proto_ids

方法原型标识符列表,描述方法的签名信息。每个 proto_id_item 结构包含:

struct proto_id_item {
   
   
    u4 shorty_idx;          // 指向 string_ids 的索引,表示方法的简短描述符
    u4 return_type_idx;     // 指向 type_ids 的索引,表示返回值类型
    u4 parameters_off;      // 指向参数列表的偏移量,如果没有参数则为 0
};

field_ids

字段标识符列表,存储所有字段的索引信息。每个 field_id_item 结构包含:

struct field_id_item {
   
   
    u2 class_idx;       // 字段所属类的索引,指向 type_ids
    u2 type_idx;        // 字段类型的索引,指向 type_ids
    u4 name_idx;        // 字段名称的索引,指向 string_ids
};

method_ids

方法标识符列表,存储所有方法的索引信息。每个 method_id_item 结构包含:

struct method_id_item {
   
   
    u2 class_idx;     // 方法所属类的索引
    u2 proto_idx;     // 方法原型的索引
    u4 name_idx;      // 方法名称的索引
};

class_defs

类定义列表,包含类的详细信息。每个 class_def_item 结构包含:

struct class_def_item {
   
   
    u4 class_idx;           // 类的类型索引
    u4 access_flags;        // 访问标志
    u4 superclass_idx;      // 父类的类型索引
    u4 interfaces_off;      // 接口列表的偏移量
    u4 source_file_idx;     // 源文件名的字符串索引
    u4 annotations_off;     // 注解的偏移量
    u4 class_data_off;      // 类数据的偏移量
    u4 static_values_off;   // 静态字段初始值的偏移量
};

call_site_ids

调用站点标识符列表(用于 invokedynamic 指令)。每个 call_site_id_item 结构包含:

struct call_site_id_item {
   
   
    u4 call_site_off;  // 调用站点数据的偏移量
};

map_list

数据区段映射列表,描述文件中各个数据段的位置和大小:

struct map_list {
   
   
    u4 size;                    // 列表中的条目数量
    map_item list[size];        // 映射项列表
};

struct map_item {
   
   
    u2 type;            // 项目类型,例如 TYPE_HEADER_ITEM
    u2 unused;          // 未使用,必须为零
    u4 size;            // 在指定偏移量处找到的项目计数
    u4 offset;          // 从文件开头到项目的偏移量
};

type_list

类型列表,用于表示方法参数列表或接口列表:

struct type_list {
   
   
    u4 size;                    // 列表中的条目数量
    type_item list[size];       // 类型项列表
};

struct type_item {
   
   
    u2 type_idx;  // 类型的索引,指向 type_ids
};

class_data_item

类数据项,包含类的字段和方法的详细信息:

struct class_data_item {
   
   
    uleb128 static_fields_size;     // 静态字段的数量
    uleb128 instance_fields_size;   // 实例字段的数量
    uleb128 direct_methods_size;    // 直接方法的数量
    uleb128 virtual_methods_size;   // 虚方法的数量
    encoded_field static_fields[static_fields_size];           // 静态字段
    encoded_field instance_fields[instance_fields_size];       // 实例字段  
    encoded_method direct_methods[direct_methods_size];        // 直接方法
    encoded_method virtual_methods[virtual_methods_size];      // 虚方法
};

struct encoded_field {
   
   
    uleb128 field_idx_diff;     // 字段索引与前一个字段索引的差值
    uleb128 access_flags;       // 字段的访问标志
};

struct encoded_method {
   
   
    uleb128 method_idx_diff;    // 方法索引与前一个方法索引的差值
    uleb128 access_flags;       // 方法的访问标志
    uleb128 code_off;           // 指向 code_item 的偏移量,如果为 0 则表示抽象或本地方法
};

code_item

代码项,包含方法的字节码和其他相关信息:

struct code_item {
   
   
    u2 registers_size;          // 此代码使用的寄存器数量
    u2 ins_size;               // 方法的传入参数的字数
    u2 outs_size;              // 此代码进行方法调用所需的传出参数空间的字数
    u2 tries_size;             // 此实例的 try_item 数量,可以为零
    u4 debug_info_off;         // 从文件开头到此代码的调试信息序列的偏移量
    u4 insns_size;             // 指令列表的大小(以 16 位代码单元为单位)
    u2 insns[insns_size];      // 字节码的实际数组
    u2 padding;                // (可选) 使 tries 4 字节对齐的填充
    try_item tries[tries_size];                 // (可选) 异常处理器信息的数组
    encoded_catch_handler_list handlers;        // (可选) 异常处理器数据
};

struct try_item {
   
   
    u4 start_addr;      // 覆盖代码的起始地址
    u2 insn_count;      // 覆盖的 16 位代码单元的数量
    u2 handler_off;     // 到关联的 encoded_catch_handler 的字节偏移量
};

debug_info_item

调试信息项,包含源代码行号、局部变量名等调试信息:

struct debug_info_item {
   
   
    uleb128 line_start;                     // 对应于此序列的初始行号
    uleb128 parameters_size;                // 参数名称的数量
    uleb128p1 parameter_names[parameters_size]; // 参数的字符串索引
    // 后续为调试字节码序列
};

DEX 文件解析实例

构建 DEX 文件

首先创建一个简单的 Java 类:

// Hello.java
public class Hello {
   
   
    public static void main(String[] args) {
   
   
        System.out.println("Hello World");
    }
}

编译和转换为 DEX:

javac Hello.java
dx --dex --output=Hello.dex Hello.class

执行 DEX 文件

在 Android 设备上执行:

adb push Hello.dex /data/local/tmp/
adb shell dalvikvm -cp /data/local/tmp/Hello.dex Hello

解析 DEX 文件

打开 DEX 文件
FILE *fp = fopen("Hello.dex", "rb");
if (fp == NULL) {
   
   
    printf("无法打开文件\n");
    return -1;
}
解析 header_item
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值