Jump to content
  • Entries

    16114
  • Comments

    7952
  • Views

    86381380

Contributors to this blog

  • HireHackking 16114

About this blog

Hacking techniques include penetration testing, network security, reverse cracking, malware analysis, vulnerability exploitation, encryption cracking, social engineering, etc., used to identify and fix security flaws in systems.

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