
Prueba de concepto de exploit para CVE-2026-31431, una escalada local de privilegios en el kernel de Linux mediante splice() y opciones de socket manipuladas, que permite la escritura arbitraria en la memoria del kernel para sobrescribir binarios setuid.
copyFail.py es un exploit de prueba de concepto (PoC) que demuestra una vulnerabilidad de escalada de privilegios local en el kernel de Linux. Al abusar de la llamada al sistema splice() en combinación con opciones de socket manipuladas, el exploit obtiene la capacidad de corromper memoria arbitraria del kernel. Utiliza esta capacidad para sobrescribir cualquier binario setuid en disco con un ejecutable ELF personalizado, y luego ejecuta el binario manipulado para obtener privilegios elevados.
Tipo de ataque: Escalada de privilegios local (LPE) Impacto: Acceso completo a shell root (cuando se apunta a binarios setuid-root) Complejidad: Baja (script único, sin dependencias externas)
Uso:
./copyFail_cleaned.py [binary_objetivo] # predeterminado: /usr/bin/su
El PoC original tenía /usr/bin/su codificado, pero la vulnerabilidad subyacente del kernel se aplica a cualquier archivo — el atacante puede sobrescribir cualquier binario setuid que elija. La versión limpia acepta un argumento de línea de comandos opcional para especificar el objetivo, haciendo explícito este comportamiento.
El kernel de Linux proporciona una llamada al sistema splice() que copia datos entre dos descriptores de archivo completamente dentro del kernel — sin copiar datos al espacio de usuario primero. Esto se denomina "E/S de copia cero" y se utiliza por rendimiento en aplicaciones de alto rendimiento como servidores web y proxies inversos.
La vulnerabilidad surge porque el kernel no valida correctamente el estado interno de ciertas estructuras de socket cuando se utiliza splice() en sockets PACKET que han sido configurados con opciones de socket a nivel de protocolo no soportadas. Al manipular cuidadosamente estas opciones, un atacante puede corromper punteros del kernel y luego usar splice() para redirigir escrituras de memoria del kernel a ubicaciones arbitrarias.
En términos simples: el kernel tiene un atajo para mover datos internamente, y este exploit engaña a ese atajo para que escriba datos en el lugar equivocado — específicamente, en la copia en memoria de un binario del sistema.
El exploit sigue una secuencia clara de pasos:
El exploit crea un socket PACKET crudo (AF_PACKET / SOCK_RAW) y lo vincula a direcciones intencionalmente malformadas. Luego llama a setsockopt() usando SOL_PNIO (nivel de protocolo 279), que es una constante a nivel de Solaris que Linux no reconoce.
Por qué importa: Cuando el kernel encuentra un nivel de protocolo no soportado en setsockopt(), cae en un manejador genérico que no valida los datos correctamente. Esto deja la memoria del kernel en un estado inconsistente y explotable.
Después de establecer una conexión de socket mediante accept(), el exploit envía mensajes de control (ancilares) manipulados usando sendmsg(). Estos mensajes tienen encabezados de longitud deliberadamente desajustados — algunos afirman ser más cortos de lo que son, otros afirman ser más largos.
Por qué importa: El analizador de mensajes de control del kernel utiliza estas longitudes para la aritmética de punteros. Las longitudes desajustadas hacen que el analizador lea o escriba más allá de los límites previstos, corrompiendo estructuras adyacentes del kernel — específicamente, punteros que splice() seguirá más tarde.
El exploit crea una tubería (pipe) y llama a splice() dos veces:
/usr/bin/su a la tubería.Por qué importa: La llamada a splice() sigue los punteros del kernel corruptos plantados en el Paso 2, haciendo que escriba datos en una dirección de memoria del kernel elegida por el atacante en lugar del buffer del socket. Esto le da al exploit una primitiva de escritura arbitraria en memoria del kernel.
La carga útil del exploit es un ejecutable ELF de 160 bytes (ver más abajo). Cada llamada a la función del exploit escribe exactamente 4 bytes. Así que el bucle principal se ejecuta 40 veces (160 / 4 = 40), cada vez:
Después de las 40 iteraciones, los primeros 160 bytes de /usr/bin/su han sido sobrescritos.
Finalmente, el exploit ejecuta os.system("su"). El kernel carga el /usr/bin/su modificado desde su caché de páginas (que ahora contiene el shellcode del atacante en lugar del binario su real), y el shellcode se ejecuta.
La carga útil incrustada se descomprime en un ejecutable ELF x86-64 de 160 bytes que contiene este shellcode:
; Intenta syscall 105 (execveat) — puede no estar disponible en kernels antiguos
xor eax, eax
xor edi, edi
mov al, 0x69 ; syscall 105
syscall
; Respaldo: syscall 59 (execve) — la ruta confiable
lea rdi, [rip+0xf] ; RDI = puntero a "/bin/sh"
xor esi, esi ; RSI = NULL (envp)
push 0x3b ; número de syscall 59
pop eax
cdq ; RDX = NULL (argv)
syscall
; Salir limpiamente
xor edi, edi
push 0x3c ; syscall 60 (exit)
pop eax
syscall
; Sección de datos: "/bin/sh\0\0\0"
Qué hace: Lanza /bin/sh sin argumentos, heredando los privilegios del proceso que ejecutó su. Si el exploit se ejecuta como root (o con capacidades que otorgan acceso root a su), el shell resultante es un shell root.
Qué NO hace:
Esto es una escalada de privilegios directa de un solo disparo, consistente con un PoC de investigación.
copyFail.py
├── hex_to_bytes() — decodificador de cadenas hex
├── exploit_splice() — exploit principal (configuración de socket + corrupción + splice)
│ ├── Fase 1: Crear socket PACKET, bind, setsockopt (SOL_PNIO)
│ ├── Fase 2: accept() conexión
│ ├── Fase 3: sendmsg() con mensajes ancilares manipulados
│ ├── Fase 4: pipe() + splice() para corromper memoria del kernel
│ └── Fase 5: intento de recv() (consolida la corrupción)
└── Bucle principal:
├── Abrir /usr/bin/su (solo lectura)
├── Descomprimir carga útil incrustada (zlib → ELF de 160 bytes)
├── Bucle: inyectar 4 bytes por iteración (40 iteraciones en total)
└── Ejecutar su manipulado → shell root
El exploit abre /usr/bin/su con O_RDONLY (solo lectura). No necesita acceso de escritura porque la corrupción ocurre a través de la caché de páginas del kernel — la copia en memoria del archivo que el kernel usa antes de vaciar los cambios al disco. La escritura basada en splice() elude por completo los permisos normales de archivo, yendo directamente a la memoria del kernel.
/usr/bin/su modificado (comparar hash contra el gestor de paquetes)setsockopt() con niveles de protocolo desconocidossplice() en descriptores de archivo no regularessysctl para restringir la creación de sockets AF_PACKET por usuarios sin privilegioscopyFail.py (tal como se distribuye)copyFail_cleaned.py (mismo directorio)net/packet/, fs/splice.c, net/core/sock.cEste informe es únicamente para fines de investigación de seguridad y análisis defensivo.
| Constante | Valor | Propósito en el Exploit |
|---|
AF_PACKET | 17 | Familia de sockets de paquetes crudos |
SOCK_RAW | 3 | Tipo de socket crudo |
SOL_PNIO | 279 | Nivel de protocolo no soportado (Solaris) |
MSG_DONTWAIT | 0x400 | Flag de sendmsg no bloqueante |