通过在native层动态修改内存中的Dalvik指令来学习Dex文件格式

1. Prologue

只看Dex文件格式感觉会很枯燥,所以想通过动手的方式去理解Dex文件中的相关数据的作用,以及互相怎么关联起来的。实现的目标是通过so库动态将内存中的加法指令修改成乘法指令。具体的学习过程是学习了姜维的博客,弄懂后觉得这个文章很适合用来学习Dex文件格式,所以参考了网上有关Dex文件格式的文章,打算用自己的思路记录一下学习的过程。部分内容可能会赘述姜维的文章,但是我会穿插自己遇到的难懂的部分和一些内容自己的整理。

具体来说是定义了一个类,其中有一个方法实现了加法的功能,然后在so库中,寻找到这个方法的指令代码,将其修改成减法。

1
2
3
4
5
6
7
public class TestAdd {
public int add(int a,int b){
int c;
c = a + b;
return c;
}
}

2.Dex文件布局

首先说明一个下面会用到的数据结构

leb128

LEB128 ( little endian base 128 ) 格式 ,是基于 1 个 Byte 的一种不定长度的编码方式 。不定长度是指取每个字节的最高位当做标志位,若一个字节的最高位为1,则表示下个字节的内容也属于这个数值,直到一个字节的最高位为0,就相当于字符串的”\0”。比如leb128 0x0c2e。

0x0c 0x2e
0000 1100 1010 1110

可以看到高位字节的0x0c的最高位为0。
那么这个数值表示多少?转换过程如下

转换过程 数值
原始数据 0000 1100 1010 1110
因为最高位是标志位,所以删去最高位 000 1100 010 1110
按4bit从低到高重新组合 00 0110 0010 1110
结果 0x062e

这种数据格式相对于int可以有效的降低内存使用

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


dex文件可以分为3个部分,分别是文件头、索引区、数据区。图片来自四哥的博客。

img

文件头

文件头描述了整个dex文件的文件信息,各个索引分区的大小和偏移地址,这个偏移地址是相对于文件起始地址的偏移。逻辑上用header_item结构体表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ubyte 8-bit unsinged int
uint 32-bit unsigned int, little-endian
struct header_item
{
ubyte[8] magic;
unit checksum;
ubyte[20] signature;
uint file_size;
uint header_size;
unit endian_tag;
uint link_size;
uint link_off;
uint map_off;
uint string_ids_size;
uint string_ids_off;
uint type_ids_size;
uint type_ids_off;
uint proto_ids_size;
uint proto_ids_off;
uint method_ids_size;
uint method_ids_off;
uint class_defs_size;
uint class_defs_off;
uint data_size;
uint data_off;
}

索引区

string_ids

string_ids 区描述了 .dex 文件所有的字符串。在实例中,在已知通过类和方法名的情况下,通过遍历string_data_off指向的字符串,获得string_ids_item结构体数组的下标index,这个index和后面的type_ids区、method_ids区密切相关。

1
2
3
struct string_ids_item{
uint string_data_off;
}

在实例中,通过字符串找到string_ids_item的结构体数组下标代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int getStrIdx(int search_start_position, char *target_string,int size)
{
int index;
int stringidsoff;
int stringdataoff;
int *stringaddress;
int string_num_mutf8;

//header_item->string_ids_size
if(*(int *)(search_start_position + 56))
{
index = 0;
//header_item->string_ids_off
stringidsoff = search_start_position + *(int *)(search_start_position+60);
while(1) {
stringdataoff = *(int *)stringidsoff;
stringidsoff +=4;
stringaddress = (int *)(search_start_position + stringdataoff);
string_num_mutf8 = 0;
if( readUleb128(stringaddress,(int)&string_num_mutf8) == size
&& !strncmp((char *)stringaddress + string_num_mutf8,target_string,size))
break;
++index;
if(*(int *)(search_start_position+56) <= index ) {
index = -1;
break;
}
}
}else {
index = -1;
}
return index;
}

type_ids

type_ids区索引了 dex 文件里的所有数据类型 ,包括 class 类型 ,数组类型(array types)和基本类型(primitive types) ,其中descriptor_idx对应于上面提到的string_ids_item结构体数组的下标。

1
2
3
struct type_ids_item{
uint descriptor_idx;
}

