编程 Linux 内核提权风暴:从 Copy Fail 到 Dirty Frag 再到 Fragnesia——零拷贝页缓存污染漏洞家族的终极解剖

2026-05-19 06:45:04 +0800 CST views 17

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 CowCVE-2016-519520169 年COW 写时复制需要~80%
Dirty PipeCVE-2022-084720222 年pipe + splice()不需要100%
Copy FailCVE-2026-314312026.49 年AF_ALG + splice()不需要100%
Dirty Frag (xfrm)CVE-2026-432842026.59 年xfrm-ESP + MSG_SPLICE_PAGES不需要100%
Dirty Frag (RxRPC)CVE-2026-435002026.53 年RxRPC + MSG_SPLICE_PAGES不需要100%
FragnesiaCVE-2026-463002026.59 年XFRM ESP-in-TCP + SKB 合并不需要100%

可以看到一条清晰的演进脉络:

  1. Dirty Cow:需要竞争条件,成功率受 CPU 调度影响,但开创了"篡改只读页缓存"的先河
  2. Dirty Pipe:消除竞争条件,利用管道的 PIPE_BUF_FLAG_CAN_MERGE 标记,实现确定性写入
  3. Copy Fail:将攻击面从管道扩展到加密子系统,证明 Dirty Pipe 的修复只是"头痛医头"
  4. Dirty Frag:将攻击面进一步扩展到网络协议栈,攻击路径更多、门槛更低
  5. Fragnesia:Dirty Frag 的变体,利用 SKB 碎片合并的标记传播缺陷,攻击更隐蔽

1.2 核心共同点:零拷贝路径的页缓存写入失控

所有这些漏洞的本质相同:Linux 内核在零拷贝(Zero-Copy)优化路径上,允许数据直接进入页缓存(Page Cache),却缺少对写入权限的充分校验

正常情况下,修改磁盘文件需要:

  1. 用户进程拥有文件写权限
  2. 通过 write() 系统调用进入内核
  3. 内核检查权限后修改页缓存
  4. 页缓存脏页回写磁盘

但零拷贝路径(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 FailDirty 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_PAGESsendmsg() 的标志,允许将页缓存页直接作为网络数据发送:

// 用户态代码
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 的在野攻击:

攻击链路:

  1. 攻击者通过 SSH 暴力破解或钓鱼获取低权限 shell
  2. 上传 Dirty Frag 利用工具(ELF 二进制)
  3. 执行提权,获得 root
  4. 篡改 GLPI LDAP 认证文件,持久化后门
  5. 侦察系统配置,窃取敏感数据

四、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-ESPFragnesia
根因原地解密逻辑本身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;
}

关键安全属性

  1. 共享性:同一文件的同一页在内存中只有一份,所有进程共享
  2. 一致性:修改页缓存等于修改所有进程看到的文件内容
  3. 延迟写:修改页缓存后,磁盘写入是延迟的(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 根本性修复的思考

当前的修复都是"打补丁"式的——在每个发现漏洞的路径上添加检查。但这种方式的局限性很明显:

  1. 穷举困难:内核中有大量零拷贝路径,不可能逐一检查
  2. 维护负担:每个新路径都需要记住添加检查
  3. 遗漏风险:未来新代码可能再次引入类似问题

更根本的修复应该是:在页缓存层面建立写入保护机制

// 方案:为页缓存页添加不可变标记
// 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 内核零拷贝架构长期积累的技术债务集中爆发:

  1. 性能优化与安全校验的张力:零拷贝的核心价值是"减少复制",但安全校验往往需要"增加检查"——二者天然矛盾
  2. 子系统边界的模糊:网络、加密、文件系统三个子系统的交互点是最容易出问题的地方
  3. 补丁式修复的局限:每个漏洞的修复都是针对特定路径的,而不是解决根因

9.2 未来方向

Linux 内核社区正在推进几个根本性的安全改进:

  1. Memory ownership 模型:Rust for Linux 项目引入的 ownership 语义,有望在编译期捕获页缓存的非法写入
  2. Unified write path:将零拷贝路径统一到 write() 的安全检查框架下,消除接口契约不一致
  3. Page Cache protection:为页缓存页添加运行时保护标记,阻止未授权修改
  4. eBPF-based LSM:利用 eBPF 实现动态安全策略,实时拦截可疑的零拷贝操作

9.3 给开发者的忠告

  1. 永远不要假设内核路径是安全的:零拷贝、splice、sendfile 这些"优化路径"可能有独立的安全盲区
  2. 最小权限原则:容器只给需要的 capabilities,不要用 --privileged
  3. 监控先行:在生产环境部署 eBPF/Audit 监控,在攻击者之前发现问题
  4. 及时打补丁:内核安全补丁不是"可选的",是必须的
  5. 纵深防御:单一防护层永远不够,需要多层防御互相补位

附录

A. CVE 编号索引

CVE漏洞代号影响内核版本CVSS
CVE-2026-31431Copy Fail4.14+7.8
CVE-2026-43284Dirty Frag (xfrm-ESP)4.14+7.8
CVE-2026-43500Dirty Frag (RxRPC)6.2+7.8
CVE-2026-46300Fragnesia4.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系统调用审计

推荐文章

JavaScript设计模式:单例模式
2024-11-18 10:57:41 +0800 CST
JavaScript设计模式:适配器模式
2024-11-18 17:51:43 +0800 CST
动态渐变背景
2024-11-19 01:49:50 +0800 CST
Hypothesis是一个强大的Python测试库
2024-11-19 04:31:30 +0800 CST
避免 Go 语言中的接口污染
2024-11-19 05:20:53 +0800 CST
php微信文章推广管理系统
2024-11-19 00:50:36 +0800 CST
20个超实用的CSS动画库
2024-11-18 07:23:12 +0800 CST
mysql 计算附近的人
2024-11-18 13:51:11 +0800 CST
ElasticSearch 结构
2024-11-18 10:05:24 +0800 CST
前端如何给页面添加水印
2024-11-19 07:12:56 +0800 CST
程序员茄子在线接单