从内核深处拆解 Copy Fail:CVE-2026-31431 为什么是 2026 年最值得吃透的漏洞
前言:一个 732 字节的漏洞如何撬动整个 Linux 生态
2026 年 4 月 29 日,韩国安全公司 Theori 披露了一个让整个 Linux 社区倒吸一口凉气的漏洞:CVE-2026-31431,代号 Copy Fail。
一个 732 字节的 Python 脚本,可以让任意普通 Linux 用户在几秒内拿到 root shell。成功率不是 90%,不是 99%,是 100%。不需要竞态条件,不需要复杂的堆风水,不需要找到任何 ROP gadget,不需要绕过 KASLR/ASLR,不需要内核信息泄漏。
这个漏洞的特殊之处不在于"又能提权"——Linux 内核提权漏洞年年有。这个漏洞的特殊之处在于:它攻击的是内核加密子系统里一条看似无害的优化路径,根植于 2017 年的一行代码,潜伏了近 9 年才被发现。
本文不打算写成又一篇"升级内核吧"的通告。我想从系统程序员 / SRE / 安全工程师的视角,带你真正理解这个漏洞在底层是怎么工作的:内核的 authencesn 加密模板为什么要做 in-place 优化?splice() 的零拷贝语义为什么在这里成了攻击面?page cache 的共享特性如何被利用来篡改 setuid 程序?以及,作为工程师,我们怎么从代码层理解、检测和防御它。
一、背景:三个 Linux 核心子系统如何被串成攻击链
1.1 AF_ALG:内核加密能力的用户态入口
Linux 内核从 2.5.45 起引入了 Kernel Crypto API(include/crypto/),为整个内核提供统一的密码学原语。设计上分为三层:
应用层 / 用户态库(OpenSSL, GnuTLS, NSS)
↓ 系统调用(socket/bind/send/recv)
AF_ALG Socket 接口(net/ipc/algif_aead.c)
↓ 算法查找(crypto_alloc_xx)
通用加密算法接口(crypto_ahash, crypto_aead)
↓ 平台相关指令(AES-NI, ARM CE, 软件实现)
具体算法实现(aes, sha256, gcm...)
AF_ALG 是内核向用户态暴露加密能力的 socket 接口(地址族编号 38)。用户态程序可以像使用普通 socket 一样,调用 bind/accept/send/recv 来访问内核加密服务。这套接口的设计初衷是让 TLS 库能把加解密 offload 给内核的硬件加速器,同时保持代码的可移植性。
import socket
# 创建 AF_ALG socket——就像创建一个 TCP socket 一样简单
s = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
# bind 时指定算法,内核会在 crypto 子系统中查找对应实例
# authencesn 不是具体算法,是一个"算法构造器模板"
s.bind(('aead', 'authencesn(hmac(sha256),cbc(aes))'))
# 设置密钥
s.setockopt(socket.SOL_ALG, socket.ALG_SET_KEY, key)
# 发送数据加解密
s.sendmsg([plaintext], [])
关键点:以上操作完全在用户态完成,不需要 root 权限。 在大多数 Linux 发行版上,非特权用户默认就能创建 AF_ALG socket。只有在管理员通过 SELinux/AppArmor/seccomp 显式限制后,普通用户才无法使用。
1.2 authencesn:一条为协议加密而生的模板链
authencesn 全称 "Authenticated Encryption with Associated Data using ESN",是内核 crypto API 中一个算法模板(template),而不是一个独立算法。它的核心用途是 IPsec、WireGuard、DTLS 等网络协议的加密层——这些协议需要同时满足三个条件:
- 加密:数据内容保密(CBC-AES 对称加密)
- 认证:确保数据没有被篡改(HMAC-SHA 完整性校验)
- 防重放:通过扩展序列号(ESN)机制处理 64 位序列号
它的内部构造如下:
明文
↓
CBC-AES 加密(对称加密,数据保密)
↓
HMAC-SHA256 认证(对 密文 + AAD 生成标签)
↓
密文 || Authentication Tag(一次性输出)
在 /proc/crypto 里你看到的是动态组合形式:
name : authenc(hmac(sha256),cbc(aes))
driver : authenc(hmac(sha256-generic,cbc-aes-generic))
module : authencesn
重要:内核中实际注册的是 authencesn(带 ESN 的增强版),对应模板 crypto/authencesn.c。ESN 主要用于 IPv6 IPsec 场景,防止序列号回绕。
1.3 2017 年的 in-place 优化:一切的起点
2017 年,内核 commit 9d1f0d6e 在 authencesn 模块中引入了一个性能优化:in-place 加密解密。
传统加密(out-of-place)需要两个独立的缓冲区:
// 传统 out-of-place 加密
struct scatterlist src[1], dst[1];
sg_set_buf(src, plaintext, len); // 读:明文缓冲区
sg_set_buf(dst, ciphertext_out, len); // 写:密文输出缓冲区
crypto_aead_encrypt(req, src, dst, len); // 数据从 src → dst,中间经过加密
In-place 加密(单缓冲区,原地覆盖):
// In-place 加密
struct scatterlist sg[1];
sg_set_buf(sg, buffer, len); // 同一块缓冲区,既是源也是目标
crypto_aead_encrypt(req, sg, sg, len); // 内核理解:原地加密,不需要临时缓冲
性能收益在哪里?减少一次 memcpy() 意味着:
- 减少 CPU 缓存抖动
- 减少内存带宽占用
- 在高吞吐网络栈(IPsec、TLS offload)中效果可观
但 in-place 路径引入了关键的内存别名(aliasing)问题:scatterlist 的读端(sg_src)和写端(sg_dst)指向同一块内存区域。当内核后续代码需要区分"读"和"写"的 scatterlist 时,处理逻辑变得复杂。
1.4 splice():零拷贝的双刃剑
splice() 是 Linux 2.6.17 引入的系统调用,用于在两个文件描述符之间零拷贝移动数据:
#include <sys/splice.h>
ssize_t splice(int fd_in, loff_t *off_in,
int fd_out, loff_t *off_out,
size_t len, unsigned int flags);
与普通 read/write 的本质区别:
read(fd) → 内核复制 page cache 内容 → 用户缓冲区
write(fd) ← 用户缓冲区 → 内核复制 page cache
splice(fd_in → fd_out) → 内核转移 page 引用 → 无用户态复制
splice() 的核心实现依赖 pipe buffer 的引用语义:当数据从文件 splice 到管道时,管道 buffer 并不持有数据的副本,而是持有一个指向原始 page cache 页的引用计数指针。这使得数据在两个 fd 之间的移动不需要任何物理内存拷贝——只是"引用"的转移。
问题:这个引用语义在 Copy Fail 场景中成了攻击面——当一个进程修改了 pipe buffer 持有的 page cache 引用,其他引用同一 page 的进程也会看到变化。
二、攻击链路解剖:四步完成从普通用户到 root
2.1 攻击概览
┌──────────────────────────────────────────────────────────────────────┐
│ 攻击链路 │
│ │
│ [普通用户] open('/usr/bin/su') → page cache 持有文件内容 │
│ │ │
│ │ splice() 进管道 → pipe buffer 持有 page 引用 │
│ │ │
│ │ AF_ALG authencesn 解密 → scatterlist dst == page cache页│
│ │ ↓ │
│ │ 解密输出写入 page cache! │
│ │ │
│ └→ execve('/usr/bin/su') → 从已被篡改的 page cache 读取 │
│ 跳转到 root shell │
└──────────────────────────────────────────────────────────────────────┘
2.2 完整攻击流程
准备阶段:构造 page cache 陷阱
imp