🎯 CVE-2026-31431 深度技术分析:漏洞根因 · PoC/EXP · 检测指纹
CVE-2026-31431 深度技术分析
摘要:CVE-2026-31431(又名 “Copy Fail”)是 Linux 内核 crypto 子系统 algif_aead 模块中的一个高危漏洞,CVSS 评分为 7.8(HIGH)。该漏洞源于 AF_ALG 接口对 AEAD 操作错误地采用“原地(in-place)”模式,结合 splice() 机制导致无特权容器工作负载能够覆盖任意文件页缓存,从而在宿主机上实现本地提权。Talos Linux 上可进一步链接触发完整节点沦陷。
📌 漏洞概述
- CVE 编号:CVE-2026-31431
- CVSS 评分:7.8(HIGH)
- 漏洞类型:内核 crypto 子系统设计缺陷导致的页缓存越权写入,可被用于本地提权。
- 受影响组件:Linux 内核 crypto/algif_aead,特别是引入 commit
72548b093ee3之后的内核版本。 - 影响版本:包含该 commit 且尚未应用修复(revert)的 Linux 内核;Talos Linux 等基于受影响内核的发行版均受影响。
- 公开 PoC:已有多个独立实现,如
copy-fail-c、DIRTYFAIL、ptrace_may_dream,披露后迅速出现 C 语言移植版和变体。
🔬 漏洞根因分析
漏洞的核心根因是 algif_aead 在 AF_ALG 接口中错误地采用了原地(in-place)AEAD 操作。提交 72548b093ee3 为内核添加了原地操作逻辑,并引入了大量相关复杂度。然而这一优化在 AF_ALG 场景下没有任何收益,因为 AF_ALG 的源数据(通过 splice() 从用户文件页缓存映射)和目的缓冲区(内核分配的输出区域)来自不同的内存映射,原地操作只会导致映射冲突。
具体攻击机制如下:攻击者创建一个 AF_ALG 套接字,选择 authencsn 模板,并构造一个特殊的 AEAD 解密请求。密文输入并非复制到内核缓冲区,而是通过 splice() 直接引用文件页缓存页面(例如只读二进制文件或 /etc/passwd 的页缓存)。在原本的设计中,AEAD 解密应先验证认证标签,认证失败则拒绝解密。但由于原地优化的缺陷,内核在验证标签之前或验证过程中,已经将(注定失败的)解密结果写回源页面——即攻击者指定的页缓存页面。认证失败只导致系统调用返回错误,但页缓存中的内容已经被修改。
由于页缓存是内核与文件系统共享的缓存层,任何进程通过内存映射或读取操作访问该文件时,都会优先看到被篡改的页缓存内容。攻击者可以利用这一原语,逐字节或按 4 字节窗口反复“修补”目标文件,例如将 setuid 二进制文件中的指令替换为恶意 payload,或者修改 /etc/passwd 中的 UID 字段。公开 PoC(如 copy-fail-c)展示了两种变体:一种变异 setuid 二进制映像,另一种仅翻转 /etc/passwd 中的 UID。整个过程无需内核调试、无需竞态条件、无需任何特权。
漏洞的根本问题在于:crypto API 的原地(in-place)操作假设与 AF_ALG 的 splice 页面映射模型不兼容。splice 引入的页面既是输入又是输出目标,而页缓存页面通常是只读映射,内核并未在解密前对其进行正确的写保护或隔离。官方修复方式是 revert 72548b093ee3,恢复为 out-of-place 操作,并直接复制关联数据(AD),彻底移除原地操作链路。
💥 影响与危害
- 无特权容器逃逸:攻击者仅需具备创建 Kubernetes Pod 的能力,无需任何额外权限,即可在宿主机的任意文件页缓存上执行写操作。
- 任意文件页缓存篡改:可篡改只读文件(如
/usr/sbin/nft、CNI 二进制、特权 Pod 内共享的二进制)的页缓存,使磁盘 inode 保持不变但运行时内容被替换。 - 宿主机 root 代码执行:在 Talos Linux 等场景中,攻击者通过污染运行于高权限上下文的二进制(如 kube-proxy 或 CNI 插件),可在这些进程运行时执行恶意代码,获得宿主机 root 权限。
- 节点机密泄露:一旦获得宿主机 root,攻击者可以访问节点上的所有文件系统,包括 Kubernetes 节点秘密、ServiceAccount token、配置中的凭据等。
- 供应链风险:由于页缓存篡改不影响磁盘校验和,安全扫描和完整性监控可能无法发现被修改的进程映像,增加了隐蔽性。
- 影响面:所有使用受影响 Linux 内核且允许无特权用户访问 AF_ALG 套接字的环境均面临本地提权风险。CISA KEV 尚未收录,但公开 PoC 已使得漏洞利用门槛大幅降低。
🛡️ 修复与缓解
- 官方修复:内核社区已 revert commit
72548b093ee3,恢复algif_aead的 out-of-place 操作模式,并保留 AD 拷贝逻辑。各发行版应及时同步该修复。 - 升级内核:将内核更新至包含该 revert 的版本。对于 Talos Linux,应升级到官方发布的修复版本。
- 限制 AF_ALG 访问:在容器和 Kubernetes Pod 中,通过 seccomp 或 LSM 过滤
socket(AF_ALG, ...)以及splice()相关系统调用,阻止未特权容器使用该接口。 - 限制页缓存映射攻击面:对于关键宿主二进制和配置,可将其放置在只读文件系统或以其他方式避免使用页缓存(实际效果有限,核心仍需升级修复)。
- 监控可疑行为:关注 AF_ALG 套接字创建、splice 对敏感文件的映射以及文件页缓存异常变化(如基于 page cache 的完整性检测)等异常事件。
- 临时缓解措施:在无法立即升级的情况下,可通过内核启动参数或模块黑名单禁用
algif_aead(需评估业务依赖),或限制容器内用户对 /dev/crypto 等设备的访问权限。
🧪 PoC 复现
从 GitHub 公开仓库抓取的实际 PoC 代码(仓库)。
📋 代码元数据语言md来源tgies/copy-fail-c针对性✅ 已验证与漏洞相关(代码含 CVE 引用)依赖见代码注释/README用法详见代码注释中的使用说明
# Copy Fail (CVE-2026-31431) - C port
*[English (en)](README.md) ∙ [日本語 (ja)](README.ja.md) ∙ [简体中文 (zh-cn)](README.zh-cn.md) ∙ [한국어 (ko)](README.ko.md) ∙ [Русский (ru)](README.ru.md)*
Copy Fail Linux 本地提权 (CVE-2026-31431) 的跨平台 C 语言重写版。该漏洞由 Theori / Xint 于 2026 年 4 月 29 日披露。有关完整的漏洞描述、时间线以及 Theori 的发现过程,请参阅 [copy.fail](https://copy.fail/) 上的官方报告。
公开发布的概念验证 (PoC) 是一个 732 字节的 Python 脚本。这个 C 语言移植版证明了相同的漏洞利用可以表示为可移植的 C 代码,并且可以编译到 nolibc 支持的任何架构,而无需在项目自己的源代码中包含特定架构的十六进制 blob 或内联汇编。
此移植版的作者: Tony Gies <tony.gies@crashunited.com>
发现和最初披露: Theori / Xint
## 仓库结构
```
copy-fail-c/
├── exploit.c 释放器 (二进制文件变异变体)
├── exploit-passwd.c 释放器 (/etc/passwd UID 翻转变体)
├── vulnerable.c 非破坏性漏洞检查器
├── payload.c 被释放的有效载荷主体 (setgid+setuid+execve sh)
├── utils.c,
utils.h 共享的 AF_ALG/splice 页缓存变异原语
├── Makefile 构建编排
├── nolibc/ 从 torvalds/linux tools/include/nolibc 引入的代码
└── README.md 本文件
```
运行 `make` 后:
```
├── payload 微小的静态 ELF,作为字节嵌入到释放器中
├── payload.o 通过 `ld -r -b binary` 包装为可重定位 .o 的有效载荷
├── exploit 释放器,二进制文件变异变体
├── exploit-passwd 释放器,/etc/passwd UID 翻转变体
└── vulnerable 非破坏性漏洞检查器
```
`exploit.c` 以只读方式打开目标二进制文件,然后对于嵌入的有效载荷的每个 4 字节窗口,通过 AF_ALG 运行一次伪造的 AEAD 解密。其密文输入是通过 splice() 从目标的页缓存页面提供的。authencesn 模板的原地优化将 splice 的源页面同时视为密文输入和明文目标地址,因此在身份验证验证拒绝该请求时,(注定失败的)解密已经覆盖了页缓存页面。经过 4 * N 次迭代后,目标的缓存映像已被有效载荷逐字节替换。对目标执行 execve() 会加载已变异的页面;磁盘上的 inode 仍然设置了 setuid root,因此内核授予 root 凭据并运行有效载荷。
`payload.c` 是纯可移植的 C 语言: `setgid(0);
setuid(0);execve("/bin/sh",
...)`。nolibc 提供了 `_start`、系统调用机制以及特定架构的寄存器操作。
第二个变体 `exploit-passwd.c` 修改了 /etc/passwd 页缓存的四个字节,而不是 setuid 二进制文件的映像。它不需要嵌入有效载荷,可以在二进制变异路线被阻止的系统上工作,但其变现(达到提权目的)面要窄得多。
`vulnerable.c` 不是漏洞利用程序。它在工作目录中创建一个包含字符串 `init` 的本地 `testfile`,然后对该文件自身的页缓存运行相同的 `patch_chunk()` 原语,尝试将字节覆写为 `vulnerable`。如果读回的内容匹配,则当前运行的内核处于 CVE-2026-31431 的受影响范围内。磁盘上的 inode 从未被修改;`testfile` 会在退出时被删除,页缓存的变异也随之消失。可以在无特权的情况下运行。退出码为:存在漏洞时 100;原语已运行但未发生变异时 0;AF_ALG 套接字族或 authencesn 模板不可用、因此无法判断补丁状态时 2;其他运行时错误时 1。
## 构建
默认 (宿主架构原生):
```sh
make
```
交叉编译到 aarch64 (或已安装交叉工具链的任何其他 Linux 架构):
```sh
make CC=aarch64-linux-gnu-gcc LD=aarch64-linux-gnu-ld
```
引入的 nolibc 支持的架构 (根据上游): x86_64,
i386,arm,aarch64,riscv32/64,mips,ppc,s390x,loongarch,m68k,sh,sparc。nolibc 根据编译器的架构宏选择实现,因此只需选择正确的 `CC`/`LD` 即可。
构建要求:
* C 编译器 (`cc`,`gcc`,或任何交叉变体)
* 支持 `ld -r -b binary` 的链接器 (binutils ld 和 lld 均支持)
* 提供 `linux/if_alg.h` 和 `<asm/unistd.h>` 的内核 UAPI 头文件 (Debian/Ubuntu: `linux-libc-dev`;
交叉变体: 通常由交叉工具链包引入)
早于 Linux 5.6 的头文件集尚未包含引入的 nolibc 所使用的 `__kernel_old_time_t` 和 `struct __kernel_old_timespec`。`compat.h` (被强制包含到有效载荷构建中) 在它们缺失时提供这些定义,因此较旧的 `linux-libc-dev` 仍能完成构建。在 5.6 及更新的头文件上它是空操作。
没有外部库依赖。有效载荷是针对 nolibc 独立 (freestanding) 构建的;释放器仅为了 `fprintf` 和 `perror` 链接到宿主 libc。
## 架构选择
有几个微小的工具链特性在保持源代码可移植性和有效载荷小巧方面发挥了主要作用。
### nolibc
`nolibc/` 是内核的微型仅头文件 libc 替代品,源自 torvalds/linux 的 `tools/include/nolibc/`。它提供了 `_start`、可移植的 `syscall()` 宏以及内联系统调用包装器,特定架构的寄存器约定编码在 `nolibc/arch-*.h` 中。使用 `-nostdlib -static -ffreestanding -Inolibc` 构建有效载荷会生成一个微小的静态 ELF,直接调用内核,而不会引入 glibc 启动、TLS 初始化或 stack-canary 机制。结果:经过打包和剥离节头 (两者都在下文说明) 之后,x86_64 上约为 720 字节,aarch64 上约为 1.2 KB。相比之下,相同的 `payload.c` 链接到 musl-static 约为 17 KB,链接到 glibc-static 约为 700 KB。
### 用于嵌入的 `ld -r -b binary`
Makefile 通过 `ld -r -b binary -o payload.o payload` 将构建的 `payload` ELF 转换为 `payload.o`。链接器将输入字节原样作为可重定位目标文件的数据段发出,并从输入文件名合成三个符号:
```
_binary_payload_start 第一个有效载荷字节的地址
_binary_payload_end 最后一个有效载荷字节之后的一个地址
_binary_payload_size 绝对符号,其值为以字节为单位的大小
```
`exploit.c` 将前两个声明为 `extern const unsigned char[]`,并将大小计算为 `_binary_payload_end - _binary_payload_start`。
### `-Wl,-N` 结合严格的 `max-page-size`
有效载荷使用 `-Wl,-N -Wl,-z,max-page-size=0x10` 进行静态链接。这会将 `.text`/`.rodata`/`.data` 折叠成一个具有 16 字节文件对齐的单一 LOAD 段,而不是默认的内核页面对齐的每段 4 KB。这会产生来自 `ld` 的 "RWX permissions" 警告,这仅是信息性的——有效载荷的运行时内存保护对其单一目的程序无关紧要。如果没有此标志,相同的代码在 x86_64 上链接约为 13 KB (主要是段间零填充);使用它则约为 1.3 KB,这是在下文所述的剥离节头之前。
### 剥离节头表
链接之后,`objcopy --strip-section-headers` 会移除有效载荷的节头表和 `.shstrtab`。内核 ELF 加载器仅根据程序头 (program header) 来映射程序,因此这些字节在运行时永远不会被加载;而且由于 `payload` 是被逐字节嵌入的,它们还会膨胀释放的数据量以及 `patch_chunk` 的迭代次数。剥离它们可将 x86_64 有效载荷从约 1.3 KB 减小到 720 字节 (322 次 4 字节迭代减少到 180 次)。另有两个链接期标志削减其余部分:`-Wl,--build-id=none` 去掉 build-id note,`-fcf-protection=none` 在编译器支持时去掉 x86 CET note。
剥离需要 binutils >= 2.40。交叉构建通过 `OBJCOPY=` 传入目标平台的 objcopy (例如 `OBJCOPY=aarch64-linux-gnu-objcopy`);当 objcopy 无法完成时,构建会打印一条提示并保留一个有效但更大的有效载荷。
## 变体和变现可行性
此仓库发布了两种漏洞利用变体,它们共享 AF_ALG/splice 页缓存变异原语,但以不同的方式变现(实现 root 执行)。它们的可靠性特征并不相同,在推理现实世界的威胁模型时,这种差异很重要。
### 二进制文件变异变体 (`exploit`)
使用嵌入的有效载荷字节修改目标 setuid 二进制文件的页缓存,然后 exec 该二进制文件。内核根据二进制文件未被触及的磁盘 setuid 位授予 root 凭据,加载损坏的内存映像,并运行有效载荷。
只要攻击者可以在系统上的任何 root-setuid 二进制文件上调用 `open(target,
O_RDONLY)`,它就能工作。在限制读取目录后面的 setuid 二进制文件环境和无 setuid 的系统设计中,这种方法基本会失效。
### /etc/passwd UID 翻转变体 (`exploit-passwd`)
修改 /etc/passwd 页缓存的四个字节,将正在运行的用户的 UID 字段设置为 "0000"。/etc/passwd 在每个标准的 Linux 系统上都是全局可读的,因此*修改*是普遍适用的。将其转化为 root 执行依赖于某个 root 端的进程通过 getpwnam/getpwuid 解析用户,并根据解析出的 uid 行动而无需交叉验证。这样的使用者有很多;但其中许多防御性地根据内核中调用 uid 的视图或磁盘文件所有权进行交叉检查,从而打破了变现链。
#### 变现可行性矩阵
|变现方式 |是否需要 pre-root 设置 |备注 |
|---|---|---||WSL2 会话生成 |否 |WSL 每会话的 `setuid(getpwnam(default_user)->pw_uid)` 不做验证。完美工作。 ||util-linux `su` |否 |宽松的调用者身份处理。 ||shadow-utils `su` |是 |`getpwuid(getuid())` 调用者身份检查失败,因为变异取消映射了真实 uid。 ||sshd (默认 `StrictModes yes`) |是 (禁用 StrictModes) |StrictModes 要求 home 目录归 root 或 `pw->pw_uid` 所有。变异使 pw_uid=0;磁盘所有者保持原样;不匹配导致拒绝身份验证。 ||MTA 本地投递 (postfix,exim 等) |视情况而定 |
取决于 MDA 的 home 权限验证。需要针对具体 MTA 测试。 |#### `su` 失败后的利用转移 (Pivoting)
`exploit-passwd` 在变异后执行 `su <user>` 作为最简单的变现方式。这对 util-linux `su` 有效,但对 shadow-utils `su` 会因 "Cannot determine your user name." 失败。此时页面缓存变异仍然有效,可以转移到任何其他变现方式 (例如,使用通过 getpwnam 解析用户而不进行交叉检查的守护进程)。
测试完成后,请以 root 身份运行 `echo 3 >
/proc/sys/vm/drop_caches` 以清除损坏的页面缓存。
## 受影响的内核
```
下限: torvalds/linux 72548b093ee3 2017 年 8 月,v4.14
(AF_ALG iov_iter 重构,引入了
通过 splice 到 AEAD 散布列表
来实现文件页面写入的原语)
上限: torvalds/linux a664bf3d603d 2026 年 4 月,
主线
(回退了 2017 年的 algif_aead
原地优化;分离了源和目标散布列表
以便页面缓存页面不能再作为
可写的加密目标)
```
在此之间: 每一个没有向后移植该修复的主要发行版内核。
Ubuntu、RHEL、SUSE、Amazon Linux 和 Debian 在披露时都确认其自带的云镜像内核存在漏洞。发行版级别的向后移植修复在 2026 年 4 月 29 日左右与公开披露同时开始推出。要验证目标内核是否在受影响范围内,请检查 `a664bf3d603d` (或其特定于发行版的向后移植) 是否存在于内核的 git 日志或发行版的更改日志中。
## 商业支持
如需付费的安全审查、定制移植或非公开安全公告,请通过作者的咨询公司 Crash United,
LLC 与其联系。
联系方式: tony.gies@crashunited.com · https://crashunited.com
GitHub: [@tgies](https://github.com/tgies) · X/Twitter: [@me_irl](https://x.com/me_irl)
## 许可证和致谢
CVE-2026-31431 的发现和最初披露: Theori / Xint。
官方分析报告: <https://copy.fail/>。
此 C 移植版: Tony Gies <tony.gies@crashunited.com>
`nolibc/`: 从 Linux 内核树引入,双重许可 LGPL-2.1-or-later 或 MIT (见 `nolibc/nolibc.h` 和各个文件的 SPDX 头文件)。
此仓库中的释放器和有效载荷源代码在与其依赖的 nolibc 树相同的双重 LGPL-2.1-or-later 或 MIT 条款下发布,以保持简单的许可证兼容性,方便任何将整个目录引入自己工作的人。
漏洞利用和有效载荷是出于安全研究和防御检测目的发布的。对您不拥有或未获得明确测试授权的系统使用是您自己的问题,与作者无关。⚔️ EXP 利用代码
截至分析时,Exploit-DB 未收录该 CVE 的公开利用代码。可利用上述 PoC 进行验证,或关注 Exploit-DB 更新。
🕵️ 检测指纹
针对该 CVE 的自动化检测规则(可直接用于扫描与审计)。
🛡️ Semgrep 审计规则: CVE-2026-31431.yaml
📋 代码元数据语言yaml来源rules/semgrep/CVE-2026-31431.yaml针对性✅ 按 CVE 匹配依赖semgrep用法semgrep --config CVE-2026-31431.yaml
rules:
- id: CVE-2026-31431-rce-go
languages:
- go
severity: ERROR
message: "Potential page cache corruption via AF_ALG and splice() leading to local privilege escalation. Avoid direct usage of AF_ALG sockets with splice() on file page-cache pages."
patterns:
- pattern: |syscall.Splice($R,nil,$W,nil,$N,0)
fix: |
// Avoid using splice() with AF_ALG sockets on file-backed pages.
// Use safe I/O operations like io.Copy instead of splice.
metadata:
cwe: "CWE-277"
owasp: "A5: Broken Access Control"
technology: talos
references:
- "https://github.com/advisories/GHSA-m38g-vww2-mvgx"
- id: CVE-2026-31431-rce-go-alg
languages:
- go
severity: ERROR
message: "Potential page cache corruption via AF_ALG and splice() leading to local privilege escalation. Avoid direct usage of AF_ALG sockets with splice() on file page-cache pages."
patterns:
- pattern: |
syscall.Splice($R,nil,$W,nil,$N,0)
fix: |// Avoid using splice() with AF_ALG sockets on file-backed pages.
// Use safe I/O operations like io.Copy instead of splice.
metadata:
cwe: "CWE-277"
owasp: "A5: Broken Access Control"
technology: talos
references:
- "https://github.com/advisories/GHSA-m38g-vww2-mvgx"🛡️ CodeQL 审计规则: CVE-2026-31431.ql
📋 代码元数据语言ql来源rules/codeql/CVE-2026-31431.ql针对性✅ 按 CVE 匹配依赖codeql用法codeql database run
/**
* @kind path-problem
* @id go/command-injection/cve-2026-31431
* @name Unsafe splice() in AF_ALG leading to arbitrary code execution via nftables
* @description Untrusted workload using AF_ALG and splice() can corrupt page-cache pages,
leading to arbitrary code execution in kube-proxy via nft binary poisoning
* @problem.severity error
* @tags security
* external/cwe/cwe-078
*/
import go
import semmle.go.security.dataflow.CommandInjectionCustomizations
import CommandInjectionFlow::PathGraph
/**
* A source of untrusted input from inside a container (workload)
*/
class WorkloadSource extends DataFlow::Node {
WorkloadSource() {// Environment variables,command line arguments,network input,etc.
any(DataFlow::Node src).(RemoteFlowSource) = src
or
// Any input received via AF_ALG socket from the container
exists(FileReadAccess f |f.getFile().getAbsolutePath().matches("%/proc/%/fd/%"))
}}
/**
* A sink that represents the splice() system call on AF_ALG socket
* which can corrupt page-cache pages
*/
class SpliceAlgSink extends DataFlow::Node {SpliceAlgSink() {exists(FunctionCall fc |fc.getTarget().getName() = "splice" and
fc.getAnArgument().(DataFlow::Node).asExpr().getType().(PointerType).getBaseType().getName() = "alg_socket"
)
}}
/**
* Sink for nft execution which can be poisoned via page-cache corruption
*/
class NftExecutionSink extends DataFlow::Node {NftExecutionSink() {exists(FunctionCall fc |
fc.getTarget().getName() = "exec" or
fc.getTarget().getName() = "execve" or
fc.getTarget().getName() = "syscall.Exec"
) and
fc.getAnArgument().(DataFlow::Node).asExpr().(StringLiteral).getValue().matches("*/nft*")
}}class PoC_CommandInjectionConfig extends TaintTracking::Configuration {PoC_CommandInjectionConfig() {this = "PoC_CommandInjectionConfig" }
override predicate isSource(DataFlow::Node source) {source instanceof WorkloadSource
}override predicate isSink(DataFlow::Node sink) {sink instanceof SpliceAlgSink or
sink instanceof NftExecutionSink
}}from PoC_CommandInjectionConfig cfg,DataFlow::PathNode source,DataFlow::PathNode sink
where cfg.hasFlowPath(source,sink)
select sink.getNode(),source,sink,
"Untrusted workload data flows to splice() or nft execution,enabling page-cache corruption and privilege escalation"🤖 本文由漏洞情报系统自动聚合生成 · 2026-08-09 18:09 · 数据源: NVD/GitHub-Advisory/OSV/CISA-KEV/Exploit-DB/PoC-in-GitHub + 检测规则库