深入分析QEMU虚拟机逃逸漏洞
2019-08-25 10:39:00 Author: xz.aliyun.com(查看原文) 阅读量:175 收藏

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

参考文献

原文地址


文章来源: http://xz.aliyun.com/t/6085
如有侵权请联系:admin#unsafe.sh