AF_UNIX MSG_OOB UAF & SKB-based kernel primitives

Tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks

TL;DR

  • Linux >=6.9 introduced a flawed manage_oob() refactor (5aa57d9f2d53) for AF_UNIX MSG_OOB handling. Gestapelte zero-length SKBs umgingen die Logik, die u->oob_skb löschen soll, sodass ein normales recv() das Out-of-band-SKB freigeben konnte, während der Pointer weiterhin live war — daraus resultierte CVE-2025-38236.
  • Ein erneutes Auslösen von recv(..., MSG_OOB) dereferenziert das hängende struct sk_buff. Mit MSG_PEEK wird der Pfad unix_stream_recv_urg() -> __skb_datagram_iter() -> copy_to_user() zu einem stabilen 1-Byte-arbiträren Kernel-Lese-Primitive; ohne MSG_PEEK erhöht das Primitive UNIXCB(oob_skb).consumed bei Offset 0x44, d. h. es addiert +4 GiB zum oberen DWORD eines beliebigen 64-Bit-Werts, der bei Offset 0x40 im neu allozierten Objekt liegt.
  • Durch Drainen von order-0/1 nicht verschiebbaren Seiten (page-table spray), erzwungenes Freigeben einer SKB-Slab-Seite in den buddy-Allocator und Wiederverwendung der physischen Seite als pipe buffer fälscht der Exploit SKB-Metadaten in kontrolliertem Speicher, um die hängende Seite zu identifizieren und das Lese-Primitive in .data, vmemmap, per-CPU- und page-table-Regionen zu pivotieren — trotz usercopy hardening.
  • Dieselbe Seite kann später als oberste Kernel-Stack-Seite eines frisch geklonten Threads recycelt werden. CONFIG_RANDOMIZE_KSTACK_OFFSET wird so zu einem Orakel: durch Abtasten des Stack-Layouts während pipe_write() blockiert, wartet der Angreifer, bis die ausgelagerte copy_page_from_iter()-Länge (R14) bei Offset 0x40 landet, und löst dann die +4 GiB-Inkrementierung aus, um den Stack-Wert zu korruptieren.
  • Eine selbstschleifende skb_shinfo()->frag_list hält den UAF-Syscall im Kernel-Raum in einer Schleife, bis ein kooperierender Thread copy_from_iter() verzögert (via mprotect() über ein VMA mit einem einzelnen MADV_DONTNEED-Loch). Das Aufbrechen der Schleife gibt die Inkrementierung genau dann frei, wenn das Stack-Ziel live ist, wodurch das bytes-Argument aufgeblasen wird und copy_page_from_iter() über die pipe buffer-Seite hinaus in die nächste physische Seite schreibt.
  • Durch Überwachen der pipe-buffer-PFNs und der Page Tables mit dem Lese-Primitive stellt der Angreifer sicher, dass die folgende Seite eine PTE-Seite ist, wandelt den OOB-Copy in beliebige PTE-Schreibvorgänge um und erzielt uneingeschränkten Kernel read/write/execute. Chrome verringerte die Erreichbarkeit, indem es MSG_OOB für Renderer blockierte (6711812), und Linux behob den Logikfehler in 32ca245464e1 und führte zudem CONFIG_AF_UNIX_OOB ein, um die Funktion optional zu machen.

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

unix_stream_read_generic() erwartet, dass jedes von manage_oob() zurückgegebene SKB unix_skb_len() > 0 hat. Nach 93c99f21db36 übersprang manage_oob() den Cleanup-Pfad skb == u->oob_skb, wann immer es zuerst ein von recv(MSG_OOB) übriggelassenes zero-length SKB entfernte. Der nachfolgende Fix (5aa57d9f2d53) setzte zwar vom ersten zero-length SKB zu skb_peek_next() fort, prüfte aber die Länge nicht erneut. Bei zwei aufeinanderfolgenden zero-length SKBs gab die Funktion das zweite leere SKB zurück; unix_stream_read_generic() übersprang es dann, ohne manage_oob() erneut aufzurufen, sodass das echte OOB-SKB dequeued und freigegeben wurde, während u->oob_skb weiterhin darauf zeigte.

Minimal trigger sequence

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

