跳到正文
Calif:Research(AI 漏洞研究)·· 1 天前精選AI 評分76

QEMU 與 UTM 虛擬機逃逸研究拆解 virtio-gpu 漏洞利用鏈

QEMU and UTM Escape

AI 導讀

Calif 團隊展示 Claude 協助構建的 QEMU virtio-gpu 漏洞利用鏈,可從虛擬機逃逸並在主機執行程式碼。利用鏈透過虛擬機連接 QEMU 的 VNC 伺服器,讀取主機 heap 記憶體以支援後續攻擊。攻擊要求 QEMU 啟用 VNC,UTM 還須使用模擬模式;漏洞已於 QEMU 11.0.0 修復,但未回補至 10.x stable。

推薦理由

文章拆解 QEMU virtio-gpu 整數溢位如何結合 VNC 記憶體洩漏,形成虛擬機至主機的程式碼執行鏈,並交代觸發條件。

正文 · 繁體中文

講述 guest 透過 VNC 連入自己的 host,並像觀看螢幕保護程式般觀看 heap。

本文屬於 MAD Bugs 系列,這是我們的「人工智能發現漏洞月」;我們將前沿模型與人類專業知識結合,並公開所得成果。

開始深入探討前,先説一則消息。Dion Blazakis 和 Stefan Esser 即將加入 Calif。Dion 剛剛才 ~~逃離~~ 離開水果公司,所以我們覺得推出一個 macOS VM 逃逸漏洞利用程式會很有趣。

我們的目標是 QEMU 和 UTM。QEMU 是開源機器模擬器及虛擬化程式,為大部分 Linux 虛擬化堆疊提供支援,包括 libvirt、OpenStack、KubeVirt,以及不少雲端平台的 KVM 部分。UTM 則是適用於 App Store 的 macOS 和 iOS 前端,並以 QEMU 為核心。它在 GitHub 上約有 30K 顆星,為想在 Apple Silicon 上執行 Windows 或 Linux、又不想處理 VMware 的 Mac 用戶提供服務(VMware 現在技術上已免費,但據説要先向 Broadcom 的冤大頭捐血,下載連結才會出現)。

我們留意到 UTM 隨附自家的 QEMU(10.0.2),而 UTM 隨附版本與上游版本之間存在版本差異。我們向 Claude 提出的第一批提示詞是:

找出 UTM 版本與最新版本之間已修補、而且可用作在 UTM 上逃逸的漏洞?審查 QEMU,尋找一個新的 guest-to-host 逃逸漏洞,特別要能在 mac/osx/utm 上奏效。

再提出幾個提示詞後,它便在 QEMU 的 virtio-gpu 裝置中找到一條由 guest 到 host 的程式碼執行攻擊鏈,並撰寫了約 1,500 行 C 程式碼,可編譯成單一靜態二進制檔。把它放進有漏洞的 VM 內一個非特權程序中,host 上的計算機便會開啟。

PoC 影片:

影響範圍説明。有人討論過這個漏洞利用的影響,因此我們想澄清自己的主張。VM 安全模型假設你在 guest 中擁有 root 權限,而且 guest 會執行不受信任的程式碼。這個漏洞利用破壞了 QEMU 的安全模型:我們從 guest 逃逸到 host,並在那裏執行任意程式碼。這條攻擊鏈確實要求啟用 QEMU 的 VNC 伺服器。VNC 是大部分無頭部署環境(Proxmox、libvirt、OpenStack)的預設設定,但 UTM 出廠時會關閉這項功能。在 UTM 上,VM 亦必須設定為模擬模式,因為 UTM 預設透過 Apple 的 Virtualization framework 進行虛擬化,而這會完全繞過 QEMU。威脅模型並不是「誘使用戶下載預先設定好的惡意 UTM 映像檔」,而是「攻擊者已在一部隔離 VM 上取得 root 權限,而該 VM 正以模擬模式在啟用 VNC 的 UTM 上執行」。在 macOS 上,應用程式亦會在 Apple 的 App Sandbox 內執行,因此要完全逃逸還需要第二個漏洞。我們認為這層防護並不算特別堅固,但現在我們需要另一個漏洞來證明自己所言屬實。

