qemu pwn-基础知识
字数 1231 2025-08-24 23:51:15

QEMU PWN 基础知识详解

QEMU概述

QEMU是一个纯软件实现的虚拟化模拟器,几乎可以模拟任何硬件设备。每个QEMU虚拟机对应host上的一个QEMU进程,虚拟机的执行线程(如CPU线程、I/O线程等)对应QEMU进程的一个线程。

虚拟机内存结构

虚拟机内存对应的真实内存结构如下:

Guest' processes Virtual addr space Page Table
Guest kernel
Guest's phy. memory
QEMU process Virtual addr space Page Table
Physical memory

QEMU进程会为虚拟机mmap分配出相应虚拟机申请大小的内存,用于给该虚拟机当作物理内存。例如,使用1G内存启动的虚拟机:

0x7fe37fe00000 0x7fe3bfe00000 rw-p 40000000 0  // 虚拟机对应的内存

虚拟地址到物理地址转换

在QEMU虚拟机中申请内存后,可以通过以下步骤在宿主机中找到对应内存:

  1. 将虚拟机中的虚拟地址转化为物理地址
  2. 该物理地址即为QEMU进程为其分配出来的相应偏移
  3. 使用该地址加上QEMU进程中的内存基址

示例转换代码:

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <fcntl.h>
#include <assert.h>
#include <inttypes.h>

#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)

int fd;

uint32_t page_offset(uint32_t addr) {
    return addr & ((1 << PAGE_SHIFT) - 1);
}

uint64_t gva_to_gfn(void *addr) {
    uint64_t pme, gfn;
    size_t offset;
    
    offset = ((uintptr_t)addr >> 9) & ~7;
    lseek(fd, offset, SEEK_SET);
    read(fd, &pme, 8);
    if (!(pme & PFN_PRESENT))
        return -1;
    gfn = pme & PFN_PFN;
    return gfn;
}

uint64_t gva_to_gpa(void *addr) {
    uint64_t gfn = gva_to_gfn(addr);
    assert(gfn != -1);
    return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}

int main() {
    uint8_t *ptr;
    uint64_t ptr_mem;
    
    fd = open("/proc/self/pagemap", O_RDONLY);
    if (fd < 0) {
        perror("open");
        exit(1);
    }
    
    ptr = malloc(256);
    strcpy(ptr, "Where am I?");
    printf("%s\n", ptr);
    
    ptr_mem = gva_to_gpa(ptr);
    printf("Your physical address is at 0x%" PRIx64 "\n", ptr_mem);
    
    getchar();
    return 0;
}

PCI设备基础

PCI配置空间

PCI设备都有一个256字节的配置空间(PCI Configuration Space),其中头部64字节是PCI标准规定的。关键部分包括:

  • 前16字节:包含头部的类型、设备的总类、设备的性质以及制造商等信息
  • 6个BAR(Base Address Registers):记录设备所需要的地址空间的类型、基址及其他属性

BAR格式

BAR的格式如下:

bit 63-4: 地址
bit 3: 预取标志(仅MMIO)
bit 2: 地址大小(1=64位,0=32位)
bit 1: 区间大小(1=>1M,0<=1M)
bit 0: 空间类型(0=MMIO,1=PMIO)

MMIO与PMIO

  • MMIO (Memory-mapped I/O)

    • 内存和I/O设备共享同一个地址空间
    • CPU使用普通访存指令访问设备I/O
    • 需要预留地址区域给I/O设备
  • PMIO (Port-mapped I/O)

    • 内存和I/O设备有各自的地址空间
    • CPU使用专门的I/O指令(IN/OUT)访问I/O端口
    • 也称为Isolated I/O

查看PCI设备

使用lspci命令查看PCI设备:

lspci  # 显示所有PCI设备
lspci -t -v  # 树状形式输出PCI结构
lspci -v -m -n -s 00:03.0  # 查看特定设备详细信息

设备寻址格式为域:总线:设备.功能,例如0000:00:03.0

查看设备配置空间

hexdump /sys/devices/pci0000:00/0000:00:03.0/config

查看设备内存空间

lspci -v -s 00:03.0 -x  # 显示设备内存和I/O空间信息
cat /sys/devices/pci0000:00/0000:00:03.0/resource  # 查看资源分配

QOM编程模型

QEMU使用QOM(QEMU Object Module)面向对象编程模型来实现设备模拟。关键结构体:

TypeInfo

用户定义Type的数据结构:

struct TypeInfo {
    const char *name;
    const char *parent;
    size_t instance_size;
    void (*instance_init)(Object *obj);
    void (*instance_post_init)(Object *obj);
    void (*instance_finalize)(Object *obj);
    bool abstract;
    size_t class_size;
    void (*class_init)(ObjectClass *klass, void *data);
    void (*class_base_init)(ObjectClass *klass, void *data);
    void (*class_finalize)(ObjectClass *klass, void *data);
    void *class_data;
    InterfaceInfo *interfaces;
};

TypeImpl

由TypeInfo注册后生成的类型实现。

ObjectClass

所有类的基类:

struct ObjectClass {
    Type type;
    GSList *interfaces;
    const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE];
    const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE];
    ObjectUnparent *unparent;
    GHashTable *properties;
};

Object

具体设备实例:

struct Object {
    ObjectClass *class;
    ObjectFree *free;
    GHashTable *properties;
    uint32_t ref;
    Object *parent;
};

设备实现示例

static const TypeInfo pci_testdev_info = {
    .name = TYPE_PCI_TEST_DEV,
    .parent = TYPE_PCI_DEVICE,
    .instance_size = sizeof(PCITestDevState),
    .class_init = pci_testdev_class_init,
};

