Jump to content

在這個挑戰中,我們被賦予了運行在hypervisor中的linux虛擬機的root權限,目標是實現hypervisor逃逸,以訪問宿主機系統上的旗標文件。在這個過程中,我們發現了多個安全漏洞,通過它們可以實現lkvm的零日攻擊,使得具有虛擬機訪問權限的攻擊者能夠在宿主機上執行任意命令。

在這篇文章中,當提到行號時,請參考kvmtool的git checkout 39181fc6429f4e9e71473284940e35857b42772a。

攻擊面由於我們是在hypervisor內運行,並且宿主機和虛擬機內存之間實現了顯式的隔離,因此,我們需要找到一種方法與宿主機的進程進行通信。實際上,我們可以通過pci與宿主機進行通信,因為Lkvm通過pci模擬了3個硬件設備:virtio-console、virtio-net和virtio-balloon。我們可以使用內存映射的IO與這些設備進行交互,也就是對特定的物理內存地址進行讀取和寫入操作。如果我們對0xd2000000-0xd20000ff(balloon-virtio)範圍內的地址進行寫操作,虛擬機就會被中斷,並且控制流將被傳遞給linux系統的kvm驅動程序,然後進一步傳遞給lkvm進程。

信息洩露當從這個地址執行讀取操作時,我們首先遇到的一個函數是virtio/pci.c第148行的virtio_pci__data_in函數:

image.png

該函數可以將偏移量映射為bar(地址範圍),這意味著,如果我們在地址0xD2000000+VIRTIO_PCI_QUEUE_NUM==0xD2000008處執行讀取操作,我們將進入第二個case子句。需要注意的是,這裡的默認case子句非常有趣:在第118行調用virtio_pci__specific_data_in:

image.png

在這裡,我們總是以else if的case子句結束,因為這不是MSIX操作。另外,config_offset是根據從virtio_pci__data_in傳遞的偏移量來計算的,我們看到它具有完整的訪問權限,並且沒有進行任何綁定檢查。並且,config_offset的值是在調用virtio__get_dev_specific_field時作為返回參數進行計算的。如果我們沒有執行MSIX操作,config_offset就是設置為傳遞給virtio__get_dev_specific_field的第一個參數的值,其偏移量為-20。

到目前為止,我們只討論了virtio和pci泛型函數,但這裡調用了ops-get_config,在本例中,它從balloon驅動程序中提取了u8*配置。這個函數只是一個簡單的getter,代碼如下所示:

image.png

正如我們在下面所看到的,virtio_balloon_config是結構體的最後一個元素;讀者可能已經註意到了,config結構體非常小。由於bar(地址範圍)為0x100(0xD2000000-0xD20000FF),因此,只要將偏移量設置為大於20,我們就能以0x10020+sizeof(virtio_balloon_config)的形式訪問這個config結構體。在這個地址範圍內執行寫入操作時,相當於對config結構體執行寫操作,這意味著我們獲得了一個越界讀/寫原語。

image.png

這段內存並沒有分配在堆棧上,而是分配到一個mmaped區域中。這意味著我們無法通過破壞這個內存區來控製程序流。但是,我們能夠利用這個漏洞來洩露信息,即洩露兩個感興趣的指針,其中一個指向bln_dev結構體本身的地址,另一個指向lkvm二進製文件的基址。

為了在用戶空間進程中洩漏這兩個指針,我們可以使用/dev/mem來訪問虛擬機的物理內存,具體代碼如下所示:

image.png

這裡,leak_u64使用ioread8從virtio-balloon所在的0xD2000000處的mmap/dev/mem區域讀取數據。我們將這20個字節加上一個越界的偏移量,使其正好指向lkvm可執行文件的地址,這樣我們就能實現信息洩漏了。對於bln_dev洩漏,我們可以重複相同的過程。

獲得程序流程的控制權現在終於到了最有趣的部分:控制rip。假如我們能利用前面的漏洞來編寫任意的越界代碼,那麼,我們可以破壞哪些有趣的數據呢?在下圖中,我在virtio_pci__specific_data_in函數中設置了一個斷點來檢查bln_dev內存。在這裡,我轉儲了位於config結構體後面的內存內容。其中,我們看到一些名為exit_lists的結構體,不幸的是,由於我們可以突破0x100的限制,所以,這些結構體都是可達的。但這些到底是什麼?

1.png

由於virtio_pci__specific_data_in中的偏移量-20導致轉儲不是0x10對齊的,所以地址可能會有點亂

當lkvm二進製文件關閉時,它會進行拆卸處理,包括調用一些退出處理程序,這就是我們在這裡發現的東西。如果我們查看init.c內部的第51行,我們會發現代碼非常平易近人:

image.png

這裡可以看到,在退出lkvm時,會遍歷struct init_item數組,並從數組中的最後一個元素開始,對每個元素調用t-init函數。這就是前面發現的exit_lists。列表中的每個條目都是指向init_item的指針(該結構體也用於初始化,它也因此得名)。如果我們能夠控制其中的指針,我們就就能偽造一個init_item,並在終止虛擬機操作系統時改變程序流程。