現代記憶體破壞漏洞利用需要兩種原語:用來破壞狀態的寫入,以及用來繞過 ASLR、得知目標位置的讀取。這個漏洞免費提供寫入原語;讀取原語才是新穎之處。據我們所知,這是公開案例中的首例:透過 QEMU 自己的 VNC 伺服器洩露記憶體,並由 guest 本身透過 SLIRP loopback 存取。

具體而言,guest 透過 QEMU 模擬的網卡,在 10.0.2.2:5900 向其宿主機的 VNC 連接埠建立 TCP socket,並傳送 FramebufferUpdateRequest;QEMU 隨即把自身堆積區域的一部分序列化為像素位元組,傳回 guest。此時 guest 就像觀看螢幕保護程式一樣,窺看 QEMU 的位址空間。Claude 僅憑一個提示詞,便自主組合出這個讀取原語:

以最佳方式找出解決方法。不要修改 QEMU 原始碼。必須只從 guest 端運作。研究如何把寫入轉為讀取。

我們檢視過的已公開 QEMU 逃逸漏洞(OtterSec 的 virtio-snd、Talbi/Fariello 的 RTL8139、較早期的 SLIRP ICMP 資訊洩漏)都沒有利用 VNC 伺服器作為資訊洩漏途徑。

原來,這個漏洞是透過 ZDI(ZDI-CAN-27578)通報,並已在 QEMU 11.0.0 中修復(2026 年 4 月 21 日),但沒有回移至任何 10.x 穩定版。我們開始調查時並不知道這件事,而重新發現漏洞本身也是一段值得一提的經過。

即使這個逃逸漏洞現已修補,它存在的時間可能比 Cloudburst 更長。

漏洞

hw/display/virtio-gpu.c 有一個函式 calc_image_hostmem,用來計算分配 2D 像素緩衝區所需的位元組數:

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

簡單介紹一下 pixman,之後還會多次提及:它是低階 2D 像素操作函式庫,為 Cairo 和 X server 提供支援;QEMU 亦使用它來表示系統內的每個顯示表面。一個 pixman_image_t 本質上是由 (format, width, height, stride, raw pointer) 元組,以及對其進行合成/縮放的例程組成。QEMU 的 virtio-gpu 為 guest 分配 2D 資源時,就是在分配緩衝區,並把它包裝成 pixman_image_t。

calc_image_hostmem 中的每個中間值都是 32 位元的 int。當使用 bpp = 32 和由 guest 提供的 width = 0x40000001 時,width * bpp 乘法會發生回繞,向上取整至 32 位元的技巧會對錯誤的數值取整,而 stride 則縮減至 4。使用 height = 128 時,calc_image_hostmem 會傳回 512。接著 QEMU 分配 512 個位元組,將其交給 pixman 作為 pixman_image_create_bits(BGRA, 0x40000001, 128, ptr, stride=4),並把未溢位的原始 0x40000001 存入 res->width。

此資源之後的所有界限檢查(在 set_scanout、transfer_to_host_2d 中)都會以 res->width 為準。這個數值並不可靠。guest 可定址的像素座標範圍,最高可超出實際 512 位元組緩衝區約 4 GB。

漏洞本身就是這樣,但箇中原因很有意思。Pixman 的 pixman_image_create_bits(format, width, height, bits, rowstride) 有兩種模式。傳入 bits = NULL 時,pixman 會自行分配緩衝區、執行自己的溢位檢查,並忽略你的 rowstride。傳入 bits = <pre-allocated pointer> 時,pixman 便完全信任呼叫方:它會使用你的指標和 stride,不作任何檢查,因為 API 契約假定呼叫方已完成驗證。

在2023 年的一次提交之前,virtio-gpu 使用第一種模式。雖然有 calc_image_hostmem,但它只用來計算 res->hostmem,即用於執行記憶體預算限制的每 VM 記帳數值。實際分配由 pixman 負責,而 pixman 會檢查溢位。有問題的 int stride 當時只會令一個計數器的數值失實,並非緩衝區大小失實。

2023 年的提交改用了第二種模式。Windows 顯示表面需要可共用的 HANDLE,因此緩衝區必須由 QEMU 使用 qemu_win32_map_alloc() 分配,而非由 pixman 分配。於是 virtio-gpu 開始自行分配 calc_image_hostmem(...) 個位元組,再把指標和 stride 傳給 pixman。提交訊息甚至特別指出這項行為變更:「向 pixman_image_create_bits() 提供 bits 時,也必須提供 rowstride(bits 為 NULL 時,這個參數會被忽略)。」