Vom unix_stream_recv_urg() bereitgestellte Primitive

  1. 1-byte arbitrary read (repeatable): state->recv_actor() führt letztlich copy_to_user(user, skb_sourced_addr, 1) aus. Wenn das dangling SKB in vom Angreifer kontrollierten Speicher (oder in ein kontrolliertes Alias wie eine pipe-Seite) neu alloziert wird, kopiert jeder recv(MSG_OOB | MSG_PEEK) ein Byte aus einer beliebigen Kernel-Adresse, die von __check_object_size() erlaubt ist, in den Userspace, ohne abzustürzen. Das Setzen von MSG_PEEK erhält den dangling Pointer und erlaubt unbegrenzte Reads.
  2. Constrained write: Wenn MSG_PEEK nicht gesetzt ist, erhöht UNIXCB(oob_skb).consumed += 1 das 32-Bit-Feld bei Offset 0x44. Bei 0x100-ausgerichteten SKB-Allokationen liegt dieses vier Bytes oberhalb eines 8-Byte-ausgerichteten Wortes und verwandelt die Primitive in eine +4 GiB-Inkrement-Operation des Wortes an Offset 0x40. Um dies in einen Kernel-Write zu verwandeln, muss ein sensibles 64-Bit-Wert an diesem Offset positioniert werden.

Reallocating the SKB page for arbitrary read

  1. Drain order-0/1 unmovable freelists: Mappe ein großes read-only anonymes VMA und fault jede Seite, um Page-Table-Allokation zu erzwingen (order-0 unmovable). Das Auffüllen von ~10% des RAM mit Page-Tables stellt sicher, dass nach Erschöpfung der order-0-Listen skbuff_head_cache frische Buddy-Seiten zieht.
  2. Spray SKBs and isolate a slab page: Verwende Dutzende Stream-Socketpairs und queue Hunderte kleiner Nachrichten pro Socket (~0x100 Bytes pro SKB), um skbuff_head_cache zu füllen. Freee ausgewählte SKBs, um eine Ziel-Slab-Seite vollständig unter Angreifer-Kontrolle zu bringen, und überwache deren struct page-Refcount mittels der entstehenden Read-Primitive.
  3. Return the slab page to the buddy allocator: Freee jedes Objekt auf der Seite und führe dann genug zusätzliche Allokationen/Frees durch, um die Seite aus den SLUB per-CPU partial lists und per-CPU page lists zu drücken, sodass sie als order-1-Seite in die Buddy-Freeliste gelangt.
  4. Reallocate as pipe buffer: Erzeuge Hunderte Pipes; jede Pipe reserviert mindestens zwei 0x1000-Byte-Datenseiten (PIPE_MIN_DEF_BUFFERS). Wenn der Buddy-Allocator eine order-1-Seite splittet, benutzt eine Hälfte die freigegebene SKB-Seite wieder. Um zu finden, welche Pipe und welcher Offset oob_skb aliasiert, schreibe eindeutige Marker-Bytes in fake SKBs, die in den Pipe-Seiten verteilt sind, und rufe wiederholt recv(MSG_OOB | MSG_PEEK) auf, bis der Marker zurückgegeben wird.
  5. Forge a stable SKB layout: Fülle die aliasierte Pipe-Seite mit einem gefälschten struct sk_buff, dessen data/head-Pointer und skb_shared_info-Struktur auf beliebige Kernel-Adressen von Interesse zeigen. Da x86_64 SMAP innerhalb von copy_to_user() deaktiviert, können User-Mode-Adressen als Staging-Buffer dienen, bis Kernel-Pointer bekannt sind.
  6. Respect usercopy hardening: Der Copy gelingt gegen .data/.bss, vmemmap-Einträge, per-CPU vmalloc-Bereiche, Kernel-Stacks anderer Threads und direct-map-Seiten, die keine höheren Folio-Grenzen überschreiten. Reads gegen .text oder spezialisierte Caches, die von __check_heap_object() abgelehnt werden, liefern einfach -EFAULT zurück, ohne den Prozess zu killen.