image.png

查看上面init_item的struct定義,我們就會發現它其實非常簡單:其中包含2個鍊錶指針、一個名稱指針和我們想要控制的函數指針,它相對於init_item頂部的偏移量為0x18。

實際上,我們之前在virtio_pci__data_in函數中還發現了其他功能,而不僅僅是對config進行讀取和寫入操作。下面,讓我們看一下virtio/pci.c第287行中該函數的等價物data_out:

image.png

這裡,我們對VIRTIO_PCI_QUEUE_PFN的第二個case子句非常感興趣,因為它調用了virtio-balloon特定的init虛擬隊列函數。我們可以在virtio/balloon.c中的第200行找到這個函數,具體如下所示:

image.png

正如我們所看到的,當調用vring_init時,它將vr-desc=p設置為完全處於我們控制之下的虛擬機物理頁面。我們可以看到,vring_init是從init_vq中調用的,參數是p,而p是從virtio_get_vq中獲得的,在那裡它可以找到給定頁幀號(pfn)的宿主機虛擬地址。在init_vq中,我們看到參數vq被用來計算進入bdev-vqs數組的偏移量。這個queue=bdev-vqs[vq]; 語句又是完全沒有任何約束檢查的,儘管在任何給定時間只有3個隊列。這意味著,只要控制了vq參數,我們就可以有效地插入一個指向虛擬機內存的邊界之外的指針。

在virtio_pci__data_out的代碼清單中,對init_vq的調用是作為vq的參數通過vpci-queue_selector進行傳遞的,在同一個清單中,我們還發現完全可以通過switch語句中的VIRTIO_PCI_QUEUE_SEL子句來控制vpci-queue_selector。

通過下面vring*vr的結構體定義,我們可以看到它有4個成員,大小為0x20,這意味著我們不能在任意位置插入這個指針。實際上,只有在偏移量0x20*x+8處,我們才能完全控制x。

image.png

大家肯定還記得,我們的exit_lists離這個bdev結構體的位置並不遠,而且,現在我們還獲得了一個未綁定的、指示插入位置的指針,所以,我們只要將vq設置為0x16,那麼,我們就能在這個exit_lists的最後一個條目中插入這個指針,具體代碼如下所示:

image.png

這裡,我們將頁幀號設置為0x1,這表示虛擬機物理地址0x1000,並再次使用/dev/mem,將物理地址0x1000映射為我們具有讀寫權限的用戶空間進程中的一個地址。顯然,以任何正常的方式重新引導或退出虛擬機操作系統,都不會調用這些退出處理程序,但幸運的是,在未定義的指令導致內核崩潰時,卻會調用這些程序。哈哈,大家還記得echo c /proc/sysrq-trigger嗎?

退出前的內存轉儲:

1.png

0x41代表字母A

這裡我們看到,所有從上面的memset插入的“A”,都出現在exit_lists+72內的地址上。很明顯,我們現在已經控制了程序流程,因為t-init(kvm)調用的這個地址完全處於我們的控制之下。既然已經得到了控制權,我們自然就可以重定向程序流程了。

現在,我們需要將代碼重定向到一個目標,以便在宿主機系統上執行代碼或命令,幸運的是,這個二進製文件含有返回函數virtio_net_exec_script的ret gadget:

1.png

超級好用的ret gadget

現在,如果我們能夠控制$rdi寄存器並跳轉到virtio_net_exec_script中用紅色箭頭標記的指令,就可以成功調用execl(command_we_control,),從而在宿主機系統上執行命令。

綜合起來現在,我們已經能夠偽造init_item,啟動調用exevl的ROP鏈,或者說是JOP鏈,接下來,我們將藉助於Jump oriented programming技術實現我們的目標。總而言之,我們現在利用第一個安全漏洞實現了指針洩露,並能控制該內存區域中一些字節的值,因為我們可以在洩漏的內區域中隨意執行寫入操作。同時,我們還找到了一種控制rip的方法,但遺憾的是,我們還無法控制函數調用t-init(kvm)中的參數kvm。

當我們第一次調用t-init時,$rbx指向我們偽造的init_item,也就是處於我們的控制之下的一段內存。

首先,我們要跳到這個gadget: mov rax, qword ptr [rbx +0x28]; mov rdi, rbx; mov rsi, qword ptr [rax + 8]; call qword ptr [rax];

這將交換rbx和rdi寄存器中的值,使我們能夠控制任何函數調用的第一個參數,並再次通過[$rbx +0x28]獲取一個新的跳轉位置。

現在,我們可以直接跳到前面介紹的那個超級棒的gadget代碼處了,因為我們現在能夠控制$rdi了。

1.png大功告成

現在,代碼將調用execl('/bin/sh', '', null);並返回一個shell!我們已經為這個漏洞申請了編號CVE-2021-45464,目前正在等待批准。至於完整的exploit,請參考原文末尾;但要注意的是,對於所有版本的lkvm來說,必須對利用代碼進行相應的修改:根據特定的二進制代碼修改gadget的偏移量。

0 Comments

Recommended Comments

There are no comments to display.

Guest
Add a comment...