通过string_idx_item的下标,找到type_ids_item的下标的代码如下,后面会说明这么索引的结果是什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
signed int getTypeIdx(int search_start_position, int strIdx)
{
int typeIdsSize;
int typeIdsOff;
int typeid_to_stringid;
signed int result;
int next_typeIdsOff;
int next_typeid_to_stringid;

typeIdsSize = *(int *)(search_start_position + 64);
if ( !typeIdsSize )
return -1;
typeIdsOff = search_start_position + *(int *)(search_start_position + 68);
typeid_to_stringid = *(int *)typeIdsOff;
result = 0;
next_typeIdsOff = typeIdsOff + 4;
if ( typeid_to_stringid != strIdx )
{
while ( 1 )
{
++result;
if ( result == typeIdsSize )
break;
next_typeid_to_stringid = *(int *)next_typeIdsOff;
next_typeIdsOff += 4;
if ( next_typeid_to_stringid == strIdx )
return result;
}
return -1;
}
return result;//返回下标
}

method_ids

method_ids记录了dex文件中的方法信息,通过string_idx的下标和type_idx的下标,可以索引到指定的method_ids结构体。

1
2
3
4
5
struct method_ids_item{
ushort class_idx;
ushort proto_idx;
uint name_idx;
}

在实例中,索引指定方法的结构体,获取method_ids下标的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int  getMethodIdx(int search_start_position, int method_strIdx, int class_typeIdx)
{
int methodIdsSize;
int classIdx;
signed int result;

methodIdsSize = *(int *)(search_start_position + 88);//header_item->methodIdsSize
if ( methodIdsSize )
{
classIdx = search_start_position + *(int *)(search_start_position + 92);//header_item->methodIdsOff
result = 0;
while ( *(short *)classIdx != class_typeIdx || *(int *)(classIdx + 4) != method_strIdx )
{
++result;
classIdx += 8;
if ( result == methodIdsSize )
{result = -1;break;}
}
}
else
{
result = -1;
}
return result;
}

数据区

class_defs

1
2
3
4
5
6
7
8
9
10
11
struct class_def_item
{
uint class_idx; //-->type_ids
uint access_flags;
uint superclass_idx; //-->type_ids
uint interface_off; //-->type_list
uint source_file_idx; //-->string_ids
uint annotations_off; //-->annotation_directory_item
uint class_data_off; //-->class_data_item
uint static_value_off; //-->encoded_array_item
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int getClassItem(int search_start_position, int class_typeIdx)
{
int classDefsSize;

int classDefsOff;
int result;
int classIdx;
int count;

classDefsSize = *(int *)(search_start_position + 96);
classDefsOff = *(int *)(search_start_position + 100);
result = 0;
if (classDefsSize)
{
classIdx = search_start_position + classDefsOff;
result = classIdx;
if ( *(int *)classIdx != class_typeIdx)
{
count = 0;
while (1)
{
++count;
if (count == classDefsSize)
break;
result += 32;
if ( *(int *)(result) == class_typeIdx)
return result;
}
result = 0;
}
}
return result;//返回class_def_item结构体的地址
}

class_data_item

class_data_item结构体描述了static字段,类字段,static方法,类方法等信息。其中direct_methodsvirtual_methods是结构体数组,一个方法对应一个元素。这个结构体主要通过class_defs的class_data_off字段索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 filed_idx_diff;
uleb128 access_flags;
}
struct encoded_method
{
uleb128 method_idx_diff;
uleb128 access_flags;
uleb128 code_off;
}

encoded_method结构体的code_off一个指向 data 区的偏移地址 ,目标是 method 的代码实现 ,被指向的结构是code_item,而method_idx_diff字段前缀 methd_idx 表示它的值是 method_ids 的一个 下标,后缀 _diff 表示它是于另外一个 method_idx 的一个差值 ,就是相对于 encodeed_method [] 数组里上一个元素的 method_idx 的差值。

code_item

code_item结构体描述了某个 method 的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
struct code_item
{
ushort registers_size;
ushort ins_size;
ushort outs_size;
ushort tries_size;
uint debug_info_off;
uint insns_size;
ushort insns [ insns_size ];
ushort paddding; // optional
try_item tries [ tyies_size ]; // optional
encoded_catch_handler_list handlers; // optional
}

