尊龙凯官网入口ADLab:Linux内核CVE-2017-11176误差剖析与复现

宣布时间 2019-01-04
误差配景

Linux内核中的POSIX 新闻行列实现中保存一个UAF误差CVE-2017-11176 。攻击者可以使用该误差导致拒绝服务或执行恣意代码 。本文将从误差成因、补丁剖析以及误差复现等多个角度对该误差举行详细剖析 。

误差剖析


Posix新闻行列允许异步事务通知,当往一个空行列安排一个新闻时,Posix新闻行列允许爆发一个信号或启动一个线程 。这种异步事务通知挪用mq_notify函数实现,mq_notify为指定行列建设或删除异步通知 。由于mq_notify函数在进入retry流程时没有将sock指针设置为NULL,可能导致UAF误差 。


从补丁代码可知,将sock设置为NULL即可 。


尊龙凯时 - 人生就是搏!


接下来看看误差因由,这里以4.1.0版本源码为例 。


尊龙凯时 - 人生就是搏!


在mq_notify函数中, u_notification是从用户层传进来的,1193行判断u_notification是否为空,若是非空,通过copy_from_user将u_notification中的数据拷贝到notification中,这里将数据从用户层拷贝到了内核层 。若是拷贝失败,直接退出 。


尊龙凯时 - 人生就是搏!


接下来,nc和sock划分置空 。行1203,若是u_notification不为空,首先依次判断notification.sigev_notify必需为SIGEV_NONE或SIGEV_SIGNAL或SIGEV_THREAD 。若是notification.sigev_notify为SIGEV_SIGNAL,就判断该信号是否正当 。


尊龙凯时 - 人生就是搏!


行1212,若是notification.sigev_notify为SIGEV_THREAD,进入要害代码块 。行1216,通过alloc_skb建设一个notify_skb,用于吸收数据 。行1221,通过copy_from_user将notification.sigev_value.sival_ptr指向的数据拷贝到nc->data中 。这里必需乐成,不然直接退出;行1229,挪用skb_put设置新闻数据头部 。行1231到行1248是retry循环体 。行1232,挪用fdget函数获取文件形貌符 。行1237,挪用netlink_getsockbyfilp函数通过文件形貌符获取netlink_sock,详细看一下netlink_getsockbyfilp函数 。


尊龙凯时 - 人生就是搏!


挪用file_inode通过filp找到对应的inode节点,然后通过SOCK_I函数处置惩罚inode节点 。


尊龙凯时 - 人生就是搏!


这里通过宏container_of在socket_alloc结构体中找出socket成员 。这里诠释一下,SOCKET_I返回值是socket结构体 。着实sock结构体中第一个成员sock_common也是socket类型,是一个迷你版socket 。


尊龙凯时 - 人生就是搏!


下面看一下sock_common结构体 。


尊龙凯时 - 人生就是搏!


行1609,获取到sock后,然后判断sock->sk_family是否即是AF_NETLINK 。行1613,接着挪用sock_hold增添引用计数 。sock_hold函数如下:


尊龙凯时 - 人生就是搏!


这里atomic_inc举行sk_refcnt加1 。netlink_getsockbyfilp函数返回sock,这时sock的引用计数加1 。接下来,行1246,挪用netlink_attachskb 。这是个要害函数,该函数功效是将skb绑定到netlink socket上,详细要害代码如下:


尊龙凯时 - 人生就是搏!


行1683,挪用sock_put镌汰引用计数一次,最后return 1,函数返回,直接goto到retry标签地方 。


尊龙凯时 - 人生就是搏!


这里行1237和行1246,这两处挪用正好举行了引用计数抵消 。行1247的if语句中并没有将sock置空,再看行1233,若是f.file为空,那就直接goto到out标签 。out标签代码如下:


尊龙凯时 - 人生就是搏!


行1306,判断sock是否为空,若是不为空,挪用netlink_detachskb函数 。


尊龙凯时 - 人生就是搏!


