0x01 基本介紹KVM(用於基於內核的虛擬機)是基於Linux 的雲環境的標準管理程序。除了Azure ,幾乎所有大型雲服務和託管服務提供商都在KVM 之上運行,將其變成了雲服務中的基本安全邊界之一。
在這篇文章中,我記錄了KVM 和AMD CPU 中發現的一個漏洞,並研究瞭如何將此bug轉化為完整的虛擬機逃逸。據我所知,這是KVM guest到物理主機突破的第一篇公開文章,它不依賴於用戶空間組件(如QEMU)中的漏洞。此漏洞被分配的漏洞編號是CVE-2021-29657,影響內核版本v5.10-rc1 到v5.12-rc6, 並於2021 年3月底做了修補。由於該漏洞僅在v5.10 中才可被利用並在大約5 個月後被發現,因此KVM 的大多數部署設備不受影響。我認為這個問題是一個有趣的案例研究,需要構建一個穩定的guest到主機的KVM 逃逸路徑,使得獲取KVM虛擬機管理程序權限不再僅僅是理論問題。
https://bugs.chromium.org/p/project-zero/issues/detail?id=2177q=owner%3Afwilhelm%40google.comcan=1
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=a58d9166a756a0f4a6618e4f593232593d6df134下文首先簡要概述KVM 的架構,然後再深入研究漏洞及其利用。
0x02 KVMKVM 是一個基於Linux 的開源管理程序,支持x86、ARM、PowerPC 和S/390 上的硬件加速虛擬化。與其他大型開源管理程序Xen 相比,KVM 與Linux 內核深度集成,並在其調度、內存管理和硬件集成的基礎上構建,以提供高效的虛擬化。
KVM 由一個或多個內核模塊(kvm.ko 加上kvm-intel.ko 或x86 上的kvm-amd.ko)實現,它們通過/dev/kvm 設備向用戶空間進程公開一個基於IOCTL 的低級API。使用此API,用戶空間進程(通常稱為Virtual Machine Manager 的VMM)可以創建新的VM,分配vCPU 和內存,並攔截內存或IO 訪問以提供對模擬或虛擬化感知硬件設備的訪問。 長期以來,QEMU一直是基於KVM 的虛擬化的標準用戶空間選擇,但在最近幾年,LKVM、crosvm 或Firecracker等替代方案開始流行。
低級API:https://www.kernel.org/doc/html/latest/virt/kvm/api.html
[LKVM](https://github.com/lkvm/lkvm)
[crosvm](https://chromium.googlesource.com/chromiumos/platform/crosvm/)
[Firecracker](https://github.com/firecracker-microvm/firecracker)雖然KVM 依賴於單獨的用戶空間組件起初可能看起來很複雜,但它有一個非常優秀的好處:在KVM 主機上運行的每個VM 都有一個到Linux 進程的1:1 映射,使其可以使用標準Linux 工具進行管理。
這意味著,例如,可以通過轉儲其用戶空間進程的分配內存來檢查guest的內存,或者可以輕鬆應用CPU 時間和內存的資源限制。此外,KVM 可以將大部分與設備模擬相關的工作卸載到用戶空間組件。除了與中斷處理相關的幾個性能敏感設備之外,所有用於提供虛擬磁盤、網絡或GPU 訪問的複雜低級代碼都可以在用戶空間中實現。
在查看KVM 相關漏洞和利用的公開文章時,很明顯這種設計是一個明智的決定。大多數公開的漏洞和所有公開可用的漏洞都會影響QEMU 及其對模擬/半虛擬化設備的支持。
儘管KVM 的內核攻擊面比默認QEMU 配置或類似的用戶空間VMM 暴露的要小得多,但KVM 漏洞對攻擊者來說非常的有價值:
儘管可以對用戶空間VMM 進行沙箱化以減少VM 逃逸的影響,但KVM 本身沒有這樣的選項。一旦攻擊者能夠在主機內核的上下文中實現代碼執行(或類似的強大原語,如對頁表的寫訪問),系統就會完全受到損害。
由於QEMU 的安全歷史有些糟糕,像crosvm 或Firecracker 這樣的新用戶空間VMM 是用Rust編寫的。當然,由於KVM API 的不正確使用或漏洞利用,仍然可能存在非內存安全漏洞或問題,但使用Rust 有效地防止了過去在基於C 的用戶空間VMM 中發現的大部分漏洞。
最後,純KVM 漏洞可以針對使用專有或大量修改的用戶空間VMM 的目標。雖然大型雲提供商不會公開詳細介紹他們的虛擬化堆棧組件,但可以假設他們不依賴未修復的QEMU 版本來處理其生產工作負載。相比之下,KVM 較小的代碼庫不太可能進行大量修改。
考慮到這些優勢,我決定花一些時間挖掘一個KVM 漏洞,該漏洞可能會實現從客戶機到主機的逃逸。過去,我在KVM 對Intel CPU 上嵌套虛擬化的支持組件中挖掘到一些漏洞,因此可以想像AMD 的相同功能似乎也是一個很好的挖掘點。因為最近AMD 在服務器領域的市場份額增加意味著KVM 的AMD 實現突然成為一個比過去幾年更有趣的目標。
https://bugs.chromium.org/p/project-zero/issues/list?q=vmxowner%3Afwilhelmcan=1嵌套虛擬化,VM(稱為L1)生成嵌套guest(L2)的能力,在很長一段時間內也是一個小眾功能。然而,由於硬件改進減少了它的開銷並增加了客戶需求,它變得越來越廣泛可用。例如,微軟正在大力推動基於虛擬化的安全性作為較新Windows 版本的一部分,需要嵌套虛擬化來支持雲託管的Windows 安裝。 默認情況下,KVM 在AMD 和Intel上都啟用對嵌套虛擬化的支持,因此如果管理員或用戶空間VMM 沒有明確禁用它,它就是惡意或受感染VM 的攻擊面的一部分。
https://docs.microsoft.com/en-us/windows-hardware/design/device-experiences/oem-vbsAMD 的虛擬化擴展稱為SVM(用於安全虛擬機),為了支持嵌套虛擬化,主機管理程序需要攔截由其guest執行的所有SVM 指令,模擬它們的行為並保持其狀態與底層硬件同步。正確實現這一點非常困難,並且存在很大的複雜邏輯缺陷,使其成為手動代碼審查的完美目標。
0x03 漏洞細節在深入研究KVM 代碼庫和我發現的漏洞之前,我想快速介紹一下AMD SVM 的工作原理,以使文章的其餘部分更容易理解。有關詳細文檔,請參閱AMD64 架構程序員手冊,第2 卷:系統編程第15 章。如果通過設置EFER MSR 中的SVME 位啟用SVM 支持,則SVM 將支持6 條新指令到x86-64。這些指令中最有趣的是VMRUN ,它負責運行VMguest。 VMRUN通過RAX 寄存器獲取一個隱式參數,該參數指向“虛擬機控制塊”(VMCB)數據結構的頁面對齊物理地址,它描述了VM 的狀態和配置。
AMD64架構程序員手冊,第2卷:系統編程第15章:https://www.amd.com/system/files/TechDocs/24593.pdfVMCB 分為兩部分:首先是狀態保存區,它存儲所有guest寄存器的值,包括段和控制寄存器。第二,控制區,描述了虛擬機的配置。 Control 區域描述了為VM 啟用的虛擬化功能,設置攔截哪些VM 操作以觸發VM 退出並存儲一些基本配置值,例如用於嵌套分頁的頁表地址。
用於嵌套分頁的頁表地址:https://en.wikipedia.org/wiki/Second_Level_Address_Translation如果正確準備了VMCB,VMRUN 將首先將主機狀態保存在主機保存區的內存區域中,其地址是通過向VM_HSAVE_PA MSR 寫入物理地址來配置的。一旦保存了主機狀態,CPU 就會切換到VM 上下文,並且VMRUN 僅在由於某種原因觸發VM 退出時才返回。
SVM 的一個有趣方面是,VM 退出後的許多狀態恢復必須由管理程序完成。一旦發生VM 退出,只有RIP、RSP 和RAX 會恢復為之前的主機值,所有其他通用寄存器仍包含guest值。此外,完整的上下文切換需要手動執行VMSAVE/VMLOAD 指令,這些指令從內存中保存/加載額外的系統寄存器(FS、SS、LDTR、STAR、LSTAR……)。
為了使嵌套虛擬化工作,KVM 會攔截VMRUN 指令的執行,並基於L1 guest準備的VMCB(在KVM 術語中稱為vmcb12)創建自己的VMCB。當然,KVM 不能信任guest提供的vmcb12,並且需要仔細驗證最終在傳遞給硬件(稱為vmcb02)的真實VMCB 中的所有字段。
AMD 上用於嵌套虛擬化的KVM 代碼大部分在arch/x86/kvm/svm/nested.c中實現,攔截嵌套guest的VMRUN 指令的代碼在nested_svm_vmrun 中實現:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/kvm/svm/nested.c?h=v5.11
intnested_svm_vmrun(structvcpu_svm*svm)
{
intret;
structvmcb*vmcb12;
structvmcb*hsave=svm-nested.hsave;
structvmcb*vmcb=svm-vmcb;
structkvm_host_mapmap;
u64vmcb12_gpa;
vmcb12_gpa=svm-vmcb-save.rax;**1**
ret=kvm_vcpu_map(svm-vcpu,gpa_to_gfn(vmcb12_gpa),map);**2**
…
ret=kvm_skip_emulated_instruction(svm-vcpu);
vmcb12=map.hva;
if(!nested_vmcb_checks(svm,vmcb12)){**3**
vmcb12-control.exit_code=SVM_EXIT_ERR;
vmcb12-control.exit_code_hi=0;
vmcb12-control.exit_info_1=0;
vmcb12-control.exit_info_2=0;
gotoout;
}
.
/*
*Savetheoldvmcb,sowedon'tneedtopickwhatwesave,butcan
*restoreeverythingwhenaVMEXIToccurs
*/
hsave-save.es=vmcb-save.es;
hsave-save.cs=vmcb-save.cs;
hsave-save.ss=vmcb-save.ss;
hsave-save.ds=vmcb-save.ds;
hsave-save.gdtr=vmcb-save.gdtr;
hsave-save.idtr=vmcb-save.idtr;
hsave-save.efer=svm-vcpu.arch.efer;
hsave-save.cr0=kvm_read_cr0(svm-vcpu);
hsave-save.cr4=svm-vcpu.arch.cr4;
hsave-save.rflags=kvm_get_rflags(svm-vcpu);
hsave-save.rip=kvm_rip_read(svm-vcpu);
hsave-save.rsp=vmcb-save.rsp;
hsave-save.rax=vmcb-save.rax;
if(npt_enabled)
hsave-save.cr3=vmcb-save.cr3;
else
hsave-save.cr3=kvm_read_cr3(svm-vcpu);
copy_vmcb_control_area(hsave-control,vmcb-control);
svm-nested.nested_run_pending=1;
if(enter_svm_guest_mode(svm,vmcb12_gpa,vmcb12))**4**
gotoout_exit_err;
if(nested_svm_vmrun_msrpm(svm))
gotoout;
out_exit_err:
svm-nested.nested_run_pending=0;
svm-vmcb-control.exit_code=SVM_EXIT_ERR;
svm-vmcb-control.exit_code_hi=0;
svm-vmcb-control.exit_info_1=0;
svm-vmcb-control.exit_info_2=0;
nested_svm_vmexit(svm);
out:
kvm_vcpu_unmap(svm-vcpu,map,true);
returnret;
}該函數首先從當前活動的vmcb ( svm-vcmb ) 中獲取1中的RAX 的值。對於使用嵌套分頁的guest,RAX 包含一個guest物理地址(GPA),需要先將其轉換為主機物理地址(HPA)。 kvm_vcpu_map ( 2 ) 負責此轉換並將底層頁面映射到可由KVM 直接訪問的主機虛擬地址(HVA)。
映射VMCB 後, 將調用nested_vmcb_checks在3 中進行一些基本驗證。之後, 在KVM通過調用enter_svm_guest_mode ( 4 )進入嵌套的guest context之前,將存儲在svm-vmcb中的L1 guest context 複製到主機保存區svm-nested.hsave中。
intenter_svm_guest_mode(structvcpu_svm*svm,u64vmcb12_gpa,structvmcb*vmcb12)
{
intret;
svm-nested.vmcb12_gpa=vmcb12_gpa;
load_nested_vmcb_control(svm,vmcb12-control);
nested_prepare_vmcb_save(svm,vmcb12);
nested_prepare_vmcb_control(svm);
ret=nested_svm_load_cr3(svm-vcpu,vmcb12-save.cr3,
nested_npt_enabled(svm));
if(ret)
returnret;
svm_set_gif(svm,true);
return0;
}
staticvoidload_nested_vmcb_control(structvcpu_svm*svm,
structvmcb_control_area*control)
{
copy_vmcb_control_area(svm-nested.ctl,control);
.
}查看enter_svm_guest_mode 我們可以看到KVM 將vmcb12 控制區直接複製到svm-nested.ctl 中,並且不會對複制的值進行任何進一步的檢查。
熟悉double fetch 或Time-of-Check-to-Time-of-Use 漏洞的讀者可能已經在這裡看到了一個潛在問題:在nested_svm_vmrun 開頭對nested_vmcb_checks的調用對VMCB的副本執行所有檢查,該副本是存儲在guest內存中。這意味著具有多個CPU 內核的guest可以在nested_vmcb_checks 中驗證VMCB 中的字段後,在將它們複製到load_nested_vmcb_control 中的svm-nested.ctl 之前修改它們。
讓我們看一下nested_vmcb_checks ,看看可以用這種方法繞過什麼樣的檢查:
staticboolnested_vmcb_check_controls(structvmcb_control_area*control)
{
if((vmcb_is_intercept(control,INTERCEPT_VMRUN))==0)
returnfalse;
if(control-asid==0)
returnfalse;
if((control-nested_ctlSVM_NESTED_CTL_NP_ENABLE)
!npt_enabled)
returnfalse;
returntrue;
}control-asid 沒有使用,最後一次檢查僅與不支持嵌套分頁的系統相關。
SVM VMCB 包含一個bit,用於在guest內部執行時啟用或禁用VMRUN 指令的攔截。硬件實際上不支持清除此位,並會導致立即VMEXIT,因此,nested_vmcb_check_controls 中的檢查只是複制了此行為。當我們通過反复翻轉INTERCEPT_VMRUN 位的值來競爭並繞過檢查時,我們最終可能會遇到svm-nested.ctl 包含0 代替INTERCEPT_VMRUN 位的情況。要了解影響,我們首先需要了解嵌套的vmexit 在KVM 中是如何處理的:
主SVM退出處理句柄是在arch/x86/kvm/svm.c中的函數handle_exit ,每當發生VMEXIT被調用就會調用此函數。當KVM 運行嵌套的guest時,它首先必須檢查退出是否應該由它自己或L1 管理程序處理。為此,它調用了函數nested_svm_exit_handled ( 5 ),如果vmexit 將由L1 管理程序處理並且不需要由L0 管理程序進一步處理,則該函數將返回NESTED_EXIT_DONE :
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/kvm/svm/svm.c?h=v5.11
staticinthandle_exit(structkvm_vcpu*vcpu,fastpath_texit_fastpath)
{
structvcpu_svm*svm=to_svm(vcpu);
structkvm_run*kvm_run=vcpu-run;
u32exit_code=svm-vmcb-control.exit_code;
…
if(is_guest_mode(vcpu)){
intvmexit;
trace_kvm_nested_vmexit(exit_code,vcpu,KVM_ISA_SVM);
vmexit=nested_svm_exit_special(svm);
if(vmexit==NESTED_EXIT_CONTINUE)
vmexit=nested_svm_exit_handled(svm);**5**
if(vmexit==NESTED_EXIT_DONE)
return1;
}
}
staticintnested_svm_intercept(structvcpu_svm*svm)
{
//exit_code==INTERCEPT_VMRUNwhentheL2guestexecutesvmrun
u32exit_code
Recommended Comments