CVE-2017-1000112-UFO 学习总结
今天看到有人发了CVE-2017-1000112-UFO的分析,就把之前的学习报告整理一下,做个对比学习。
别人的分析报告:
https://securingtomorrow.mcafee.com/mcafee-labs/linux-kernel-vulnerability-can-lead-to-privilege-escalation-analyzing-cve-2017-1000112/
以下是我自己整理的学习报告:
一 调试环境搭建
1.1 编译内核
为了能够调试驱动程序需要让目标机的操作系统支持调试模式,这样就需要从新编译内核,让目标机支持调试模式。 (1) 配置内核参数 进入下载的内核目录,执行命令 make menuconfig
就会出现如下图形界面,勾选下面几项:
为了能够支持KGDB调试上面这几项都需要选择上,在内核驱动调试过程中需要在驱动中下断点,这样就需要在内核地址上进行写操作,所以需要将下面这个选项去掉
内核参数设置完成后,保持设置的config文件,默认保存文件名为.config文件,保持在当前目录下。为了确保我们对内核写保护已经禁止,在开始编译内核之前,再检查一次config文件,打开.config文件,如下图:
确保红框中的两项是注释状态,如果不是注释状态可以直接在这里修改将他们的值改成N。这样基本上就完成了内核参数的配置。 (2) 编译内核 保持好设置后,编译内核:
Make
Make bzImage
Make modules
Make modules_install
Make install
1.2 配置虚拟机
调试需要目标机与客户机两台虚拟机。在这里用了一个取巧的方式,不是安装两台虚拟机,而是直接将上面编译出来的ubuntu操作系统直接在vmware克隆一份,这样就有了两台ubuntu虚拟机,一台作为目标机,一台作为客户机,当然这两个系统都支持了kgdb调试模式,都使用了相同的内核。
(1) 配置串口通信
(2) 客户机调试配置
(3) 目标机调试配置
执行sudo update-grub使上面的配置生效
(4) 检查环境是否搭建成功 目标机上执行:
客户机上执行:
客户机启动调试后就会出现如上信息。此时客户机与目标机的调试环境就建立成功了。
1.3 安装对应版本的linux内核镜像
(1)目标机上安装对应版本的linux内核镜像 下载地址:http://security.ubuntu.com/ubuntu/pool/main/l/linux/
(2)客户机上安装对应版本的带有符号表的linux内核镜像 下载地址:http://ddebs.ubuntu.com/pool/main/l/linux/,并且源码下载,建立软链接使得调试的时候能够跟踪源码。 具体安装过程见:http://binxian.chetui.org/?p=140
1.4 关闭系统SMAP防护
因为调试的EXP没有绕过SMAP,所以调试的时候需要关闭SMAP,编辑/etc/default/grub,添加nosmap启动参数。
二 基础知识点
2.1 什么是UFO?
- UFO(UDP Fragment Offload)是硬件网卡提供的一种特性,由内核和驱动配合完成相关功能。其目的是由网卡硬件来完成本来需要软件进行的分段(分片)操作用于提升效率和性能。减少Linux 内核传输层和网络层的计算工作,将这些计算工作offload(卸载)到物理网卡。UDP协议层本身不对大的数据报进行分片,而是交给IP层去做。因此,UFO就是将IP分片offload到网卡中进行。
- 如大家所知,在网络上传输的数据包不能大于mtu,当用户发送大于mtu的数据报文时,通常会在传输层(或者在特殊情况下在IP层分片,比如ip转发或ipsec时)就会按mtu大小进行分段,防止发送出去的报文大于mtu,为提升该操作的性能,新的网卡硬件基本都实现了UFO功能,可以使分段(或分片)操作在网卡硬件完成,此时用户态就可以发送长度大于mtu的包,而且不必在协议栈中进行分段(或分片)。
- 这就意味着当开启UFO时,可以支持发送超过MTU大小的数据报。
- ip_ufo_append_data函数大致原理为:当硬件支持且打开了UFO、udp包大小大于mtu会进入此流程,将用户态数据拷贝拷skb中的非线性区中(即skb_shared_info->frags[],原本用于SG)。
- 主要流程为:从sock发送队列中取skb,如果发送队列为空,则新分配一个skb;如果不为空,则直接使用该skb;然后,判断per task的page_frag中是否有空间可用,有的话,就直接从用户态拷贝数据到该page_frag中,如果没有空间,则分配新的page,放入page_frag中,然后再从用户态拷贝数据到其中,最后将该page_frag中的page链入skb的非线性区中(即skb_shared_info->frags[]).
- 进入ip_ufo_append_data的调用流程为:
udp_sendmsg--> ip_append_data--> __ip_append_data--> ip_ufo_append_data
2.2 内核的防护手段
(1)KASLR:表示内核地址空间布局随机化,它通过随机化内核的基址值,使一些内核攻击更难实现。需要泄露内核符号的基地址来绕过
(2)SMEP:(Supervisor Mode Execution Prevention),在现代intel处理器上,当设置了CR4存器的控制位时,会保护特权进程(比如在内核态的程序)不能在不含supervisor标志(对于ARM处理器,就是PXN标志)的内存区域执行代码。(直白地说就是内核程序不能跳转到用户态执行代码),这种保护使得以往的exploit使用的ret2user的方法直接失效。ret2user即在内核控制执行流,使之跳转到用户可控的用户空间执行代码的技术。因为SMEP,在用户空间的页表的虚拟地址并没有supervisor标志,当跳转到用户态时,会触发异常。
要检查SMEP是否被激活,我们可以简单地读取/proc/cpuinfo,检查是否有smep这个字段。
(3)SMAP:( Supervisor Mode Access Prevention),同理,这个和SMEP差不多,只不过SMEP负责执行控制,这里负责读写控制。因此内核态不能读写用户态的内存数据。那你可能会疑惑了,如果这样限制的话,内核和用户态程序怎么交流?通过修改标志位,使某位置临时取消SMAP,来实现精确位置的读写。
(4)内核提权 内核中无法通过system(“/bin/bash”)来提权,可以通过commit_creds(prepare_kernel_cred(0));来达到提权的目的。其中,prepare_kernel_cred()创建一个新的cred,参数为0则将cred中的uid, gid设置为0,对应于root用户。随后,commit_creds()将这个cred应用于当前进程。此时,进程便提升到了root权限。
三 漏洞形成的原因
漏洞形成函数ip_append_data->__ip_append_data的执行流程:
ip_append_data()是一个比较复杂的函数,主要是将接收到大数据包分成多个小于或等于MTU的SKB,为网络层要实现的IP分片作准备。例如,假设待发送的数据包大小为4000B,先前输出队列非空,且最后一个SKB还没填满,剩余500B。这时传输层调用ip_append_data(),则首先会将剩余空间的SKB填满。进入循环,每次循环都分配一个SKB,通过getfrag将数据从传输层复制数据,并将其添加到输出队列的末尾,直至复制完所有待输出的数据。
Skb结构图:
漏洞形成的原因在于内核是通过SO_NO_CHECK的标志来判断用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()进行复制操作时造成写越界。
Shellcode的触发:覆写skb结构里的destructor_arg->callback,指向shellcode,在之后释放skb操作中,会调用skb_release_data函数:
触发shellcode。
四 对POC的调试理解
4.1 POC的执行流程
- (1)探测内核版本,通过读取/etc/lsb-release中的DISTRIB_CODENAME属性并通过uname系统命令来获取当前内核版本号,并和写好的数组进行比较,没有就退出了。
- (2)检测是否开启了smep和smap防护,通过读取/proc/cpuinfo,检查是否有smep和smap的字段,如果有就说明开启了。
- (3)建立用户空间
- (4)找到一个有用的地址。检查syslog文件中字符串“Freeing unused” 到第一个“-”之间以“ffffff”开头的地址。后面可以通过该地址和每个固定偏移地址相加得到相关指令的地址。因为符号表间的相对位置是不变的,变的只是基地址。
- (5)绕过SMEP,构造一个ROP来修改CR4寄存器的第20个bit,但这需要地址泄露来保证稳定性,该exp没有泄露地址,依赖于系统版本,有待改进
- (6)触发漏洞,执行payload,先构造buffer,通过UFO的路径输出,MSG_MORE:标识后续还有数据待发送。之后设置SO_NO_CHECK标志,发送长度为1的数据,通过non-UFO输出。
4.2 POC的调试过程
(1)下断点:
(2)进入UFO路径
(3)进入non-UFO执行路径
(4)non-UFO路径下copy < 0 小于0表示一些数据必须从当前的IP帧删除,移到新的地方。Copy <= 0,表示队列中最后一个skb剩余的空间已经没有了,所以必须重新分配一个新的sk_buff。
(5)skb_copy_and_csum_bits()执行后的buffer
(6)查看buffer中覆写的skb
(7)查看rop链的入口:
(8)由于运行版本不对,构造的rop链地址不正确导致系统崩溃,但也可以看出该漏洞可以进行拒绝服务攻击。
(9)poc正确运行的效果:
五 对补丁的分析
补丁1:
打补丁之前,进不进ufo路径主要由sk_no_check_tx决定。
打补丁之后,即使设置了sk_no_check_tx,只要开启了gso(Generic Segmentation Offload,可以理解成在数据推送网卡前进行分片,相当于对UFO的一种优化),一样会进入ufo路径,这样漏洞就无法触发了。
补丁2:
原理其实和上面的一样,原来只是由no_check决定,现在必须设置no_check的同时还要关闭gso( Generic Segmentation Offload),这样才能进入non-ufo。
补丁3:
补丁3主要是针对ipv6的。原理和ipv4的一样。
六 影响版本
http://www.securityfocus.com/bid/100262
影响linux kernel 4.12.3之前的版本,在4.14的版本将移除UFO机制。
七 参考资料
- Linux 内核源码剖析- TCP.IP 实现, 樊东东, 莫澜, 上册, 2011
- A Guide to Kernel Exploitation Attacking the Core
- http://seclists.org/oss-sec/2017/q3/286
八 附录–__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;
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);
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); //获取链路层首部及IP首部(包括选项)的长度 hh_len=16
fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0);
maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen; //1500,mtu=1500,fragheaderlen=20
//IP数据报的数据需要4字节对齐,为加速计算直接将IP数据报的数据根据当前MTU8字节对齐,然后重新得到用于分片的长度
maxnonfragsize = ip_sk_ignore_df(sk) ? 0xFFFF : mtu;
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 &&//如果IP数据报没有分片,且输出网络设备支持硬件执行校验和,则设置CHECKSUM_PARTIAL,表示由硬件来执行校验和 transhdrlen = 8, length = 3492
length + fragheaderlen <= mtu &&
rt->dst.dev->features & NETIF_F_V4_CSUM &&
!(flags & MSG_MORE) &&
!exthdrlen)
csummode = CHECKSUM_PARTIAL;
cork->length += length;//如果输出的是UDP数据报且需要分片,同时输出网络设备支持UDP分片卸载(UDP fragmentation offload),则由ip_ufo_append_data()进行分片输出处理。
if ((((length + (skb ? skb->len : fragheaderlen)) > mtu) ||
- (skb && skb_is_gso(skb))) &&
(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);
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)
goto alloc_new_skb;//获取输出队列末尾的SKB,如果获取不到,说明输出队列为空,则需分配一个新的SKB用于复制数据
while (length > 0) {//循环处理待输出数据,直至所有的数据都处理完成
/* Check if the remaining data fits into current packet. */
copy = mtu - skb->len; //得到上一个SKB的剩余空间大小,也就是本次复制数据的长度 mtu=1500,skb->3512
if (copy < length)//length为数据的长度,空间小于数据大小,就要 length=1
copy = maxfraglen - skb->len; //maxfraglen=1500, copy=-2012
if (copy <= 0) { //当本次复制数据的长度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)
fraggap = skb_prev->len - maxfraglen;// fraggap=3512-1500=2012
else
fraggap = 0;
/*
* If remaining data exceeds the mtu,
* we know we need more fragment(s).
*/
datalen = length + fraggap; //datalen=1+2012=2013
if (datalen > mtu - fragheaderlen) //如果剩余的数据一个分片不够容纳,则根据MTU重新计算本次可发送的数据长度
datalen = maxfraglen - fragheaderlen; //datalen=1500-20=1480
fraglen = datalen + fragheaderlen; //根据本次复制的数据长度以及IP首部长度,计算三层首部及数据的总长度
if ((flags & MSG_MORE) &&
!(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) { //根据是否存在传输层首部,确定用何种方法分配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;
/*
* Fill in the control structures
*/
skb->ip_summed = csummode; //填充用于校验的控制信息
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);
skb_set_network_header(skb, exthdrlen);
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( //并以8字节对齐MTU为长度截取上一个SKB的数据
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;
/*
* Put the packet on the pending queue.
*/
__skb_queue_tail(queue, skb);//将复制完数据的SKB添加到输出队列的尾部,接着复制剩下的数据
continue;
}
if (copy > length)
copy = length; //如果上个SKB剩余的空间大于剩余待发送的数据长度,则剩下的数据可以一次完成
if (!(rt->dst.dev->features&NETIF_F_SG)) {
unsigned int off;//如果输出网络设备不支持聚合分散I/O,则将数据复制到线性区域的剩余空间
off = skb->len;
if (getfrag(from, skb_put(skb, copy),
offset, copy, off, skb) < 0) {
__skb_trim(skb, off);
err = -EFAULT;
goto error;
}
} else {
int i = skb_shinfo(skb)->nr_frags;
err = -ENOMEM;
if (!sk_page_frag_refill(sk, pfrag))
goto error;
if (!skb_can_coalesce(skb, i, pfrag->page,
pfrag->offset)) {
err = -EMSGSIZE;
if (i == MAX_SKB_FRAGS)
goto error;
__skb_fill_page_desc(skb, i, pfrag->page,
pfrag->offset, 0);
skb_shinfo(skb)->nr_frags = ++i;
get_page(pfrag->page);
}
copy = min_t(int, copy, pfrag->size - pfrag->offset);
if (getfrag(from,
page_address(pfrag->page) + pfrag->offset,
offset, copy, skb->len, skb) < 0)
goto error_efault;
pfrag->offset += copy;
skb_frag_size_add(&skb_shinfo(skb)->frags[i - 1], copy);
skb->len += copy;
skb->data_len += copy;
skb->truesize += copy;
atomic_add(copy, &sk->sk_wmem_alloc);
}
offset += copy;
length -= copy;
}
return 0;
error_efault:
err = -EFAULT;
error:
cork->length -= length;
IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTDISCARDS);
return err;
}