
Port en C multiplataforma de la LPE de Linux Copy Fail (CVE-2026-31431). Divulgado el 2026-04-29 por Theori / Xint.
English (en) ∙ 日本語 (ja) ∙ 简体中文 (zh-cn) ∙ 한국어 (ko) ∙ Русский (ru)
Una reimplementación en C multiplataforma del LPE de Linux Copy Fail (CVE-2026-31431), divulgado el 29 de abril de 2026 por Theori / Xint. Consulta el informe canónico en copy.fail para obtener la descripción completa de la vulnerabilidad, la cronología y el proceso de descubrimiento de Theori.
La prueba de concepto publicada es un script de Python de 732 bytes. Este port en C demuestra que el mismo exploit puede expresarse como C portable compilable a cualquier arquitectura que soporte nolibc, sin blobs hexadecimales por arquitectura ni ensamblador inline en el código fuente del propio proyecto.
Autor de este port: Tony Gies [email protected]. Descubrimiento y divulgación original: Theori / Xint.
copy-fail-c/
├── exploit.c the dropper (binary-mutation variant)
├── exploit-passwd.c the dropper (/etc/passwd UID-flip variant)
├── vulnerable.c non-destructive vulnerability checker
├── payload.c the body that gets dropped (setgid+setuid+execve sh)
├── utils.c, utils.h shared AF_ALG/splice page-cache mutation primitive
├── Makefile build orchestration
├── nolibc/ vendored from torvalds/linux tools/include/nolibc
└── README.md this file
Después de make:
├── payload tiny static ELF, embedded into the dropper as bytes
├── payload.o payload wrapped as a relocatable .o by `ld -r -b binary`
├── exploit dropper, binary-mutation variant
├── exploit-passwd dropper, /etc/passwd UID-flip variant
└── vulnerable non-destructive vulnerability checker
exploit.c abre el binario objetivo en modo de solo lectura y, para cada ventana de 4 bytes del payload embebido, ejecuta un descifrado AEAD falso a través de AF_ALG cuya entrada de texto cifrado se suministra mediante splice() desde las páginas de page-cache del objetivo. La optimización in-place de la plantilla authencesn trata las páginas de origen de splice() a la vez como entrada de texto cifrado y como destino de texto plano, por lo que el descifrado (fallido) ya ha sobrescrito la página de page-cache cuando la verificación de autenticación rechaza la solicitud. Tras 4 * N iteraciones, la imagen en caché del objetivo se ha reemplazado byte a byte por el payload. Al ejecutar execve() sobre el objetivo se cargan las páginas mutadas; el inodo en disco sigue siendo setuid root, así que el kernel concede las credenciales root y ejecuta el payload.
payload.c es C portable simple: setgid(0); setuid(0); execve("/bin/sh", ...). nolibc proporciona el _start, la maquinaria de syscalls y el manejo de registros específico de cada arquitectura.
Una segunda variante, exploit-passwd.c, muta cuatro bytes de la page cache de /etc/passwd en lugar de la imagen de un binario setuid. No necesita ningún payload embebido y funciona en sistemas donde la vía de mutación de binarios está bloqueada, pero su superficie de cashout es mucho más reducida.
vulnerable.c no es un exploit. Crea un testfile local que contiene la cadena init y, a continuación, ejecuta la misma primitiva patch_chunk() contra la page cache de ese propio archivo para sobrescribir los bytes con vulnerable. Si el contenido leído coincide, el kernel en ejecución está dentro de la ventana de CVE-2026-31431. El inodo en disco nunca se modifica; testfile se elimina al salir; la mutación de la page cache se desvanece con él. Se ejecuta sin privilegios. Sale con el código 100 si es vulnerable, 0 si la primitiva se ejecutó pero la mutación no se aplicó, 2 si la familia de sockets AF_ALG o la plantilla authencesn no está disponible, por lo que no se puede determinar el estado de parcheo, y 1 para otros errores de ejecución.
Predeterminado (arquitectura nativa del host):
make
Compilación cruzada para aarch64 (o cualquier otra arquitectura Linux para la que tengas instalada una cadena de herramientas cruzada):
make CC=aarch64-linux-gnu-gcc LD=aarch64-linux-gnu-ld
Arquitecturas soportadas por el nolibc incluido (según upstream): x86_64, i386, arm, aarch64, riscv32/64, mips, ppc, s390x, loongarch, m68k, sh, sparc. nolibc selecciona la arquitectura según las macros de arquitectura del compilador, por lo que basta con elegir el CC/LD adecuado.
Requisitos para compilar:
cc, gcc o cualquier variante cruzada)ld -r -b binary (tanto el ld de binutils como lld lo soportan)linux/if_alg.h y <asm/unistd.h>
(Debian/Ubuntu: linux-libc-dev; variantes cruzadas: normalmente las
incluye el paquete de la cadena de herramientas cruzada)Los conjuntos de cabeceras anteriores a Linux 5.6 son anteriores a __kernel_old_time_t y struct __kernel_old_timespec, que utiliza el nolibc incluido. compat.h (incluido forzosamente en la compilación del payload) los proporciona cuando faltan, por lo que con un linux-libc-dev más antiguo la compilación sigue funcionando. Es un no-op con cabeceras 5.6+.
No hay dependencias de bibliotecas externas. El payload se compila freestanding contra nolibc; el dropper enlaza contra la libc del host solo por fprintf y perror.
Unas pocas características menores de la cadena de herramientas hacen la mayor parte del trabajo pesado para mantener el código fuente portable y el payload pequeño.
nolibc/ es el pequeño reemplazo de libc basado solo en cabeceras del kernel, incluido desde torvalds/linux tools/include/nolibc/. Proporciona _start, una macro syscall() portable y wrappers inline de syscalls, con las convenciones de registro por arquitectura codificadas en nolibc/arch-*.h. Compilar el payload con -nostdlib -static -ffreestanding -Inolibc produce un ELF estático diminuto que llama directamente al kernel sin arrastrar el arranque de glibc, la inicialización de TLS ni la infraestructura de stack canary. El resultado, una vez empaquetado y con las secciones eliminadas (ambos se detallan más abajo): ~720 bytes en x86_64, ~1.2 KB en aarch64, frente a ~17 KB para el mismo payload.c enlazado contra musl-static o ~700 KB contra glibc-static.
ld -r -b binary para el embebidoEl Makefile convierte el ELF payload compilado en payload.o mediante ld -r -b binary -o payload.o payload. El enlazador emite los bytes de entrada tal cual como la sección de datos de un archivo objeto reubicable y sintetiza tres símbolos a partir del nombre del archivo de entrada:
_binary_payload_start address of first payload byte
_binary_payload_end address one past the last payload byte
_binary_payload_size absolute symbol whose value is the size in bytes
exploit.c declara los dos primeros como extern const unsigned char[] y calcula el tamaño como _binary_payload_end - _binary_payload_start.
-Wl,-N junto con un max-page-size ajustadoEl payload se enlaza estáticamente con -Wl,-N -Wl,-z,max-page-size=0x10, que colapsa .text/.rodata/.data en un único segmento LOAD con alineación de archivo de 16 bytes, en lugar de los 4 KB por segmento alineados a página del kernel que se usan por defecto. Esto produce una advertencia de "permisos RWX" de ld, que es solo informativa: la protección de memoria en tiempo de ejecución del payload no es relevante para su programa de un solo propósito. Sin esta bandera, el mismo código enlaza a ~13 KB en x86_64 (en su mayoría relleno de ceros entre segmentos); con ella, ~1.3 KB antes de eliminar las cabeceras de sección, como se describe más abajo.
Después del enlazado, objcopy --strip-section-headers elimina la tabla de cabeceras de sección del payload y .shstrtab. El cargador de ELF del kernel mapea un programa basándose únicamente en sus cabeceras de programa, por lo que esos bytes nunca se cargan en tiempo de ejecución y, como payload se embebe tal cual, también aumentan el tamaño del drop y el número de iteraciones de patch_chunk. Al eliminarlas, el payload x86_64 pasa de ~1.3 KB a 720 bytes (de 322 iteraciones de cuatro bytes a 180). Dos banderas de tiempo de enlazado recortan el resto: -Wl,--build-id=none elimina la nota build-id y -fcf-protection=none elimina la nota CET de x86 donde el compilador lo soporta.
Esta eliminación requiere binutils >= 2.40. Las compilaciones cruzadas pasan el objcopy del objetivo mediante OBJCOPY= (p. ej. OBJCOPY=aarch64-linux-gnu-objcopy); cuando objcopy no puede hacerlo, la compilación imprime una nota y conserva un payload válido, aunque más grande.
Este repositorio incluye dos variantes del exploit que comparten la primitiva de mutación de page-cache AF_ALG/splice pero materializan la ejecución como root de forma diferente. Sus perfiles de fiabilidad no son equivalentes, y la diferencia importa al razonar sobre modelos de amenaza del mundo real.
exploit)Muta la page cache de un binario setuid objetivo con los bytes del payload embebido y, a continuación, ejecuta el binario. El kernel concede las credenciales root a partir del bit setuid intacto en disco del binario, carga la imagen corrupta en memoria y ejecuta el payload.
Funciona donde el atacante pueda hacer open(target, O_RDONLY) sobre cualquier binario root-setuid del sistema. Queda más o menos neutralizada por entornos que protegen los binarios setuid tras directorios de lectura restringida y por diseños de sistema sin setuid.
exploit-passwd)Muta cuatro bytes de la page cache de /etc/passwd para establecer el campo UID del usuario en ejecución a "0000". /etc/passwd es legible por todos en cualquier sistema Linux estándar, por lo que la mutación es universal. Convertirla en ejecución como root depende de que algún proceso del lado de root resuelva el usuario mediante getpwnam/getpwuid y actúe sobre el uid resuelto sin validación cruzada. Existen muchos consumidores de este tipo; muchos de ellos hacen comprobaciones cruzadas defensivas contra la visión del kernel del uid que llama o contra la propiedad del archivo en disco, rompiendo el cashout.
suexploit-passwd ejecuta su <usuario> después de la mutación, como el cashout más simple posible. Eso funciona contra el su de util-linux, pero falla contra el su de shadow-utils con "Cannot determine your user name." En ese punto la mutación de la page cache sigue activa, y es posible pivotar a cualquier otro cashout (p. ej. usando un demonio que resuelva usuarios mediante getpwnam sin comprobaciones cruzadas). Cuando termines de probar, ejecuta echo 3 > /proc/sys/vm/drop_caches como root para limpiar la page cache corrupta.
floor: torvalds/linux 72548b093ee3 August 2017, v4.14
(AF_ALG iov_iter rework that
introduced the file-page write
primitive via splice into the AEAD
scatterlist)
ceiling: torvalds/linux a664bf3d603d April 2026, mainline
(reverts the 2017 algif_aead
in-place optimization; separates
source and destination scatterlists
so page-cache pages can no longer
be a writable crypto destination)
En medio: todos los kernels de las principales distribuciones que no aplicaron el backport del parche. Ubuntu, RHEL, SUSE, Amazon Linux y Debian fueron confirmados todos como vulnerables en sus kernels estándar de imágenes cloud en el momento de la divulgación. Los backports a nivel de distribución comenzaron a implementarse alrededor del 29 de abril de 2026, junto con la divulgación pública. Para verificar si un kernel objetivo está dentro de la ventana, comprueba si a664bf3d603d (o su backport específico de la distribución) aparece en el git log del kernel o en el changelog de la distribución.
Para revisiones de seguridad remuneradas, trabajos de portabilidad personalizados o avisos de seguridad privados, contacta con el autor a través de su consultora, Crash United, LLC.
Contacto: [email protected] · https://crashunited.com
GitHub: @tgies · X/Twitter: @me_irl
Descubrimiento y divulgación original de CVE-2026-31431: Theori / Xint. Informe público: https://copy.fail/.
Este port en C: Tony Gies [email protected]
nolibc/: incluido desde el árbol del kernel de Linux, con licencia dual LGPL-2.1-or-later O MIT (consulta nolibc/nolibc.h y las cabeceras SPDX de cada archivo).
Los fuentes del dropper y del payload de este repositorio se publican bajo los mismos términos de licencia dual LGPL-2.1-or-later O MIT que el árbol nolibc del que dependen, para mantener la licencia trivialmente compatible para cualquiera que incorpore este directorio completo en su propio trabajo.
El exploit y el payload se publican con fines de investigación de seguridad y detección defensiva. El uso contra sistemas que no posees o para los que no tienes autorización explícita de prueba es problema tuyo, no del autor.
| Cashout | Configuración previa a root necesaria | Notas |
|---|
| Creación de sesión WSL2 | No | El setuid(getpwnam(default_user)->pw_uid) por sesión de WSL no hace ninguna validación. Funciona sin problemas. |
su de util-linux | No | Manejo permisivo de la identidad del llamador. |
su de shadow-utils | Sí | La comprobación de identidad del llamador getpwuid(getuid()) falla porque la mutación desmapea el uid real. |
sshd (StrictModes yes por defecto) | Sí (desactivar StrictModes) | StrictModes exige que el directorio home pertenezca a root o a pw->pw_uid. La mutación hace que pw_uid=0; el propietario en disco permanece en el uid original; el desajuste rechaza la autenticación. |
| Entrega local de MTA (postfix, exim, etc.) | Variable | Depende de la validación de permisos del home por parte del MDA. Probar con cada MTA. |