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

Posted 看雪学院

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内核分析 | CVE-2017-1000112(UDP Fragment Offload)相关的知识,希望对你有一定的参考价值。

本文为看雪论坛优秀文章

看雪论坛作者ID:ScUpax0s




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

CVE 基础

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


在分析 CVE-2017-1000112 之前首先应该明白如下概念。


TCP/IP

 
从上到下分别是:应用层message -> 传输层segment(UDP) -> 网络层datagram(IP) -> 链路层frame。
 
封装层级:{ 数据帧frame{ IP包{ UDP包{ message/data } } } }

UFO机制


UFO 是 UDP Fragment Offload 的简称。
 
我们知道发送ipv4数据包的过程中,一个链路层帧所能承载的最大数据量为做最大传送单元(MTU)。当要求发送的IP数据报比数据链路层的MTU大时,必把该数据报分割成多个IP数据报才能发送(即ipv4的分片,可能发生在ip层或者传输层)。
 
而 UFO机制 则是通过网卡的配合辅助进行ipv4报文分片。将分片的过程从协议栈中移到网卡硬件中,从而提升效率,减少堆栈开销。
 
在 linuxfoundation 中给出了如下描述:
 
IPv4/IPv6: UFO (UDP Fragmentation Offload) Scatter-gather approach: UFO is a feature wherein the Linux kernel network stack will offload the IP fragmentation functionality of large UDP datagram to hardware. This will reduce the overhead of stack in fragmenting the large UDP datagram to MTU sized packets
 
UFO的commit在这里: UFO commit in linux kernel ,2005

UDP corking机制


cork翻译为软木塞。
 
cork机制是一种优化机制。他就像一个塞子一样,使得数据先不发出去,等到拔去塞子后再发出去。以防止不停的去封装发送碎片化的小数据包,使得利用率降低。开启corkin后,内核会尽力把小数据包拼接成一个大的数据包(一个MTU)再发送出去,当然若一定时间后(一般为200ms),内核仍然没有组合成一个MTU时也必须发送现有的数据。
 
这么做有利有弊,好处是缓解了碎片化,提高了利用效率。但是也损失了一些实时性。
 
具体的话代码如下:
 
在函数 int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len) 
Linux内核分析 | CVE-2017-1000112(UDP Fragment Offload)
 
在 do_append_data: 做数据追加。
 
Linux内核分析 | CVE-2017-1000112(UDP Fragment Offload)
 
如果追加数据失败,则调用 udp_flush_pending_frame 丢弃数据。底层调用 __ip_flush_pending_frames

4.4.0内核下 ip_append_data 源码分析


基本流程


对于UDP,分片工作主要在 ip_append_data(ip_apepend_page) 中完成。实际上这是比较复杂的一个函数,我们只挑关键的说。
 
他的作用主要是将上层下来的数据进行整形,如果是大数据包进行切割,变成多个小于或等于MTU的SKB。如果是小数据包,并且开启了聚合,就会将若干个数据包整合。
 
并且给出一张网上的流程图:
 
Linux内核分析 | CVE-2017-1000112(UDP Fragment Offload)
 
源码位置:
https://elixir.bootlin.com/linux/v4.4/source/net/ipv4/ip_output.c#L864
 
我们顺着流程图来分析。

ip_append_data的准备工作


首先是这一部分:
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) //是否开启MSG_PROBE return 0;  if (skb_queue_empty(&sk->sk_write_queue)) { err = ip_setup_cork(sk, &inet->cork.base, ipc, rtp); if (err) return err; } else { /*队列不为空,则使用上次的路由,IP选项,以及分片长度 */ transhdrlen = 0; }  return __ip_append_data(sk, fl4, &sk->sk_write_queue, &inet->cork.base, sk_page_frag(sk), getfrag, from, length, transhdrlen, flags);}

getfrag(void *from, char *to, int offset,int len, int odd, struct sk_buff *skb) 函数主要是做了将数据赋值到 skb中。而一个skb就是一个sk_buff 结构体的指针。struct sk_buff - socket buffer 就是一个sock的缓冲区。

参数 int transhdrlen 是一个代表着传输层header的长度,同时也是标志是否为第一个fragment的标志。

参数 unsigned int flags 则是一个标志。在本函数中主要用到了 MSG_PROBE(进行MTU路径探测,而不真正进行数据发送)、MSG_MORE(代表后续还有数据被发送)

在 ip_append_data 中首先判断flags是否开启了 MSG_PROBE 选项,如果开启了,那么就直接返回0。
 
接下来判断sk_buff队列是否为空,如果是空的话通过 ip_setup_cork 初始化 cork 变量。
 
如果不空那么设置 transhdrlen = 0 说明不是第一个fragment。
 
接下来调用:__ip_append_data 也是主要的处理流程。

__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, ){ struct inet_sock *inet = inet_sk(sk); struct sk_buff *skb; //分配一个新的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); //获取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); //获取链路层header长度  fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0); //IP头部长度  maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen; //最大IP头部长度,考虑了对齐 maxnonfragsize = ip_sk_ignore_df(sk) ? 0xFFFF : mtu; //是否超过64k?最大为64k。  if (cork->length + length > maxnonfragsize - fragheaderlen) { ip_local_error(sk, EMSGSIZE, fl4->daddr, inet->inet_dport, mtu - (opt ? opt->optlen : 0)); return -EMSGSIZE; }  /* * transhdrlen > 0 means that this is the first fragment and we wish * it won't be fragmented in the future. transhdrlen!=0说明ip_append_data工作在第一个片段 transhdrlen =0说明ip_append_data没有工作在第一个片段 */ if (transhdrlen && length + fragheaderlen <= mtu && rt->dst.dev->features & NETIF_F_V4_CSUM && !(flags & MSG_MORE) && !exthdrlen) csummode = CHECKSUM_PARTIAL; //校验和计算?  cork->length += length; //软木塞长度更新 if (((length > mtu) || (skb && skb_is_gso(skb))) && (sk->sk_protocol == IPPROTO_UDP) && (rt->dst.dev->features & NETIF_F_UFO) && !rt->dst.header_len && (sk->sk_type == SOCK_DGRAM)) { /* 对于一个UDP包如果满足: 1.要发送的数据长度大于mtu,需要进行分片 2.开启了UFO支持 */ err = ip_ufo_append_data(sk, queue, getfrag, from, length, hh_len, fragheaderlen, transhdrlen, maxfraglen, flags); //调用支持UFO机制的ip_ufo_append_data 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. */  //如果skb为空,即sk_buff队列此时为空,那么跳转到alloc_new_skb。 if (!skb) goto alloc_new_skb;


接下来涉及到一个非常关键的 copy 变量。

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

copy 代表的是最后一个skb的剩余空间。

 
skb->len 表示此 SKB 管理的 Data Buffer 中数据的总长度
 
他有如下几种情况:
copy < 0:即mtu < skb->len 溢出了。有些数据需要从当前的IP分片中移动到新的片段中
copy > 0:最后一个skb还有空间。
copy = 0:最后一个skb被填满。

copy<0

此时我们需要分配新的skb来存储溢出的数据。
 
由于这里代码较长,只分析关键部分。
 
这里有很多个不同的长度,图示如下:
Linux内核分析 | CVE-2017-1000112(UDP Fragment Offload)
 
首先计算我们究竟要分配多大的空间:
if (copy <= 0) { 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; if (skb_prev)//当skb存在时,需要计算究竟要从上一个skb中拿多长的数据到下一个新的skb fraggap = skb_prev->len - maxfraglen; //这里其实就是负的copy else fraggap = 0;  /* * If remaining data exceeds the mtu, * we know we need more fragment(s). */ datalen = length + fraggap; if (datalen > mtu - fragheaderlen) datalen = maxfraglen - fragheaderlen; fraglen = datalen + fragheaderlen;  if ((flags & MSG_MORE) && !(rt->dst.dev->features&NETIF_F_SG)) alloclen = mtu; //最大分配大小 else alloclen = fraglen; //确切分配大小,fraglen = datalen + fragheaderlen  alloclen += exthdrlen;  /* 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;

当我们初步确定了需要分配的新的skb的大小 alloclen 后:
if (transhdrlen) {  //如果是第一个分片 //调用sock_alloc_send_skb分配新的skb skb = sock_alloc_send_skb(sk, alloclen + hh_len + 15, (flags & MSG_DONTWAIT), &err); } else { //如果不是第一个分片 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) goto error; //分配失败跳转到error

当分配成功后,首先初始化skb中的一些控制数据
/* * Fill in the control structures */ skb->ip_summed = csummode; skb->csum = 0; skb_reserve(skb, hh_len);

接下来初始化时间戳
/* 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;  if (fraggap) { //填充原来skb的尾部 skb->csum = skb_copy_and_csum_bits( skb_prev, maxfraglen, data + transhdrlen, fraggap, 0); // skb_copy_and_csum_bits函数将数据从第一个创建的sk_buff复制到新分配的sk_buff skb_prev->csum = csum_sub(skb_prev->csum, skb->csum); data += fraggap; pskb_trim_unique(skb_prev, maxfraglen); }  copy = datalen - transhdrlen - fraggap; 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; exthdrlen = 0; csummode = CHECKSUM_NONE;  /* * Put the packet on the pending queue. */ __skb_queue_tail(queue, skb); //把新skb插入skb队列队尾 continue; }

在这里可能产生两种情况:
 
(1)分配的新skb->len刚好是MTU
Linux内核分析 | CVE-2017-1000112(UDP Fragment Offload)
(2)分配的新skb->len刚好是确切的大小(小于mtu)  
Linux内核分析 | CVE-2017-1000112(UDP Fragment Offload)
 
在这里我们只讨论最复杂的这种情况(并且也是跟我们漏洞最相关的这种情况),其余的更加细节的可以看:
 
https://blog.csdn.net/minghe_uestc/article/details/7836920?utm_source=blogxgwz2



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

漏洞分析与利用

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


poc/exp


来自国外某带师傅
Linux内核分析 | CVE-2017-1000112(UDP Fragment Offload)


poc/exp分析


最主要的其实就是 两次send
#define SHINFO_OFFSET 3164 int size = SHINFO_OFFSET + sizeof(struct skb_shared_info);int rv = send(s, buffer, size, MSG_MORE);int val = 1;rv = setsockopt(s, SOL_SOCKET, SO_NO_CHECK, &val, sizeof(val));send(s, buffer, 1, 0); //第二次send的size为1close(s);

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将其转换成一个转换成网络数据格式的数字。

当我们建立好socket并初始化之后,第一次send,带上标记为MSG_MORE告诉系统我们接下来还有数据要发送。此时走UFO路径
 
如果要发送的是UDP数据包,且系统支持UFO,并且需要分片(length > mtu),那么 send() 最终会进入:ip_ufo_append_data
 
Linux内核分析 | CVE-2017-1000112(UDP Fragment Offload)
 
源码如下:
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); //取skb队列的队尾  if (!skb) { skb = sock_alloc_send_skb(sk, 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;  __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; append: return skb_append_datato_frags(sk, skb, getfrag, from, (length - transhdrlen));}

调用 sock_alloc_send_skb 分配一个新的skb,然后把数据放到新的skb的非线性区域中。(skb_share_info)
Linux内核分析 | CVE-2017-1000112(UDP Fragment Offload)
其结构体定义如下:
struct skb_shared_info { unsigned char nr_frags; __u8 tx_flags; unsigned short gso_size; /* Warning: this field is not always filled in (UFO)! */ unsigned short gso_segs; unsigned short gso_type; struct sk_buff *frag_list; struct skb_shared_hwtstamps hwtstamps; 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];};

最后新的skb入队。
 
注意在本条UFO路径中我们skb中数据的大小是大于mtu的!
 
更细节的:
Linux内核分析 | CVE-2017-1000112(UDP Fragment Offload)
通过  skb_shinfo(SKB)  宏也可以看出来skb_shared_info与skb之间的关系。

在第二次send之前,我们调用 setsockopt 来设置了 SO_NO_CHECK 标志,即不校验checksum。(内核是通过SO_NO_CHECK的标志来判断用UFO机制还是non-UFO机制,这一点在刚刚的源码中并不明显。请直接看下面漏洞补丁那里的patch)
 
#define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB)))

造成接下来 第二次send的时候越过UFO路径而执行non-UFO的代码。
 
在non-UFO中,此时计算的 copy = mtu - skb_len 小于0,此时的skb是直接从skb队尾取出来的,也就是第一次send时new alloc出来的len > mtu的skb。
 
由于copy < 0,那么在 non-UFO 路径上触发了重新分配skb的操作。
 
而在重新分配结束后调用了:
copy = mtu - skb->len; if(copy<0){char *data;unsigned int datalen;unsigned int fraglen;unsigned int fraggap;unsigned int alloclen; struct sk_buff *skb_prev = skb; fraggap = skb_prev->len - maxfraglen;//这里其实就是负的copy datalen = length + fraggap;  skb_copy_and_csum_bits(skb_prev, maxfraglen,data + transhdrlen, fraggap, 0); }

其中的 skb_copy_and_csum_bits 将旧的 skb_prev 中的数据(UFO路径中的skb)复制到新分配的sk_buff中(即skb_shared_info->frags[]中的page_frag),从而造成溢出。
 
而对于 skb_shared_info 存在一个成员 void * destructor_arg 他是skb释放时的在 kfree_skb 中底层对于其产生的一个析构函数的调用。

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

这里很有意思的一个处理,直接将一个void  赋给一个  ubuf_info  类型。
struct ubuf_info { void (*callback)(struct ubuf_info *, bool zerocopy_success); void *ctx; unsigned long desc;};

其作用是:

当完成了skb DMA时,通过他调用回调函数做析构,释放缓冲区。并且此时skb引用计数为0。

而ctx负责跟踪设备上下文。desc负责跟踪用户空间的缓冲区索引。

zerocopy_success代表是否发生  零拷贝

至此,通过覆盖 skb_shared_info
.destructor_arg 就可以实现程序流劫持了。具体的打法有很多:
 
1. 如果不开smep/kaslr,那么直接ret2usr即可。
 
2. 如果开了smep,可以做内核rop先劫持cr4来关闭smep。然后jmp到payload
 
3. 如果开了kaslr。我看了一下网上最通用的exp,用了一种非常有意思的方式来bypass KASLR。通过使用 klogctl 读取内核日志,然后在内核日志中查找 'Freeing unused' 这个字符串。

然后找到与其同一行的ffffff开头的数字,最后 & 0xffffffffff000000ul 拿到一个地址,由于偏移不变,那么接下来就有了其他gadgets/函数在内核中的准确地址了。
 
exp地址:https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-1000112/poc.c


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

漏洞补丁

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


patch:https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git/commit/?id=85f1bd9a7b5a79d5baa8bf44af19658f7bf77bfa
 
When iteratively building a UDP datagram with MSG_MORE and that
datagram exceeds MTU, consistently choose UFO or fragmentation.
 
Once skb_is_gso, always apply ufo. Conversely, once a datagram is
split across multiple skbs, do not consider ufo.
 
Sendpage already maintains the first invariant, only add the second.
IPv6 does not have a sendpage implementation to modify.
 
A gso skb must have a partial checksum, do not follow sk_no_check_tx
in udp_send_skb.
 
Found by syzkaller.
 
大概就是说,之前的话主要是由于 SO_NO_CHECK 可以控制UFO路径切换造成问题。但是现在的话一旦我们有了gso(Generic Segmentation Offload一种UFO分片优化,发生在数据送到网卡之前),那么就会调用ufo,而不是产生路径切换的隐患。
 
同时作者也说了如果数据报被分片到多个skb中,那么不要使用ufo了。

参考

UFO (UDP Fragmentation Offload)
Packet fragmentation and segmentation offload in UDP and VXLAN  
Linux网络协议栈--ip_append_data函数分析
关于网络编程中MTU、TCP、UDP优化配置的一些总结
sk_buff整理笔记(五、队列管理函数)
skb_buff 详解(二)
CVE-2017-1000112-UFO 学习总结



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

- End -



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


看雪ID:ScUpax0s

https://bbs.pediy.com/user-home-876323.htm

   *本文由看雪论坛 ScUpax0s 原创,转载请注明来自看雪社区。




# 往期推荐












Linux内核分析 | CVE-2017-1000112(UDP Fragment Offload)
公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



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

球分享

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

球点赞

球在看



点击“阅读原文”,了解更多!

以上是关于Linux内核分析 | CVE-2017-1000112(UDP Fragment Offload)的主要内容,如果未能解决你的问题,请参考以下文章

《Linux内核分析》 第六节 分析Linux内核创建一个新进程的过程

Linux内核分析:Linux内核启动流程分析

Linux内核分析之跟踪分析Linux内核的启动过程

linux内核分析第六周-分析Linux内核创建一个新进程的过程

20135239 益西拉姆 linux内核分析 跟踪分析Linux内核的启动过程

linux内核分析作业3:跟踪分析Linux内核的启动过程