CVE-2019-14378是在QEMU网络后端中发现的一个指针计算错误漏洞,当重新组装大型IPv4分段数据包以进行处理时,就会触发该漏洞。在本文中,我们将对该漏洞的本身及其利用方法进行详细的介绍。
QEMU内部网络功能分为两部分:
默认情况下,QEMU会为guest虚拟机创建SLiRP用户网络后端和适当的虚拟网络设备(例如e1000 PCI卡)
实际上,本文介绍的漏洞是在SLiRP中的数据包重组代码中发现的。
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 = 不允许分段。
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
插入该队列。/* * 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_src
和ip_dst
可用于将受控数据写入计算得到的位置。如果计算出的ip位于未映射的内存空间中,这就可能会导致qemu发生崩溃。
我们面对的情况是:
delta
,我们就能向m->m_ext处的内存空间写入受控数据。为此,我们需要精确地控制堆。让我们看看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_get、
m_free、
m_inc和
m_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_src
和fp->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
以执行任意写操作。
0xcafe
数据包的m_len,使m_data+m_len指向0xdead
数据包的m_data我们需要借助数据泄漏来绕过ASLR和PIE防御机制。为此,我们需要借助一些方法将数据传回给客户机。事实证明,有一个非常常见的服务非常适合用于完成这项任务:ICMP回应请求。我们知道,SLiRP网关会响应ICMP回应请求,以指出数据包的有效载荷(payload)没有发生变化。
我们已经找到了实现任意写操作的方法,但是具体将数据写到哪里呢?这需要通过泄漏某些重要的数据来确定。
我们可以部分覆盖m_data
并在堆上写入数据。
通过数据泄漏,我们可以:
m_data
,使其指向堆上伪造的报头定时器(更准确地说是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可以控制:
您可以在[CVE-2019-14378](https://github.com/vishnudevtj/exploits/tree/master/qemu/CVE-2019-14378)找到完整的exploit。
打赏我,让我更有动力~
© 2016 - 2024 掌控者 All Rights Reserved.