authencesn 中的一个逻辑缺陷,通过 AF_ALG 和 splice() 串联,最终转化为对系统上任意可读文件页缓存的受控 4 字节写入。无需竞态条件、无需偏移、无需编译载荷。自 2017 年以来,同一份 732 字节脚本即可在每一个 Linux 发行版上获得 root 权限。
CVE-2026-31431 - Copy Fail 是 Linux 内核 authencesn 加密模板中的一个逻辑缺陷。它允许非特权本地用户对系统上任意可读文件的页缓存执行一次受控的 4 字节写入,而无需修改磁盘上的文件。
该漏洞并不单独存在于三个组件中的任何一个,而是源于它们之间的交互:``` 2011 ────────────────────────────────────────────────────────────────────── - authencesn added to the kernel (a5079d084f8b). - Uses the caller's destination scatterlist as scratch space. - Reorder ESN bytes before HMAC computation. - Only caller: internal xfrm layer. Harmless.
2015 ────────────────────────────────────────────────────────────────────── - algif_aead.c gains AEAD support with splice() path (104880a6b470). - splice() can deliver page cache pages to the TX scatterlist. - AF_ALG uses out-of-place operation: req->src != req->dst. - Page cache pages remain read-only. Not exploitable.
2017 ────────────────────────────────────────────────────────────────────── - In-place optimization in algif_aead.c (72548b093ee3). - Copies AAD+CT to RX buffer but chains authentication tag pages via sg_chain(). - Sets req->src = req->dst. - Page cache pages now reside in WRITABLE dst. - authencesn writes past boundary → page cache corruption.
2026 ────────────────────────────────────────────────────────────────────── - Copy Fail - CVE-2026-31431. Discovered by Theori / Xint Code. - Exploitable across all distros since 2017.
---
---
---
<div id='root-cause'/>
## ***🧬 根本原因分析***
<div id='primitive'/>
### ***AF_ALG + splice() 原语***
AF_ALG (*[AF_ALG = 38](https://docs.kernel.org/crypto/userspace-if.html#user-space-api-general-remarks)*) 是一种套接字类型,它将内核加密 API 暴露给非特权用户空间。非特权进程可以:
1. 打开一个 AF_ALG / SOCK_SEQPACKET 套接字。
2. bind() 到内核 crypto API 暴露的任何可用 AEAD 模板。
3. 通过 setsockopt(SOL_ALG, ALG_SET_KEY, ...) 在配置的算法上设置加密密钥。
4. 调用 accept() 获取一个专用的操作套接字,该套接字将处理加密和解密请求。
5. 使用 sendmsg() 发送构造的数据,并通过 recvmsg() 接收处理后的结果,从而与内核 crypto 子系统完全交互。
它在所有主要发行版的内核配置中默认启用(CONFIG_CRYPTO_USER_API_AEAD=y)。
**[splice(2)](https://man7.org/linux/man-pages/man2/splice.2.html)** 在文件描述符之间传输数据,无需复制——它传递页面的引用,而不是副本。相关流程:```
open("/usr/bin/su") -> fd_file
pipe() -> pipe_rd, pipe_wr
# moves N bytes from the file into the pipe
# the pipe buffer now contains a reference to the same physical page in the page cache
splice(fd_file, pipe_wr, N)
# delivers that reference to the AF_ALG socket
# the TX scatterlist of algif_aead now points to the page cache page of /usr/bin/su
splice(pipe_rd, alg_fd, N)
The TX scatterlist of the AF_ALG socket contains direct references to the same physical pages used by the kernel for every read(), mmap(), and execve() of the file. No copy is involved.
Commit 72548b093ee3, algif_aead.c. 对于解密,该实现如下:
sg_chain() 将认证标签页链接起来,在 RX SGL 中保留页缓存引用。req->src = req->dst,两者都指向合并后的 RX SGL。```
TX SGL (input from splice):
[ page cache page: AAD || CT || Tag ]In-place operation: RX SGL (req->dst): [ user buffer: AAD (copy) || CT (copy) ] --sg_chain--> [ Tag (page cache pages) ] req->src = req->dst = RX SGL
Result: page cache pages from /usr/bin/su are now part of the WRITABLE scatterlist passed to the crypto algorithm.
<div id='authencesn'/>
### ***authencesn 中的越界写入(out-of-bounds write)***
authencesn 是 IPsec 在扩展序列号(RFC 4303)场景下使用的内核 AEAD 包装器。IPsec 使用 64 位序列号:
- seqno_hi - 高 32 位(AAD 的第 0-3 字节)
- seqno_lo - 低 32 位(AAD 的第 4-7 字节)
只有 seqno_lo 会在线路上传输;seqno_hi 是隐式上下文。为了计算 HMAC,authencesn 需要重新排列这些字节:将 seqno_hi 放在哈希输入的起始位置,而 seqno_lo 放在末尾。
它通过将调用方的目标 scatterlist 用作临时空间来执行这种重排:```c
/* crypto/authencesn.c - crypto_authenc_esn_decrypt() */
// [1] Read bytes 0-7 of the AAD from dst
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0);
// [2] Overwrite dst[4..7] with seqno_hi (temporary modification for HMAC)
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
// [3] *** THE BUG ***
// Writes seqno_lo at dst[assoclen + cryptlen]
// This offset is AFTER the authentication tag - outside the legitimate AEAD output region.
// authencesn uses this position as scratch space and NEVER restores the original bytes.
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);
调用 [3] 在 dst[assoclen + cryptlen] 处写入 4 字节。AEAD API 的解密输出约定是 AAD || 明文 —— 正好是 assoclen + (cryptlen - authsize) 字节。assoclen + cryptlen 位于认证标签之外。authencesn 写入了不属于它的内存。
crypto_authenc_esn_decrypt_tail() 会读回 seqno_lo 以重建正确的 AAD,但从未恢复 dst[assoclen + cryptlen] 处的原始字节。无论 HMAC 校验成功还是失败,这次覆盖都是永久性的。
内核中没有任何其他标准 AEAD 算法以这种方式运行。GCM、CCM 和标准 authenc 都严格将其写入限制在合法的输出区域内。
在 algif_aead 的 2017 年之后引入的就地路径中,作为 req->dst 传递给 authencesn 的 scatterlist 具有以下结构:``` req->dst: [ RX buffer (user memory) ] [ Tag region (page cache pages) ] [ AAD (copy) || CT (copy) ] [ from /usr/bin/su ] [<---- assoclen + cryptlen bytes --->] [<--- sg_chain from TX SGL ---->] ^ authencesn writes here: dst[assoclen + cryptlen] = seqno_lo (4 bytes controlled by the attacker)
scatterwalk_map_and_copy 并不关心页面所有权,它只是通过 kmap_local_page 映射 scatterlist 指向的任何页面并向其中写入。当 req->dst 中存在页面缓存页时,它最终会映射 "/usr/bin/su" 的缓存页面,并将 seqno_lo 直接写入该文件的内核内存副本中。
HMAC 是在重排后的字节上计算的,因而校验失败(密文由攻击者控制)。recvmsg() 返回错误。对页面缓存的 4 字节写入仍然存在。
---