AF_UNIX MSG_OOB UAF & SKB-based kernel primitives

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

TL;DR

  • Linux >=6.9 a introduit un refactor défectueux manage_oob() (5aa57d9f2d53) pour la gestion de AF_UNIX MSG_OOB. Des SKB empilés de longueur zéro contournent la logique qui efface u->oob_skb, donc un recv() normal pouvait free l’SKB out-of-band tandis que le pointeur restait vivant, menant à CVE-2025-38236.
  • Relancer recv(..., MSG_OOB) déréférence le struct sk_buff pendouillant. Avec MSG_PEEK, le chemin unix_stream_recv_urg() -> __skb_datagram_iter() -> copy_to_user() devient une lecture arbitraire stable d’1 octet dans le kernel ; sans MSG_PEEK le primitive incrémente UNIXCB(oob_skb).consumed à l’offset 0x44, i.e. ajoute +4 GiB au dword supérieur de toute valeur 64-bit placée à l’offset 0x40 dans l’objet réalloué.
  • En drainant des pages non déplaçables d’ordre 0/1 (page-table spray), en forçant la libération d’une page de slab SKB dans l’allocator buddy, et en réutilisant la page physique comme pipe buffer, l’exploit forge des métadonnées SKB dans une mémoire contrôlée pour identifier la page pendouillante et pivoter le primitive de lecture vers .data, vmemmap, per-CPU et les régions de page-table malgré le usercopy hardening.
  • La même page peut ensuite être recyclée comme page supérieure de kernel-stack d’un thread nouvellement cloné. CONFIG_RANDOMIZE_KSTACK_OFFSET devient un oracle : en sondant la disposition de la pile pendant que pipe_write() bloque, l’attaquant attend que la longueur renversée de copy_page_from_iter() (R14) tombe à l’offset 0x40, puis déclenche l’incrément de +4 GiB pour corrompre la valeur sur la pile.
  • Une skb_shinfo()->frag_list auto-bouclante maintient l’appel UAF en boucle dans l’espace kernel jusqu’à ce qu’un thread coopérant bloque copy_from_iter() (via mprotect() sur un VMA contenant un trou MADV_DONTNEED). Casser la boucle libère l’incrément exactement quand la cible sur la pile est vivante, gonflant l’argument bytes de sorte que copy_page_from_iter() écrit au-delà de la page de pipe buffer vers la page physique suivante.
  • En surveillant les PFNs des pipe-buffers et les tables de pages avec le primitive de lecture, l’attaquant s’assure que la page suivante est une page PTE, convertit la copie OOB en écritures PTE arbitraires, et obtient lecture/écriture/exécution kernel sans restriction. Chrome a atténué l’accès en bloquant MSG_OOB depuis les renderers (6711812), et Linux a corrigé la faille logique dans 32ca245464e1 et introduit CONFIG_AF_UNIX_OOB pour rendre la fonctionnalité optionnelle.

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

unix_stream_read_generic() s’attend à ce que chaque SKB retourné par manage_oob() ait unix_skb_len() > 0. Après 93c99f21db36, manage_oob() a sauté le chemin de nettoyage skb == u->oob_skb chaque fois qu’il supprimait en premier un SKB de longueur zéro laissé par recv(MSG_OOB). La correction ultérieure (5aa57d9f2d53) avançait encore depuis le premier SKB de longueur zéro vers skb_peek_next() sans re-vérifier la longueur. Avec deux SKB consécutifs de longueur zéro, la fonction retournait le second SKB vide ; unix_stream_read_generic() l’ignorait ensuite sans appeler à nouveau manage_oob(), de sorte que le vrai SKB OOB était désenfilé et free alors que u->oob_skb pointait encore dessus.

Séquence minimale de déclenchement

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 exposées par unix_stream_recv_urg()

  1. 1-byte arbitrary read (repeatable): state->recv_actor() effectue finalement copy_to_user(user, skb_sourced_addr, 1). Si le SKB dangling est réalloué dans une zone contrôlée par l’attaquant (ou dans un alias contrôlé comme une pipe page), chaque recv(MSG_OOB | MSG_PEEK) copie un octet depuis une adresse kernel arbitraire autorisée par __check_object_size() vers l’espace utilisateur sans provoquer de plantage. Garder MSG_PEEK activé préserve le pointeur suspendu pour des lectures illimitées.
  2. Constrained write : Quand MSG_PEEK est désactivé, UNIXCB(oob_skb).consumed += 1 incrémente le champ 32 bits à l’offset 0x44. Sur des allocations SKB alignées sur 0x100 ceci se situe quatre octets au-dessus d’un mot aligné sur 8 octets, convertissant la primitive en un incrément de +4 GiB du mot hébergé à l’offset 0x40. Transformer cela en écriture kernel nécessite de positionner une valeur 64 bits sensible à cet offset.