pixman 因為 API 契約允許而移除了溢位檢查;同一個有問題的函式,亦從計算記帳數值變成用來提供受信任的分配大小,卻沒有人重新審核。呼叫方並沒有進行驗證。

漏洞鏈

Exploit chain: guest → virtio-gpu overflow → SLIRP/VNC leak → OOB write → system()

這個漏洞直接提供越界寫入:transfer_to_host_2d 會將由客體控制的位元組複製到 pixbuf + x * bpp,適用於任何 x < 0x40000001。但它本身不會提供越界讀取,這代表無法繞過 ASLR,因此這種寫入原語大多隻對令主機程序崩潰有用。

我們認為,Claude 解決讀取原語問題的方法,是這個漏洞利用中最漂亮的部分。我們想逐步講解這個方法,因為我們花了一點時間才相信它行得通。

set_scanout 是 virtio-gpu 指令,意思是「這個 pixman_image_t 是目前使用中的顯示表面;將它顯示在螢幕上。」其參數的界限檢查使用同一個有問題的 res->width,因此客體可以將目前使用中的顯示表面設定為指向 512-byte 緩衝區以外 1 GB 的記憶體。

QEMU 內置 VNC 伺服器。按定義,它的工作是將目前使用中的顯示表面編碼為像素數據,並將這些位元組傳送給任何連接到 5900 連接埠的 TCP 用戶端。

QEMU 預設的使用者模式網絡堆疊 SLIRP,讓客體可透過 10.0.2.2 連接至主機。因此客體會向 10.0.2.2:5900 開啟 TCP socket(即透過 QEMU 自己模擬的網絡介面卡,連接到客體所屬主機的 VNC 連接埠),傳送一個 FramebufferUpdateRequest,而 QEMU 的 VNC 伺服器便會客氣地將自身 heap 中的一段記憶體序列化為像素位元組,再經由 socket 傳回。

一個 FramebufferUpdateRequest 會傳回 width × height × 4 位元組,因此掃描時每次讀取 16 KB 頁面,針對性查找則每次讀取 256 位元組。將主機記憶體編碼為像素的可愛副作用是,協定層不會解讀資料,也沒有剖析器或跳脱處理;位址範圍內的每個位元組都會原封不動地傳回,只是速度比你想要的稍慢一些。

有了讀取原語,接下來就是相當典型的 macOS arm64 攻擊鏈。每次向前掃描 16 KB,尋找 Mach-O 標頭;透過 sizeofcmds 識別 pixman;讀取 GOT[free] 以推算共享快取位移;計算 system()。植入一個假造的 pixman_implementation_t,其 fast_paths 陣列中有一個萬用字元項目,而該項目的 func 為 system()。在 arm64 上,實作指標是 func 的第一個引數,因此我們把命令字串放在同一個結構的偏移量 0 處,讓它一物兩用。再透過兩次越界寫入,停用 pixman 的 TLS 快速路徑快取,並覆寫 _global_implementation。最後的 RESOURCE_FLUSH 會觸發 VNC 合成操作;pixman 遍歷我們假造的鏈結,萬用字元符合條件,於是執行 system()。

命令字串必須符合 15 個位元組的限制(fast_paths 指標位於偏移量 0x10),所以 open -a Calculator 太長。open /*/*/Calc* 剛好是 15 個位元組,而 /bin/sh 會將 glob 展開為 /System/Applications/Calculator.app。(我們首次嘗試使用的 /S*/A*/Ca* 也會匹配 Calendar.app,示範效果就沒那麼有説服力。)

UTM 還有另一個變數。它的 QEMU 透過 qemu_pixman_image_new_shareable 配置 virtio-gpu 像素緩衝區;這是 memfd + mmap,而非 malloc,因此漏洞利用緩衝區會落在 UTM 隨附的二十多個框架之間的位址空洞,而不是大型物件 heap。dyld 每次啟動都會重新排列這些框架;在相當比例的啟動情況下,pixman(2.4 MB,是其中最小的框架之一)的載入位址會低於第一個足以容納我們緩衝區的空洞。越界寫入只能向前延伸,因此 pixman 的 _global_implementation 在記憶體中便會實際位於我們後方,無法成功實施上述劫持。

