Jump to content
  • Entries

    16114
  • Comments

    7952
  • Views

    86375189

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.

在這個挑戰中,我們被賦予了運行在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的偏移量。