Introspecting allocators with the read primitive

  • Break KASLR: Lese beliebige IDT-Deskriptoren aus dem fixed mapping bei CPU_ENTRY_AREA_RO_IDT_VADDR (0xfffffe0000000000) und subtrahiere den bekannten Handler-Offset, um die Kernel-Base wiederherzustellen.
  • SLUB/buddy state: Globale .data-Symbole offenbaren kmem_cache-Basen, während vmemmap-Einträge die Typ-Flags jeder Seite, Freelist-Pointer und die besitzende Cache offenlegen. Das Scannen per-CPU vmalloc-Segmente deckt struct kmem_cache_cpu-Instanzen auf, sodass die nächste Allokationsadresse wichtiger Caches (z. B. skbuff_head_cache, kmalloc-cg-192) vorhersagbar wird.
  • Page tables: Anstatt mm_struct zu lesen (durch usercopy blockiert), durchlaufe die globale pgd_list (struct ptdesc) und gleiche das aktuelle mm_struct über cpu_tlbstate.loaded_mm ab. Sobald das Root-pgd bekannt ist, kann die Primitive jede Page-Table traversieren, um PFNs für Pipe-Buffers, Page-Tables und Kernel-Stacks zu ermitteln.

Recycling the SKB page as the top kernel-stack page

  1. Freee die kontrollierte Pipe-Seite erneut und bestätige via vmemmap, dass ihr Refcount wieder null ist.
  2. Alloziere sofort vier Helfer-Pipe-Seiten und freee sie dann in umgekehrter Reihenfolge, sodass das LIFO-Verhalten des Buddy-Allocators deterministisch ist.
  3. Rufe clone() auf, um einen Helper-Thread zu erzeugen; Linux-Stacks sind auf x86_64 vier Seiten groß, also werden die vier zuletzt freigegebenen Seiten sein Stack, wobei die zuletzt freigegebene Seite (die ehemalige SKB-Seite) an den höchsten Adressen liegt.
  4. Verifiziere per Page-Table-Walk, dass die top stack PFN des Helper-Threads der rekonstruierten SKB-PFN entspricht.
  5. Nutze den arbitrary read, um das Stack-Layout zu beobachten, während du den Thread in pipe_write() lenkst. CONFIG_RANDOMIZE_KSTACK_OFFSET subtrahiert pro Syscall einen zufälligen 0x0–0x3f0 (aligned) Wert vom RSP; wiederholte Writes kombiniert mit poll()/read() von einem anderen Thread zeigen, wann der Writer mit dem gewünschten Offset blockiert. Mit etwas Glück liegt das verschüttete copy_page_from_iter() bytes-Argument (R14) bei Offset 0x40 innerhalb der rekonstruierten Seite.

Placing fake SKB metadata on the stack

  • Verwende sendmsg() auf einem AF_UNIX datagram socket: der Kernel kopiert das user-sockaddr_un in ein stack-residentes sockaddr_storage (bis zu 108 Bytes) und die ancillary data in einen weiteren On-Stack-Buffer, bevor der Syscall blockiert und auf Queue-Platz wartet. Das erlaubt das Platzieren einer präzisen Fake-SKB-Struktur im Stack-Speicher.
  • Erkenne, wann der Copy abgeschlossen ist, indem du eine 1-Byte-Control-Message lieferst, die sich in einer unmapped User-Seite befindet; ____sys_sendmsg() faultet diese ein, sodass ein Helper-Thread, der mincore() auf dieser Adresse polled, erkennt, wann die Zielseite präsent ist.
  • Zero-initialisierte Padding durch CONFIG_INIT_STACK_ALL_ZERO füllt ungenutzte Felder bequem auf, wodurch ein gültiger SKB-Header ohne zusätzliche Writes entsteht.

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

  • Forged skb_shinfo(fakeskb)->frag_list zeigt auf ein zweites Fake-SKB (im vom Angreifer kontrollierten User-Speicher), das len = 0 und next = &self hat. Wenn skb_walk_frags() diese Liste innerhalb von __skb_datagram_iter() iteriert, dreht die Ausführung in einer Endlosschleife, weil der Iterator nie NULL erreicht und die Copy-Schleife keinen Fortschritt macht.
  • Halte den recv-Syscall im Kernel laufen, indem das zweite Fake-SKB sich selbst referenziert. Wenn es Zeit ist, das Inkrement auszulösen, ändere einfach aus dem Userspace den next-Pointer des zweiten SKB auf NULL. Die Schleife beendet sich und unix_stream_recv_urg() führt sofort UNIXCB(oob_skb).consumed += 1 aus, was das Objekt betrifft, das gerade die rekonstruierten Stack-Seite bei Offset 0x40 belegt.

