特征
- 漏洞
- https://github.com/advisories/GHSA-2274-3hgr-wxv6
- Copy Fail是一个在Linux中可以轻松利用的逻辑漏洞,适用于过去9年发布的所有主要发行版。一个小型、便携的Python脚本可以在所有平台上获得root权限。
- 影响范围
- 主线内核:4.14~6.18.21、6.19~6.19.11
- 修复版本:≥ 6.18.22、≥ 6.19.12、7.0+
- 前提:内核开启 CONFIG_CRYPTO_USER_API_AEAD(algif_aead 模块可用)
- 不受影响
- 内核 < 4.14 或 ≥ 6.18.22 / ≥ 6.19.12
内核编译时 未开启 CRYPTO_USER_API_AEAD(模块不存在)
- 内核 < 4.14 或 ≥ 6.18.22 / ≥ 6.19.12
测试脚本
python3 -c ' import socket try: s = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0) s.bind(("aead", "hmac(sha256)")) print("❌ 危险:AF_ALG AEAD 可用,存在漏洞") except Exception: print("✅ 安全:AF_ALG AEAD 已禁用/不受影响") '
alpine检测
vm-alpine:/www/opencode$ uname -r
6.18.24-0-virt ----- 已合入官方修复, 内核版本不满足
vm-alpine:/www/opencode$ lsmod | grep algif_aead
vm-alpine:/www/opencode$ cat /boot/config-$(uname -r) | grep CRYPTO_USER_API_AEAD
CONFIG_CRYPTO_USER_API_AEAD=m ----- 功能编译进内核内置,无调用入口
✅ 安全:AF_ALG AEAD 已禁用/不受影响
oracle linux检测
[ghost@instance-20260324-0911 ~]$ uname -r
6.12.0-108.64.6.3.el9uek.aarch64 ----- 落在漏洞受影响区间(4.14~6.18.21)
[ghost@instance-20260324-0911 ~]$ lsmod | grep algif_aead
[ghost@instance-20260324-0911 ~]$ grep CRYPTO_USER_API_AEAD /boot/config-$(uname -r)
CONFIG_CRYPTO_USER_API_AEAD=y ----- 功能编译进内核内置,无调用入口
✅ 安全:AF_ALG AEAD 已禁用/不受影响
🟡 存在原生漏洞风险, 但系统出厂默认已限制 AF_ALG 用户态访问[ghost@trilium-260324-1c1g ~]$ uname -r
3.10.0-1160.118.1.el7.x86_64 ----- 低于漏洞起始版本 4.14
[ghost@trilium-260324-1c1g ~]$ lsmod | grep algif_aead
[ghost@trilium-260324-1c1g ~]$ grep CRYPTO_USER_API_AEAD /boot/config-$(uname -r)
----- 内核未编译对应加密用户态接口
✅ 安全:AF_ALG AEAD 已禁用/不受影响攻击脚本demo
python3 cve_2026_31431_detect.py#!/usr/bin/env python3
"""
CVE-2026-31431 "Copy Fail" - Vulnerability Detection Script
Checks whether the current system is vulnerable to the authencesn
page cache corruption / local privilege escalation.
Liam Romanis (30/04/2022)
Checks performed:
1. Kernel version against known vulnerable range
2. Patch commit presence (via kernel config or kallsyms heuristics)
3. algif_aead module status
4. AF_ALG socket availability to unprivileged users
5. Python version (os.splice availability)
6. Presence of setuid binaries in page cache attack surface
7. modprobe blacklist / seccomp mitigations
"""
import os
import sys
import glob
import socket
import struct
import subprocess
from pathlib import Path
BOLD = "\033[1m"
RED = "\033[91m"
YELLOW = "\033[93m"
GREEN = "\033[92m"
CYAN = "\033[96m"
RESET = "\033[0m"
FIX_COMMIT = "a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5"
VULN_COMMIT = "72548b093ee3"
results = []
def header(msg):
print(f"\n{BOLD}{CYAN}=== {msg} ==={RESET}")
def result(name, vulnerable, reason, detail=None):
status = f"{RED}VULNERABLE{RESET}" if vulnerable else f"{GREEN}OK{RESET}"
print(f" [{status}] {name}")
print(f" Reason : {reason}")
if detail:
print(f" Detail : {detail}")
results.append((name, vulnerable, reason))
def info(msg):
print(f" {CYAN}[INFO]{RESET} {msg}")
def check_kernel_version():
header("Kernel Version")
try:
uname = os.uname()
release = uname.release
info(f"Kernel release : {release}")
# Parse major.minor.patch
parts = release.split("-")[0].split(".")
major, minor = int(parts[0]), int(parts[1])
patch = int(parts[2]) if len(parts) > 2 else 0
if (major, minor) < (4, 10):
result("Kernel version", False,
"Kernel predates the in-place AEAD optimization (< 4.10)",
f"Release: {release}")
elif (major, minor) >= (6, 15):
result("Kernel version", False,
"Kernel mainline >= 6.15 likely contains the fix",
f"Release: {release} — verify with patch check below")
else:
result("Kernel version", True,
"Kernel is in the vulnerable range (4.10 – 6.14)",
f"Release: {release} — patch status must be confirmed")
except Exception as e:
result("Kernel version", True,
f"Could not parse kernel version: {e}", "Assume vulnerable")
def check_patch_presence():
header("Patch Presence")
sig_path = Path("/proc/version_signature")
if sig_path.exists():
sig = sig_path.read_text().strip()
info(f"Version signature: {sig}")
patched = False
pkg_info = None
try:
out = subprocess.check_output(
["dpkg", "-l", "linux-image-*"],
stderr=subprocess.DEVNULL, text=True
)
pkg_info = f"dpkg: {out.splitlines()[0] if out else 'no output'}"
except FileNotFoundError:
pass
if not pkg_info:
try:
out = subprocess.check_output(
["rpm", "-q", "kernel"],
stderr=subprocess.DEVNULL, text=True
).strip()
pkg_info = f"rpm: {out}"
except FileNotFoundError:
pass
if pkg_info:
info(f"Package info: {pkg_info}")
build_files = [
"/proc/version",
"/proc/sys/kernel/version",
]
for bf in build_files:
try:
content = Path(bf).read_text()
if FIX_COMMIT[:12] in content:
patched = True
info(f"Fix commit prefix found in {bf}")
except Exception:
pass
if patched:
result("Patch presence", False,
"Fix commit reference found in kernel build info",
f"Fix: {FIX_COMMIT[:16]}...")
else:
result("Patch presence", True,
"Cannot confirm fix commit is present — treat as unpatched",
"Definitive check: compare installed kernel package version "
"against your distro's security advisory for CVE-2026-31431")
def check_algif_aead():
header("algif_aead Module")
loaded = False
try:
modules = Path("/proc/modules").read_text()
if "algif_aead" in modules:
loaded = True
info("algif_aead is currently loaded in the kernel")
else:
info("algif_aead is not currently loaded (may load on demand)")
except Exception as e:
info(f"Could not read /proc/modules: {e}")
# Check modprobe blacklist
blacklisted = False
blacklist_locations = [
"/etc/modprobe.d/",
"/usr/lib/modprobe.d/",
"/run/modprobe.d/",
]
for loc in blacklist_locations:
for f in glob.glob(f"{loc}*.conf"):
try:
content = Path(f).read_text()
if "algif_aead" in content and "blacklist" in content.lower() or \
"algif_aead" in content and "/bin/false" in content:
blacklisted = True
info(f"algif_aead blacklist entry found in: {f}")
except Exception:
pass
if blacklisted:
result("algif_aead module", False,
"Module is blacklisted via modprobe — mitigation in place")
elif loaded:
result("algif_aead module", True,
"Module is loaded and available for exploitation")
else:
result("algif_aead module", True,
"Module is not loaded but will auto-load on AF_ALG bind() — "
"no blacklist found, so it remains exploitable on demand")
def check_afalg_socket():
header("AF_ALG Socket Availability")
AF_ALG = 38
SOCK_SEQPACKET = 5
try:
s = socket.socket(AF_ALG, SOCK_SEQPACKET, 0)
s.close()
result("AF_ALG socket", True,
"Unprivileged AF_ALG socket creation succeeded — "
"crypto subsystem accessible without privileges")
except PermissionError:
result("AF_ALG socket", False,
"AF_ALG socket creation denied — likely restricted by seccomp "
"or kernel config (CONFIG_CRYPTO_USER_API_AEAD disabled)")
except OSError as e:
result("AF_ALG socket", False,
f"AF_ALG unavailable: {e}")
def check_python_splice():
header("Python os.splice Availability")
ver = sys.version_info
info(f"Python version: {ver.major}.{ver.minor}.{ver.micro}")
if ver >= (3, 10):
result("Python os.splice", True,
f"Python {ver.major}.{ver.minor} >= 3.10: os.splice() is available, "
"enabling the pure-Python exploit path with no compiled dependencies")
else:
result("Python os.splice (pure-Python path)", False,
f"Python {ver.major}.{ver.minor} < 3.10: os.splice() unavailable — "
"pure-Python path blocked, but C-based exploit remains trivial")
# ── 6. Setuid binary exposure ─────────────────────────────────────────────────
def check_setuid_binaries():
header("Setuid Binary Exposure")
common_targets = [
"/usr/bin/su",
"/usr/bin/sudo",
"/usr/bin/passwd",
"/usr/bin/newgrp",
"/usr/bin/chsh",
]
found = []
for path in common_targets:
try:
st = os.stat(path)
if st.st_mode & 0o4000: # setuid bit
found.append(path)
info(f"Setuid binary present: {path} "
f"(mode {oct(st.st_mode)}, owner uid={st.st_uid})")
except FileNotFoundError:
pass
if found:
result("Setuid binaries", True,
f"{len(found)} setuid-root binaries present and readable — "
"page cache of these files is a valid write target",
", ".join(found))
else:
result("Setuid binaries", False,
"No common setuid-root binaries found at standard paths — "
"attack surface reduced (unusual system configuration)")
def check_mitigations():
header("Mitigations")
# AppArmor
aa_path = Path("/sys/kernel/security/apparmor/profiles")
if aa_path.exists():
info("AppArmor appears active")
else:
info("AppArmor not detected")
# SELinux
se_path = Path("/sys/fs/selinux/enforce")
if se_path.exists():
try:
enforcing = se_path.read_text().strip() == "1"
info(f"SELinux present, enforcing={enforcing}")
except Exception:
info("SELinux present (could not read enforce status)")
else:
info("SELinux not detected")
# seccomp (check if current process has it — not a system-wide check)
try:
status = Path("/proc/self/status").read_text()
for line in status.splitlines():
if line.startswith("Seccomp:"):
val = line.split(":")[1].strip()
mode = {"0": "disabled", "1": "strict", "2": "filter"}.get(val, val)
info(f"This process seccomp mode: {mode}")
except Exception:
pass
info("Note: LSM/seccomp mitigations above are process/policy-specific. "
"The definitive mitigation is patching the kernel or blacklisting algif_aead.")
def summary():
header("Summary")
vulnerable_checks = [(n, r) for n, v, r in results if v]
safe_checks = [(n, r) for n, v, r in results if not v]
if vulnerable_checks:
print(f"\n {BOLD}{RED}SYSTEM IS LIKELY VULNERABLE TO CVE-2026-31431{RESET}\n")
print(f" {len(vulnerable_checks)} vulnerable condition(s) found:\n")
for name, reason in vulnerable_checks:
print(f" {RED}✗{RESET} {name}")
print(f" {reason}\n")
else:
print(f"\n {BOLD}{GREEN}No vulnerable conditions detected{RESET}\n")
if safe_checks:
print(f" {len(safe_checks)} mitigated/safe condition(s):\n")
for name, reason in safe_checks:
print(f" {GREEN}✓{RESET} {name}")
print(f"""
{BOLD}Recommended actions:{RESET}
1. Apply your distribution's kernel update for CVE-2026-31431
2. Until patched, blacklist the module:
echo 'install algif_aead /bin/false' > /etc/modprobe.d/disable-algif-aead.conf
rmmod algif_aead 2>/dev/null
3. Verify fix: check distro security advisory for patched package version
4. Monitor: {CYAN}https://github.com/torvalds/linux/commit/a664bf3d603d{RESET}
""")
if __name__ == "__main__":
print(f"{BOLD}CVE-2026-31431 'Copy Fail' — Vulnerability Detection{RESET}")
print(f"authencesn page cache corruption / local privilege escalation")
print(f"Running as uid={os.getuid()}, euid={os.geteuid()}\n")
print(f"Liam Romanis\n")
check_kernel_version()
check_patch_presence()
check_algif_aead()
check_afalg_socket()
check_python_splice()
check_setuid_binaries()
check_mitigations()
summary()