CVE-2026-31431
注:本文章所有内容均无AIGC部分,部分内容摘自其他文章,AI在整个过程中只参与了部分逻辑讨论。
首先抛出一个问题,就效果而言,怎么会只有7分多???
CVE-2026-31431,这是一个利用 AF_ALG 与 splice() 组合实现 4 字节页缓存写入的认证擦除写入漏洞。一个 732 字节的概念验证代码可在 Ubuntu、Amazon Linux、RHEL、SUSE 上获取 root 权限。
如描述所言,这是一个提权漏洞。漏洞允许任何无特权本地用户在2017后的任意linux发行版中无条件提升至root权限,且过程无感,不触发任何本地检测。
行为上,它是通过对页缓存进行可控4字节的任意写入,setuid,目标很显然 su 收益最高。
路径上,由于是操作页缓存,且内核从未将受损页标记为脏页以进行回写,所以没有任何磁盘文件被修改,也因此常规的磁盘校验和无法检测此修改。
且,由于页缓存跨主机共享,所以同一漏洞原语可以突破容器边界,也就是说还有容器逃逸的事。
对比:
看到很多资料都会拿 DirtyCow 来做对比,我这边也聊聊吧
DirtyCow (CVE-2016-5195)
是所有备受关注的提权漏洞中很著名的一个,多次出现在很多渗透靶机中作为考题的存在,此漏洞逻辑上需要在虚拟内存子系统的写时复制路径中赢得条件竞争,所以需要多次尝试,且有系统崩溃的风险。并受特定版本限制,需要精确操控管道缓冲区。
CopyFail (CVE-2026-31431)
它属于一种逻辑缺陷,和前者的条件竞争无关,不需要任何时间窗口。 并且,可移植性超级强,因为是内核中的逻辑缺陷,所以完全相同的Payload可以在所有发行版和架构上跑通。无需针对性调整小参数,也无需编译,属于非常底层的一个漏洞。
利用及条件:此漏洞依赖于 python 作为操作窗口,但无需任何额外库,仅需标准库(os、socket、zlib),且需要 3.10+ 版本需求(支持 os.splice)。
RootCause: 页缓存页面位于可写的 Scatterlist
和前面所描述的利用条件对应,该漏洞的核心是 splice() 函数。
Splice():
内核中的一个系统调用,主要作用是在两个文件描述符之间移动数据,且过程完全在内核态完成,无需将数据拷贝到用户态,也就是 0拷贝操作。
主要是为了解决传统I/O的多次拷贝带来的性能开销问题,从结构上,他直接划掉了所有用户态的参与。
splice不拷贝数据本身,它只在内核中拷贝数据的内存引用。指把 PageCache 中包含该数据的物理页引用传递给目标FD。过程中 0次CPU拷贝,只有两次上下文切换。Linux规定,
splice()的两个文件描述符中,必须有一个是管道。用来承载那些指向实际数据的内存引用。
当然还有一个参与者
AF_ALG:
内核中的一种Socket地址簇(AddressFamily),类似于
AF_INET(IPv4)或AF_UNIX(本地套接字)。在Linux 2.6.38引入,核心目的是为用户态程序提供一个标准、统一的接口,去直接调用内核态的 CryptoAPI。
这样说可能会觉得很突兀,那Linux为什么需要这个接口?
- 硬件加速:现在CPU或者主板上一般都有专门的硬件加密引擎。内核驱动负责管理他们,用户态程序通过
AF_ALG就可以享受硬件加速,就不需要自己写内联汇编或驱动通信了。- 统一实现:内核里本身已经集成了很多密码学算法,通过
AF_ALG暴露出来,可以让用户态省去链接巨大加密库的一个流程。
AF_ALG的生命周期也和传统 Socket 有设计区别,我们常见的 Socket 认知是:Socket() -> Connect() -> Send()/recv()。但是AF_ALG采用了一种很奇特的 Two-Stage(两段式)架构,为了适配密码学操作中的"算法配置"与"数据处理"分离的特性。它更像是借用了
bind()和accept()的语义,但是却有不同的物理意义:
ControlSocket (控制套接字)
- 程序调用
socket(AF_ALG,SOCK_SEQPACKET,0)创建一个控制用的 FD。bind()定位算法,和 bind 的传统用法不同(绑定ip/端口),这里用于指定算法类型和算法名称,如本题漏洞中的authencesn(hmac(sha256),abc(aes))。setsocket(),通过设置套接字选项,将所需 key 传递给内核。RequestSocket / Operational FD (请求套接字)
accept()创建实例:这里是最"奇葩"的一步,程序在控制套接字上调用accept(),但这时内核并没有接受什么网络连接,而是克隆出来一个新的 FD。让这个新的 FD 集成刚刚配置的算法和密钥,处理实际的数据流。这样就可以用一套算法密钥并发处理多个独立数据流。控制套接字负责状态配置,请求套接字负责执行计算。数据的交互通过
sendmsg()和recvmsg()完成,深度依赖内核的 Scatterlist (SGL,分散/聚集列表) 机制。所以,用户可以打开 socket 绑定至任意 AEAD 模板,对任意数据执行加密或解密操作。整个过程无需特权。
当用户将文件拼接到管道,再传入 AF_ALG 时,该套接字的 Scatterlist 会持有该文件内核缓存页的直接引用。这些页面不是被复制过来的,就像前面提到的,他是被直接引用,也就是 Scatterlist 指向的物理页面每次 read、mmap、execve 操作该文件时使用的页面完全相同。
在标准的AEAD流程中,输入的内存布局由三部分连续的数据组成,AAD || Ciphertext || Tag。
2017年linux引入了一个性能优化(commit 72548b093ee3),为了将解密后的明文返回给用户态,增加了一个输出缓冲区。但是这里的处理策略并不严谨,在处理流程中,内核通过 memcpy_sglist,将输入的 SGL 中的 ADD 和 Cipher 真实拷贝到了新分配的 RX_Buffer (也就是这里所提到的性能优化所诞生的输出缓冲区),这部分内存是私有的,可写的,极其安全的。但是对于末尾长度为 authsize 的 Tag,内核为了节省CPU周期,并没有采用物理拷贝。内核保留 Tag 内的 scatterlist,为其调用了 sg_chain() 函数,直接将输入SGL中指向Tag的内存引用,链到了 RX_Buffer 的尾部。
Input SGL: AAD || CT || Tag
| | ^
| copy | | sg_chain (still references page cache pages)
v v |
Output SGL: AAD || CT -----+
在 algif_aead.c 中,recvmsg() 将模式设置为 in-place,也就是说此时一个 scatterlist 同时作为 Crypto 的输入和输出。
也就是因为上述的半拷贝策略,结合内核的 In-place 模式,让这个漏洞有径可寻:
输出 Scatterlist 现在包含两个区域,即用户的 recvmsg 缓冲区,其中包含已复制的 AAD 和 Cipher,后接 ChainedTag,它仍引用目标文件的原始缓存页。
内核通过设置
req->src = req->dst,将源 SGL 和目标 SGL 折叠到一起了。即二者均指向此组合链的头部。req->src ----+ | v req->dst --> [ AAD || CT ] --> [ Tag (page cache pages) ] | | | | +-- RX buffer ---+ +-- chained from TX SGL -+ | (user mem) | (file's page cache)这时候作为写入目标(dst)的输出 SGL 直接与页缓存相连接,仅通过偏移边界分割。每个 AEAD 算法都会将其写入限制在预期目标范围内,但 API 中并没有任何强制约束,也没有任何文档将其视为必要条件。
最终,一个 AEAD 算法打破了这个本就无约束的逻辑上下文。
Trigger: Authencesn的临时写入
如果说前面所述的都是利用的路径及设计缺陷,那 authencesn 就是一切的导火索,他直接提供了对整个框架的预期外操作契机。
在linux内核的Crypto框架中,有一个心照不宣的约定,即对于解密操作,dst 只能用来接收 AAD || Cipher 。其合法写入长度是严格限定的,assoclen + (cryptlen - authsize) 字节。
Authencesn,一个专于支持IPsec 64位扩展序列号ESN的包装算法,打破了这个约定。
在IPsec的网络传输里,64位的序列号会被拆成两半,网络包里只传递低32位(seqno_lo),高32位(seqno_hi)是隐式推导的。但在计算HMAC时,需要将高低位重新拼接在一起。
为了完成这个操作,Authencesn 并没有选择申请一块新的临时内存,而是直接把调用者传进的 dst 作为了 ScratchSpace 使用。
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0);
// 读取dst前8个字节,也就是原始AAD中的两个序列号。这一步是合法的
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
// 将seqno_hi覆盖写入到dst的第4-7字节。这也是合法的,因为offset4依然在合法的AAD内,而且临时修改之后会被还原
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);
// 将seqno_lo写入到dst的assoclen + cryptlen处。这是问题的关键,因为assoclen + crtptlen这个位置已经超出了合法的输出边界,刚好跨过明文区域,触及到了Tag区域。
最离谱的是,虽然算法本身在报错前会把 AAD 区域复原,但它永远不会去清理或恢复在 assoclen + cryptlen 这个位置的4字节。无论操作成功还是失败,该位置都被视为可丢弃的临时空间。
内核中没有其他标准AEAD算法会这样操作,GCM、CCM和常规Authenc都将写入限制在 RX_Buffer,唯独 Authencesn 会越界写入。
如果这只是一个简单的越界写,顶多导致内核Panic,或者破坏无用的heap。
但是结合前面所述的 sg_chain,在 AF_ALG 的 in-place 路径中,越过 assoclen + cryptlen 刚好跨到 sh_chain,也就是跨过了 RX_Buffer 的区域,直接操作了 sg_chain 链接的物理页。
所以 authencesn 会把此处作为临时内存写下4字节,也就是32位的 seqno_lo,通过 kmap_local_page 映射到页缓存页面,并将 seqno_lo 直接写入内核缓存的副本。随后的 HMAC 计算正常执行并失败,因此 recvmsg() 返回错误,但是 4字节的写入操作仍然被保留。
所以,逻辑就这样闭环了,可见此漏洞并不是单独某一个模块的逻辑问题造成的,而是三者交汇打出的一个逻辑。根据历史更新来看,这个漏洞已经随着2017年的最后一块拼图的更新(In-Place、Authencesn临时写入、splice),存在了近10年之久。
利用
SocketSetup
用python的标准socket库创建一个
AF_ALG套接字并绑定到Authencesn(hmac(sha256),cbc(aes))。
Construct
由于每次只能写入4字节,所以需要把shellcode切分成多个块,多次发送:
sendmsg: 将目标4字节伪装成AAD的seqno_lo,发送给内核。splice: 将目标文件(/usr/bin/su)的对应页缓存0拷贝到内核中。计算assoclen和spliceoffset,确保每次写入连续性。
Trigger
最后调用
recv()尝试触发解密读取操作,也就是触发解密流程。就像前面说的一样,由于篡改了数据,所以HMAC肯定校验失败,但是内存污染已经生效。
Execute
调用
su即可,内核会直接从内存中读取页缓存并执行,过程中不会再去读磁盘。也就直接执行了我们所写入的 Gadget。由于
su本身就是一个 suid 程序,自身具有 root 权限,所以 shellcode 将以 root 运行。
整个流程完全避开了 KASLR、SMEP、SMAP,包括 CFI 在这种数据流篡改面前也没办法。因为自始至终都没有触碰内核的执行流控制结构,只是借助了一个合法接口替换了另一个合法程序在内存中的缓存。
修复方案(摘取于其他文章)
Patch(a664bf3d603d)将 algif_aead.c 恢复为 out-of-place 模式,完全移除了 2017 年引入的 in-place 优化。Fixes 标签指向 72548b093ee3 commit; 该commit引入了 in-place 设计,证实了将页缓存页面链入 scatterlist 是根本原因。
漏洞代码将 req->src = req->dst,两者均指向一个 scatterlist,其中通过 splice() 获取的页缓存页面被链入可写目标。修复方案将两者分离:
// Before: src and dst are the same scatterlist (in-place)
aead_request_set_crypt(&areq->cra_u.aead_req, rsgl_src, // RX SGL
areq->first_rsgl.sgl.sgt.sgl, used, ctx->iv); // RX SGL (same)
// After: src is the TX SGL, dst is the RX SGL (out-of-place)
aead_request_set_crypt(&areq->cra_u.aead_req, tsgl_src, // TX SGL
areq->first_rsgl.sgl.sgt.sgl, used, ctx->iv); // RX SGL (different)
req->src 现在指向 TX SGL(可能包含来自 splice 的页缓存页面)。req->dst 指向 RX SGL(用户的 recvmsg 缓冲区)。只有 AAD 从 src 复制到 dst。整个将页缓存标签页链接到 scatterlist 的 sg_chain 机制已被移除。
Commit Tags 明确写道:“在 algif_aead 中原地操作没有益处,因为源和目标来自不同的映射。”
结语
逻辑说完了,我们盘点其他的,在我了解了这个漏洞的背景后,这居然又是一起由AI辅助发现的一个漏洞。
所以,技术已死 : ( hhhh,开玩笑呢,至少目前阶段还是处于ai辅助挖掘,还没有一套完整的框架可以支撑ai完全自主话挖掘,目前的ai更倾向于被大模型赋能的漏扫工具,完全依赖于庞大的context来发掘漏洞,且本身并不具备复现验证等减少误报的功能,所以,任重而道远,持续进步。
Q: 回到最开始抛出的问题,为什么这么逆天的漏洞CVSS只有7.8分? A: 并不代表这个漏洞本身并不严重,只是CVSS的评分中,网络层面的权重会更高,说白了提权漏洞还是需要有一个用户作为媒介,并不是说我可以见人就打,而是需要你在已经入侵的情况下才能进一步利用。因此,CVSS评分规定,要达到9以上的评分,漏洞必须是网络级的。所以相较于其他漏洞,7.8可能已经是这个类别中的极限了。它依然具有很强的威胁性和通用性,和学习价值。