AF_UNIX MSG_OOB UAF & SKB-based kernel primitives

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

TL;DR

  • Linux >=6.9 introduced a flawed manage_oob() refactor (5aa57d9f2d53) for AF_UNIX MSG_OOB handling. Stacked zero-length SKBs bypassed the logic that clears u->oob_skb, so a normal recv() could free the out-of-band SKB while the pointer remained live, leading to CVE-2025-38236.
  • Re-triggering recv(..., MSG_OOB) dereferences the dangling struct sk_buff. With MSG_PEEK, the path unix_stream_recv_urg() -> __skb_datagram_iter() -> copy_to_user() becomes a stable 1-byte arbitrary kernel read; without MSG_PEEK the primitive increments UNIXCB(oob_skb).consumed at offset 0x44, i.e., adds +4 GiB to the upper dword of any 64-bit value placed at offset 0x40 inside the reallocated object.
  • By draining order-0/1 unmovable pages (page-table spray), force-freeing an SKB slab page into the buddy allocator, and reusing the physical page as a pipe buffer, the exploit forges SKB metadata in controlled memory to identify the dangling page and pivot the read primitive into .data, vmemmap, per-CPU, and page-table regions despite usercopy hardening.
  • The same page can later be recycled as the top kernel-stack page of a freshly cloned thread. CONFIG_RANDOMIZE_KSTACK_OFFSET becomes an oracle: by probing the stack layout while pipe_write() blocks, the attacker waits until the spilled copy_page_from_iter() length (R14) lands at offset 0x40, then fires the +4 GiB increment to corrupt the stack value.
  • A self-looping skb_shinfo()->frag_list keeps the UAF syscall spinning in kernel space until a cooperating thread stalls copy_from_iter() (via mprotect() over a VMA containing a single MADV_DONTNEED hole). Breaking the loop releases the increment exactly when the stack target is live, inflating the bytes argument so copy_page_from_iter() writes past the pipe buffer page into the next physical page.
  • By monitoring pipe-buffer PFNs and page tables with the read primitive, the attacker ensures the following page is a PTE page, converts the OOB copy into arbitrary PTE writes, and obtains unrestricted kernel read/write/execute. Chrome mitigated reachability by blocking MSG_OOB from renderers (6711812), and Linux fixed the logic flaw in 32ca245464e1 plus introduced CONFIG_AF_UNIX_OOB to make the feature optional.

Root cause: manage_oob() assumes only one zero-length SKB

unix_stream_read_generic() expects every SKB returned by manage_oob() to have unix_skb_len() > 0. After 93c99f21db36, manage_oob() skipped the skb == u->oob_skb cleanup path whenever it first removed a zero-length SKB left behind by recv(MSG_OOB). The subsequent fix (5aa57d9f2d53) still advanced from the first zero-length SKB to skb_peek_next() without re-checking the length. With two consecutive zero-length SKBs, the function returned the second empty SKB; unix_stream_read_generic() then skipped it without calling manage_oob() again, so the true OOB SKB was dequeued and freed while u->oob_skb still pointed to it.

Secuencia mínima de activación

char byte;
int socks[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, socks);
for (int i = 0; i < 2; ++i) {
send(socks[1], "A", 1, MSG_OOB);
recv(socks[0], &byte, 1, MSG_OOB);
}
send(socks[1], "A", 1, MSG_OOB);   // SKB3, u->oob_skb = SKB3
recv(socks[0], &byte, 1, 0);         // normal recv frees SKB3
recv(socks[0], &byte, 1, MSG_OOB);   // dangling u->oob_skb

Primitivas expuestas por unix_stream_recv_urg()

  1. Lectura arbitraria de 1 byte (repetible): state->recv_actor() finalmente ejecuta copy_to_user(user, skb_sourced_addr, 1). Si el SKB colgante se realoca en memoria controlada por el atacante (o en un alias controlado como una página de pipe), cada recv(MSG_OOB | MSG_PEEK) copia un byte desde una dirección kernel arbitraria permitida por __check_object_size() hacia el espacio de usuario sin provocar un crash. Mantener MSG_PEEK activo preserva el puntero colgante para lecturas ilimitadas.
  2. Escritura restringida: Cuando MSG_PEEK está desactivado, UNIXCB(oob_skb).consumed += 1 incrementa el campo de 32 bits en el offset 0x44. En asignaciones de SKB alineadas a 0x100 esto queda cuatro bytes por encima de una palabra alineada a 8 bytes, convirtiendo la primitiva en un incremento de +4 GiB de la palabra alojada en el offset 0x40. Convertir esto en una escritura kernel requiere posicionar un valor sensible de 64 bits en ese offset.