static void pci_testdev_class_init(ObjectClass *klass, void *data) {
    DeviceClass *dc = DEVICE_CLASS(klass);
    PCIDeviceClass *k = PCI_DEVICE_CLASS(klass);
    
    k->init = pci_testdev_init;
    k->exit = pci_testdev_uninit;
    dc->desc = "PCI Test Device";
}

内存区域操作

QEMU使用MemoryRegion表示内存空间,使用MemoryRegionOps定义操作:

MMIO操作示例

static const MemoryRegionOps pci_testdev_mmio_ops = {
    .read = pci_testdev_read,
    .write = pci_testdev_mmio_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .impl = {
        .min_access_size = 1,
        .max_access_size = 1,
    },
};

PMIO操作示例

static const MemoryRegionOps pci_testdev_pio_ops = {
    .read = pci_testdev_read,
    .write = pci_testdev_pio_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .impl = {
        .min_access_size = 1,
        .max_access_size = 1,
    },
};

初始化内存区域

memory_region_init_io(&d->mmio, OBJECT(d), &pci_testdev_mmio_ops, d,
                     "pci-testdev-mmio", IOTEST_MEMSIZE * 2);
memory_region_init_io(&d->portio, OBJECT(d), &pci_testdev_pio_ops, d,
                     "pci-testdev-portio", IOTEST_IOSIZE * 2);

pci_register_bar(pci_dev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &d->mmio);
pci_register_bar(pci_dev, 1, PCI_BASE_ADDRESS_SPACE_IO, &d->portio);

访问I/O空间

内核态访问MMIO

#include <asm/io.h>
#include <linux/ioport.h>

long addr = ioremap(ioaddr, iomemsize);
readb(addr); readw(addr); readl(addr); readq(addr);
writeb(val, addr); writew(val, addr); writel(val, addr); writeq(val, addr);
iounmap(addr);

用户态访问MMIO

#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/io.h>

unsigned char *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));
}

int main(int argc, char *argv[]) {
    int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
    mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
    
    mmio_read(0x128);
    mmio_write(0x128, 1337);
}

内核态访问PMIO

#include <asm/io.h>
#include <linux/ioport.h>

inb(port); inw(port); inl(port);
outb(val, port); outw(val, port); outl(val, port);

用户态访问PMIO

#include <sys/io.h>

iopl(3);
inb(port); inw(port); inl(port);
outb(val, port); outw(val, port); outl(val, port);

总结

  1. QEMU虚拟机内存通过mmap分配,虚拟地址可通过页表转换为宿主机中的地址
  2. PCI设备通过配置空间和BAR寄存器定义其I/O空间
  3. QOM模型是QEMU设备模拟的核心,采用面向对象方式实现
  4. MemoryRegion和MemoryRegionOps用于定义设备的I/O操作
  5. MMIO和PMIO可以通过内核模块或用户态程序访问
QEMU PWN 基础知识详解 QEMU概述 QEMU是一个纯软件实现的虚拟化模拟器,几乎可以模拟任何硬件设备。每个QEMU虚拟机对应host上的一个QEMU进程,虚拟机的执行线程(如CPU线程、I/O线程等)对应QEMU进程的一个线程。 虚拟机内存结构 虚拟机内存对应的真实内存结构如下: QEMU进程会为虚拟机mmap分配出相应虚拟机申请大小的内存,用于给该虚拟机当作物理内存。例如,使用1G内存启动的虚拟机: 虚拟地址到物理地址转换 在QEMU虚拟机中申请内存后,可以通过以下步骤在宿主机中找到对应内存: 将虚拟机中的虚拟地址转化为物理地址 该物理地址即为QEMU进程为其分配出来的相应偏移 使用该地址加上QEMU进程中的内存基址 示例转换代码: PCI设备基础 PCI配置空间 PCI设备都有一个256字节的配置空间(PCI Configuration Space),其中头部64字节是PCI标准规定的。关键部分包括: 前16字节:包含头部的类型、设备的总类、设备的性质以及制造商等信息 6个BAR(Base Address Registers):记录设备所需要的地址空间的类型、基址及其他属性 BAR格式 BAR的格式如下: MMIO与PMIO MMIO (Memory-mapped I/O) : 内存和I/O设备共享同一个地址空间 CPU使用普通访存指令访问设备I/O 需要预留地址区域给I/O设备 PMIO (Port-mapped I/O) : 内存和I/O设备有各自的地址空间 CPU使用专门的I/O指令(IN/OUT)访问I/O端口 也称为Isolated I/O 查看PCI设备 使用 lspci 命令查看PCI设备: 设备寻址格式为 域:总线:设备.功能 ,例如 0000:00:03.0 。 查看设备配置空间 查看设备内存空间 QOM编程模型 QEMU使用QOM(QEMU Object Module)面向对象编程模型来实现设备模拟。关键结构体: TypeInfo 用户定义Type的数据结构: TypeImpl 由TypeInfo注册后生成的类型实现。 ObjectClass 所有类的基类: Object 具体设备实例: 设备实现示例 内存区域操作 QEMU使用 MemoryRegion 表示内存空间,使用 MemoryRegionOps 定义操作: MMIO操作示例 PMIO操作示例 初始化内存区域 访问I/O空间 内核态访问MMIO 用户态访问MMIO 内核态访问PMIO 用户态访问PMIO 总结 QEMU虚拟机内存通过mmap分配,虚拟地址可通过页表转换为宿主机中的地址 PCI设备通过配置空间和BAR寄存器定义其I/O空间 QOM模型是QEMU设备模拟的核心,采用面向对象方式实现 MemoryRegion和MemoryRegionOps用于定义设备的I/O操作 MMIO和PMIO可以通过内核模块或用户态程序访问