替代方案是直接以 QEMU 為目標。它的映像檔是一個 29 MB 的區塊,大得令緩衝區幾乎永遠不會落在它上方,因此掃描程序會另外攜帶一張用於 QEMU 的 __TEXT 指紋表,並改由 QEMU 的 GOT 推導 system()。控制流程劫持的目標改為 QEMU 的 __la_symbol_ptr[g_free](可寫入、向前約 70 MB,輕易在可觸及範圍內):一次越界寫入便可將其指向 system();觸發方式則是使用刻意縮短的 RESOURCE_ATTACH_BACKING,其入口位元組拼出 shell 命令。virtio_gpu_create_mapping_iov g_mallocs 會配置一個暫存緩衝區,將我們的位元組原樣複製進去,然後長度檢查失敗,錯誤路徑接着呼叫 g_free(ents),而它此時已是 system("open -a Calculator")。這條路徑還有一個好處:沒有 15-byte 限制;命令可以和 virtqueue 描述符一樣長。

這條攻擊鏈需要客體系統連接到 VNC 伺服器。這幾乎是所有以無頭模式執行 QEMU 的環境的預設設定:Proxmox、libvirt 的預設 <graphics type='vnc'/>、OpenStack,以及所有使用 -vnc :0 啟動虛擬機的 CI 執行器。在 UTM 中,這並非預設設定,而且需要一行設定 -vnc :0。不過,這個漏洞仍存在於所有 UTM 安裝版本中。

重現

所有內容都放在儲存庫中的 /qemu/:

檔案內容
WRITEUP.md完整技術公告
exploit.c獨立 QEMU PoC(自製 pixman,不含 SPICE)
exploit_utm.cSPICE 安全版 UTM.app 變體
exploit_linux.cLinux aarch64 主機版本
run_poc_macos.sh一鍵建置有漏洞的 QEMU 10.0.2 + HVF、建置 exploit,並啟動計算機
run_poc_linux.sh同上,適用於 Linux 主機
run_poc_utm.shUTM.app 虛擬機配置工具
./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

結語

我們不知道 Claude 是如何找到這個漏洞的。我們最初的提示詞要求它比較 UTM 的 QEMU 與上游版本,而修補程式的提交當時已公開;模型有可能發現了 c035d5ea,再反向推導,也有可能是在沒有任何背景資料的情況下審查 virtio-gpu.c,自行重新發現這個溢位問題。我們無法從對話記錄中判斷是哪一種情況,但兩種結果都挺有意思:前者代表前沿模型能從修補程式差異中挖掘出可行的虛擬機逃逸攻擊,速度比下游專案推出修補程式還快;後者則代表它能在沒有人指引的情況下,找到 ZDI 曾付費收購的同一個漏洞。

就我們所知,這個漏洞本身只是簡單的整數溢位,但這次 exploit 是首宗有文件記錄的案例,展示 AI 進行創意性的漏洞利用原語設計:將 QEMU 中三個互不相關的子系統(virtio-gpu、VNC 伺服器、SLIRP loopback)串連起來,製造出一種此前無人公開的資訊洩漏。

之後,它將攻擊鏈移植到 Linux aarch64;在我們指出原本的版本會在 UTM 的顯示更新執行緒中崩潰後,又重新建置成可在 SPICE 環境安全運作的 UTM 變體;macOS 的 chained-fixups 證實會令 GOT 變成唯讀後,它又將做法從「覆寫 GOT[free]」改為使用可寫入的 BSS;當 ASLR 令 pixman 位於緩衝區後方時,它又加入了 QEMU-g_free 備用方案。這些轉向都沒有真人指點答案;完整提示詞記錄只有十多條單行提示。

不過,Claude 尚未(重新)發現 KMART 或 MHST[^1] 等高階技巧來利用這個漏洞,因此我們當中的頂尖高手仍略勝一籌,至少目前如此。

[^1]: Kortchinsky-Midturi ARM ROP Technique 和 Midturi Heap Spray Technique。這些是約十五年前由 MSRC 和 SWI Pentest 團隊發明的傳奇漏洞利用技術。CC @crypt0ad

程式碼及 PoC →

來源:Calif:Research(AI 漏洞研究) · calif.io