宝运莱官方网站ADLab:Linux内核CVE-2017-11176缝隙分析与复现
颁布功夫 2019-01-04Linux内核中的POSIX 新闻队列实现中存在一个UAF缝隙CVE-2017-11176。攻击者能够利用该缝隙导致回绝服务或执行肆意代码。本文将从缝隙成因、补丁分析以及缝隙复现等多个角度对该缝隙进行具体分析。
缝隙分析
Posix新闻队列允许异步事务通知,当往一个空队列搁置一个新闻时,Posix新闻队列允许产生一个信号或启动一个线程。这种异步事务通知挪用mq_notify函数实现,mq_notify为指定队列成立或删除异步通知。由于mq_notify函数在进入retry流程时没有将sock指针设置为NULL,可能导致UAF缝隙。
接下来看看缝隙起因,这里以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标签,进行开释,进行了二次开释,导致缝隙。这个缝隙是属于前提竞争型的二次开释缝隙,只在一个线程中,是无法触发缝隙。
行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。
如果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。
行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。
行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。
(2)通过sock_setsockopt尽可能地减幼sk->rcvbuf的值。
这段代码会让当前列程进入期待状态,直接block。若是不想进入期待状态,只有设置sock_flag为SOCK_DEAD。但是若是把sock_flag设置成SOCK_DEAD,那后面也没有必要进行,因而这里是必然要进入期待状态的。一种奇妙的步骤是直接挪用wake_up_interruptible强行唤醒线程。那若何挪用wake_up_interruptible呢?函数挪用链极度简短:netlink_setsockopt->wake_up_interruptible。
行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。
行1232,通过fdget获取notification.sigev_signo的fd。Notification.sigev_signo是用户态传进来的,因而齐全能够在用户层直接close这个socket。在用户层close这个socket后,行1233,进入if逻辑,而后跳到out标签。
这个时辰sock长短空的,if判断为真,进入netlink_destachskb,接着就是free崩溃。
缝隙复现
凭据内查对象内存分配规定, netlink_sock对象应该从kmalloc-1024这个缓存中进行分配。
slab分配器在分配对象时,遵守后进先出的规定。下面是slab分配器开释对象的过程。
要开释的对象objp放在了ac->entry[]的结尾。下面是slab分配器分配对象的过程:
分配对象直接从ac->entry[]结尾弹出一个对象。
所以一个刚刚被开释的对象是排在链表末段,若是此时刚好在统一缓存中进行对象分配,那刚刚开释的对象就会被沉新分配出去,这就出现两个指针指向统一块内存地址。要想保障申请的内存正好落在缝隙对象的内存地位中,必要把握住几点:
堆喷对象使用的内核缓存应该和缝隙对象内存在统一个缓存中。即大幼必须落在统一个kmalloc-X中。
ac自身是array_chche结构体,该结构体是本地高速缓存,每个CPU对应一个,所以还要保障堆喷申请的对象和缝隙对象在统一个CPU本地高速缓存中。
若是堆喷申请的对象只是短暂驻留,当该函数返回时将申请的对象进行了开释,导致无法正确占位。所以要能保障申请的对象不被开释,至少保障在使用缝隙对象时不被开释,这里要选取驻留式内存占位,能够采取让某些系统挪用过程阻塞。
slab缓存碎片化问题,这里要占位的对象大幼为1008,对象尺寸比力大,占据四分之一页,比力整齐,应该没有碎片化问题。
那么若何判断堆喷是否成功呢?
选取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执行提权代码。成功提权后如下所示:


京公网安备11010802024551号