AF_UNIX MSG_OOB UAF & SKB-based kernel primitives

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks

TL;DR

  • Linux >=6.9 ha introdotto un refactor difettoso di manage_oob() (5aa57d9f2d53) per la gestione AF_UNIX MSG_OOB. SKB a lunghezza zero impilati bypassavano la logica che azzera u->oob_skb, quindi una normale recv() poteva liberare lo SKB out-of-band mentre il puntatore restava valido, portando a CVE-2025-38236.
  • Riattivare recv(..., MSG_OOB) dereferenzia il struct sk_buff pendente. Con MSG_PEEK, il percorso unix_stream_recv_urg() -> __skb_datagram_iter() -> copy_to_user() diventa una lettura kernel arbitraria stabile di 1 byte; senza MSG_PEEK il primitivo incrementa UNIXCB(oob_skb).consumed all’offset 0x44, ossia aggiunge +4 GiB al dword alto di qualsiasi valore a 64 bit posizionato all’offset 0x40 dentro l’oggetto riallocato.
  • Svuotando pagine unmovable di order-0/1 (page-table spray), forzando il free di una pagina slab SKB nel buddy allocator e riutilizzando la pagina fisica come pipe buffer, l’exploit forgia i metadata dello SKB in memoria controllata per identificare la pagina dangling e pivotare la primitive di lettura verso .data, vmemmap, per-CPU e regioni di page-table nonostante il usercopy hardening.
  • La stessa pagina può poi essere riciclata come pagina superiore dello kernel-stack di un thread appena clonata. CONFIG_RANDOMIZE_KSTACK_OFFSET diventa un oracolo: sondando il layout dello stack mentre pipe_write() è bloccata, l’attaccante aspetta che la lunghezza spillata di copy_page_from_iter() (R14) finisca all’offset 0x40, poi scatta l’incremento di +4 GiB per corrompere il valore sullo stack.
  • Una skb_shinfo()->frag_list in loop mantiene la syscall UAF in esecuzione nello spazio kernel finché un thread cooperante non blocca copy_from_iter() (via mprotect() su una VMA contenente un singolo hole MADV_DONTNEED). Rompere il loop rilascia l’incremento esattamente quando l’obiettivo sullo stack è live, gonfiando l’argomento bytes così che copy_page_from_iter() scriva oltre la pagina del pipe buffer nella pagina fisica successiva.
  • Monitorando i PFN del pipe-buffer e le page table con la primitive di lettura, l’attaccante si assicura che la pagina successiva sia una PTE page, converte la copia OOB in scritture arbitrarie di PTE e ottiene accesso kernel R/W/X illimitato. Chrome ha mitigato la raggiungibilità bloccando MSG_OOB dai renderer (6711812), e Linux ha corretto la logica in 32ca245464e1 aggiungendo CONFIG_AF_UNIX_OOB per rendere la feature opzionale.

Causa principale: manage_oob() presuppone un solo zero-length SKB

unix_stream_read_generic() si aspetta che ogni SKB restituito da manage_oob() abbia unix_skb_len() > 0. Dopo 93c99f21db36, manage_oob() saltava il percorso di cleanup skb == u->oob_skb ogni volta che rimuoveva per primo uno SKB a lunghezza zero lasciato da recv(MSG_OOB). La successiva correzione (5aa57d9f2d53) avanzava comunque dallo primo zero-length SKB con skb_peek_next() senza ricontrollare la lunghezza. Con due SKB consecutivi a lunghezza zero, la funzione restituiva il secondo SKB vuoto; unix_stream_read_generic() lo saltava senza richiamare manage_oob() di nuovo, quindi il vero OOB SKB veniva estratto dalla coda e liberato mentre u->oob_skb continuava a puntarci.

Sequenza minima di trigger

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

Primitive esposte da unix_stream_recv_urg()

  1. 1-byte arbitrary read (repeatable): state->recv_actor() esegue infine copy_to_user(user, skb_sourced_addr, 1). Se l’SKB pendente viene riallocato in memoria controllata dall’attaccante (o in un alias controllato come una pipe page), ogni recv(MSG_OOB | MSG_PEEK) copia un byte da un indirizzo kernel arbitrario consentito da __check_object_size() nello spazio utente senza causare il crash. Mantenere MSG_PEEK impostato preserva il puntatore pendente per letture illimitate.
  2. Constrained write: Quando MSG_PEEK è disabilitato, UNIXCB(oob_skb).consumed += 1 incrementa il campo a 32 bit all’offset 0x44. Su allocazioni SKB allineate a 0x100 questo si trova quattro byte sopra una parola allineata a 8 byte, convertendo la primitiva in un incremento di +4 GiB della parola ospitata all’offset 0x40. Trasformare ciò in una scrittura kernel richiede il posizionamento di un valore sensibile a 64 bit in quell’offset.

Riallocare la pagina SKB per lettura arbitraria

  1. Svuotare le freelist unmovable di ordine 0/1: Mappa un grande VMA anonimo read-only e provoca fault su ogni pagina per forzare l’allocazione delle page-table (order-0 unmovable). Riempire ~10% della RAM con page table garantisce che le successive allocazioni di skbuff_head_cache prendano nuove pagine dal buddy una volta esaurite le liste di ordine-0.
  2. Spray di SKB e isolamento di una slab page: Usa dozzine di stream socketpair e metti in coda centinaia di piccoli messaggi per socket (~0x100 byte per SKB) per popolare skbuff_head_cache. Free alcune SKB scelte per portare una pagina slab bersaglio completamente sotto controllo dell’attaccante e monitora il suo refcount di struct page tramite la primitiva di lettura emersa.
  3. Restituire la slab page al buddy allocator: Free ogni oggetto sulla pagina, poi esegui allocazioni/free addizionali sufficienti a spingere la pagina fuori dalle liste partial per-CPU di SLUB e dalle per-CPU page lists in modo che diventi una pagina order-1 nella freelist del buddy.
  4. Riallocare come pipe buffer: Crea centinaia di pipe; ogni pipe riserva almeno due pagine dati da 0x1000 byte (PIPE_MIN_DEF_BUFFERS). Quando il buddy allocator splitta una pagina order-1, una metà riusa la pagina SKB liberata. Per individuare quale pipe e quale offset aliasano oob_skb, scrivi byte marker unici in fake SKB posti attraverso le pipe pages e emetti ripetute chiamate recv(MSG_OOB | MSG_PEEK) finché non viene restituito il marker.
  5. Falsificare un layout SKB stabile: Popola la pipe page aliasata con un fake struct sk_buff i cui puntatori data/head e la struttura skb_shared_info puntano ad indirizzi kernel arbitrari di interesse. Poiché x86_64 disabilita SMAP dentro copy_to_user(), indirizzi in user-mode possono servire come buffer di staging finché i puntatori kernel non sono noti.
  6. Rispettare l’hardening usercopy: La copy ha successo contro .data/.bss, vmemmap entries, intervalli vmalloc per-CPU, stack kernel di altri thread e pagine direct-map che non attraversano confini di folio di ordine superiore. Le letture contro .text o cache specializzate rifiutate da __check_heap_object() semplicemente ritornano -EFAULT senza killare il processo.

Ispezionare gli allocator con la primitiva di lettura

  • Break KASLR: Leggi qualsiasi descrittore IDT dal fixed mapping a CPU_ENTRY_AREA_RO_IDT_VADDR (0xfffffe0000000000) e sottrai l’offset del handler noto per recuperare la base del kernel.
  • Stato SLUB/buddy: Simboli globali .data rivelano le basi di kmem_cache, mentre le vmemmap entries espongono i flag di tipo di ogni pagina, il puntatore freelist e la cache proprietaria. Scannerizzare i segmenti vmalloc per-CPU scopre istanze di struct kmem_cache_cpu così l’indirizzo dell’allocazione successiva di cache chiave (es., skbuff_head_cache, kmalloc-cg-192) diventa predicibile.
  • Page tables: Invece di leggere mm_struct (bloccato da usercopy), cammina il global pgd_list (struct ptdesc) e abbina l’attuale mm_struct tramite cpu_tlbstate.loaded_mm. Una volta noto il root pgd, la primitiva può attraversare tutte le page table per mappare i PFN di pipe buffers, page table e stack kernel.

Riciclare la pagina SKB come la pagina superiore dello stack del kernel

  1. Free la pipe page controllata di nuovo e conferma via vmemmap che il suo refcount ritorni a zero.
  2. Alloca immediatamente quattro pagine pipe helper e poi freele in ordine inverso in modo che il comportamento LIFO del buddy allocator sia deterministico.
  3. Chiama clone() per spawnare un thread helper; gli stack Linux sono quattro pagine su x86_64, quindi le quattro pagine più recentemente freeate diventano il suo stack, con l’ultima pagina freeata (l’ex pagina SKB) agli indirizzi più alti.
  4. Verifica tramite page-table walk che il PFN dello stack superiore del thread helper corrisponda al PFN della SKB riciclata.
  5. Usa la lettura arbitraria per osservare il layout dello stack mentre guidi il thread dentro pipe_write(). CONFIG_RANDOMIZE_KSTACK_OFFSET sottrae un valore casuale 0x0–0x3f0 (allineato) da RSP per syscall; scritture ripetute combinate con poll()/read() da un altro thread rivelano quando lo writer si blocca con l’offset desiderato. Quando fortunati, l’argomento bytes di copy_page_from_iter() (R14) si trova a offset 0x40 dentro la pagina riciclata.

Posizionare metadati SKB falsi sullo stack

  • Usa sendmsg() su una AF_UNIX datagram socket: il kernel copia il sockaddr_un utente in uno sockaddr_storage residente sullo stack (fino a 108 byte) e i dati ancillari in un altro buffer on-stack prima che la syscall si blocchi in attesa di spazio in coda. Questo permette di piantare una struttura fake SKB precisa nella memoria dello stack.
  • Rileva quando la copy è terminata fornendo un controllo (control message) di 1 byte situato in una pagina utente non mappata; ____sys_sendmsg() la fa faultare, quindi un thread helper che fa polling su mincore() su quell’indirizzo apprende quando la pagina di destinazione è presente.
  • L’azzeramento del padding dovuto a CONFIG_INIT_STACK_ALL_ZERO riempie convenientemente i campi inutilizzati, completando un header SKB valido senza scritture aggiuntive.

Temporizzare l’incremento +4 GiB con una frag list auto-iterante

  • Forgia skb_shinfo(fakeskb)->frag_list per puntare a una seconda fake SKB (memorizzata in memoria utente controllata) che ha len = 0 e next = &self. Quando skb_walk_frags() itera questa lista dentro __skb_datagram_iter(), l’esecuzione si blocca indefinitamente perché l’iteratore non raggiunge mai NULL e il loop di copy non fa progressi.
  • Mantieni la syscall recv in esecuzione dentro il kernel lasciando la seconda fake SKB auto-iterare. Quando è il momento di sparare l’incremento, semplicemente cambia da user-space il puntatore next della seconda SKB da user a NULL. Il loop esce e unix_stream_recv_urg() esegue immediatamente UNIXCB(oob_skb).consumed += 1 una sola volta, influenzando qualunque oggetto occupi attualmente la pagina stack riciclata all’offset 0x40.

Bloccare copy_from_iter() senza userfaultfd

  • Mappa un grande VMA anonimo RW e fallo faultare completamente.
  • Crea un buco di una singola pagina con madvise(MADV_DONTNEED, hole, PAGE_SIZE) e inserisci quell’indirizzo dentro l’iov_iter usato per write(pipefd, user_buf, 0x3000).
  • In parallelo, chiama mprotect() sull’intero VMA da un altro thread. La syscall prende la mmap write lock e cammina ogni PTE. Quando il writer della pipe raggiunge il buco, il page fault handler si blocca sulla mmap lock tenuta da mprotect(), mettendo in pausa copy_from_iter() in un punto deterministico mentre il valore bytes spillato risiede sul segmento di stack ospitato dalla pagina SKB riciclata.

Trasformare l’incremento in scritture arbitrarie di PTE

  1. Spara l’incremento: Rilascia il frag loop mentre copy_from_iter() è in pausa in modo che l’incremento di +4 GiB colpisca la variabile bytes.
  2. Overflow della copy: Una volta che il fault riprende, copy_page_from_iter() crede di poter copiare >4 GiB nella pipe page corrente. Dopo aver riempito i legittimi 0x2000 byte (due pipe buffer), esegue un’altra iterazione e scrive i rimanenti dati utente nella qualunque pagina fisica segua il PFN del pipe buffer.
  3. Pianificare l’adiacenza: Usando la telemetria dell’allocator, forza il buddy allocator a posizionare una page PTE posseduta dal processo immediatamente dopo la pagina del pipe buffer bersaglio (es., alternando allocazioni di pipe page e toccando nuovi range virtuali per triggerare l’allocazione di page-table finché i PFN non si allineano dentro lo stesso 2 MiB pageblock).
  4. Sovrascrivere le page table: Codifica le voci PTE desiderate nei 0x1000 byte extra dei dati utente così che la OOB copy_from_iter() riempia la pagina adiacente con voci scelte dall’attaccante, garantendo mappature user RW/RWX di memoria fisica kernel o riscrivendo voci esistenti per disabilitare SMEP/SMAP.

Mitigazioni / idee di hardening

  • Kernel: Applica 32ca245464e1479bfea8592b9db227fdc1641705 (revalida correttamente gli SKB) e considera la disabilitazione completa di AF_UNIX OOB a meno che non sia strettamente necessario tramite CONFIG_AF_UNIX_OOB (5155cbcdbf03). Indurire manage_oob() con controlli di sanità addizionali (es., loop fino a unix_skb_len() > 0) e auditare altri protocolli socket per assunzioni simili.
  • Sandboxing: Filtrare i flag MSG_OOB/MSG_PEEK nei profili seccomp o nelle API broker di livello superiore (cambiamento Chrome 6711812 ora blocca MSG_OOB lato renderer).
  • Difese dell’allocator: Rafforzare la randomizzazione delle freelist SLUB o imporre page coloring per cache per complicare il riciclo deterministico delle pagine; limitare il numero di pipe buffer per pipeline riduce anche l’affidabilità della riallocazione.
  • Monitoraggio: Esporre tramite telemetria allocazioni di page-table ad alto rate o uso anomalo delle pipe — questo exploit consuma grandi quantità di page table e pipe buffer.

References

Tip

Impara e pratica il hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Impara e pratica il hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporta HackTricks