Xen hypervisor的跨虛擬機代碼執行

前言

2017-03-14,我給 Xen』s security teamp 報告了一個 Bug。該 Bug 允許位於paravirtualized(半虛擬化)Guest 中的一個擁有 root 許可權的的攻擊者跳出 hypervisor 的管理,完全控制宿主機的物理內存。Xen Project 在 2017-04-04

發布了一個公告和 Patch 補丁。

背景知識

在 x86-64 上,Xen PV(paravirtualized) guests 和 hypervisor 共享虛擬地址空間。粗略的內存布局如下:

移除點擊此處添加圖片說明文字

Xen 允許 guest 內核執行 hypercall,即使用 Sytem V AMD64 AMI 來實現從 guest 內核到 hypervisor 的一個必備的系統調用。通常是使用指令 syscall 實現,最多 6 個參數,通過寄存器傳遞。就像正常內核的 syscall,Xen hypercall 通常直接

Advertisements

使用 guest 上的指針來作為參數。由於 hypervisor 共享它的地址空間,故它能理解直接傳遞過來的 guest-virtual 指針。

就像所有內核一樣,在需要解引用guest-virtual指針的時候,Xen必須確保它們並沒有實際指向hypervisor-owned的內存區域。它實際上使用用戶態的accessor(和Linux內核中的那些相似)來完成這些任務。

access_ok(addr,size) :檢查是否一個 guest-supplied 的虛擬地址可以安全訪問,換句話說,它會檢查訪問內存區域不會修改到hypervisor內存。

__copy_to_guest(hnd, ptr, nr) :從 hypervisor 的 ptr 地址拷貝 nr 個位元組到 guest地址 hnd ,但是不檢查 hnd 是否有效。

Advertisements

copy_to_guest(hnd, ptr, nr) :從hypervisor的 ptr 地址拷貝 nr 個位元組到 guest 地址 hnd ,驗證 hnd 有效性。

在 Linux 內核中,宏 access_ok() 檢測地址範圍 addr 到 addr+size-1 是否可以安全訪問,使用任意的內存訪問模式。然而,Xen 的 access_ok() 並不確保這些:

/*

* Valid if in +ve half of 48‐bit address space, or above Xen‐reserved area.

* This is also valid for range checks (addr, addr+size). As long as the

* start address is outside the Xen‐reserved area then we will access a

* non‐canonical address (and thus fault) before ever reaching VIRT_START.

*/

#define __addr_ok(addr) \

(((unsigned long)(addr) < (1UL<<47)) || \

((unsigned long)(addr) >= HYPERVISOR_VIRT_END))

#define access_ok(addr, size) \

(__addr_ok(addr) || is_compat_arg_xlat_range(addr, size))

Xen 通常只是檢查指針 addr 位於地址空間或者內核空間,但不檢查大小 size 。如果實際的 Guest Memory 訪問從地址addr附近開始,線性的處理,只要一個 guest memory 訪問失敗,則 bails out 是有意義的,因為大片的 non-canonical 地址空間正好充當了一個大的保護區。然而,如果一個 hypercall 想要訪問一個起始於 64-bit offset 的 guest buffer,則它需要確保調用時 access_ok() 填入正確的偏移,檢查整個 userspace buffer 是不安全的。

Xen 提供了圍繞 access_ok() 的封裝來檢查 guest 中訪問數組。如果想檢查是否可以安全的訪問一個數組(從下標 0 開始),可以使用 guest_handle_okay(hnd,nr) 。然而,如果你想檢查不從下標 0 開始的數組,應該使用 guest_handle_subrange_okay(hnd, first, last) 。

當我看到了 access_ok 的定義,發現其缺乏完整的安全性驗證,所以,我開始查找調用它的地方,來看是否有不安全的使用行為。

Hypercall 的搶佔機制(Preemption)

當調度 Tick 發生,Xen 需要快速的從當前執行的 vCPU 切換到另一個虛擬機的vCPU。然而簡單的中斷 hypercall 的執行是不會起作用的(eg.hypercall 可能正擁有一個自旋鎖),故 Xen(像其他操作系統)需要一些機制來延遲 vCPU 的切

換,直到它足夠安全的來執行。

在 Xen 里,hypercall 的搶佔是通過使用 自願性搶佔(voluntary preemption) 實現的:任何長時間運行的 hypercall 代碼需要提前調用 hypercall_preempt_check() 來檢查是否調度器想要調度到另一個 vCPU 上。如果該事件發生了,hypercall

代碼退出到 guest,因此,以信號的方式通知調度器,搶佔當前任務是安全的,調整了 hypercall argument 參數后(在guest register或者guest 內存),只要當前 vCPU 再次被調度,它將重新進入 hypercall 之後,執行剩餘的工作。

Hypercall 無法區分搶佔之後的正常的 hypercall entry 和 hypercall re-entry。

Xen 使用 Hypercall re-entry 的機制,因為 Xen 並不是每一個 vCPU 都有一個hypervisor 棧。它是針對每一個物理核有一個 hypervisor 棧。這意味著,別的操作系統,比如 Linux,可以簡單的離開一個中斷的 syscall 的狀態,而 Xen 無法輕鬆的做到。

這個設計意味著對於一些 hypercall,允許他們適當地 resume 它們的工作,額外的數據保存到 guest memory 中,而 guest momory 的數據有可能被修改 guest 精心修改而用來攻擊 hypervisor。

memory_exchange()

hypercall HYPERVISOR_memory_op(XENMEM_exchange, arg) 調用函數 memory_exchange(arg)(source code: xen/common/memory.c) 。該函數允許一個 guest 將它們擁有的的物理內存用來交換一些在物理地址連續性上有限制的新的物理內存。該功能對於想實現 DMA 功能的 guest 非常有用,因為 DMA 需要物理上連續的緩衝區。

該 hypercall 需要一個結構為 struct xen_memory_exchage 的參數,定義如下:

truct xen_memory_reservation {

XEN_GUEST_HANDLE(xen_pfn_t) extent_start;

xen_ulong_t nr_extents;

unsigned int extent_order;

unsigned int mem_flags;

/*

* Domain whose reservation is being changed.

* Unprivileged domains can specify only DOMID_SELF.

*/

domid_t domid;

};

struct xen_memory_exchange {

/*

* [IN] Details of memory extents to be exchanged (GMFN bases).

* Note that @in.address_bits is ignored and unused.

*/

struct xen_memory_reservation in;

/*

* [IN/OUT] Details of new memory extents.

* We require that:

* 1. @in.domid == @out.domid

* 2. @in.nr_extents << @in.extent_order ==

* @out.nr_extents << @out.extent_order

* 3. @in.extent_start and @out.extent_start lists must not overlap

* 4. @out.extent_start lists GPFN bases to be populated

* 5. @out.extent_start is overwritten with allocated GMFN bases

*/

struct xen_memory_reservation out;

/*

* [OUT] Number of input extents that were successfully exchanged:

* 1. The first @nr_exchanged input extents were successfully

* deallocated.

* 2. The corresponding first entries in the output extent list correctly

* indicate the GMFNs that were successfully exchanged.

* 3. All other input and output extents are untouched.

* 4. If not all input exents are exchanged then the return code of this

* command will be non‐zero.

* 5. THIS FIELD MUST BE INITIALISED TO ZERO BY THE CALLER!

*/

xen_ulong_t nr_exchanged;

};

和該 Bug 相關的成員有: in.extent_start, in.nr_extents, out.extent_start, out.nr_extents 和 nr_exchangednr_exchanged 文檔中默認總是被 guest 初始化為 0,這是因為,它不僅用來返回一個結果,同時 hypercall 的搶佔中也會用到。當 memory_exchange() 被搶佔后, nr_exchaged 存儲它的進度,這樣,當下一次執行 memory_exchage() 時,使用 nr_exchanged 來決定輸入數組中 in.extent_start和out.extent_start 的哪個點應該被resume。原來的 memory_exchange() 並不檢查用戶空間的數組指針,在使用 __copy_from_guest_offset和__copy_to_guest_offset() 訪問它們之前,而且不進行自身的任何檢查,故,使用提供的hypervisor指針,可能導致Xen去讀或者寫hypervisor內存–一個非常嚴重的Bug。該問題在2012年被發現(XSA-29,CVE-2012-5513),同時進行了如下修補(https://xenbits.xen.org/xsa/xsa29-4.1.patch):

diff ‐‐git a/xen/common/memory.c b/xen/common/memory.c

index 4e7c234..59379d3 100644

‐‐‐ a/xen/common/memory.c

+++ b/xen/common/memory.c

@@ ‐289,6 +289,13 @@ static long memory_exchange(XEN_GUEST_HANDLE(xen_memory_exchange_t)

arg)

goto fail_early;

}

+ if ( !guest_handle_okay(exch.in.extent_start, exch.in.nr_extents) ||

+ !guest_handle_okay(exch.out.extent_start, exch.out.nr_extents) )

+ {

+ rc = ‐EFAULT;

+ goto fail_early;

+ }

+

