最新消息:图 床

qemu-pwn 強網杯 2019 兩道 qemu 逃逸題 writeup

COOL IAM 174浏览 0评论

作者:raycp
原文鏈接:https://mp.weixin.qq.com/s/rJJYXIUWUh33G0KnvYT06w

終於到了這裡,把qwb2019的這兩題qemu逃逸題復現之後,qemu pwn的復現到這裡就告一段落,接下來將會去分析幾個qemu的cve。qwb初賽和決賽各有一道qemu逃逸題,初賽是qwct,決賽是ExecChrome

因為通過前面的幾題分析,對這類pwn題有了一定的掌握。部分分析過程可以省略,所以此次也是將兩題寫在了一起。

qwct

描述

文件目錄:

$ ll
-rwxrw-rw-  1 raycp raycp  179 Aug 26 06:01 launch.sh
drwxr-xr-x  6 raycp raycp 4.0K Sep  6  2017 pc-bios
-rwxr-xr-x  1 raycp raycp  53M May 25 18:07 QWCT_qemu-system-x86_64
-rw-rw-r--  1 raycp raycp 3.1M Aug 28 04:42 rootfs.cpio
-r-xr-xr-x  1 raycp raycp 8.2M Jun  3 23:37 vmlinuz-5.0.5-generic

launch.sh

1
2
#!/bin/bash
./qemu-system-x86_64 -initrd ./rootfs.cpio -nographic -kernel ./vmlinuz-5.0.5-generic -L pc-bios/  -append "priority=low console=ttyS0" -device qwb -monitor /dev/null

漏洞應該會在qwb設備中。

分析

解壓文件:

mkdir cpio
cd cpio
mv ../rootfs.cpio ./
cpio -idmv < rootfs.cpio

qemu-system-x86_64拖到IDA裡面,同時sudo ./launch.sh運行起來。

程序報錯:

./qemu-system-x86_64: error while loading shared libraries: libncursesw.so.6: cannot open shared object file: No such file or directory

解決方法:

sudo wget -O /tmp/libtinfo6 http://mirrors.kernel.org/ubuntu/pool/main/n/ncurses/libtinfo6_6.1+20180210-4ubuntu1_amd64.deb
sudo dpkg -i /tmp/libtinfo6
sudo rm /tmp/libtinfo6

sudo wget -O /tmp/libncursesw6 http://mirrors.kernel.org/ubuntu/pool/main/n/ncurses/libncursesw6_6.1+20180210-4ubuntu1_amd64.deb 
sudo dpkg -i /tmp/libncursesw6
sudo rm /tmp/libncursesw6

又報錯:

./qemu-system-x86_64: error while loading shared libraries: libgfapi.so.0: cannot open shared object file: No such file or directory

解決方法:

sudo wget -O /tmp/glusterfs-common http://mirrors.kernel.org/ubuntu/pool/universe/g/glusterfs/glusterfs-common_3.7.6-1ubuntu1_amd64.deb
sudo dpkg -i /tmp/glusterfs-common 
sudo rm /tmp/glusterfs-common 

sudo apt-get install liblvm2app2.2
sudo apt --fix-broken install

IDA分析結束后,搜索qwb相關函數。

qwb_class_init函數,知道了它的vendor_iddevice_id以及realizepci_qwb_realize

  k->revision = 0x10;
  k->class_id = 0xFF;
  k->realize = (void (__cdecl *)(PCIDevice_0 *, Error_0 **))pci_qwb_realize;
  k->exit = (PCIUnregisterFunc *)pci_qwb_uninit;
  k->vendor_id = 0x1234;
  k->device_id = 0x8848u;
  v2->categories[0] |= 0x80uLL;

去看pci_qwb_realize函數,看到它只註冊了一個大小為0x100000的mmio,結構體為qwb_mmio_ops,其對應的IO函數為qwb_mmio_read以及qwb_mmio_write

在分析函數前,看下它的QwbState相關結構體,後續會分析會使用得到。

00000000 crypto_status   struc ; (sizeof=0x1818, align=0x8, mappedto_4600)
00000000                                         ; XREF: QwbState/r
00000000 statu           dq ?
00000008 crypt_key       db 2048 dup(?)
00000808 input_buf       db 2048 dup(?)
00001008 output_buf      db 2048 dup(?)
00001808 encrypt_function dq ?                   ; offset
00001810 decrypt_function dq ?                   ; offset
00001818 crypto_status   ends
00001818
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 QwbState        struc ; (sizeof=0x2250, align=0x10, copyof_4601)
00000000 pdev            PCIDevice_0 ?
000008E0 mmio            MemoryRegion_0 ?
000009D0 thread          QemuThread_0 ?
000009D8 crypto_statu_mutex QemuMutex_0 ?
00000A08 crypto_buf_mutex QemuMutex_0 ?
00000A38 crypto          crypto_status ?
00002250 QwbState        ends

先看qwb_mmio_write函數,該函數的主要功能為兩個:

  • 當addr為0x1000至0x17ff時,且當opaque->crypto.statu為3時,設置opaque->crypto.crypt_key[addr-0x1000]的值為value。
  • 當addr為0x2000至0x27ff時,且當opaque->crypto.statu為1時,設置opaque->crypto.input_buf[addr-0x2000]的值為value。

可以看到qwb_mmio_write函數的主要功能就是設置input_buf以及crypto_key,且由於緩衝區空間大小都是0x800,輸入剛好可以填滿,不存在溢出。

接下來看qwb_mmio_read函數,該函數功能較複雜,包括:

  • 當addr為0時,且當opaque->crypto.statu不為5時,初始化所有的緩衝區空間,包括input_bufoutput_buf以及crypt_key
  • 當addr為1時,且當opaque->crypto.statu為2或者0時,設置statu為3。
  • 當addr為2時,且當opaque->crypto.statu為4或者0時,設置statu為1。
  • 當addr為3時,且當opaque->crypto.statu為3時,設置statu為4。
  • 當addr為4時,且當opaque->crypto.statu為1時,設置statu為2。
  • 當addr為5時,且當opaque->crypto.statu為2或者4時,設置opaque->crypto.encrypt_function的值為aes_encrypt_function函數。
  • 當addr為6時,且當opaque->crypto.statu為2或者4時,設置opaque->crypto.decrypt_function的值為aes_decrypto_function函數。
  • 當addr為7時,且當opaque->crypto.statu為2或者4時,設置opaque->crypto.encrypt_function的值為stream_encrypto_function函數。
  • 當addr為8時,且當opaque->crypto.statu為2或者4時,設置opaque->crypto.decrypt_function的值為stream_decrypto_function函數。
  • 當addr為9時,且當opaque->crypto.statu為2或者4時,且當opaque->crypto.encrypt_function的值不為空時,創建線程qwb_encrypt_processing_thread,並設置statu為5。
  • 當addr為10時,且當opaque->crypto.statu為2或者4時,且當opaque->crypto.decrypt_function的值不為空時,創建線程qwb_decrypt_processing_thread,並設置statu為7。
  • 其餘情況則可以根據addr的值讀取input_buffcrypto_key以及output_buff

qwb_encrypt_processing_thread線程以及qwb_decrypt_processing_thread,則是在線程中調用相應的opaque->crypto.encrypt_function函數以及opaque->crypto.decrypt_function去實現加解密。

stream相關的加解密函數則是實現了一個簡單的異或,而aes相關的加解密函數則是對輸入進行aes加解密,並在最後附上了一個校驗值。

所以整個設備的功能主要是實現了一個加解密功能,算法可以選擇是流算法或aes算法,主要基於crypto_status結構體來記錄關鍵數據。

經過分析該設備中存在兩個漏洞,一個是越界讀,一個是越界寫。

越界讀是在qwb_mmio_read函數中,其對於output_buff讀取的判斷條件為:只要小於strlen(output_buff),就可以讀取相應數據。乍一看沒有問題,可是當加解密的數據長度剛好填滿了output_buff即長度為0x800時,調用strlen(output_buff)時會導致獲得的長度大於0x800,因為拼接上了後面的encrypt_function指針的數據。使得越界讀到encrypt_function指針的數據,實現程序地址的泄露。

越界寫在存在於aes_decrypto_function以及aes_encrypto_function函數中,兩個函數都在對輸入數據進行aes加密后,在output_buff的末尾拼接了一個8字節的校驗值,該校驗值導致越界寫,關鍵代碼如下:

len = strlen((const char *)input);
...
    *(_QWORD *)crc = 0LL;
    v19 = 0;
    c = 0;
    for ( i = 0LL; ; c = crc[i & 7] )
    {
      c ^= output[i];
      idx = i++;
      crc[idx & 7] = c;
      if ( len == i )
        break;
    }
  }
  else
  {
    *(_QWORD *)crc = 0LL;
  }
  *(_QWORD *)&output[len] = *(_QWORD *)crc;

如果len長度剛好為0x800,則會導致最後的校驗值寫入到output_buff[0x800]處,導致越界覆蓋了encrypt_function指針。

利用

如何利用上述的兩個漏洞拿到shell呢,大致也是分為四步。

第一步將input_buff以及cyrpto_key填滿,然後調用stream_encypt_functionoutput_buff填滿,再利用越界讀,讀出stream_encypt_function函數的地址,根據偏移計算出system plt的地址。

第二步構造能夠得到system plt校驗值的input_buff,因為是異或得到的校驗值,所以比較容易構造。然後將輸入以及key填進去,調用aes_encypt_function函數加密,將output_buff讀出來保存。

第三步是將上一步保存的output_buff數據輸入到input_buff中,再使用相同的key調用aes_decypt_function函數進行解密,這樣解密出來的數據的校驗值就剛好會是system plt,且會覆蓋至encrypt_function指針。

第四步是將參數賦值到input_buff中,最後調用encrypt_function,實現system函數的調用,拿到flag。

ExecChrome

qwb 2019 final的題,主辦方給了一個虛擬機,虛擬機的用戶名是qwb,密碼是123456。進去以後sudo ./launch.sh啟動虛擬機,qemu虛擬機用戶名是ubuntu,密碼是123456launch.sh內容如下:

1
2
3
4
#!/bin/bash
while true
    do ./qemu-system-x86_64 -m 1024 -smp 2 -boot c -cpu host -hda ubuntu_server.qcow2 --enable-kvm -drive file=./blknvme,if=none,id=D22 -device nvme,drive=D22,serial=1234 -net user,hostfwd=tcp::2222-:22 -net nic && sleep 5
done

分析

根據參數-device nvme,可以推斷應該主要是這個設備的問題,搜相關函數,看到有很多的函數。經過一番搜索以後發現是根據已有的設備改的代碼,目錄是hw/block/nvme.c

經過對比,發現主要是在nvme_mmio_read以及nvme_mmio_write裡面修改了部分代碼,研究相應代碼。

先看nvme_mmio_read,原來的代碼是:

if (addr < sizeof(n->bar)) {
        memcpy(&val, ptr + addr, size);
    }

修改後的代碼是:

memcpy(&val, &ptr[addr], size);

可以看到少了對於size的檢查,可能會存在越界讀。

再看nvme_mmio_write中,該函數調用了nvme_write_bar函數。經過對比,題目對nvme_write_bar函數中添加了部分代碼,添加的代碼的內容為:

default:
      ...
      if ( size == 2 )
      {
        *(_WORD *)((char *)&n->bar.cap + offset) = data;
      }
      else if ( size > 2 )
      {
        if ( size == 4 )
        {
          *(_DWORD *)((char *)&n->bar.cap + offset) = data;
        }
        else if ( size == 8 )
        {
          *(uint64_t *)((char *)&n->bar.cap + offset) = data;
        }
      }
      else if ( size == 1 )
      {
        *((_BYTE *)&n->bar.cap + offset) = data;
      }
      break;
  }

可以看到似乎也存在越界寫功能。

再去虛擬機中看mmio空間的大小:

lspci -vv -s 00:04.0
00:04.0 Non-Volatile memory controller: Intel Corporation QEMU NVM Express Controller (rev 02) (prog-if 02 [NVM Express])
    Subsystem: Red Hat, Inc. QEMU Virtual Machine
    Physical Slot: 4
    Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR+ FastB2B- DisINTx+
    Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
    Latency: 0
    Interrupt: pin A routed to IRQ 10
    Region 0: Memory at febf0000 (64-bit, non-prefetchable) [size=8K]
    Region 4: Memory at febf3000 (32-bit, non-prefetchable) [size=4K]

可以看到mmio大小為8k,而NvmeCtrl->bar大小卻只有0x40,結合上面的分析,確定該設備存在越界讀寫漏洞。

NvmeCtrl        struc ; (sizeof=0x1C50, align=0x10, copyof_4151)
00000000 parent_obj      PCIDevice_0 ?
000008E0 iomem           MemoryRegion_0 ?
000009D0 ctrl_mem        MemoryRegion_0 ?
00000AC0 bar             NvmeBar_0 ?
00000B00 conf            BlockConf_0 ?
00000B38 page_size       dd ?
00000B3C page_bits       dw ?
00000B3E max_prp_ents    dw ?
00000B40 cqe_size        dw ?
00000B42 sqe_size        dw ?
00000B44 reg_size        dd ?
00000B48 num_namespaces  dd ?
00000B4C num_queues      dd ?
00000B50 max_q_ents      dd ?
00000B54                 db ? ; undefined
00000B55                 db ? ; undefined
00000B56                 db ? ; undefined
00000B57                 db ? ; undefined
00000B58 ns_size         dq ?
00000B60 cmb_size_mb     dd ?
00000B64 cmbsz           dd ?
00000B68 cmbloc          dd ?
00000B6C                 db ? ; undefined
00000B6D                 db ? ; undefined
00000B6E                 db ? ; undefined
00000B6F                 db ? ; undefined
00000B70 cmbuf           dq ?                    ; offset
00000B78 irq_status      dq ?
00000B80 serial          dq ?                    ; offset
00000B88 namespaces      dq ?                    ; offset
00000B90 sq              dq ?                    ; offset
00000B98 cq              dq ?                    ; offset
00000BA0 admin_sq        NvmeSQueue_0 ?
00000C00 admin_cq        NvmeCQueue_0 ?
00000C50 id_ctrl         NvmeIdCtrl_0 ?
00001C50 NvmeCtrl        ends

利用

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

  1. 利用越界讀,泄露程序基址與堆地址。
  2. 利用越界寫覆蓋qemu timer控制程序執行流

因為程序開啟了PIE,所以第一步需要先泄露地址。首先是得到system地址,在與bar地址偏移0x1ff0的地方找到了存在程序地址的地方,利用mmio_read越界讀出來,然後根據偏移計算出system地址。其次是得到NvmeCtrl->bar地址的空間以實現可以拿到最終傳參的地址,在與bar地址偏移0x1f98的地方找到了存在堆地址的地方,根據偏移可以計算出NvmeCtrl->bar地址。

關鍵的是如何控制程序執行流,主要原理是利用了NvmeCtrl結構體中的admin_sqadmin_sq中存在一個timer結構體,可以利用它來控制程序執行流。

00000000 NvmeSQueue_0    struc ; (sizeof=0x60, align=0x8, copyof_4154)
00000000                                         ; XREF: NvmeCtrl_0/r
00000000                                         ; NvmeCtrl/r
00000000 ctrl            dq ?                    ; offset
00000008 sqid            dw ?
0000000A cqid            dw ?
0000000C head            dd ?
00000010 tail            dd ?
00000014 size            dd ?
00000018 dma_addr        dq ?
00000020 timer           dq ?                    ; offset
00000028 io_req          dq ?                    ; offset
00000030 req_list        $FE468C6164B384978313660BA47FFEDA ?
00000040 out_req_list    $FE468C6164B384978313660BA47FFEDA ?
00000050 entry           $53C797D9CC370671B1F6BB504B4B2727 ?
00000060 NvmeSQueue_0    ends
00000000 ; ---------------------------------------------------------------------------
00000000 QEMUTimer       struc ; (sizeof=0x30, align=0x8, copyof_729)
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
00000030

主要有兩種方式:

一種是偽造timer,利用虛擬機重啟或關機時會觸發時鐘timer,調用cb(opaque)控制程序執行流的方法,關鍵代碼如下所示:

void main_loop_wait(int nonblocking)
{
    ...

    /* CPU thread can infinitely wait for event after
       missing the warp */
    qemu_start_warp_timer();
    qemu_clock_run_all_timers();
}

bool timerlist_run_timers(QEMUTimerList *timer_list)
{
    ...
        timer_list->active_timers = ts->next;
        ts->next = NULL;
        ts->expire_time = -1;
        cb = ts->cb;
        opaque = ts->opaque;

        /* run the callback (the timer list can be modified) */
        qemu_mutex_unlock(&timer_list->active_timers_lock);
        cb(opaque);   // we can hajack the control flow here
        qemu_mutex_lock(&timer_list->active_timers_lock);

        progress = true;
    }
    ...
    return progress;
}

可以在堆中偽造好timer結構體,其cb為system地址,opaque為參數的地址。利用越界將admin_sq中的timer指針覆蓋成該偽造的結構體,當reboot時就可以成功控制程序的執行流。一個關鍵的點是timer結構體中的timer_list指針需要正確,因為之前泄露了堆地址,因此可以通過偏移計算得到原來的timer_list結構體的值,將它覆蓋成原來的就好。但是由於結構體都是堆地址,會導致和泄漏的地址的偏移可能不固定。但是它的地址和堆基址的偏移時一致的,因為我們可以通過計算堆基址來得到timer_list的地址,具體可以去看exp中的內容。

另一種方式則是在nvme_mmio_write中存在一條調用鏈:nvme_mmio_write->nvme_process_db->timer_mod->timer_mod_ns->timerlist_rearm->timerlist_notify->(timer_list->notify_cb)(timer_list->notify_opaque,timer_list->clock->type),也可以成功控制程序執行流。

我的exp中使用的是第一種利用方式。

小結

qemu ctf pwn題分析到這就暫告一段落,接下來會分析一些qemu cve來進一步了解相關漏洞。

相關腳本以及文件鏈接


转载请注明:IAMCOOL » qemu-pwn 強網杯 2019 兩道 qemu 逃逸題 writeup

0 0 vote
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x