
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 ajustado