Stalling copy_from_iter() without userfaultfd

  • Mappe ein riesiges anonymes RW-VMA und fault es vollständig ein.
  • Punch ein Single-Page-Hole mit madvise(MADV_DONTNEED, hole, PAGE_SIZE) und setze diese Adresse in den iov_iter, der für write(pipefd, user_buf, 0x3000) verwendet wird.
  • Parallel rufe mprotect() auf dem gesamten VMA von einem anderen Thread auf. Der Syscall nimmt den mmap write lock und traversiert jede PTE. Wenn der Pipe-Writer das Hole erreicht, blockiert der Page-Fault-Handler am vom mprotect() gehaltenen mmap-Lock und pausiert copy_from_iter() an einem deterministischen Punkt, während der verschüttete bytes-Wert auf dem Stack-Segment liegt, das von der rekonstruierten SKB-Seite gehostet wird.

Turning the increment into arbitrary PTE writes

  1. Fire the increment: Löse die Frag-Schleife, während copy_from_iter() gestallt ist, sodass das +4 GiB-Inkrement die bytes-Variable trifft.
  2. Overflow the copy: Sobald der Fault fortgesetzt wird, glaubt copy_page_from_iter(), es könne >4 GiB in die aktuelle Pipe-Seite kopieren. Nachdem die legitimen 0x2000 Bytes (zwei Pipe-Buffers) gefüllt sind, führt es eine weitere Iteration aus und schreibt die verbleibenden User-Daten in welche physische Seite auch immer der Pipe-Buffer-PFN folgt.
  3. Arrange adjacency: Verwende Allocator-Telemetrie, um den Buddy-Allocator dazu zu bringen, eine prosesseigene PTE-Seite unmittelbar nach der Ziel-Pipe-Buffer-Seite zu platzieren (z. B. abwechselnd Pipe-Seiten allozieren und neue virtuelle Bereiche touchen, um Page-Table-Allokation zu triggern, bis die PFNs innerhalb desselben 2 MiB pageblocks ausgerichtet sind).
  4. Overwrite page tables: Kode gewünschte PTE-Einträge in den zusätzlichen 0x1000 Bytes User-Daten, sodass der OOB copy_from_iter() die benachbarte Seite mit vom Angreifer gewählten Einträgen füllt und RW/RWX-User-Mappings von Kernel-physischem Speicher gewährt oder bestehende Einträge so umschreibt, dass SMEP/SMAP deaktiviert werden.

Mitigations / hardening ideas

  • Kernel: Wende 32ca245464e1479bfea8592b9db227fdc1641705 an (validiert SKBs korrekt nach) und erwäge, AF_UNIX OOB komplett zu deaktivieren, sofern nicht strikt benötigt, via CONFIG_AF_UNIX_OOB (5155cbcdbf03). Härtet manage_oob() mit zusätzlichen Sanity-Checks (z. B. Schleife bis unix_skb_len() > 0) und auditiere andere Socket-Protokolle auf ähnliche Annahmen.
  • Sandboxing: Filtere MSG_OOB/MSG_PEEK-Flags in seccomp-Profilen oder höherstufigen Broker-APIs (Chrome-Änderung 6711812 blockiert jetzt renderer-seitiges MSG_OOB).
  • Allocator defenses: Das Verstärken von SLUB-Freelist-Randomisierung oder das Erzwingen von Per-Cache-Page-Coloring würde deterministisches Page-Recycling erschweren; eine Limitierung der Pipe-Buffer-Anzahl reduziert ebenfalls die Realloc-Reliabilität.
  • Monitoring: Exponiere hohe Raten von Page-Table-Allokationen oder abnormales Pipe-Usage via Telemetrie — dieser Exploit verbraucht große Mengen an Page-Tables und Pipe-Buffern.

References

Tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks