CVE-2026-31431(Copy Fail) check

特征

  • 漏洞
    • 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(模块不存在)
  • 测试脚本

    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

来源: https://github.com/liamromanis101/CVE-2026-31431-Copy-Fail---Vulnerability-Detection-Script/blob/main/cve-20256-31431-detect.py 

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()