通过一个类的class_data_item地址和其方法的method_idx获取这个方法对应的code_item结构的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
int getCodeItem(int search_start_position, int class_def_item_address, int methodIdx)
{

int *classDataOff;

int staticFieldsSize;
int *classDataOff_new_start;
int instanceFieldsSize;

int directMethodsSize;
int virtualMethodSize;

int *after_skipstaticfield_address;
int *DexMethod_start_address;
int result;
int DexMethod_methodIdx;
int *DexMethod_accessFlagsstart_address;
int Uleb_bytes_read;
int tmp;

//获取DexClassData结构体的地址
classDataOff = (int *)(*(int *)(class_def_item_address + 24) + search_start_position);
LOGD(" classDataOff = %x", classDataOff);

Uleb_bytes_read = 0;
staticFieldsSize = readUleb128(classDataOff, (int)&Uleb_bytes_read);
LOGD("staticFieldsSize= %d",staticFieldsSize);

classDataOff_new_start = (int *)((char *)classDataOff + Uleb_bytes_read);
LOGD("staticFieldsSize_addr= %x",classDataOff_new_start);

instanceFieldsSize = readUleb128(classDataOff_new_start, (int)&Uleb_bytes_read);
LOGD("instanceFieldsSize= %d",instanceFieldsSize);

classDataOff_new_start = (int *)((char *)classDataOff_new_start + Uleb_bytes_read);
LOGD("instanceFieldsSize_addr= %x",classDataOff_new_start);

directMethodsSize = readUleb128(classDataOff_new_start, (int)&Uleb_bytes_read);
LOGD("directMethodsSize= %d",directMethodsSize);

classDataOff_new_start = (int *)((char *)classDataOff_new_start + Uleb_bytes_read);
LOGD("directMethod_addr= %x",classDataOff_new_start);

virtualMethodSize = readUleb128(classDataOff_new_start, (int)&Uleb_bytes_read);
LOGD("virtualMethodsSize= %d",virtualMethodSize);

after_skipstaticfield_address = skipUleb128(2 * staticFieldsSize, (int *)((char *)classDataOff_new_start + Uleb_bytes_read));
LOGD("after_skipstaticfield_address = %x", after_skipstaticfield_address);

DexMethod_start_address = skipUleb128(2 * instanceFieldsSize, after_skipstaticfield_address);
LOGD("DexMethod_start_address = %x", DexMethod_start_address);

result = 0;
if(directMethodsSize)
{
DexMethod_methodIdx = 0;
int DexMethod_methodIdx_tmp = 0;
do {

DexMethod_methodIdx_tmp = 0;
DexMethod_methodIdx = readUleb128(DexMethod_start_address, (int)&Uleb_bytes_read);
DexMethod_methodIdx_tmp = readUleb128(DexMethod_start_address, (int)&Uleb_bytes_read);

LOGD("DexMethod_direct_methodIdx = %x", DexMethod_methodIdx);
LOGD("DexMethod_direct_methodIdx_tmp = %x", DexMethod_methodIdx_tmp);

DexMethod_accessFlagsstart_address = (int *)((char *)DexMethod_start_address + Uleb_bytes_read);
if (DexMethod_methodIdx == methodIdx)
{
readUleb128(DexMethod_accessFlagsstart_address, (int)&Uleb_bytes_read);
return readUleb128((int *)((char *)DexMethod_accessFlagsstart_address + Uleb_bytes_read),
(int)&Uleb_bytes_read) + search_start_position;
}
--directMethodsSize;
DexMethod_start_address = skipUleb128(2, DexMethod_accessFlagsstart_address);
}while (directMethodsSize);
result = 0;
}

if (virtualMethodSize)
{
DexMethod_methodIdx = 0;
int DexMethod_methodIdx_tmp = 0;
do
{
DexMethod_methodIdx_tmp = 0;
DexMethod_methodIdx = readUleb128(DexMethod_start_address, (int)&Uleb_bytes_read);
DexMethod_methodIdx_tmp = readUleb128(DexMethod_start_address, (int)&Uleb_bytes_read);

LOGD("DexMethod_virtual_methodIdx = %x", DexMethod_methodIdx);
LOGD("DexMethod_virtual_methodIdx_tmp = %x", DexMethod_methodIdx_tmp);

DexMethod_accessFlagsstart_address = (int *)((char *)DexMethod_start_address + Uleb_bytes_read);
if (DexMethod_methodIdx == methodIdx)
{
readUleb128(DexMethod_accessFlagsstart_address, (int)&Uleb_bytes_read);
return readUleb128((int *)((char *)DexMethod_accessFlagsstart_address + Uleb_bytes_read), (int)&Uleb_bytes_read) + search_start_position;
}
--virtualMethodSize;
DexMethod_start_address = skipUleb128(2, DexMethod_accessFlagsstart_address);
}while ( virtualMethodSize );
result = 0;
}

return result;
}

