前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CVE-2017-1000112-UFO 学习总结

CVE-2017-1000112-UFO 学习总结

作者头像
De4dCr0w
发布2019-02-27 16:12:34
发布2019-02-27 16:12:34
2.3K1
举报
文章被收录于专栏:二进制漏洞研究

今天看到有人发了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) 编译内核 保持好设置后,编译内核:

代码语言:javascript
复制
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机制。

七 参考资料

八 附录–__ip_append_data源码的注释

代码语言:javascript
复制
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;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一 调试环境搭建
    • 1.1 编译内核
      • 1.2 配置虚拟机
        • 1.3 安装对应版本的linux内核镜像
          • 1.4 关闭系统SMAP防护
          • 二 基础知识点
            • 2.1 什么是UFO?
            • 2.2 内核的防护手段
            • 三 漏洞形成的原因
            • 四 对POC的调试理解
              • 4.1 POC的执行流程
                • 4.2 POC的调试过程
                • 五 对补丁的分析
                • 六 影响版本
                • 七 参考资料
                • 八 附录–__ip_append_data源码的注释
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档