释放skb,并镌汰sk引用计数,举行释放 。 那么就有问题了,若是我们建设A线程坚持netlink_attachskb返回1,并重复retry逻辑,这个时间sock的引用计数是坚持平衡的,一加一减,可是sock并不是为空 。同时再建设B线程去关闭netlink socket对应的文件形貌符 。由于B线程关闭了netlink socket的文件形貌符,那A线程在retry逻辑中,行1232,挪用fdget时会失败,然后直接goto到out标签,举行释放,举行了二次释放,导致误差 。这个误差是属于条件竞争型的二次释放误差,只在一个线程中,是无法触发误差 。


这个误差原理较量简朴,可是怎样触发这个误差照旧较量重大 。首先,怎样让netlink_attachskb返回1,从而顺遂进入retry逻辑 。再次回看netlink_attachskb的实现 。


尊龙凯时 - 人生就是搏!


行1657,通过nlk_sk函数通过sk获取netlink_sock 。这里的nlk_sk如下 。


尊龙凯时 - 人生就是搏!


通过挪用宏container_of获取netlink_sock 。netlink_sock结构体如下:


尊龙凯时 - 人生就是搏!


netlink_sock结构体第一个成员是sock类型,而sock结构体的第一个成员是socket 。行1660,第一个if判断必需得进入 。


尊龙凯时 - 人生就是搏!


!netlink_skb_is_mmaped(skb)一定返回true,要害是sk->sk_rmem_alloc>sk->sk_rcvbuf || test_bit(NETLINK_CONGESTED, &nlk->state)效果必需是true 。


这里通过设置sk->sk_rmem_alloc的巨细绕过check更为利便,代码如下 。


尊龙凯时 - 人生就是搏!


若是if判断欠亨过,接着挪用netlink_skb_set_owner_r函数,如下所示 。


尊龙凯时 - 人生就是搏!


行878,挪用宏atomic_add,该宏执行原子加操作 。这行代码的寄义是:在sk->sk_rmem_alloc的基础上加上skb->truesize 。等同于sk->sk_rmem_alloc += skb->truesize 。既然该函数里这行代码可以直接增添sk->sk_rmem_alloc的巨细,那么可不可以多次挪用netlink_skb_set_owner_r函数增添sk->rmem_alloc的值?理论上是完全可以的,看看怎样从用户层抵达这个函数 。


尊龙凯时 - 人生就是搏!


通过understand工具可以快速找到netlink_skb_set_owner_r的挪用链:netlink_sendmsg->netlink_unicast->netlink_attachskb->netlink_skb_set_owner_r 。


怎样顺遂的通过函数挪用路径?这里需要剖析怎样从netlink_sendmsg抵达netlink_skb_set_owner_r 。netlink_sendmsg函数实现如下 。


尊龙凯时 - 人生就是搏!


行2285,首先判断msg->msg_flag不可为MSG_OOB,继续往下看 。

尊龙凯时 - 人生就是搏!


行2292,判断msg->msg_namelen的长度,这里必需不为空,虽然也不会为空 。进入if后,判断addr->nl_family是否即是AF_NETLINK 。行2299,判断dst_group或dst_portid不为空,dst_group体现多播模式,dst_portid来自于addr->nl_pid,因此包管dst_portid不为空较量容易 。接下来:


尊龙凯时 - 人生就是搏!


行2320,判断了msg->msg_iter.iov->iov_base不可为空 。并且len不可以大于sk->sk_sndbuf-32 。


尊龙凯时 - 人生就是搏!


着实整个函数中,用户层可控的只有这么多 。直接看netlink_unicast的挪用 。


尊龙凯时 - 人生就是搏!


netlink_unicast函数实现如下:


尊龙凯时 - 人生就是搏!