if ( !multipage_allocation_permitted(current‐>domain,

exch.in.extent_order) ||

The Bug

如下代碼片段所示,64bit resumption 偏移 nr_exchanged ,可以被 guest 控制,由於Xen』s 的 hypercall resumption 機制,可以被 guest 用來從 out.extent_start 選擇一個偏移用來寫:

static long memory_exchange(XEN_GUEST_HANDLE_PARAM(xen_memory_exchange_t) arg)

{

[...]

[...]

if ( !guest_handle_okay(exch.in.extent_start, exch.in.nr_extents) ||

!guest_handle_okay(exch.out.extent_start, exch.out.nr_extents) )

{

rc = ‐EFAULT;

goto fail_early;

} [

...]

for ( i = (exch.nr_exchanged >> in_chunk_order);

i < (exch.in.nr_extents >> in_chunk_order);

i++ )

{

[...]

for ( j = 0; (page = page_list_remove_head(&out_chunk_list)); ++j )

{

[...]

if ( !paging_mode_translate(d) )

{

[...]

if ( __copy_to_guest_offset(exch.out.extent_start,

(i << out_chunk_order) + j,

&mfn, 1) )

rc = ‐EFAULT;

}

}[

...]

}[

...]

}

然而, guest_handle_okay() 只檢查了是否可以安全訪問從下標 0 開始的 guest 數組 exch.out.extent_start 。 guest_handle_subrange_okay 才應該是正確的方式。因此,一個 attacker 可以攻擊者可以通過如下條件來達到給任意地址寫 8 個位元組的數據的目的:

exch.in.extent_order 和 exch.out.extent_order 為0(新頁大小的塊替換原來的頁大小的塊)。

exch.out.extent_start 和 exch.nr_exchanged : exch.out.extent_start 指向用戶空間內存,然而 exch.out.extent_start+8*exch.nr_exchanged 指向hypervisor內存中的目標地址,當exch.out.extent_start趨近與NULL時,可以這樣計算:

exch.out.extent_start=target_add%8, exch.nr_exchanged=target_addr/8 。

exch.in.nr_extents 和 exch.out.nr_extents 為 exch.nr_exchanged+1 。

exch.in.extent_start 為 input_buffer‐8*exch.nr_exchanged(input_buffer是一個合理的指向物理頁的guest_內核地址指針) 這個確保總是指向guest用戶空間範圍(通過了access_ok()檢查),因為 exch.out.extent_start 粗略地指向用戶空間地址範圍的起始地址,而且,guest內核地址範圍和用戶空間地址範圍一樣大。

寫入到攻擊者控制的地址中的值是一個PFN號。

利用該bug:獲取頁表的控制

在一個忙碌的系統上,控制由內核寫的頁號是非常困難的。因此,出於穩定性的考慮,有必要將該 bug 視為一個 primitive,即循環地在一個固定的地址寫 8 個位元組的數據,同時大部分有效的位初始化為 0(由於有限的物理內存),同時少量的有效位初始化為隨機值。對於我的 exploit,我決定將這個 primitive 視為寫一個必須地隨機位元組和緊隨 7 個垃圾位元組的方式。

事實證明,對於一個 x86-64PV guest,有這樣一個 primitive,對於穩定的利用是非常有效的,因為:

x86-64PV guest 知道所有它可以訪問的物理頁的頁號。

x86-64PV guest 可以映射屬於它們 domain 的頁表(4個level的)為可讀。Xem只阻止將它們映射為可寫。

Xen 映射所有的物理內存為可寫,在地址 0xffff830000000000。

攻擊的目標是將 level 3 頁表(我稱為「受害頁表」)中的一個 entry 指向一個 guest 有寫許可權的頁(我稱為「假頁表」)。這意味著,攻擊者必須寫入 8 個位元組數據,同時要有假頁表的物理頁號和一些其它的 Flag,同時還要確保,之後的 8 個位元組

的頁表項保持禁用狀態(eg.通過設置下一個entry的第一個位元組為0)。最終,攻擊者必須寫8個控制地位元組,之後地 7 個位元組不用關心。

因為所有相關頁的物理頁號和可寫的映射的物理地址對於 guest 來說都是可知的,因此,找出寫到哪和要寫什麼非常輕鬆,所以,唯一的問題,就是如何利用 primitive 來真正地寫入數據。

因為攻擊者想使用 primivite 來寫到一個可讀地頁面,故寫入一個位元組隨機數據和7位元組垃圾數據的方式可以輕鬆地轉換為寫入一個位元組的控制數據和 7 個位元組的垃圾數據,而寫入一個位元組的控制數據和 7 個位元組垃圾數據的 primitive 可以轉換為寫入控制數據和 7 個位元組垃圾數據的 primitive,通過寫入位元組到連續的地址,這才是真正的 primitive needed。

到此時,攻擊者可以控制一個實時的頁表,來允許攻擊者映射任意地物理地址到guest 的虛擬地址。也意味著攻擊者可以從內存中可靠的讀取和寫入,包括代碼和數據等,到 hypervisor 和該系統上所有其他的虛擬機中。

在其他虛擬機中執行 shell 命令

在此時,攻擊者可以完全控制機器了,相當於擁有了 hypervisor 的特權級,同時通過搜索物理內存可以輕易地獲取一些機密信息。但是一個實際主義的攻擊者,考慮到更多的被檢測到的風險,很可能不會注入代碼到虛擬機中。

但是在別的 VM 中運行任意一個 shell 命令更低調一些。所以,我打算繼續完善我的exploit 使其可以給所有的其他 PV 虛擬機注入一個 shell 命令。

第一步,我打算在 hypervisor 上下文中,獲取可靠的代碼執行能力。通過讀寫物理內存的能力,一個相對 OS (或者 hypervisor )獨立的以 kernel/hypervisor 許可權調用任意地址的方式是使用非特權指令 SIDT 來定位 IDT 表,同時寫入一個 DPL3 的 IDT entry,之後產生中斷。Xen 支持 SMEP 和 SMAP,故不可能將 IDT Entry 指到 guest 內存中,但是使用讀寫頁表項的能力,可以映射一個 guest 擁有的,位於 hypervisor上下文的 shellcode 頁為 non‐user‐accessible ,這樣可以繞過 SMEP。

之後,在 hypervisor 上下文中,可以通過讀寫 IA32_LSTAR MSR 寄存起來 hook Syscall 入口點。Syscall 入口點,同時適用於來自 guest 用戶空間的 syscall 和來自 guest 內核的 hypercall。通過映射一個攻擊者控制的頁面到 guest‐user‐accessible 內存,改變寄存器狀態,調用 sysret,有可能將用戶空間的執行轉移到任意 guest 用戶的 shellcode上,該操作獨立於 hypervisor 和 guest 操作系統。

我的 exploit 注入 shellcode 到所有 guest 用戶空間每一個調用 write() Syscall 的進程。當 shellcode 運行時,它檢查它是否它是否以 root 許可權運行,同時是否在 guest 的文件系統不存在 lockfile 文件。如果這些條件被滿足,調用 clone() syscall 來創建一個子進程來執行任意的 shell 命令。(注意,我的 exploit 並沒有結束自身,故當attacking domain 之後關閉了,hoo k點將導致 hypervisor 崩潰)。

如下是一個 Qube OS3.2 成功攻擊的一個截圖。該代碼實在一個非特權的 domain 「test1234」中執行的,截圖顯示,它成功注入代碼到 dom0 和 firewallvm 虛擬機中:

移除點擊此處添加圖片說明文字

結論

我堅信導致該問題的根本原因是由於 accessok() 的低安全驗證。當前版本的 access_ok() 是 2005 年提交的,兩年後,發布了 Xen 和 XSA 的第一個版本。而且看起來老的代碼比新的代碼更可能包含多的相對直接的安全缺陷,因為當時提交時並沒有太多考慮安全因素,如此老代碼一直沿用至今。

當基於這些設想的安全相關的代碼被優化時,一定要留意防止這些設想被利用。 access_ok() 事實上常用來檢測是否整個範圍和 hypervisor 內存重複,這樣將阻止該bug 的產生。不幸的是,2005 年, a commit with "x86_64 fixes/cleanups" 改變了 access_ok() 在 x86_64 上的行為,一直沿用到現在的版本。就我目前認為,唯一沒有直接使 MEMOP_increase_revervation 和 MEMOP_decrease_reservation hypercall有漏洞的原因是因為 do_dom_mem_op() 的參數 nr_extents 是 32 位的—-一個相對脆弱的防禦。

然而,已經發現了好些 Xen 漏洞是隻影響 PV guest 的,因為當處理 HVM guest 時,代碼中的那些問題不是必須的,我堅信這個 bug 不是其中的一個。對於 PV guest 來說訪問 guest 虛擬內存比 HVM guest 更直接:對於 PVguest,raw_copy_from_guest 調用 copy_from_user() ,只是簡單的做了一次邊界判斷,之後便是有內存頁 fixup 的一個memcpy,和正常操作系統執行用戶態空間內存檢查一致。對於 HVM guest, raw_copy_from_guest() 調用 copy_from_user_hvm() ,會做一個遍歷 guest 頁表的page-wise 的拷貝(因為內存區域可能物理上是不連續的,同時 hypervisor 並沒有一個連續的虛擬映射),同時 guest frame 查找是對每一個頁的,包括引用,映射 guest頁到 hypervisor 內存和比如阻止 HVM guest 寫只讀頁的各種檢查等。故對於 HVM,處理 guest memory 的複雜性要高於 PV。

對於安全研究員來說,我認為了解正常內核調用後來理解半虛擬化不是很難。如果你審計過內核代碼,其實 hypercall entry(lstar_enter and int80_direct_trap in xen/arch/x86/x86_64/entry.S) ,基本的 hypercall(for x86 PV: listed in the pv_hypercall_table in xen/arch/x86/pv/hypercall.c) 設計處理和正常的

系統調用看起來差不多。

本文由 看雪翻譯小組 ghostway 編譯,來源Dimitri Fourny

往期熱門內容推薦


等你來挑戰!| 看雪 CTF 2017 攻擊篇

【終於等到你!】看雪 CTF 2017

春風十里,我在等你

Headless Chrome入門

使用最新的代碼重用攻擊繞過執行流保護(一)

菜鳥調試經典老遊戲之富甲天下3

......

Advertisements

你可能會喜歡