Reasignar la página SKB para lectura arbitraria

  1. Drenar freelists inamovibles de orden-0/1: Mapear una VMA anónima de solo lectura muy grande y forzar faltas en cada página para forzar la asignación de tablas de páginas (unmovable order-0). Llenar ~10% de la RAM con tablas de páginas asegura que las siguientes asignaciones de skbuff_head_cache tomen páginas del buddy frescas una vez que se agoten las listas de order-0.
  2. Spray de SKBs y aislar una página de slab: Usar docenas de socketpairs stream y encolar cientos de mensajes pequeños por socket (~0x100 bytes por SKB) para poblar skbuff_head_cache. Liberar SKBs elegidos para llevar una página de slab objetivo totalmente bajo control del atacante y monitorizar su refcount de struct page mediante la primitiva de lectura emergente.
  3. Devolver la página de slab al buddy allocator: Liberar cada objeto en la página, luego realizar suficientes asignaciones/liberaciones adicionales para expulsar la página de las listas parciales per-CPU de SLUB y de las listas de páginas per-CPU para que se convierta en una página order-1 en el freelist del buddy.
  4. Realocar como buffer de pipe: Crear cientos de pipes; cada pipe reserva al menos dos páginas de datos de 0x1000 bytes (PIPE_MIN_DEF_BUFFERS). Cuando el buddy allocator divide una página order-1, una mitad reutiliza la página SKB liberada. Para localizar qué pipe y qué offset hacen alias con oob_skb, escribir bytes marcadores únicos en SKBs falsos almacenados a lo largo de las páginas de pipe y emitir llamadas repetidas a recv(MSG_OOB | MSG_PEEK) hasta que el marcador sea devuelto.
  5. Forjar un layout SKB estable: Poblar la página de pipe aliada con un struct sk_buff falso cuyos punteros data/head y la estructura skb_shared_info apunten a direcciones kernel arbitrarias de interés. Debido a que x86_64 deshabilita SMAP dentro de copy_to_user(), direcciones en espacio de usuario pueden servir como buffers temporales hasta que los punteros del kernel sean conocidos.
  6. Respetar el hardening de usercopy: La copia tiene éxito contra .data/.bss, entradas vmemmap, rangos vmalloc per-CPU, stacks kernel de otros hilos y páginas de direct-map que no atraviesan límites de folio de orden superior. Lecturas contra .text o caches especializadas rechazadas por __check_heap_object() simplemente retornan -EFAULT sin matar el proceso.

Introspección de allocators con la primitiva de lectura

  • Romper KASLR: Leer cualquier descriptor de IDT desde el mapeo fijo en CPU_ENTRY_AREA_RO_IDT_VADDR (0xfffffe0000000000) y restar el offset conocido del handler para recuperar la base del kernel.
  • Estado SLUB/buddy: Símbolos globales .data revelan bases de kmem_cache, mientras que entradas vmemmap exponen las flags de tipo de cada página, el puntero freelist y la cache propietaria. Escanear segmentos vmalloc per-CPU descubre instancias de struct kmem_cache_cpu así la próxima dirección de asignación de caches clave (p.ej., skbuff_head_cache, kmalloc-cg-192) se vuelve predecible.
  • Tablas de páginas: En lugar de leer mm_struct (bloqueado por usercopy), recorrer la lista global pgd_list (struct ptdesc) y emparejar el mm_struct actual vía cpu_tlbstate.loaded_mm. Una vez conocido el pgd raíz, la primitiva puede atravesar cada tabla de páginas para mapear PFNs para buffers de pipe, tablas de páginas y stacks del kernel.

Reciclar la página SKB como la página superior de la pila del kernel

  1. Liberar la página de pipe controlada otra vez y confirmar vía vmemmap que su refcount vuelve a cero.
  2. Inmediatamente asignar cuatro páginas helper de pipe y luego liberarlas en orden inverso para que el comportamiento LIFO del buddy allocator sea determinista.
  3. Llamar a clone() para crear un hilo helper; las pilas de Linux son de cuatro páginas en x86_64, así que las cuatro páginas liberadas más recientemente se convierten en su pila, con la última página liberada (la antigua página SKB) en las direcciones más altas.
  4. Verificar vía recorrido de tablas de páginas que el PFN de la parte superior de la pila del hilo helper coincide con el PFN SKB reciclado.
  5. Usar la lectura arbitraria para observar el layout de la pila mientras se conduce el hilo hacia pipe_write(). CONFIG_RANDOMIZE_KSTACK_OFFSET resta un offset aleatorio de 0x0–0x3f0 (alineado) de RSP por syscall; escrituras repetidas combinadas con poll()/read() desde otro hilo revelan cuando el escritor bloquea con el offset deseado. Cuando hay suerte, el argumento bytes derramado por copy_page_from_iter() (R14) queda en el offset 0x40 dentro de la página reciclada.

Colocar metadatos SKB falsos en la pila

  • Usar sendmsg() en un socket datagrama AF_UNIX: el kernel copia el sockaddr_un del usuario en un sockaddr_storage residente en pila (hasta 108 bytes) y los datos ancilares en otro buffer en pila antes de que la syscall bloquee esperando espacio en la cola. Esto permite plantar una estructura SKB falsa precisa en la memoria de la pila.
  • Detectar cuando la copia terminó suministrando un mensaje de control de 1 byte ubicado en una página de usuario no mapeada; ____sys_sendmsg() la trae por fallo de página, así que un hilo helper que haga polling con mincore() sobre esa dirección aprende cuándo la página destino está presente.
  • El padding inicializado a cero por CONFIG_INIT_STACK_ALL_ZERO rellena convenientemente campos no usados, completando un header SKB válido sin escrituras adicionales.

Sincronizar el incremento de +4 GiB con una frag list auto-enlazante

  • Forjar skb_shinfo(fakeskb)->frag_list para apuntar a un segundo SKB falso (almacenado en memoria de usuario controlada) que tenga len = 0 y next = &self. Cuando skb_walk_frags() itera esta lista dentro de __skb_datagram_iter(), la ejecución entra en un bucle infinito porque el iterador nunca llega a NULL y el bucle de copia no progresa.
  • Mantener la syscall recv ejecutándose dentro del kernel dejando que el segundo SKB haga el self-loop. Cuando sea momento de disparar el incremento, simplemente cambiar el puntero next del segundo SKB desde espacio de usuario a NULL. El bucle sale y unix_stream_recv_urg() ejecuta inmediatamente UNIXCB(oob_skb).consumed += 1 una vez, afectando cualquier objeto que ocupe actualmente la página de pila reciclada en el offset 0x40.

Bloquear copy_from_iter() sin userfaultfd

  • Mapear una VMA anónima RW gigantesca y forzar su fallo completamente.
  • Abrir un hueco de una sola página con madvise(MADV_DONTNEED, hole, PAGE_SIZE) y colocar esa dirección dentro del iov_iter usado para write(pipefd, user_buf, 0x3000).
  • En paralelo, llamar a mprotect() sobre la VMA completa desde otro hilo. La syscall toma el write lock del mmap y recorre cada PTE. Cuando el escritor de pipe alcanza el hueco, el manejador de fallos de página bloquea en el mmap lock sostenido por mprotect(), pausando copy_from_iter() en un punto determinista mientras el valor bytes derramado reside en el segmento de pila alojado por la página SKB reciclada.

Convertir el incremento en escrituras arbitrarias de PTE

  1. Disparar el incremento: Liberar el frag loop mientras copy_from_iter() está detenido para que el incremento de +4 GiB afecte a la variable bytes.
  2. Desbordar la copia: Una vez que la falla se reanuda, copy_page_from_iter() cree que puede copiar >4 GiB en la página de pipe actual. Tras llenar los 0x2000 bytes legítimos (dos buffers de pipe), ejecuta otra iteración y escribe los datos de usuario restantes en la página física que sigue al PFN del buffer de pipe.
  3. Alinear la adyacencia: Usando telemetría del allocator, forzar al buddy allocator a colocar una página PTE perteneciente al proceso inmediatamente después de la página objetivo del buffer de pipe (p.ej., alternando entre asignar páginas de pipe y tocar nuevos rangos virtuales para forzar asignación de tablas de páginas hasta que los PFNs se alineen dentro del mismo pageblock de 2 MiB).
  4. Sobrescribir tablas de páginas: Codificar entradas PTE deseadas en los 0x1000 bytes extra de datos de usuario para que el OOB copy_from_iter() rellene la página vecina con entradas elegidas por el atacante, concediendo mapeos usuario RW/RWX de memoria física del kernel o reescribiendo entradas existentes para deshabilitar SMEP/SMAP.

Mitigaciones / ideas de hardening

  • Kernel: Aplicar 32ca245464e1479bfea8592b9db227fdc1641705 (revalida correctamente los SKBs) y considerar deshabilitar AF_UNIX OOB completamente a menos que sea estrictamente necesario vía CONFIG_AF_UNIX_OOB (5155cbcdbf03). Endurecer manage_oob() con chequeos de sanity adicionales (p.ej., buclear hasta unix_skb_len() > 0) y auditar otros protocolos de socket por supuestos similares.
  • Sandboxing: Filtrar las banderas MSG_OOB/MSG_PEEK en perfiles seccomp o APIs de broker de más alto nivel (el cambio de Chrome 6711812 ahora bloquea MSG_OOB en renderers).
  • Defensas de allocator: Fortalecer la aleatorización del freelist de SLUB o imponer coloring por página por cache complicaría el reciclado determinista de páginas; limitar el número de buffers de pipe en pipeline también reduce la fiabilidad de la realocación.
  • Monitoreo: Exponer vía telemetría la alta tasa de asignación de tablas de páginas o uso anómalo de pipes—este exploit consume grandes cantidades de tablas de páginas y buffers de pipe.

References

Tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks