AF_UNIX MSG_OOB UAF & SKB-based kernel primitives

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

TL;DR

  • Linux >=6.9 introduziu um refactor defeituoso em manage_oob() (5aa57d9f2d53) para o tratamento de AF_UNIX MSG_OOB. SKBs empilhados com comprimento zero contornaram a lógica que limpa u->oob_skb, de modo que um recv() normal podia liberar o SKB out-of-band enquanto o ponteiro permanecia vivo, resultando em CVE-2025-38236.
  • Re-disparar recv(..., MSG_OOB) desreferencia o struct sk_buff pendente. Com MSG_PEEK, o caminho unix_stream_recv_urg() -> __skb_datagram_iter() -> copy_to_user() torna-se uma leitura arbitrária de kernel estável de 1-byte; sem MSG_PEEK o primitivo incrementa UNIXCB(oob_skb).consumed no offset 0x44, isto é, adiciona +4 GiB ao dword superior de qualquer valor 64-bit colocado no offset 0x40 dentro do objeto realocado.
  • Drenando páginas não-movíveis de ordem 0/1 (page-table spray), forçando a liberação de uma página de slab de SKB para o buddy allocator, e reutilizando a página física como pipe buffer, o exploit forja metadados de SKB em memória controlada para identificar a página pendente e pivotar o primitivo de leitura para .data, vmemmap, per-CPU e regiões de page-table apesar do hardening de usercopy.
  • A mesma página pode depois ser reciclada como a página superior da kernel-stack de uma thread recém-clonada. CONFIG_RANDOMIZE_KSTACK_OFFSET torna-se um oráculo: ao sondar o layout da stack enquanto pipe_write() bloqueia, o atacante espera até que o comprimento derramado de copy_page_from_iter() (R14) caia no offset 0x40, e então dispara o incremento de +4 GiB para corromper o valor na stack.
  • Uma skb_shinfo()->frag_list em loop mantém a syscall UAF girando no espaço do kernel até que uma thread cooperante trave copy_from_iter() (via mprotect() sobre um VMA contendo um único buraco MADV_DONTNEED). Quebrar o loop libera o incremento exatamente quando o alvo na stack está vivo, inflando o argumento bytes de modo que copy_page_from_iter() escreva além da página do pipe buffer para a próxima página física.
  • Monitorando PFNs dos pipe-buffers e page tables com o primitivo de leitura, o atacante garante que a página seguinte é uma página de PTE, converte a cópia OOB em escritas arbitrárias de PTE, e obtém leitura/escrita/execução irrestrita no kernel. O Chrome mitigou a exponibilidade bloqueando MSG_OOB de renderers (6711812), e o Linux corrigiu a falha lógica em 32ca245464e1 além de introduzir CONFIG_AF_UNIX_OOB para tornar a feature opcional.

Causa raiz: manage_oob() assume apenas um SKB de comprimento zero

unix_stream_read_generic() espera que todo SKB retornado por manage_oob() tenha unix_skb_len() > 0. Após 93c99f21db36, manage_oob() pulou o caminho de limpeza skb == u->oob_skb sempre que primeiro removia um SKB de comprimento zero deixado por recv(MSG_OOB). A correção subsequente (5aa57d9f2d53) ainda avançou do primeiro SKB de comprimento zero para skb_peek_next() sem re-verificar o comprimento. Com dois SKBs consecutivos de comprimento zero, a função retornou o segundo SKB vazio; unix_stream_read_generic() então o ignorou sem chamar manage_oob() novamente, de modo que o verdadeiro OOB SKB foi dequeued e freeado enquanto u->oob_skb ainda apontava para ele.

Sequência mínima de gatilhos

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

Primitives exposed by unix_stream_recv_urg()

  1. Leitura arbitrária de 1 byte (repetível): state->recv_actor() acaba realizando copy_to_user(user, skb_sourced_addr, 1). Se o SKB pendente for realocado em memória controlada pelo atacante (ou em um alias controlado como uma página de pipe), cada recv(MSG_OOB | MSG_PEEK) copia um byte de um endereço kernel arbitrário permitido por __check_object_size() para o espaço do usuário sem travar. Manter MSG_PEEK ativado preserva o ponteiro pendente para leituras ilimitadas.
  2. Escrita apenas parcialmente controlada: Quando MSG_PEEK está desativado, UNIXCB(oob_skb).consumed += 1 incrementa o campo de 32 bits no deslocamento 0x44. Em alocações SKB alinhadas a 0x100 isso fica quatro bytes acima de uma palavra alinhada a 8 bytes, convertendo o primitivo em um incremento de +4 GiB da palavra hospedada no offset 0x40. Transformar isso em uma escrita kernel requer posicionar um valor 64-bit sensível naquele offset.

Reallocating the SKB page for arbitrary read

  1. Drain order-0/1 unmovable freelists: Mapear um VMA anônimo enorme read-only e falhar cada página para forçar a alocação de page-tables (order-0 unmovable). Preencher ~10% da RAM com page tables garante que alocações subsequentes de skbuff_head_cache usem páginas fresh do buddy uma vez que as listas order-0 se esgotem.
  2. Spray SKBs and isolate a slab page: Use dezenas de stream socketpairs e enfileire centenas de pequenas mensagens por socket (~0x100 bytes por SKB) para popular skbuff_head_cache. Free selecione SKBs para forçar uma página de slab alvo inteiramente sob controle do atacante e monitore o refcount de struct page emergente via o primitivo de leitura.
  3. Return the slab page to the buddy allocator: Free todos os objetos na página, então faça alocações/frees adicionais suficientes para empurrar a página para fora das listas parciais per-CPU do SLUB e das listas de páginas per-CPU para que ela se torne uma página order-1 na freelist do buddy.
  4. Reallocate as pipe buffer: Crie centenas de pipes; cada pipe reserva pelo menos duas páginas de dados de 0x1000 bytes (PIPE_MIN_DEF_BUFFERS). Quando o buddy allocator split uma página order-1, metade reutiliza a página SKB liberada. Para localizar qual pipe e qual offset alias oob_skb, escreva bytes marcadores únicos em fake SKBs armazenados por toda a páginas de pipe e execute recv(MSG_OOB | MSG_PEEK) repetidos até que o marcador seja retornado.
  5. Forge a stable SKB layout: Popule a página de pipe aliasada com um fake struct sk_buff cujos ponteiros data/head e a estrutura skb_shared_info apontam para endereços kernel arbitrários de interesse. Como x86_64 desabilita SMAP dentro de copy_to_user(), endereços em user-mode podem servir como buffers de staging até que os ponteiros kernel sejam conhecidos.
  6. Respect usercopy hardening: O copy tem sucesso contra .data/.bss, entradas vmemmap, ranges vmalloc per-CPU, stacks kernel de outras threads, e páginas direct-map que não atravessam limites de folio de ordem superior. Leituras contra .text ou caches especializados rejeitadas por __check_heap_object() simplesmente retornam -EFAULT sem matar o processo.

Introspecting allocators with the read primitive

  • Break KASLR: Leia qualquer descritor IDT do mapeamento fixo em CPU_ENTRY_AREA_RO_IDT_VADDR (0xfffffe0000000000) e subtraia o deslocamento conhecido do handler para recuperar a base do kernel.
  • SLUB/buddy state: Símbolos globais em .data revelam bases de kmem_cache, enquanto entradas vmemmap expõem flags de tipo de cada página, ponteiro de freelist, e o cache dono. Escanear segmentos vmalloc per-CPU revela instâncias de struct kmem_cache_cpu de modo que o próximo endereço de alocação de caches chave (ex.: skbuff_head_cache, kmalloc-cg-192) se torne previsível.
  • Page tables: Em vez de ler mm_struct (bloqueado por usercopy), percorra a pgd_list global (struct ptdesc) e case o mm_struct atual via cpu_tlbstate.loaded_mm. Uma vez que o pgd raiz é conhecido, o primitivo pode atravessar todas as page tables para mapear PFNs para pipe buffers, page tables e stacks do kernel.

Recycling the SKB page as the top kernel-stack page

  1. Free a página de pipe controlada novamente e confirme via vmemmap que seu refcount retorna a zero.
  2. Imediatamente aloque quatro páginas de pipe auxiliares e então libere-as em ordem inversa para que o comportamento LIFO do buddy allocator seja determinístico.
  3. Chame clone() para gerar uma thread auxiliar; stacks do Linux têm quatro páginas em x86_64, então as quatro páginas mais recentemente liberadas tornam-se sua stack, com a última página liberada (a antiga página SKB) nos endereços mais altos.
  4. Verifique via page-table walk que o PFN do top stack da thread auxiliar é igual ao PFN da SKB reciclada.
  5. Use a leitura arbitrária para observar o layout da stack enquanto direciona a thread para pipe_write(). CONFIG_RANDOMIZE_KSTACK_OFFSET subtrai um valor randômico de 0x0–0x3f0 (alinhado) de RSP por syscall; escritas repetidas combinadas com poll()/read() de outra thread revelam quando o writer bloqueia com o offset desejado. Quando sortudo, o argumento bytes derramado de copy_page_from_iter() (R14) fica no offset 0x40 dentro da página reciclada.