Reallocating the SKB page for arbitrary read

  1. Drain order-0/1 unmovable freelists : Mappez une énorme VMA anonyme en lecture seule et faultez chaque page pour forcer l’allocation des tables de pages (order-0 unmovable). Remplir ~10% de la RAM avec des tables de pages garantit que les allocations suivantes de skbuff_head_cache récupéreront des pages buddy fraîches une fois les listes order-0 épuisées.
  2. Spray SKBs and isolate a slab page : Utilisez des dizaines de stream socketpairs et enfilez des centaines de petits messages par socket (~0x100 octets par SKB) pour remplir skbuff_head_cache. Freez des SKB choisis pour pousser une page de slab cible entièrement sous contrôle de l’attaquant et surveillez son refcount struct page via la primitive de lecture émergente.
  3. Return the slab page to the buddy allocator : Freez chaque objet sur la page, puis effectuez suffisamment d’allocations/frees supplémentaires pour expulser la page des listes partielles per-CPU de SLUB et des listes per-CPU de pages afin qu’elle devienne une page order-1 dans le buddy freelist.
  4. Reallocate as pipe buffer : Créez des centaines de pipes ; chaque pipe réserve au moins deux pages de données de 0x1000 octets (PIPE_MIN_DEF_BUFFERS). Quand le buddy allocator scinde une page order-1, une moitié réutilise la page SKB libérée. Pour localiser quel pipe et quel offset aliasent oob_skb, écrivez des octets marqueurs uniques dans de faux SKB stockés à travers les pages de pipe et appelez répétitivement recv(MSG_OOB | MSG_PEEK) jusqu’à ce que le marqueur soit retourné.
  5. Forge a stable SKB layout : Remplissez la page de pipe aliasée avec un faux struct sk_buff dont les pointeurs data/head et la structure skb_shared_info pointent vers des adresses kernel arbitraires d’intérêt. Parce que x86_64 désactive SMAP à l’intérieur de copy_to_user(), des adresses en mode utilisateur peuvent servir de buffers de staging tant que les pointeurs kernel ne sont pas connus.
  6. Respect usercopy hardening : La copie réussit contre .data/.bss, les entrées vmemmap, les plages vmalloc per-CPU, les stacks kernel d’autres threads et les pages direct-map qui ne chevauchent pas des folios d’ordre supérieur. Les lectures contre .text ou des caches spécialisés rejetés par __check_heap_object() retournent simplement -EFAULT sans tuer le processus.

Introspecting allocators with the read primitive

  • Break KASLR : Lisez n’importe quel descripteur IDT depuis le mapping fixe à CPU_ENTRY_AREA_RO_IDT_VADDR (0xfffffe0000000000) et soustrayez l’offset connu du handler pour récupérer la base du kernel.
  • SLUB/buddy state : Les symboles globaux .data révèlent les bases de kmem_cache, tandis que les entrées vmemmap exposent les flags de type de chaque page, le pointeur de freelist et le cache propriétaire. Scanner les segments vmalloc per-CPU découvre des instances struct kmem_cache_cpu de sorte que l’adresse d’allocation suivante de caches clés (p.ex. skbuff_head_cache, kmalloc-cg-192) devienne prédictible.
  • Page tables : Plutôt que de lire mm_struct (bloqué par usercopy), parcourez la pgd_list globale (struct ptdesc) et faites correspondre le mm_struct courant via cpu_tlbstate.loaded_mm. Une fois le pgd racine connu, la primitive peut traverser toutes les tables de pages pour mapper les PFN des pipe buffers, des tables de pages et des stacks kernel.

Recycling the SKB page as the top kernel-stack page

  1. Freez à nouveau la page de pipe contrôlée et confirmez via vmemmap que son refcount revient à zéro.
  2. Allouez immédiatement quatre pages de pipe aide et freez-les ensuite dans l’ordre inverse afin que le comportement LIFO du buddy allocator soit déterministe.
  3. Appelez clone() pour lancer un thread helper ; les stacks Linux font quatre pages sur x86_64, donc les quatre pages les plus récemment freed deviennent sa stack, la dernière page freed (l’ancienne page SKB) étant aux adresses les plus hautes.
  4. Vérifiez via un parcours des tables de pages que le PFN de la top stack du thread helper est égal au PFN SKB recyclé.
  5. Utilisez la lecture arbitraire pour observer le layout de la stack tout en orientant le thread vers pipe_write(). CONFIG_RANDOMIZE_KSTACK_OFFSET soustrait un offset aléatoire 0x0–0x3f0 (aligné) de RSP par syscall ; des écritures répétées combinées à poll()/read() depuis un autre thread révèlent quand l’écrivain se bloque avec l’offset désiré. Avec de la chance, l’argument bytes renvoyé par copy_page_from_iter() (R14) se trouve à l’offset 0x40 à l’intérieur de la page recyclée.

