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

2019-08-26 约 958 字 预计阅读 5 分钟

声明:本文 【深入分析QEMU虚拟机逃逸漏洞】 由作者 mss**** 于 2019-08-25 09:39:00 首发 先知社区 曾经 浏览数 1555 次

感谢 mss**** 的辛苦付出!

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

参考文献

原文地址

关键词:[‘安全技术’, ‘二进制安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now