Placing fake SKB metadata on the stack

  • Use sendmsg() em um socket AF_UNIX datagram: o kernel copia o sockaddr_un do usuário para um sockaddr_storage residente na stack (até 108 bytes) e os dados ancilares para outro buffer on-stack antes da syscall bloquear esperando espaço na fila. Isso permite plantar uma estrutura fake SKB precisa na memória da stack.
  • Detecte quando a cópia terminou fornecendo uma control message de 1 byte localizada em uma página de usuário não mapeada; ____sys_sendmsg() a faz faultar, então uma thread auxiliar fazendo poll em mincore() nesse endereço aprende quando a página de destino está presente.
  • O padding inicializado com zero por CONFIG_INIT_STACK_ALL_ZERO preenche convenientemente campos não usados, completando um header SKB válido sem escritas adicionais.

Timing the +4 GiB increment with a self-looping frag list

  • Forge skb_shinfo(fakeskb)->frag_list para apontar para um segundo fake SKB (armazenado em memória user controlada) que tem len = 0 e next = &self. Quando skb_walk_frags() itera essa lista dentro de __skb_datagram_iter(), a execução gira indefinidamente porque o iterator nunca alcança NULL e o loop de copy não faz progresso.
  • Mantenha a syscall recv rodando dentro do kernel deixando o segundo fake SKB em self-loop. Quando for hora de disparar o incremento, simplesmente mude o ponteiro next do segundo SKB a partir do espaço usuário para NULL. O loop sai e unix_stream_recv_urg() executa imediatamente UNIXCB(oob_skb).consumed += 1 uma vez, afetando qualquer objeto que atualmente ocupe a página de stack reciclada no offset 0x40.

Stalling copy_from_iter() without userfaultfd

  • Mapear um VMA anônimo RW gigante e faultá-lo completamente.
  • Punch um hole de página única com madvise(MADV_DONTNEED, hole, PAGE_SIZE) e coloque esse endereço dentro do iov_iter usado para write(pipefd, user_buf, 0x3000).
  • Em paralelo, chame mprotect() em todo o VMA a partir de outra thread. A syscall pega o mmap write lock e anda por cada PTE. Quando o writer do pipe atinge o hole, o page fault handler bloqueia no mmap lock mantido por mprotect(), pausando copy_from_iter() em um ponto determinístico enquanto o valor bytes derramado reside no segmento de stack hospedado pela página SKB reciclada.

Turning the increment into arbitrary PTE writes

  1. Fire the increment: Libere o frag loop enquanto copy_from_iter() está parado para que o incremento de +4 GiB acerte a variável bytes.
  2. Overflow the copy: Uma vez que o fault retoma, copy_page_from_iter() acredita que pode copiar >4 GiB para a página de pipe atual. Depois de preencher os legítimos 0x2000 bytes (duas pipe buffers), executa outra iteração e escreve os dados de usuário restantes na página física que segue o PFN do buffer de pipe.
  3. Arrange adjacency: Usando telemetria do allocator, force o buddy allocator a colocar uma página de PTE de propriedade do processo imediatamente após a página de buffer do pipe alvo (ex.: alternar entre alocar páginas de pipe e tocar novos ranges virtuais para disparar alocação de page-tables até que os PFNs se alinhem dentro do mesmo pageblock de 2 MiB).
  4. Overwrite page tables: Encode entradas PTE desejadas nos 0x1000 bytes extras de dados de usuário para que o OOB copy_from_iter() preencha a página vizinha com entradas escolhidas pelo atacante, concedendo mapeamentos user RW/RWX de memória física kernel ou reescrevendo entradas existentes para desabilitar SMEP/SMAP.

Mitigations / hardening ideas

  • Kernel: Apply 32ca245464e1479bfea8592b9db227fdc1641705 (properly revalidates SKBs) e considere desabilitar AF_UNIX OOB entirely a menos que estritamente necessário via CONFIG_AF_UNIX_OOB (5155cbcdbf03). Harden manage_oob() com checagens adicionais de sanidade (ex.: loop até unix_skb_len() > 0) e audite outros protocolos de socket para suposições similares.
  • Sandboxing: Filtrar flags MSG_OOB/MSG_PEEK em perfis seccomp ou APIs broker de nível superior (mudança do Chrome 6711812 agora bloqueia MSG_OOB do lado do renderer).
  • Allocator defenses: Fortalecer randomização de freelist do SLUB ou impor page coloring por cache complicaria o reciclamento determinístico de páginas; limitar a contagem de pipe buffers também reduz a confiabilidade de realocação.
  • Monitoring: Expor alocações de page-table em alta taxa ou uso anômalo de pipes via telemetria—este exploit consome grandes quantidades de page tables e pipe buffers.

References

Tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks