深入分析QEMU虚拟机逃逸漏洞

isnull   ·   发表于 2019-08-25 11:37:46   ·   漏洞文章

CVE-2019-14378是在QEMU网络后端中发现的一个指针计算错误漏洞,当重新组装大型IPv4分段数据包以进行处理时,就会触发该漏洞。在本文中,我们将对该漏洞的本身及其利用方法进行详细的介绍。

漏洞详情

QEMU内部网络功能分为两部分:

  • 提供给客户机的虚拟网络设备(例如PCI网卡)。
  • 与模拟NIC交互的网络后端(例如,将数据包推送至宿主机的网络)。
  • 默认情况下,QEMU会为guest虚拟机创建SLiRP用户网络后端和适当的虚拟网络设备(例如e1000 PCI卡)

    实际上,本文介绍的漏洞是在SLiRP中的数据包重组代码中发现的。

    IP分段

    IP协议在传输数据包时,将数据包分为若干分段进行传输,并在目标系统中进行重组,这一过程称为IP分段(Fragmentation)。这么做的好处,就是分段后的数据包可以顺利通过最大传输单元(MTU)小于原始数据包大小的链路。

    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    |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    分段标志位 :

    标志位的长度为3 bit

  • Bit 0: 保留未用,必须为零

  • Bit 1: (DF) 0 = 允许进行分段,1 = 不允许分段。

  • Bit 2: (MF) 0 = Last Fragment, 1 = More Fragments.
  • Fragment Offset: 13 bit
  • struct mbuf {
        /* 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;
    
        char *m_data; /* Current location of data */
        int m_len; /* Amount of data in this mbuf, from m_data */
    
        ...
    
        char *m_ext;
        /* start of dynamic buffer area, must be last element */
        char m_dat[];
    };

    `mbuf结构用于存储接收到的IP层信息。该结构含有两个缓冲区,其中m_dat缓冲区位于结构内部,如果m_dat无法完整保存数据包,则在堆上分配m_ext缓冲区。

    进行NAT转换时,如果传入的数据包是分段的,那么,在编辑和重新传输之前首先需要进行重组。这个重组过程是由ip_reass(Slirp *slirp, struct ip *ip, struct ipq *fp) 函数完成的。其中,ip用于存放当前IP数据包的数据,fp一个存放分段数据包的链接列表。

  • ip_reass 将执行下列步骤:
  • 如果第一个分段到达(fp == NULL),则创建一个重组队列并将ip插入该队列。
  • 检查该分段是否与先前收到的分段重复,如果重复的话,则将其丢弃。
  • 如果收到了所有分段数据包,则对其进行重组。然后,为生成的新IP数据包创建一个新的头部,方法是修改第一个数据包的头部;
  • /*
     * Take incoming datagram fragment and try to
     * reassemble it into whole datagram.  If a chain for
     * reassembly of this datagram already exists, then it
     * is given as fp; otherwise have to make a chain.
     */
    static struct ip *ip_reass(Slirp *slirp, struct ip *ip, struct ipq *fp)
    {
        ...
        ...
    
        /*
         * 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);
        }
        /*
         * Create header for new ip packet by
         * modifying header of first packet;
         * dequeue and discard fragment reassembly header.
         * Make header visible.
         */
        q = fp->frag_link.next;
    
        /*
         * If the fragments concatenated to an mbuf that's
         * bigger than the total size of the fragment, then and
         * m_ext buffer was alloced. But fp->ipq_next points to
         * the old buffer (in the mbuf), so we must point ip
         * into the new buffer.
         */
        if (m->m_flags & M_EXT) {
            int delta = (char *)q - m->m_dat;
            q = (struct ipasfrag *)(m->m_ext + delta);
        }

    本文介绍的漏洞位于计算变量delta的代码中。这些代码假定第一个分段数据包不会被分配到外部缓冲区(m_ext)中。当数据包数据位于mbuf->m_dat 中时,q - m->dat计算是正确的(因为q位于m_dat缓冲区内;q是一个含有分段链接列表和数据包数据的结构)。否则,如果分配了m_ext缓冲区,那么q将被存放到外部缓冲区中,因此关于delta的计算将是错误的。

    slirp/src/ip_input.c:ip_reass
        ip = fragtoip(q);
        ip->ip_len = next;
        ip->ip_tos &= ~1;
        ip->ip_src = fp->ipq_src;
        ip->ip_dst = fp->ipq_dst;

    后来新计算的指针q被转换成ip结构并修改了其值。由于delta的计算是错误的,所以,ip将指向不正确的位置,而且ip_srcip_dst可用于将受控数据写入计算得到的位置。如果计算出的ip位于未映射的内存空间中,这就可能会导致qemu发生崩溃。

    漏洞利用

    我们面对的情况是:

  • 如果我们能够控制delta,我们就能向m->m_ext处的内存空间写入受控数据。为此,我们需要精确地控制堆。
  • 需要泄漏某些东东,以绕过ASLR保护机制。
  • 堆上没有可用于实现代码执行的函数指针。因此,我们必须获取任意写操作权限。
  • 控制堆

    让我们看看slirp是如何分配堆对象的。

    // How much room is in the mbuf, from m_data to the end of the mbuf
    #define M_ROOM(m)\
        ((m->m_flags & M_EXT) ? (((m)->m_ext + (m)->m_size) - (m)->m_data) :\
                                (((m)->m_dat + (m)->m_size) - (m)->m_data))
    // How much free room there is
    #define M_FREEROOM(m) (M_ROOM(m) - (m)->m_len)
    
    slirp/src/slirp.c:slirp_input
    
          m = m_get(slirp); // m_get return mbuf object, internally calls g_malloc(0x668)
          ...
          /* Note: we add 2 to align the IP header on 4 bytes,
           * and add the margin for the tcpiphdr overhead  */
          if (M_FREEROOM(m) < pkt_len + TCPIPHDR_DELTA + 2) { // TCPIPHDR_DELTA + 2 =
              m_inc(m, pkt_len + TCPIPHDR_DELTA + 2); // allocates new m_ext buffer since m_dat is insufficiant
          }
          ...
    
          if (proto == ETH_P_IP) {
              ip_input(m);

    其中,m_getm_freem_incm_cat`是用于处理动态内存分配的包装器。当新的数据包到达时,将分配新的mbuf对象,并且,如果m_dat的空间足以存储数据包数据,则使用它;否则的话,则使用“m_inc”分配新的外部缓冲区,并将数据复制到该缓冲区中。

    slirp/src/ip_input.c:ip_input
        /*
            * 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)
                return;
    
    slirp/src/ip_input.c:ip_reass
        /*
         * If first fragment to arrive, create a reassembly queue.
         */
        if (fp == NULL) {
            struct mbuf *t = m_get(slirp);
            ...

    如果传入的数据包被分段,则使用新的mbuf对象来存储数据包(fp),直到所有片段都到达为止。当下一部分到达时,它们将被放入该列表,以进行排队。

    这为我们提供了一个很好的原语,借助它,我们可以根据堆大小( > 0x608 )来分配受控的内存块。要记住的几件事情是,对于每个数据包,都会为其分配mbuf(0x670)缓冲区,如果它是第一个片段,还将分配另一个mbuf(fp:分段队列)。

    malloc(0x670)
    if(pkt_len + TCPIPHDR_DELTA + 2 > 0x608)
       malloc(pkt_len + TCPIPHDR_DELTA + 2)
    if(ip->ip_off & IP_MF)
       malloc(0x670)

    我们可以使用它执行堆喷射操作,这样后面的内存分配都将在顶部内存块中进行,这就给我们提供了一个可预测的堆状态。

    实现堆的受控写操作

    这样,我们就可以控制堆了。让我们看看如何使用这个漏洞来覆盖某些有用的东西。

    q = fp->frag_link.next; // Points to first fragment
        if (m->m_flags & M_EXT) {
            int delta = (char *)q - m->m_dat;
            q = (struct ipasfrag *)(m->m_ext + delta);
        }

    假设堆的布局如下所示:

    +------------+
                |     q      |
                +------------+
                |            |
                |            |
                |  padding   |
                |            |
                |            |
                +------------+
                |   m->m_dat |
                +------------+

    现在,delta将会-padding,然后与m->m_ext相加,这样我们就可以向该偏移量处执行写操作了。因此,只要能够控制这个padding,我们就能够控制delta。

    当所有片段都到达时,它们会通过m_cat函数连接成一个mbuf对象。

    slirp/src/muf.c
    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);
    }
    slirp/src/muf.c
    void m_inc(struct mbuf *m, int size)
    {
        ...
        if (m->m_flags & M_EXT) {
            gapsize = m->m_data - m->m_ext;
            m->m_ext = g_realloc(m->m_ext, size + gapsize);
        ...
    }

    函数m_inc会调用realloc函数,而realloc函数将返回相同的内存块,如果它可以容纳所请求的内存大小的话。因此,即使在重组数据包之后,我们也可以访问第一个数据包的m->m_ext缓冲区。注意,m_ext缓冲区将被分配给第一个分段数据包,而q将指向该缓冲区。并且,-padding也将相对于q而言的。这只是为了让事情变得更轻松。

    +------------+
                |  target    |
                +------------+
                |            |
                |            |
                |  padding   |
                |            |
                |            |
    m-m_ext  -> +------------+  // q = m->m_ext + -padding  will point to target
                |     q      |  // delta = -paddig
                +------------+
                |            |
                |            |
                |  padding   |
                |            |
                |            |
                +------------+
                |   m->m_dat |
                +------------+

    因此,在完成指针运算后,q将指向target

    slirp/src/ip_input.c:ip_reass
        ip = fragtoip(q);
        ...
        ip->ip_src = fp->ipq_src;
        ip->ip_dst = fp->ipq_dst;

    由于我们可以控制fp->ipq_srcfp->ipq_dst了(即数据包的源和目标ip),所以,我们自然可以覆盖目标内容了。

    任意写操作

    我的初始目标是覆盖m_data字段,这样我们就可以使用完成数据包重组的m_cat()函数来执行任意写操作了。不过,由于某些对齐和偏移问题,这似乎是难以完成的。

    slirp/src/muf.c:m_cat
        memcpy(m->m_data + m->m_len, n->m_data, n->m_len);

    不过,我们却能够覆盖对象的m_len字段。由于没有对m_cat函数进行相应的检查,所以,我们可以使用m_len来执行相对于m_data的任意写操作。这样的话,我们就可以无视对齐的问题了——我们可以这种方法来覆盖不同对象的m_data以执行任意写操作。

  • 发送一个id为“0xdead”且MF位为1的数据包
  • 发送一个id为“0xcafe”且且MF位为1的数据包
  • 触发漏洞,从而覆盖0xcafe数据包的m_len,使m_data+m_len指向0xdead数据包的m_data
  • 发送一个id为“0xcafe”且MF位为0的数据包,以触发重组过程并用目标地址覆盖“0xdead”数据包的m_data
  • 发送一个id为“0xdead”且MF位为0的数据包,该数据包会将其内容写入m_data。
  • 实现数据泄露

    我们需要借助数据泄漏来绕过ASLR和PIE防御机制。为此,我们需要借助一些方法将数据传回给客户机。事实证明,有一个非常常见的服务非常适合用于完成这项任务:ICMP回应请求。我们知道,SLiRP网关会响应ICMP回应请求,以指出数据包的有效载荷(payload)没有发生变化。

    我们已经找到了实现任意写操作的方法,但是具体将数据写到哪里呢?这需要通过泄漏某些重要的数据来确定。

    我们可以部分覆盖m_data并在堆上写入数据。

    通过数据泄漏,我们可以:

  • 通过任意写操作在堆上创建伪ICMP报头
  • 发送设置了MF位的ICMP请求。
  • 部分覆盖m_data,使其指向堆上伪造的报头
  • 通过发送MF位为0的数据包来结束ICMP请求。
  • 接收从宿主机泄漏的重要数据。
  • 实现代码执行

    定时器(更准确地说是QEMUTimers)为我们提供了一种在经过一段时间间隔后调用给定例程(回调函数)的方法,为此,只需传递一个指向该例程的不透明指针即可。

    struct QEMUTimer {
        int64_t expire_time;        /* in nanoseconds */
        QEMUTimerList *timer_list;
        QEMUTimerCB *cb;
        void *opaque;
        QEMUTimer *next;
        int scale;
    };
    
    struct QEMUTimerList {
        QEMUClock *clock;
        QemuMutex active_timers_lock;
        QEMUTimer *active_timers;
        QLIST_ENTRY(QEMUTimerList) list;
        QEMUTimerListNotifyCB *notify_cb;
        void *notify_opaque;
        QemuEvent timers_done_ev;
    };

    main_loop_tlg是bss中的一个数组,其中包含与不同定时器关联的QEMUTimerList。它们实际上就是存放QEMUTimer结构的列表。qemu会循环遍历这些定时器,以检查是否有到期的,如果有的话,则使用参数opaque来调用cb函数。

    RIP可以控制:

  • 创建伪造的QEMUTimer,赋予回调函数system权限,以opaque为其参数
  • 创建伪造的QEMUTImerList,其中包含我们伪造的QEMUTimer
  • 使用伪造的QEMUTimerList覆盖main_loop_tlg的元素
  • 您可以在[CVE-2019-14378](https://github.com/vishnudevtj/exploits/tree/master/qemu/CVE-2019-14378)找到完整的exploit。

    演示视频:https://blog.bi0s.in/2019/08/20/Pwn/VM-Escape/2019-07-29-qemu-vm-escape-cve-2019-14378/cve_2019_14378.mp4

    参考文献

  • QEMU Networking

  • IP Fragmentation

  • Internet Header Format

  • Virtunoid: A KVM Guest - Host privilege

  • CVE-2019-6778


  • 转自先知社区

    打赏我,让我更有动力~

    0 条回复   |  直到 2019-8-25 | 1219 次浏览
    登录后才可发表内容
    返回顶部 投诉反馈

    © 2016 - 2024 掌控者 All Rights Reserved.