Placing fake SKB metadata on the stack

  • Utilisez sendmsg() sur un socket AF_UNIX datagram : le kernel copie le sockaddr_un utilisateur dans un sockaddr_storage résidant sur la stack (jusqu’à 108 octets) et les données accessoires dans un autre buffer sur la stack avant que le syscall ne bloque en attendant de l’espace dans la queue. Cela permet d’implanter une structure fake SKB précise dans la mémoire de la stack.
  • Détectez la fin de la copie en fournissant un message de contrôle d’1 octet situé dans une page utilisateur non mappée ; ____sys_sendmsg() la fault, ainsi un thread aide faisant du poll sur mincore() à cette adresse apprend quand la page destination est présente.
  • Le padding initialisé à zéro par CONFIG_INIT_STACK_ALL_ZERO remplit commodément les champs inutilisés, complétant un header SKB valide sans écritures supplémentaires.

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

  • Forgez skb_shinfo(fakeskb)->frag_list pour pointer vers un second fake SKB (stocké en mémoire utilisateur contrôlée) qui a len = 0 et next = &self. Quand skb_walk_frags() itère cette liste à l’intérieur de __skb_datagram_iter(), l’exécution tourne indéfiniment car l’itérateur n’atteint jamais NULL et la boucle de copie ne progresse pas.
  • Gardez l’appel recv en cours dans le kernel en laissant le second fake SKB en auto-boucle. Quand il est temps de déclencher l’incrément, changez simplement le pointeur next du second SKB depuis l’espace utilisateur vers NULL. La boucle se termine et unix_stream_recv_urg() exécute immédiatement UNIXCB(oob_skb).consumed += 1 une fois, affectant l’objet qui occupe actuellement la page de stack recyclée à l’offset 0x40.

Stalling copy_from_iter() without userfaultfd

  • Mappez une gigantesque VMA anonyme RW et faultez-la entièrement.
  • Creusez un trou d’une page avec madvise(MADV_DONTNEED, hole, PAGE_SIZE) et placez cette adresse dans l’iov_iter utilisé pour write(pipefd, user_buf, 0x3000).
  • En parallèle, appelez mprotect() sur toute la VMA depuis un autre thread. Le syscall prend le write lock du mmap et parcourt chaque PTE. Quand le writer de pipe atteint le trou, le handler de page fault bloque sur le mmap lock détenu par mprotect(), mettant en pause copy_from_iter() à un point déterministe pendant que la valeur bytes épanchée réside sur le segment de stack hébergé par la page SKB recyclée.

Turning the increment into arbitrary PTE writes

  1. Fire the increment : Libérez la boucle de frags pendant que copy_from_iter() est en pause pour que l’incrément +4 GiB atteigne la variable bytes.
  2. Overflow the copy : Une fois le fault repris, copy_page_from_iter() croit pouvoir copier >4 GiB dans la page de pipe courante. Après avoir rempli les 0x2000 octets légitimes (deux pipe buffers), il exécute une autre itération et écrit les données utilisateur restantes dans la page physique qui suit le PFN du pipe buffer.
  3. Arrange adjacency : En utilisant la télémétrie de l’allocator, forcez le buddy allocator à placer une page PTE appartenant au processus immédiatement après la page de pipe cible (p.ex. alternez entre allouer des pages de pipe et toucher de nouvelles plages virtuelles pour déclencher l’allocation de tables de pages jusqu’à ce que les PFN s’alignent à l’intérieur du même pageblock de 2 MiB).
  4. Overwrite page tables : Encodez des entrées PTE désirées dans les 0x1000 octets supplémentaires de données utilisateur de sorte que le copy_from_iter() OOB remplisse la page voisine avec des entrées choisies par l’attaquant, accordant des mappings user RW/RWX de la mémoire physique kernel ou réécrivant des entrées existantes pour désactiver SMEP/SMAP.

Mitigations / hardening ideas

  • Kernel : Appliquez 32ca245464e1479bfea8592b9db227fdc1641705 (revalide correctement les SKB) et envisagez de désactiver AF_UNIX OOB entièrement sauf si strictement nécessaire via CONFIG_AF_UNIX_OOB (5155cbcdbf03). Renforcez manage_oob() avec des vérifications de sanity supplémentaires (p.ex. bouclez jusqu’à unix_skb_len() > 0) et auditez d’autres protocoles socket pour des hypothèses similaires.
  • Sandboxing : Filtrez les flags MSG_OOB/MSG_PEEK dans les profils seccomp ou les APIs broker de plus haut niveau (le changement Chrome 6711812 bloque désormais MSG_OOB côté renderer).
  • Allocator defenses : Renforcer la randomisation des freelists SLUB ou appliquer un coloring par page par cache compliquerait le recyclage déterministe des pages ; limiter le nombre de buffers de pipe réduit aussi la fiabilité de la réallocation.
  • Monitoring : Exposez l’allocation élevée de tables de pages ou l’utilisation anormale de pipes via la télémétrie — cet exploit consomme de grandes quantités de tables de pages et de pipe buffers.

References

Tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks