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。