整个函数中,用户能控制的未几 。行1783,设置了timeo,这里要包管nonblock为msg->msg_flags&MSG_DONTWAIT,这样线程才不会被block 。行1790,判断sk是否为内核版的sk,在用户层建设socket时应使用NETLINK_USERSOCK 。行1793,判断是否有sk_filter,这里包管不进入该if语句,不要设置过滤器 。行1800,直接挪用netlink_attachskb,乐成抵达netlink_skb_set_owner_r函数 。这算是通过挪用netlink_sendmsg来增添sk->sk_rmem_alloc的历程 。着实我们不但可以增添sk->sk_rmem_alloc,还可以减小sk->sk_rcvbuf 。


那么怎样减小sk->sk_rcvbuf?在setsockopt函数中,找到sock_setsockopt函数中对sk->sk_rcvbuf的操作 。


尊龙凯时 - 人生就是搏!


行773,sk->sk_rcvbuf取val*2和SOCK_MIN_RCVBUF之间的最大值 。行755,val取val和sysctl_rmem_max之间的最小值 。行749,这个case为SO_RCVBUF 。继续往上看 。


尊龙凯时 - 人生就是搏!


行693,要包管optlen不小于sizeof(int) 。行696,将optval赋值到val中,这里optval是用户可控的 。行703,switch分发optname,以是要包管optname为SO_RCVBUF 。这样就可以包管顺遂抵达修改sk->rcvbuf的代码处 。

到这里,我们通过两种方法举行绕过netlink_attachskb函数中的第一个check 。


(1)通过netlink_sendmsg增添sk->sk_rmem_alloc的值.

(2)通过sock_setsockopt尽可能地减小sk->rcvbuf的值 。


进入if语句后,看如下代码:


尊龙凯时 - 人生就是搏!


这段代码会让目今线程进入期待状态,直接block 。若是不想进入期待状态,只有设置sock_flag为SOCK_DEAD 。可是若是把sock_flag设置成SOCK_DEAD,那后面也没有须要举行,因此这里是一定要进入期待状态的 。一种巧妙的要领是直接挪用wake_up_interruptible强行叫醒线程 。那怎样挪用wake_up_interruptible呢?函数挪用链很是简短:netlink_setsockopt->wake_up_interruptible 。


在Netlink_setsockopt函数中:


尊龙凯时 - 人生就是搏!


行2182,挪用wake_up_interruptible叫醒线程 。行2178,case为NETLINK_NO_ENOBUFS 。


尊龙凯时 - 人生就是搏!


行2131,判断level必需为SOL_NETLINK,行2134,判断optname不可为NETLINK_RX_RING和NETLINK_TX_RING,同时包管optlen大于即是sizeof(int) 。行2139,switch分发optname,这里要包管optname为NETLINK_NO_ENOBUFS 。到这里,基本上就可以包管netlink_attachskb返回1 。


包管进入retry循环后,这个时间sock已经不为空 。接下来要使retry循环中蜕化,直接跳转到out,代码如下:


尊龙凯时 - 人生就是搏!


行1232,通过fdget获取notification.sigev_signo的fd 。Notification.sigev_signo是用户态传进来的,因此完全可以在用户层直接close这个socket 。在用户层close这个socket后,行1233,进入if逻辑,然后跳到out标签 。


尊龙凯时 - 人生就是搏!


这个时间sock是非空的,if判断为真,进入netlink_destachskb,接着就是free瓦解 。


误差复现


关于UAF类型的误差,通用要领就是使用堆喷射占位 。本次误差中被多次释放的工具是netlink_sock工具 。netlink_sock工具巨细为0x3f0字节,即是1008byte 。


尊龙凯时 - 人生就是搏!


凭证内核工具内存分派规则, netlink_sock工具应该从kmalloc-1024这个缓存中举行分派 。
slab分派器在分派工具时,遵守后进先出的规则 。下面是slab分派器释下班具的历程 。


尊龙凯时 - 人生就是搏!


要释放的工具objp放在了ac->entry[]的最后 。下面是slab分派器分派工具的历程:


尊龙凯时 - 人生就是搏!


分派工具直接从ac->entry[]最后弹出一个工具 。


