Linux 内核提权风暴:从 Copy Fail 到 Dirty Frag 再到 Fragnesia——零拷贝页缓存污染漏洞家族的终极解剖
引言:三周三个高危,Linux 内核安全堤坝为何崩塌?
2026 年 4 月底到 5 月中旬,Linux 内核安全领域经历了一场前所未有的"暴击":
- 4 月 29 日:Copy Fail(CVE-2026-31431)公开披露,利用 AF_ALG 加密接口 + splice() 零拷贝路径污染页缓存,9 年潜伏,100% 确定性提权
- 5 月 7 日:Dirty Frag(CVE-2026-43284 / CVE-2026-43500)曝光,将攻击面从本地加密子系统扩展到网络协议栈(xfrm-ESP + RxRPC),无权限依赖、无竞争条件、全发行版通杀
- 5 月 14 日:Fragnesia(CVE-2026-46300)接踵而至,属于 Dirty Frag 漏洞家族变体,利用 XFRM ESP-in-TCP 子系统的 SKBFL_SHARED_FRAG 标记传播缺陷,实现内核页缓存任意字节写入
三个漏洞,同一血脉——它们都根植于 Linux 内核"零拷贝优化路径对页缓存写入权限把控不严"这一系统性设计缺陷。这不是某个程序员的一次手滑,而是一个延续 9 年的架构级安全隐患。
本文将从内核源码层面深度解剖这三个漏洞的完整攻击链,剖析页缓存污染的底层机制,给出从检测到修复的全栈防护方案,并在最后探讨 Linux 内核零拷贝架构的未来安全演进方向。
一、历史脉络:从 Dirty Pipe 到 Dirty Frag 的"提权进化史"
1.1 漏洞家族谱系
要理解 2026 年这场内核安全风暴,必须从 2022 年的 Dirty Pipe 说起。
| 漏洞 | CVE | 发现时间 | 潜伏期 | 内核路径 | 竞争条件 | 提权成功率 |
|---|---|---|---|---|---|---|
| Dirty Cow | CVE-2016-5195 | 2016 | 9 年 | COW 写时复制 | 需要 | ~80% |
| Dirty Pipe | CVE-2022-0847 | 2022 | 2 年 | pipe + splice() | 不需要 | 100% |
| Copy Fail | CVE-2026-31431 | 2026.4 | 9 年 | AF_ALG + splice() | 不需要 | 100% |
| Dirty Frag (xfrm) | CVE-2026-43284 | 2026.5 | 9 年 | xfrm-ESP + MSG_SPLICE_PAGES | 不需要 | 100% |
| Dirty Frag (RxRPC) | CVE-2026-43500 | 2026.5 | 3 年 | RxRPC + MSG_SPLICE_PAGES | 不需要 | 100% |
| Fragnesia | CVE-2026-46300 | 2026.5 | 9 年 | XFRM ESP-in-TCP + SKB 合并 | 不需要 | 100% |
可以看到一条清晰的演进脉络:
- Dirty Cow:需要竞争条件,成功率受 CPU 调度影响,但开创了"篡改只读页缓存"的先河
- Dirty Pipe:消除竞争条件,利用管道的
PIPE_BUF_FLAG_CAN_MERGE标记,实现确定性写入 - Copy Fail:将攻击面从管道扩展到加密子系统,证明 Dirty Pipe 的修复只是"头痛医头"
- Dirty Frag:将攻击面进一步扩展到网络协议栈,攻击路径更多、门槛更低
- Fragnesia:Dirty Frag 的变体,利用 SKB 碎片合并的标记传播缺陷,攻击更隐蔽
1.2 核心共同点:零拷贝路径的页缓存写入失控
所有这些漏洞的本质相同:Linux 内核在零拷贝(Zero-Copy)优化路径上,允许数据直接进入页缓存(Page Cache),却缺少对写入权限的充分校验。
正常情况下,修改磁盘文件需要:
- 用户进程拥有文件写权限
- 通过
write()系统调用进入内核 - 内核检查权限后修改页缓存
- 页缓存脏页回写磁盘
但零拷贝路径(splice / sendfile / MSG_SPLICE_PAGES)绕过了用户态缓冲区,数据直接从内核缓冲区搬进页缓存。问题在于:内核在零拷贝路径上没有执行与 write() 相同的权限检查。
// 正常 write() 路径 - 有权限检查
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) {
// 检查文件写权限
if (!(file->f_mode & FMODE_WRITE))
return -EBADF;
// 检查 immutable/append-only 等属性
retval = locks_verify_area(inode, file, pos, count, true);
// ... 然后才写入页缓存
}
// splice() 零拷贝路径 - 缺少对目标页缓存的写权限检查
long do_splice_to(struct file *in, loff_t *ppos, struct pipe_inode_info *pipe,
size_t len, unsigned int flags) {
// 直接将页缓存页挂载到管道缓冲区
// 没有检查目标页缓存页的写入权限!
}
这就是整个漏洞家族的基因——零拷贝优化的性能考量,牺牲了安全校验的完整性。
二、Copy Fail(CVE-2026-31431):当加密 API 遇上页缓存
2.1 漏洞概览
Copy Fail 由 Theori 安全团队的研究员 Taeyang Lee 发现(借助 AI 辅助代码审计工具 Xint Code),于 2026 年 3 月 23 日报告给内核安全团队,4 月 29 日公开披露。
核心数据:
- 影响版本:Linux 4.14+(2017 年至补丁前)
- 攻击要求:仅需本地普通用户
- 利用工具:732 字节 Python 脚本
- 成功率:100%,确定性,无竞争条件
- CVSS 评分:7.8(高危)
2.2 三个子系统的致命交汇
Copy Fail 不是某个单一子系统的 bug,而是三个内核子系统交互时产生的逻辑缺陷:
子系统 1:AF_ALG——用户态加密 API
AF_ALG 是 Linux 内核提供的用户态加密接口,允许普通用户通过 socket 调用内核加密算法:
// 创建 AF_ALG socket - 普通用户即可调用
int algfd = socket(AF_ALG, SOCK_SEQPACKET, 0);
// 绑定加密算法(如 authencesn - AEAD 认证加密)
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "aead",
.salg_name = "authencesn(hmac(sha256),cbc(aes))",
};
bind(algfd, (struct sockaddr *)&sa, sizeof(sa));
子系统 2:splice()——零拷贝数据搬运
splice() 允许在两个文件描述符之间直接搬运数据,无需经过用户态缓冲区:
// 将文件内容 splice 到 AF_ALG socket 进行加密
splice(file_fd, &file_off, alg_fd, NULL, len, 0);
关键点:splice() 会将文件页缓存页直接挂载到 AF_ALG 的管道缓冲区,不会复制数据。
子系统 3:authencesn——AEAD 认证加密模板
authencesn 是 AEAD(Authenticated Encryption with Associated Data)模板,组合了 HMAC-SHA256 和 AES-CBC:
// 内核 crypto/authencesn.c 中的加密实现
static int authencesn_encrypt(struct aead_request *req) {
// Step 1: 计算 HMAC 认证标签
// Step 2: AES-CBC 加密数据
// 问题出在 Step 3:加密后的密文被写回原始页缓存页!
}
2.3 漏洞触发机制
完整的攻击流程如下:
import os, struct
from socket import *
# Step 1: 打开目标 SUID 二进制文件(如 /usr/bin/su)
target_fd = os.open("/usr/bin/su", os.O_RDONLY)
# Step 2: 创建 AF_ALG 加密 socket
alg_fd = socket(AF_ALG, SOCK_SEQPACKET, 0)
alg_fd.bind(("authencesn(hmac(sha256),cbc(aes))",))
# Step 3: 生成 accept socket
conn_fd = alg_fd.accept()
# Step 4: 设置加密密钥(用户自己随便设)
conn_fd.setsockopt(SOL_ALG, ALG_SET_KEY, key_bytes)
# Step 5: 将目标文件 splice 到加密 socket
# 这是关键:splice 将页缓存页挂到 AF_ALG 管道
os.splice(target_fd, None, conn_fd.fileno(), None, 4096, 0)
# Step 6: 读取加密结果
# 内核在 authencesn_encrypt 中将密文写回原始页缓存页!
# 因为 splice 引用的是同一个页缓存页,加密操作直接修改了 /usr/bin/su 的页缓存
encrypted = conn_fd.recv(4096)
为什么密文会写回页缓存?
// crypto/authencesn.c
static int authencesn_encrypt(struct aead_request *req) {
struct authencesn_request_ctx *rctx = aead_request_ctx(req);
// scatterwalk 将请求中的 scatterlist 映射到内核虚拟地址
// 由于数据是通过 splice() 引入的页缓存页,
// scatterwalk 映射的就是原始页缓存页的内核地址
// 加密操作直接在原始页缓存页上进行
// 加密后的密文覆盖了原始的明文!
ablkcipher_request_set_crypt(rctx->creq, src, dst, nbytes, iv);
// src 和 dst 都指向同一个页缓存页
crypto_ablkcipher_encrypt(rctx->creq);
// 加密完成后,页缓存页已被密文覆盖
}
2.4 提权利用:篡改 SUID 二进制
Copy Fail 的提权利用方式:
// 1. 打开 /usr/bin/su(SUID root 程序)
// 2. 通过 Copy Fail 修改其页缓存中的特定字节
// 将关键跳转指令修改为 NOP 或跳到 shellcode
// 3. 执行被污染的 /usr/bin/su
// 4. 由于页缓存已被修改,内核执行的是被篡改的代码
// 5. 获得 root shell
// 注意:修改的是页缓存,不是磁盘文件
// 重启后页缓存丢弃,磁盘上的文件未变
// 这使得攻击更加隐蔽——传统文件完整性检测无法发现
2.5 容器逃逸
在共享内核的容器环境(Docker、Kubernetes)中,Copy Fail 更是致命:
# 在容器内执行提权
python3 copy_fail.py # 获得 root
# 修改 /etc/passwd 的页缓存
# 添加新用户到宿主机的 /etc/passwd(共享页缓存)
# 或者修改 /usr/bin/sudo 的页缓存
# 逃逸到宿主机
nsenter --target 1 --mount --uts --ipc --net --pid -- bash
# 此时已经是宿主机的 root
三、Dirty Frag(CVE-2026-43284 / CVE-2026-43500):攻击面扩展到网络协议栈
3.1 漏洞概览
Dirty Frag 由韩国安全研究员 Hyunwoo Kim(也是 Copy Fail 的发现者之一)发现,2026 年 4 月 30 日报告,5 月 7 日公开披露。
相比 Copy Fail,Dirty Frag 的突破在于:
| 维度 | Copy Fail | Dirty Frag |
|---|---|---|
| 攻击入口 | AF_ALG 加密 socket | 网络协议栈(IPSec/RxRPC) |
| 权限要求 | 普通用户 | 普通用户 + user namespace(无需真正特权) |
| 攻击路径 | 1 条 | 2 条(xfrm-ESP + RxRPC) |
| 影响范围 | 需 AF_ALG 模块 | 需 xfrm-ESP(大多数发行版默认启用) |
| PoC 复杂度 | 732 字节 Python | 更复杂,但确定性利用 |
3.2 攻击路径 1:xfrm-ESP 原地解密漏洞
xfrm-ESP 是什么?
xfrm 是 Linux 内核的 IP 框架,ESP(Encapsulating Security Payload)是 IPSec 的加密协议。当内核收到 ESP 加密的数据包时,需要解密后才能继续处理。
// net/ipv4/esp4.c
static int esp_input(struct xfrm_state *x, struct sk_buff *skb) {
// 收到加密的 ESP 数据包
// 需要解密
// 问题:在某些条件下,解密操作"原地"进行
// 即直接在 skb 的数据页上解密
// 如果这个数据页恰好是页缓存页(通过 MSG_SPLICE_PAGES 引入)
// 解密操作就会覆盖页缓存内容!
}
MSG_SPLICE_PAGES 的角色
MSG_SPLICE_PAGES 是 sendmsg() 的标志,允许将页缓存页直接作为网络数据发送:
// 用户态代码
struct msghdr msg = {0};
msg.msg_flags = MSG_SPLICE_PAGES;
// 将文件的页缓存页直接挂到 socket 发送缓冲区
struct iovec iov = {
.iov_base = mmap_ptr, // 映射目标文件的内存
.iov_len = page_size,
};
sendmsg(sock_fd, &msg, 0);
// 页缓存页被挂到 SKB(Socket Buffer)的碎片列表中
原地解密的触发条件
// net/ipv4/esp4.c - ESP 解密路径
static int esp6_input(struct xfrm_state *x, struct sk_buff *skb) {
// 判断是否可以原地解密
if (skb_cloned(skb) || skb_shared(skb)) {
// 不能原地解密,需要复制
nskb = skb_copy(skb, GFP_ATOMIC);
} else {
// 可以原地解密!
// 如果 skb 的碎片页来自页缓存
// 解密会直接修改页缓存!
esp_output_decrypt(skb);
}
}
完整攻击流程
import socket, os
from ctypes import *
# Step 1: 创建 user namespace + network namespace
# 这样普通用户就能拥有 CAP_NET_ADMIN
os.system("unshare -Urn -- /bin/bash")
# Step 2: 配置 IPSec SA(Security Association)
# 使用 setsockopt 配置 xfrm
# ...
# Step 3: 创建 UDP socket,发送包含目标文件页缓存的数据包
# 使用 MSG_SPLICE_PAGES 将 /etc/passwd 的页缓存页挂到 SKB
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
sock.sendmsg(..., MSG_SPLICE_PAGES)
# Step 4: 内核处理 ESP 数据包
# xfrm-ESP 收到数据包,进行解密
# 解密操作原地修改了 /etc/passwd 的页缓存页
# Step 5: 读取被修改的 /etc/passwd
# 发现内容已被解密后的数据覆盖
3.3 攻击路径 2:RxRPC 原地解密漏洞
RxRPC 是内核中用于 AFS(Andrew File System)的远程过程调用协议,同样存在原地解密的问题:
// net/rxrpc/rxkad.c
static int rxkad_decrypt_skb(struct rxrpc_call *call, struct sk_buff *skb) {
// RxRPC 的解密路径
// 同样存在对 SKB 碎片页进行原地解密的问题
// 如果 SKB 碎片页来自页缓存(MSG_SPLICE_PAGES 引入)
// 解密操作会修改页缓存
}
RxRPC 路径的影响范围较小,因为大多数发行版默认不加载 RxRPC 模块。但 xfrm-ESP 路径的影响极为广泛——几乎所有 Linux 发行版都默认启用了 IPSec/xfrm 支持。
3.4 Dirty Frag 的"无需特权"技巧
Copy Fail 需要普通用户权限,而 Dirty Frag 声称"无权限依赖"。关键在于 user namespace:
// 创建 user namespace,普通用户自动获得全部 capabilities
// 包括 CAP_NET_ADMIN,足以配置 IPSec SA
clone(CLONE_NEWUSER | CLONE_NEWNET);
// 在新的 namespace 中配置 xfrm
// 拥有 CAP_NET_ADMIN,可以创建 SA、SP
struct xfrm_usersa_info sa = {
.id.proto = IPPROTO_ESP,
.mode = XFRM_MODE_TRANSPORT,
// ...
};
send(xfrm_fd, &sa, sizeof(sa), 0);
这意味着攻击者甚至不需要真正拥有系统特权——只要能创建 user namespace(这是绝大多数 Linux 系统的默认配置),就能完成攻击。
3.5 微软监测到的在野利用
微软安全团队在 5 月 13 日公开警告,已监测到 Dirty Frag 的在野攻击:
攻击链路:
- 攻击者通过 SSH 暴力破解或钓鱼获取低权限 shell
- 上传 Dirty Frag 利用工具(ELF 二进制)
- 执行提权,获得 root
- 篡改 GLPI LDAP 认证文件,持久化后门
- 侦察系统配置,窃取敏感数据
四、Fragnesia(CVE-2026-46300):SKB 碎片合并的隐蔽缺陷
4.1 漏洞概览
Fragnesia 由 V12 Security 团队的 William Bowling 发现,2026 年 5 月 14 日公开披露。它属于 Dirty Frag 漏洞家族的变体,但利用的是不同的内核代码路径。
4.2 SKBFL_SHARED_FRAG 标记传播缺陷
SKB(Socket Buffer)是 Linux 内核网络子系统的核心数据结构。当网络数据包包含多个碎片(fragments)时,SKB 通过 frag_list 将它们串联起来。
每个 SKB 有一个 skb->flags 字段,其中 SKBFL_SHARED_FRAG 标记表示碎片页是否为共享页(如来自页缓存的页)。当碎片页被标记为共享时,内核在处理时应该避免对其进行原地修改。
// include/linux/skbuff.h
#define SKBFL_SHARED_FRAG (1 << 1)
// 当通过 MSG_SPLICE_PAGES 将页缓存页挂到 SKB 时
// SKB 会被正确标记 SKBFL_SHARED_FRAG
skb_shinfo(skb)->flags |= SKBFL_SHARED_FRAG;
漏洞就在碎片合并时:
// net/core/skbuff.c
struct sk_buff *__skb_pull_tail(struct sk_buff *skb, int delta) {
// 当需要合并 SKB 碎片时
struct sk_buff *frag = skb_shinfo(skb)->frag_list;
// 关键缺陷:合并碎片时没有传播 SKBFL_SHARED_FRAG 标记!
// 即使碎片的页来自页缓存(应该标记为共享)
// 合并后的 SKB 丢失了这个标记
// 内核认为这些碎片页是私有的,可以进行原地修改
// 合并操作
skb_shinfo(skb)->frags[i] = skb_shinfo(frag)->frags[j];
// ❌ 缺少:skb_shinfo(skb)->flags |= skb_shinfo(frag)->flags & SKBFL_SHARED_FRAG;
}
4.3 攻击流程
# Step 1: 创建 user + network namespace
os.system("unshare -Urn")
# Step 2: 配置 XFRM ESP-in-TCP
# ESP-in-TCP 是 IPSec 的 TCP 封装模式
# 数据包经过 TCP 层后会被 xfrm 子系统处理
# Step 3: 构造特殊的网络数据包
# - 使用 MSG_SPLICE_PAGES 将目标文件的页缓存页挂到 SKB
# - 构造数据包使其被分成多个碎片
# - 碎片经过 TCP 重组,触发 SKB 碎片合并
# - 合并过程中 SKBFL_SHARED_FRAG 标记丢失
# Step 4: xfrm-ESP 收到合并后的 SKB
# - 由于 SKBFL_SHARED_FRAG 标记已丢失
# - 内核认为碎片页是私有的
# - 执行原地解密,覆盖页缓存内容
# Step 5: 修改 /etc/passwd 或 SUID 二进制,提权到 root
4.4 与 Dirty Frag xfrm-ESP 路径的区别
| 维度 | Dirty Frag xfrm-ESP | Fragnesia |
|---|---|---|
| 根因 | 原地解密逻辑本身 | SKB 碎片合并时标记传播缺失 |
| 触发条件 | 直接发送含页缓存碎片的 SKB | 需要触发 SKB 碎片合并(如 TCP 重组) |
| 检测难度 | 较低(SKB 仍有 SHARED_FRAG 标记) | 更高(标记在合并过程中丢失,更隐蔽) |
| 修复方向 | 修改解密逻辑,检查 SHARED_FRAG | 修改合并逻辑,传播 SHARED_FRAG 标记 |
五、页缓存污染的底层机制深度解析
5.1 页缓存(Page Cache)工作原理
页缓存是 Linux 内核最核心的性能优化机制之一。当进程读取文件时,内核将文件内容缓存在内存中,后续读写直接操作内存,避免频繁磁盘 I/O。
// mm/filemap.c
// 读取文件时查找页缓存
struct page *pagecache_get_page(struct address_space *mapping,
pgoff_t offset, int fgp_flags,
gfp_t gfp_mask) {
// 在 radix tree(xarray)中查找
page = xarray_load(&mapping->i_pages, offset);
if (page && PageUptodate(page)) {
// 命中页缓存,直接返回
return page;
}
// 未命中,从磁盘读取
page = __page_cache_alloc(gfp_mask);
// ... 从磁盘读取数据到 page
// 加入页缓存
xarray_store(&mapping->i_pages, offset, page);
return page;
}
关键安全属性:
- 共享性:同一文件的同一页在内存中只有一份,所有进程共享
- 一致性:修改页缓存等于修改所有进程看到的文件内容
- 延迟写:修改页缓存后,磁盘写入是延迟的(dirty page writeback)
正是这三条属性,使得页缓存污染成为极具威胁的攻击向量:
- 修改页缓存 = 瞬间影响所有读取该文件的进程
- SUID 程序执行时从页缓存读取 = 执行被篡改的代码
- 延迟写意味着攻击者可以在脏页回写前撤销修改,不留痕迹
5.2 零拷贝路径的安全盲区
零拷贝(Zero-Copy)是 Linux 内核为高性能 I/O 设计的关键优化:
传统 I/O 路径:
文件 → [内核缓冲区] → [用户缓冲区] → [内核缓冲区] → socket
4 次数据复制,2 次上下文切换
零拷贝路径(splice):
文件 → [页缓存页] → [管道缓冲区] → socket
0 次数据复制,页缓存页直接在不同数据结构间传递
安全盲区在于:
// write() 路径的完整安全检查链
ssize_t vfs_write(struct file *file, ...) {
// 1. 检查 FMODE_WRITE
// 2. 检查文件 immutable/append-only 标志
// 3. 检查文件锁 (flock/lockf)
// 4. 检查 DAC/MAC 权限
// 5. 检查 SELinux/AppArmor 策略
// ... 然后才写入页缓存
}
// splice() 零拷贝路径
long do_splice(struct file *in, struct file *out, ...) {
// 仅检查:in 是否可读,out 是否可写
// ❌ 不检查目标页缓存页的写权限
// ❌ 不检查 immutable 标志
// ❌ 不检查文件锁
// 直接将页缓存页挂到管道/socket
}
5.3 "原地解密"为何危险
三个漏洞都利用了"原地解密"(In-place Decryption)。要理解为什么原地解密危险,需要理解 SKB 的内存管理:
// SKB 的两种数据存储方式:
// 1. 线性数据区(head room):skb->data ~ skb->tail
// 这是 SKB 自己分配的内存,修改安全
// 2. 碎片区(frags):skb_shinfo(skb)->frags[]
// 这是引用的外部页面,可能是页缓存页
// 当通过 MSG_SPLICE_PAGES 发送数据时
// 页缓存页被挂到 frags 中
static int skb_splice_pages(struct sk_buff *skb, struct page *page, ...) {
skb_frag_t *frag = &skb_shinfo(skb)->frags[n];
// 设置碎片指向页缓存页
__skb_frag_set_page(frag, page);
// 标记为共享碎片
skb_shinfo(skb)->flags |= SKBFL_SHARED_FRAG;
}
// 当内核处理加密/解密时
int crypto_encrypt(struct sk_buff *skb) {
if (skb_shinfo(skb)->flags & SKBFL_SHARED_FRAG) {
// 共享碎片页不能原地修改!
// 必须复制后再修改
skb_make_writable(skb); // 复制碎片页
}
// 现在可以安全修改了
do_encrypt(skb);
}
但问题是:Copy Fail、Dirty Frag、Fragnesia 三个漏洞都找到了绕过 skb_make_writable() 检查的方法:
- Copy Fail:通过 AF_ALG + splice 路径,碎片页直接进入加密子系统,跳过了 SKB 层的共享标记检查
- Dirty Frag:xfrm-ESP 解密路径没有检查 SKBFL_SHARED_FRAG
- Fragnesia:SKB 碎片合并丢失 SKBFL_SHARED_FRAG 标记,使检查失效
六、代码实战:漏洞检测与防护
6.1 检测系统是否受影响
#!/bin/bash
# dirty_frag_check.sh - 检测系统是否受 Copy Fail / Dirty Frag / Fragnesia 影响
echo "=== Linux 内核提权漏洞检测工具 ==="
echo ""
# 检查内核版本
KERNEL_VERSION=$(uname -r | cut -d. -f1-2)
KERNEL_MAJOR=$(echo $KERNEL_VERSION | cut -d. -f1)
KERNEL_MINOR=$(echo $KERNEL_VERSION | cut -d. -f2)
echo "[*] 当前内核版本: $(uname -r)"
# Copy Fail / Dirty Frag / Fragnesia 影响 4.14+ 内核
if [ "$KERNEL_MAJOR" -ge 5 ] || ([ "$KERNEL_MAJOR" -eq 4 ] && [ "$KERNEL_MINOR" -ge 14 ]); then
echo "[!] 内核版本在受影响范围内 (4.14+)"
else
echo "[✓] 内核版本不受影响 (4.14 之前)"
exit 0
fi
# 检查 AF_ALG 模块(Copy Fail)
if [ -d /proc/net/alg_type ]; then
echo "[!] AF_ALG 加密接口可用 - Copy Fail (CVE-2026-31431) 可能受影响"
else
echo "[✓] AF_ALG 加密接口不可用 - Copy Fail 不受影响"
fi
# 检查 xfrm-ESP 模块(Dirty Frag / Fragnesia)
if [ -d /proc/net/xfrm ]; then
echo "[!] XFRM/ESP 可用 - Dirty Frag (CVE-2026-43284/43500) 可能受影响"
else
echo "[✓] XFRM/ESP 不可用 - Dirty Frag xfrm-ESP 路径不受影响"
fi
# 检查 RxRPC 模块(Dirty Frag)
if lsmod | grep -q rxrpc; then
echo "[!] RxRPC 模块已加载 - Dirty Frag RxRPC 路径可能受影响"
else
echo "[✓] RxRPC 模块未加载 - Dirty Frag RxRPC 路径不受影响"
fi
# 检查 user namespace(影响攻击门槛)
if [ -f /proc/sys/kernel/unprivileged_userns_clone ]; then
UNS=$(cat /proc/sys/kernel/unprivileged_userns_clone)
if [ "$UNS" -eq 1 ]; then
echo "[!] User namespace 已启用 - 攻击门槛极低(无需特权)"
else
echo "[✓] User namespace 已禁用 - Dirty Frag 攻击需要真正特权"
fi
else
echo "[!] 无法确定 user namespace 状态(默认启用)"
fi
# 检查是否已打补丁
echo ""
echo "[*] 检查补丁状态..."
# 检查 Dirty Pipe 补丁(基础修复)
if grep -q "PIPE_BUF_FLAG_CAN_MERGE" /boot/config-$(uname -r) 2>/dev/null; then
echo "[?] 需要检查具体补丁版本,请对照发行版安全公告"
fi
echo ""
echo "=== 建议 ==="
echo "1. 升级内核到已修复版本"
echo "2. 限制 user namespace: sysctl kernel.unprivileged_userns_clone=0"
echo "3. 禁用不必要的内核模块: echo 'install af_alg /bin/true' >> /etc/modprobe.d/hardening.conf"
echo "4. 部署文件完整性监控(AIDE/OSSEC)"
echo "5. 启用 SELinux/AppArmor 限制"
6.2 紧急缓解措施(补丁前)
#!/bin/bash
# dirty_frag_mitigate.sh - 紧急缓解措施
echo "=== 应用紧急缓解措施 ==="
# 1. 禁用 user namespace(最有效的缓解,但可能影响容器运行时)
echo "[*] 禁用 user namespace..."
sysctl -w kernel.unprivileged_userns_clone=0
echo "kernel.unprivileged_userns_clone=0" >> /etc/sysctl.d/99-security.conf
# 2. 限制 AF_ALG 访问(缓解 Copy Fail)
echo "[*] 禁用 AF_ALG 模块..."
echo "install af_alg /bin/true" >> /etc/modprobe.d/security.conf
rmmod af_alg 2>/dev/null
# 3. 限制 xfrm-ESP(缓解 Dirty Frag)
# 注意:这可能影响 IPSec VPN
echo "[*] 设置 xfrm 网络命名空间隔离..."
sysctl -w net.core.xfrm_acq_expires=0 2>/dev/null
# 4. 启用 Seccomp 过滤,限制 splice 系统调用
echo "[*] 创建 Seccomp 配置文件..."
cat > /etc/seccomp/splice-filter.json << 'EOF'
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["splice"],
"action": "SCMP_ACT_LOG",
"comment": "Log splice() calls for monitoring"
}
]
}
EOF
# 5. 部署 Audit 规则监控关键操作
echo "[*] 部署 Audit 规则..."
cat > /etc/audit/rules.d/dirty-frag.rules << 'EOF'
# 监控 AF_ALG socket 创建
-a always,exit -F arch=b64 -F socket=AF_ALG -S socket -k af_alg_create
# 监控 splice 系统调用
-a always,exit -F arch=b64 -S splice -k splice_call
# 监控 /etc/passwd 修改
-w /etc/passwd -p wa -k passwd_modify
# 监控 SUID 程序执行
-a always,exit -F arch=b64 -S execve -F exit=-EPERM -k suid_blocked
EOF
augenrules --load
echo "=== 缓解措施已应用 ==="
echo "注意:这些是临时缓解措施,请尽快升级内核到已修复版本"
6.3 使用 eBPF 实时检测页缓存污染
// dirty_frag_detect.bpf.c - eBPF 检测页缓存污染
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
struct event {
u32 pid;
u32 uid;
char comm[16];
char file[64];
u64 ino;
u32 caller;
};
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24);
} events SEC(".maps");
// 监控 AF_ALG socket 创建
SEC("tracepoint/syscalls/sys_enter_socket")
int trace_socket(struct trace_event_raw_sys_enter *ctx) {
int domain = ctx->args[0];
int type = ctx->args[1];
if (domain == AF_ALG) {
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();
bpf_get_current_comm(&e->comm, sizeof(e->comm));
__builtin_memcpy(e->file, "AF_ALG_SOCKET", 14);
bpf_ringbuf_submit(e, 0);
}
return 0;
}
// 监控 splice 系统调用
SEC("tracepoint/syscalls/sys_enter_splice")
int trace_splice(struct trace_event_raw_sys_enter *ctx) {
int fd_in = ctx->args[0];
int fd_out = ctx->args[2];
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();
bpf_get_current_comm(&e->comm, sizeof(e->comm));
__builtin_memcpy(e->file, "SPLICE_CALL", 12);
bpf_ringbuf_submit(e, 0);
return 0;
}
// 监控页缓存脏页标记异常
SEC("fentry/mark_buffer_dirty")
int BPF_PROG(trace_dirty_page, struct buffer_head *bh) {
struct inode *inode = bh->b_page->mapping->host;
u32 uid = bpf_get_current_uid_gid();
// 如果非 root 进程将 SUID 文件的页缓存标记为脏
if (uid != 0 && inode->i_mode & S_ISUID) {
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e) return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
e->uid = uid;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
e->ino = inode->i_ino;
bpf_ringbuf_submit(e, 0);
}
return 0;
}
char LICENSE[] SEC("license") = "GPL";
配套的用户态程序:
#!/usr/bin/env python3
# dirty_frag_monitor.py - eBPF 监控用户态程序
from bcc import BPF
import time
bpf_text = open("dirty_frag_detect.bpf.c").read()
b = BPF(text=bpf_text)
print("=== Dirty Frag / Copy Fail / Fragnesia 实时监控 ===")
print("监控 AF_ALG socket 创建、splice 调用、SUID 文件页缓存修改")
print()
def handle_event(cpu, data, size):
event = b["events"].event(data)
if b"AF_ALG" in event.file:
print(f"[⚠ AF_ALG] pid={event.pid} uid={event.uid} comm={event.comm.decode()}")
elif b"SPLICE" in event.file:
print(f"[⚠ SPLICE] pid={event.pid} uid={event.uid} comm={event.comm.decode()}")
else:
print(f"[🚨 DIRTY_PAGE] pid={event.pid} uid={event.uid} comm={event.comm.decode()} "
f"ino={event.ino} - SUID 文件页缓存被非 root 进程修改!")
b["events"].open_ring_buffer(handle_event)
while True:
try:
b.ring_buffer_poll()
time.sleep(0.1)
except KeyboardInterrupt:
break
6.4 容器环境专项防护
# Kubernetes Pod 安全策略 - 防御 Dirty Frag 容器逃逸
apiVersion: v1
kind: Pod
metadata:
name: hardened-pod
spec:
securityContext:
# 禁止特权容器
privileged: false
# 禁止使用 user namespace(阻止 Dirty Frag 攻击链)
seccompProfile:
type: RuntimeDefault
# 只读根文件系统
readOnlyRootFilesystem: true
containers:
- name: app
image: app:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
# 不添加 CAP_NET_ADMIN,阻止配置 xfrm
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
# Docker 运行时防护
docker run \
--security-opt seccomp=seccomp-profile.json \
--security-opt no-new-privileges \
--cap-drop ALL \
--read-only \
--tmpfs /tmp:rw,nosuid,nodev \
app:latest
七、内核补丁分析:修复思路与不足
7.1 Copy Fail 的修复
// 修复方案:在 AF_ALG splice 路径中添加权限检查
// crypto/algif_aead.c
static int aead_sendmsg(struct socket *sock, struct msghdr *msg, size_t size) {
// 新增:检查是否通过 splice 引入了页缓存页
if (msg->msg_flags & MSG_SPLICE_PAGES) {
struct skb_shared_info *si = skb_shinfo(skb);
if (si->flags & SKBFL_SHARED_FRAG) {
// 页缓存页不能直接用于加密操作
// 必须复制到新的内存区域
if (skb_linearize(skb)) // 复制碎片数据到线性区
return -ENOMEM;
}
}
// ... 继续加密操作
}
7.2 Dirty Frag 的修复
// 修复方案 1:在 xfrm-ESP 解密前检查 SKBFL_SHARED_FRAG
// net/ipv4/esp4.c
static int esp_input(struct xfrm_state *x, struct sk_buff *skb) {
// 新增:检查碎片页是否来自页缓存
if (skb_shinfo(skb)->flags & SKBFL_SHARED_FRAG) {
// 共享碎片页不能原地解密,必须复制
if (skb_linearize(skb))
return -ENOMEM;
}
// ... 继续解密操作
}
// 修复方案 2(更彻底):限制 MSG_SPLICE_PAGES 对页缓存页的使用
// net/core/skbuff.c
int skb_splice_from_iter(struct sock *sk, struct sk_buff *skb,
struct iov_iter *iter, size_t size) {
// 新增:如果页面来自页缓存,强制复制
if (PageUptodate(page) && page_mapping(page)) {
// 这是页缓存页,不允许直接引用
// 必须复制数据
copy_page_to_skb(skb, page, offset, size);
} else {
// 非页缓存页,可以安全引用
skb_fill_page_desc(skb, frag, page, offset, size);
}
}
7.3 Fragnesia 的修复
// 修复方案:在 SKB 碎片合并时传播 SKBFL_SHARED_FRAG 标记
// net/core/skbuff.c
struct sk_buff *__skb_pull_tail(struct sk_buff *skb, int delta) {
struct sk_buff *frag = skb_shinfo(skb)->frag_list;
// 合并碎片
for (i = 0; i < skb_shinfo(frag)->nr_frags; i++) {
skb_shinfo(skb)->frags[k] = skb_shinfo(frag)->frags[i];
k++;
}
// 关键修复:传播共享标记
+ if (skb_shinfo(frag)->flags & SKBFL_SHARED_FRAG)
+ skb_shinfo(skb)->flags |= SKBFL_SHARED_FRAG;
return skb;
}
7.4 根本性修复的思考
当前的修复都是"打补丁"式的——在每个发现漏洞的路径上添加检查。但这种方式的局限性很明显:
- 穷举困难:内核中有大量零拷贝路径,不可能逐一检查
- 维护负担:每个新路径都需要记住添加检查
- 遗漏风险:未来新代码可能再次引入类似问题
更根本的修复应该是:在页缓存层面建立写入保护机制:
// 方案:为页缓存页添加不可变标记
// mm/filemap.c
struct page *pagecache_get_page(...) {
page = xarray_load(&mapping->i_pages, offset);
// 新增:如果文件被标记为"页缓存不可变"
// 所有零拷贝路径都不能引用这个页
if (mapping->flags & AS_IMMUTABLE_CACHE) {
// 零拷贝路径被拒绝,必须走正常 write() 路径
return NULL; // 或返回需要复制的标志
}
return page;
}
// 为关键文件启用页缓存不可变
void mark_file_cache_immutable(struct file *file) {
file->f_mapping->flags |= AS_IMMUTABLE_CACHE;
}
// 在系统启动时标记所有 SUID/SGID 文件
void __init init_suid_cache_protection(void) {
// 遍历文件系统,标记所有 SUID 文件
// 这些文件的页缓存页不允许通过零拷贝路径修改
}
八、安全架构演进:从"头痛医头"到系统性防御
8.1 当前 Linux 安全模型的不足
这三个漏洞暴露了 Linux 安全模型的系统性问题:
问题 1:权限检查不完整
write() 路径有完整的权限检查链,但零拷贝路径只有最基本的检查。这本质上是接口契约不一致——同样是修改文件内容,不同的系统调用路径有不同的安全保证。
问题 2:命名空间隔离的边界模糊
User namespace 允许普通用户获取 CAP_NET_ADMIN,这本意是给容器运行时使用的。但 Dirty Frag 证明,namespace 内的 capabilities 可以被利用来攻击共享的内核数据结构(页缓存)。
问题 3:SKB 内存安全缺乏系统性保障
SKB 的碎片页可能来自页缓存,但内核在处理 SKB 时没有统一的安全策略来处理"共享 vs 私有"的问题。
8.2 防御深度体系
┌─────────────────────────────────────────────────────┐
│ Layer 5: 运行时检测 │
│ eBPF 实时监控 + Audit 规则 + SIEM 关联分析 │
├─────────────────────────────────────────────────────┤
│ Layer 4: 访问控制 │
│ Seccomp + AppArmor/SELinux + Capabilities 限制 │
├─────────────────────────────────────────────────────┤
│ Layer 3: 内核加固 │
│ User namespace 限制 + 模块黑名单 + 页缓存保护 │
├─────────────────────────────────────────────────────┤
│ Layer 2: 内核补丁 │
│ 零拷贝路径权限检查 + SKB 标记传播 + 原地操作防护 │
├─────────────────────────────────────────────────────┤
│ Layer 1: 架构重构 │
│ 页缓存不可变机制 + 零拷贝安全框架 + 统一写入策略 │
└─────────────────────────────────────────────────────┘
8.3 给运维团队的实战清单
## Dirty Frag / Copy Fail / Fragnesia 应急响应清单
### 立即执行(0-24 小时)
- [ ] 确认内核版本是否在受影响范围(4.14+)
- [ ] 禁用 user namespace(如不影响业务)
- [ ] 禁用 AF_ALG 模块
- [ ] 部署 Audit 规则监控 splice/AF_ALG 活动
- [ ] 通知安全团队,评估暴露面
### 短期措施(1-7 天)
- [ ] 升级内核到已修复版本
- [ ] 部署 eBPF 检测程序
- [ ] 审计容器运行时配置
- [ ] 检查 SUID/SGID 文件列表
- [ ] 验证文件完整性基线
### 中期优化(1-4 周)
- [ ] 实施 Seccomp 白名单策略
- [ ] 加固 Kubernetes Pod 安全策略
- [ ] 部署 SIEM 规则关联分析
- [ ] 建立内核漏洞应急响应流程
- [ ] 评估 ARM64 / RISC-V 等替代架构的影响面
### 长期演进(1-3 个月)
- [ ] 推动 Linux 内核安全上游贡献
- [ ] 评估页缓存保护方案
- [ ] 建立内核模块最小化策略
- [ ] 实施零信任主机安全模型
九、总结与展望
9.1 漏洞启示
从 Copy Fail 到 Dirty Frag 再到 Fragnesia,三周三个高危漏洞,不是偶然,而是 Linux 内核零拷贝架构长期积累的技术债务集中爆发:
- 性能优化与安全校验的张力:零拷贝的核心价值是"减少复制",但安全校验往往需要"增加检查"——二者天然矛盾
- 子系统边界的模糊:网络、加密、文件系统三个子系统的交互点是最容易出问题的地方
- 补丁式修复的局限:每个漏洞的修复都是针对特定路径的,而不是解决根因
9.2 未来方向
Linux 内核社区正在推进几个根本性的安全改进:
- Memory ownership 模型:Rust for Linux 项目引入的 ownership 语义,有望在编译期捕获页缓存的非法写入
- Unified write path:将零拷贝路径统一到
write()的安全检查框架下,消除接口契约不一致 - Page Cache protection:为页缓存页添加运行时保护标记,阻止未授权修改
- eBPF-based LSM:利用 eBPF 实现动态安全策略,实时拦截可疑的零拷贝操作
9.3 给开发者的忠告
- 永远不要假设内核路径是安全的:零拷贝、splice、sendfile 这些"优化路径"可能有独立的安全盲区
- 最小权限原则:容器只给需要的 capabilities,不要用
--privileged - 监控先行:在生产环境部署 eBPF/Audit 监控,在攻击者之前发现问题
- 及时打补丁:内核安全补丁不是"可选的",是必须的
- 纵深防御:单一防护层永远不够,需要多层防御互相补位
附录
A. CVE 编号索引
| CVE | 漏洞代号 | 影响内核版本 | CVSS |
|---|---|---|---|
| CVE-2026-31431 | Copy Fail | 4.14+ | 7.8 |
| CVE-2026-43284 | Dirty Frag (xfrm-ESP) | 4.14+ | 7.8 |
| CVE-2026-43500 | Dirty Frag (RxRPC) | 6.2+ | 7.8 |
| CVE-2026-46300 | Fragnesia | 4.14+ | 7.8 |
B. 参考链接
- Linux 内核安全邮件列表:https://lore.kernel.org/linux-crypto/
- Dirty Frag PoC:https://github.com/v4bel/Dirty-Frag
- 微软安全博客 Dirty Frag 分析:https://www.microsoft.com/en-us/security/blog/
- NIST NVD 漏洞数据库:https://nvd.nist.gov/
- Linux 内核补丁提交:https://git.kernel.org/
C. 相关工具
| 工具 | 用途 |
|---|---|
| AIDE | 文件完整性检测 |
| OSSEC | 主机入侵检测 |
| Falco | 容器运行时安全 |
| BPFTrace | 内核动态追踪 |
| Auditd | 系统调用审计 |