返回的内容就是code_item结构体的地址。

3. 实例说明

结构体之间的关联

通过上面Dex文件布局的说明,总结一下通过如何通过类名和方法名,在内存中找到方法的指令代码

  1. 已知类名和方法名,通过索引区的string_idx_item结构体,获取class_strIdx和method_strIdx

    1
    2
    3
    class_strIdx = getStrIdx(search_start_position,"Lcom/pollux/dalvikbytecode/TestAdd;",
    strlen("Lcom/pollux/dalvikbytecode/TestAdd;"));
    method_strIdx = getStrIdx(search_start_position,"add", 3);
  2. 已知class_strIdx,通过索引区的type_idx_item结构体,获取class_typeIdx

    1
    class_typeIdx = getTypeIdx(search_start_position,class_strIdx);
  3. 已知class_typeIdx和method_strIdx,通过索引区的method_idx_item结构体,获取method_typeIdx

    1
    methodIdx = getMethodIdx(search_start_position, method_strIdx, class_typeIdx);
  4. 已知class_typeIdx,获取类对应的class_defs_item结构体的地址

    1
    class_def_item_address = getClassItem(search_start_position,class_typeIdx);
  5. 有了class_defs_item结构体的地址,就可以通过其class_data_off字段,获取class_data_item结构体的地址,进而通过上面获取的method_typeIdx,在encoded_method字段中找到方法对应的code_item结构体地址,而code_item则包含了这个方法实现的具体指令代码

    1
    codeItem_address = getCodeItem(search_start_position,class_def_item_address,methodIdx);

之后就是解析指令,指令代码存放在code_item中insns字段中,2字节一个元素,所以如果code_item->insns_size=3,那么指令代码的大小为3x2=6字节

首先获取指令的大小

1
2
3
4
5
6
7
char insnssize[4];
void *code_insns_size = (void*)(codeItem_address+12);
memcpy(insnssize,code_insns_size,4);
int k = 0;
for(;k<4;k++){
LOGD("size:%d",insnssize[k]);
}

接着获取具体的指令代码

1
2
3
4
5
6
7
8
9
10
void *code_insns_address;
code_insns_address = (void *)(codeItem_address+16);
LOGD("code_insns_address = %x", code_insns_address);
//这里可以先打印方法的指令
char instrans[6];//这里的6就是insns_size*2,因为short是两个字节
memcpy(instrans, code_insns_address, 6);
int i=0;
for(;i<6;i++){
LOGD("%x",instrans[i]);
}

可以看到实现加法的指令是90 00 02 03 0F 00,一条指令格式是指令码+指令操作数,通过查找Dalvik指令的格式,可以知道加法的指令是0x90,指令格式是binop vAA, vBB, vCC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
binop vAA, vBB, vCC
90: add-int
91: sub-int
92: mul-int
93: div-int
94: rem-int
95: and-int
96: or-int

A: destination register or pair (8 bits)
B: first source register or pair (8 bits)
C: second source register or pair (8 bits)

Perform the identified binary operation on the two source registers, storing the result in the first source register.

可以知道 00 是0号寄存器,02是二号寄存器,03是3号寄存器,所以只要将90修改成91就变成减法了

1
2
3
4
5
6
7
void *codeinsns_page_address =
(void *)(codeItem_address + 16 - (codeItem_address + 16) % (unsigned int)page_size);
//修改该页的读写权限
mprotect(codeinsns_page_address,page_size, PROT_READ|PROT_WRITE);
//写入对应减法的字节码
char inject[]={0x91,0x00,0x02,0x03,0x0f,0x00};
memcpy(code_insns_address,&inject,6);

4. 总结

虽然修改内存中的指令没有难度,但是可以通过这个,可以学习到Dex中各个数据之间的关系,怎么通过类名和方法名在内存中找到对应的指令。实现加法指令变成减法指令很容易,因为并没有改变指令的整体长度,如果修改复杂的指令,就有可能改变指令的长度,而Dex文件中各个数据的寻址都是相对地址,所以如果指令长度修改了,就要对可能影响到的相对地址进行修复,就会变得很复杂。