以是一个刚刚被释放的工具是排在链表末段,若是此时恰幸亏统一缓存中举行工具分派,那刚刚释放的工具就会被重新分派出去,这就泛起两个指针指向统一块内存地址 。要想包管申请的内存正好落在误差工具的内存位置中,需要掌握住几点:


堆喷工具使用的内核缓存应该和误差工具内保存统一个缓存中 。即巨细必需落在统一个kmalloc-X中 。


ac自己是array_chche结构体,该结构体是外地高速缓存,每个CPU对应一个,以是还要包管堆喷申请的工具和误差工具在统一个CPU外地高速缓存中 。


若是堆喷申请的工具只是短暂驻留,当该函数返回时将申请的工具举行了释放,导致无法准确占位 。以是要能包管申请的工具不被释放,至少包管在使用误差工具时不被释放,这里要接纳驻留式内存占位,可以接纳让某些系统挪用历程壅闭 。


slab缓存碎片化问题,这里要占位的工具巨细为1008,工具尺寸较量大,占有四分之一页,较量整齐,应该没有碎片化问题 。


那么怎样判断堆喷是否乐成呢?


通用情形下,在举行堆喷时间,结构堆喷工具时,有须要在对应误差工具的一些特殊成员域的内存偏移处设置magic value,然后可以接纳系统挪用去获取误差工具中相关数据举行判断 。netlink_sock结构体几个要害的成员如下 。


尊龙凯时 - 人生就是搏!


接纳getsockname系统挪用获取数据,getsockname会挪用netlink_getname 。详细看一下netlink_getname函数:


尊龙凯时 - 人生就是搏!


代码1576行,将netlink_sock工具中的portid复制给nladdr->nl_pid 。代码1577行,若是nlk->group为0,将nladdr->nl_groups赋值为NULL,这里阻止解引用nlk->groups指针,直接可以在结构堆喷工具时将groups域填零 。而nladdr是从addr转换过来的,addr就是从用户层传入的缓冲区 。


堆喷乐成如下:


尊龙凯时 - 人生就是搏!


通常情形是笼罩结构体中的函数指针或者包括函数指针的结构体成员,这视情形而定 。这里选择笼罩wait期待行列 。netlink_sock结构体如下:


尊龙凯时 - 人生就是搏!


wait_queue_haed_t结构体如下:


尊龙凯时 - 人生就是搏!


task_list成员是一个双向循环链表头,task_list中链接的每一个成员都是需要处置惩罚的期待例程元素 。那该怎样使用这个成员?看如下代码 。


尊龙凯时 - 人生就是搏!


这是netlink_setsockopt函数中的代码片断,前面恢复线程复生剖析过,这里将会挪用netlink_sock工具中的期待例程,直接使用参数nlk->wait 。继续深入剖析:


尊龙凯时 - 人生就是搏!


挪用__wake_up_common函数:


尊龙凯时 - 人生就是搏!


代码70行,宏list_for_each_entry_safe遍历q->task_list中的成员,返回到curr 。代码68行,curr为wait_queue_t指针,说明q->task_list链表中存的是wait_queue_t类型的元素,wait_queue_t结构体如下:


尊龙凯时 - 人生就是搏!


wait_queue_t结构体中有一个函数指针func 。再看__wake_up_common函数中,代码73行,直接执行curr>func函数,可以通过结构__wait_queue的func参数控制RIP 。再回过头看list_for_each_entry_safe宏:


尊龙凯时 - 人生就是搏!


pos是__wait_queue元素,代码62行,对pos->member.next举行相识引用,这里的pos->member就是__wait_queue中的task_list 。__wait_queue中的task_list也是一个链表头,需要指向一个list_head,以是还必需要结构一个假的list_head以便于该宏举行解引用 。测试如下:


尊龙凯时 - 人生就是搏!


接下来就是通过ROP链绕过SMEP执行提权代码 。乐成提权后如下所示:

尊龙凯时 - 人生就是搏!