今天看到有人发了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机制。

七 参考资料

八 附录–__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;
}