AF_UNIX MSG_OOB UAF & SKB-based kernel primitives

Tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks

TL;DR

  • Linux >=6.9 wprowadził wadliwą refaktoryzację manage_oob() (5aa57d9f2d53) dla obsługi AF_UNIX MSG_OOB. Ułożone po sobie zero-długościowe SKB ominęły logikę czyszczącą u->oob_skb, więc normalne recv() mogło zwolnić out-of-band SKB podczas gdy wskaźnik pozostał żywy, prowadząc do CVE-2025-38236.
  • Ponowne wywołanie recv(..., MSG_OOB) dereferencuje wiszącą struct sk_buff. Z MSG_PEEK ścieżka unix_stream_recv_urg() -> __skb_datagram_iter() -> copy_to_user() staje się stabilnym 1-bajtowym arbitralnym odczytem jądra; bez MSG_PEEK prymityw inkrementuje UNIXCB(oob_skb).consumed na offsetcie 0x44, czyli dodaje +4 GiB do górnego dwordu dowolnej 64-bitowej wartości umieszczonej na offsetcie 0x40 wewnątrz realokowanego obiektu.
  • Poprzez opróżnienie stron nieprzenoszalnych rzędu 0/1 (page-table spray), wymuszone zwolnienie strony składającej się z SKB do buddy allocator, i ponowne użycie fizycznej strony jako pipe buffer, exploit fałszuje metadane SKB w kontrolowanej pamięci, żeby zidentyfikować wiszącą stronę i przekierować prymityw odczytu do .data, vmemmap, per-CPU oraz regionów page-table mimo usercopy hardening.
  • Ta sama strona może później zostać ponownie użyta jako górna strona stosu jądra świeżo sklonowanego wątku. CONFIG_RANDOMIZE_KSTACK_OFFSET staje się oraclem: sondując układ stosu podczas blokady pipe_write(), atakujący czeka aż rozlany przez copy_page_from_iter() length (R14) wyląduje pod offsetem 0x40, po czym odpala inkrementację +4 GiB by uszkodzić wartość na stosie.
  • Samoopierający się skb_shinfo()->frag_list utrzymuje UAF syscall w pętli w przestrzeni jądra dopóki współpracujący wątek nie zablokuje copy_from_iter() (przez mprotect() nad VMA zawierającym pojedynczą dziurę MADV_DONTNEED). Złamanie pętli zwalnia inkrementację dokładnie wtedy, gdy cel na stosie jest aktywny, zwiększając argument bytes tak, że copy_page_from_iter() zapisuje poza stroną pipe buffer do następnej fizycznej strony.
  • Monitorując PFN-y pipe-buffer i page table za pomocą prymitywu odczytu, atakujący zapewnia, że kolejna strona jest stroną PTE, konwertuje OOB copy w arbitralne zapisy PTE i uzyskuje nieograniczony kernel R/W/X. Chrome ograniczył dostępność przez zablokowanie MSG_OOB z rendererów (6711812), a Linux naprawił błąd logiczny w 32ca245464e1 oraz wprowadził CONFIG_AF_UNIX_OOB, by uczynić funkcję opcjonalną.

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

unix_stream_read_generic() oczekuje, że każde SKB zwrócone przez manage_oob() będzie miało unix_skb_len() > 0. Po 93c99f21db36, manage_oob() pominął ścieżkę czyszczenia skb == u->oob_skb za każdym razem, gdy najpierw usuwał zero-długościowe SKB pozostawione przez recv(MSG_OOB). Następna poprawka (5aa57d9f2d53) dalej przechodziła od pierwszego zero-długościowego SKB do skb_peek_next() bez ponownego sprawdzenia długości. Przy dwóch kolejnych zero-długościowych SKB funkcja zwracała drugi pusty SKB; unix_stream_read_generic() następnie go pominął bez ponownego wywołania manage_oob(), więc prawdziwe OOB SKB zostało zdequeue’owane i zwolnione podczas gdy u->oob_skb nadal na nie wskazywał.

Minimalna sekwencja wyzwalająca

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. 1-byte arbitrary read (repeatable): state->recv_actor() ostatecznie wykonuje copy_to_user(user, skb_sourced_addr, 1). Jeśli dangling SKB zostanie realokowany w pamięć kontrolowaną przez atakującego (lub do kontrolowanego aliasu, np. pipe page), każde recv(MSG_OOB | MSG_PEEK) kopiuje bajt z dowolnego adresu jądra dopuszczonego przez __check_object_size() do przestrzeni użytkownika bez wywoływania crasha. Utrzymanie MSG_PEEK pozwala zachować dangling pointer dla nieograniczonych odczytów.
  2. Constrained write: Gdy MSG_PEEK jest wyczyszczony, UNIXCB(oob_skb).consumed += 1 inkrementuje 32-bitowe pole pod offsetem 0x44. Przy alokacjach SKB wyrównanych do 0x100 pole to znajduje się cztery bajty nad słowem wyrównanym do 8 bajtów, zamieniając prymityw w inkrementację +4 GiB słowa znajdującego się pod offsetem 0x40. Aby przekształcić to w zapis jądrowy, trzeba umieścić wrażliwą 64-bitową wartość pod tym offsetem.

Reallocating the SKB page for arbitrary read

  1. Drain order-0/1 unmovable freelists: Zmapuj ogromne read-only anonymous VMA i faultuj każdą stronę, aby wymusić alokację page-table (order-0 unmovable). Wypełnienie ~10% RAMu tablicami stron gwarantuje, że późniejsze alokacje skbuff_head_cache będą pobierać świeże buddy pages po wyczerpaniu list order-0.
  2. Spray SKBs and isolate a slab page: Użyj dziesiątek stream socketpairów i umieść setki małych wiadomości na każdym sockecie (~0x100 bytes na SKB), aby zapełnić skbuff_head_cache. Zwolnij wybrane SKBy, aby doprowadzić do sytuacji, gdzie docelowa strona slab jest całkowicie pod kontrolą atakującego i monitoruj jej struct page refcount za pomocą powstającego prymitywu odczytu.
  3. Return the slab page to the buddy allocator: Zwolnij wszystkie obiekty na stronie, następnie wykonaj wystarczającą liczbę dodatkowych alokacji/zwolnień, aby wypchnąć stronę z SLUB per-CPU partial lists i per-CPU page lists, tak by stała się stroną order-1 na buddy freelist.
  4. Reallocate as pipe buffer: Utwórz setki pipe’ów; każdy pipe rezerwuje co najmniej dwie strony danych o rozmiarze 0x1000 (PIPE_MIN_DEF_BUFFERS). Gdy buddy allocator rozdziela stronę order-1, jedna połowa może ponownie użyć zwolnionej strony SKB. Aby zlokalizować, który pipe i który offset aliasuje oob_skb, zapisz unikalne marker-byty w fake SKBach umieszczonych w stronach pipe i wywołuj powtarzalnie recv(MSG_OOB | MSG_PEEK) aż marker zostanie zwrócony.
  5. Forge a stable SKB layout: Wypełnij aliasowaną stronę pipe fałszywym struct sk_buff, którego pola data/head oraz struktura skb_shared_info wskazują na dowolne interesujące adresy jądra. Ponieważ x86_64 wyłącza SMAP wewnątrz copy_to_user(), adresy w trybie użytkownika mogą służyć jako staging buffers, dopóki nie poznasz dokładnych wskaźników jądrowych.
  6. Respect usercopy hardening: Kopia powiedzie się względem .data/.bss, wpisów vmemmap, zakresów per-CPU vmalloc, stosów jądra innych wątków oraz stron direct-map, które nie rozciągają się przez granice wyższych folio. Odczyty względem .text lub specjalizowanych cache’ów odrzucanych przez __check_heap_object() zwracają po prostu -EFAULT bez zabijania procesu.

Introspecting allocators with the read primitive

  • Break KASLR: Odczytaj dowolny deskryptor IDT z fixed mapping pod CPU_ENTRY_AREA_RO_IDT_VADDR (0xfffffe0000000000) i odejmij znany offset handlera, aby odzyskać kernel base.
  • SLUB/buddy state: Globalne symbole .data ujawniają bazy kmem_cache, podczas gdy wpisy vmemmap eksponują flagi typu każdej strony, pointer na freelist i owning cache. Skanowanie per-CPU vmalloc segmentów ujawnia instancje struct kmem_cache_cpu, dzięki czemu następny adres alokacji kluczowych cache’y (np. skbuff_head_cache, kmalloc-cg-192) staje się przewidywalny.
  • Page tables: Zamiast czytać mm_struct (blokowane przez usercopy), przejdź listę pgd_list (struct ptdesc) i dopasuj bieżące mm_struct przez cpu_tlbstate.loaded_mm. Gdy root pgd jest znany, prymityw może przemierzyć każdą tabelę stron, aby zmapować PFN-y dla pipe buffers, page tables i stosów jądra.

Recycling the SKB page as the top kernel-stack page

  1. Zwolnij kontrolowaną stronę pipe ponownie i potwierdź przez vmemmap, że jej refcount wraca do zera.
  2. Natychmiast alokuj cztery pomocnicze strony pipe, a potem zwolnij je w odwrotnej kolejności, tak aby zachowanie LIFO buddy allocator było deterministyczne.
  3. Wywołaj clone() aby utworzyć pomocniczy wątek; stosy Linuxa są czterostronicowe na x86_64, więc cztery ostatnio zwolnione strony staną się jego stosem, przy czym ostatnio zwolniona strona (była strona SKB) znajdzie się pod najwyższymi adresami.
  4. Zweryfikuj przez page-table walk, że top stack PFN pomocniczego wątku równa się zrecyklingowanemu SKB PFN.
  5. Użyj arbitralnego odczytu, aby obserwować układ stosu, jednocześnie sterując wątkiem do pipe_write(). CONFIG_RANDOMIZE_KSTACK_OFFSET odejmuje losowy 0x0–0x3f0 (wyrównany) od RSP przy każdym syscall; powtarzane zapisy w połączeniu z poll()/read() z innego wątku ujawniają, kiedy writer blokuje się z pożądanym offsetem. Gdy będzie szczęście, przelany argument copy_page_from_iter() bytes (R14) znajdzie się pod offsetem 0x40 wewnątrz zrecyklingowanej strony.

Placing fake SKB metadata on the stack

  • Użyj sendmsg() na AF_UNIX datagram socket: kernel kopiuje user sockaddr_un do stosowo-rezydentnego sockaddr_storage (do 108 bytes) oraz ancillary data do innego on-stack buffer przed zablokowaniem syscalla oczekującego na miejsce w kolejce. To pozwala zasadzić precyzyjną fałszywą strukturę SKB w pamięci stosu.
  • Wykryj, kiedy kopiowanie się skończyło, dostarczając 1-bajtowy control message umieszczony na niemapowanej stronie użytkownika; ____sys_sendmsg() wymusi jej fault, więc pomocniczy wątek pollingujący mincore() na tym adresie dowie się, kiedy docelowa strona jest obecna.
  • Zerowe wypełnienie paddingu z CONFIG_INIT_STACK_ALL_ZERO wygodnie uzupełnia nieużywane pola, kończąc poprawny nagłówek SKB bez dodatkowych zapisów.

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

  • Sfałszuj skb_shinfo(fakeskb)->frag_list, aby wskazywał na drugi fałszywy SKB (przechowywany w pamięci użytkownika kontrolowanej przez atakującego) z len = 0 i next = &self. Gdy skb_walk_frags() iteruje tę listę wewnątrz __skb_datagram_iter(), wykonanie zablokuje się w nieskończonej pętli, ponieważ iterator nigdy nie osiąga NULL, a pętla kopiująca nie robi postępu.
  • Utrzymaj recv syscall uruchomiony wewnątrz jądra, pozwalając drugiemu fałszywemu SKB na self-loop. Gdy nadejdzie czas na wystrzelenie inkrementacji, po prostu zmień next drugiego SKB z przestrzeni użytkownika na NULL. Pętla wyjdzie, a unix_stream_recv_urg() natychmiast wykona UNIXCB(oob_skb).consumed += 1 raz, wpływając na dowolny obiekt aktualnie zajmujący zrecyklingowaną stronę stosu pod offsetem 0x40.

Stalling copy_from_iter() without userfaultfd

  • Zmapuj gigantyczne anonimowe RW VMA i wczytaj je w całości.
  • Wykonaj jednodonicową dziurę przez madvise(MADV_DONTNEED, hole, PAGE_SIZE) i umieść ten adres w iov_iter użytym dla write(pipefd, user_buf, 0x3000).
  • Równolegle wywołaj mprotect() na całym VMA z innego wątku. Syscall ten zdobywa mmap write lock i przebiega przez każdy PTE. Gdy pipe writer osiągnie dziurę, handler page fault blokuje się na mmap lock trzymanym przez mprotect(), zatrzymując copy_from_iter() w deterministycznym punkcie, podczas gdy przelany bytes pozostaje na segmencie stosu hostowanym przez zrecyklingowaną stronę SKB.

Turning the increment into arbitrary PTE writes

  1. Fire the increment: Zwolnij frag loop, gdy copy_from_iter() jest wstrzymany, tak aby inkrementacja +4 GiB uderzyła w zmienną bytes.
  2. Overflow the copy: Gdy fault zostanie wznowiony, copy_page_from_iter() uwierzy, że może skopiować >4 GiB do bieżącej strony pipe. Po wypełnieniu legalnych 0x2000 bajtów (dwóch pipe buffers) wykona kolejną iterację i zapisze pozostałe dane użytkownika do fizycznej strony następującej po PFN-ie bufora pipe.
  3. Arrange adjacency: Korzystając z telemetrii allocatorów, zmuszaj buddy allocator, aby umieścił stronę PTE należącą do procesu bezpośrednio po docelowej stronie bufora pipe (np. naprzemiennie alokując strony pipe i dotykając nowe zakresy wirtualne, by wymusić alokację page-table aż PFN-y zalignują się w tym samym 2 MiB pageblock).
  4. Overwrite page tables: Zakoduj pożądane wpisy PTE w dodatkowych 0x1000 bajtach danych użytkownika, tak aby OOB copy_from_iter() wypełnił sąsiednią stronę wpisami wybranymi przez atakującego, dając RW/RWX mapowania pamięci fizycznej jądra w przestrzeni użytkownika lub nadpisując istniejące wpisy w celu wyłączenia SMEP/SMAP.

Mitigations / hardening ideas

  • Kernel: Apply 32ca245464e1479bfea8592b9db227fdc1641705 (properly revalidates SKBs) i rozważyć wyłączenie AF_UNIX OOB całkowicie, chyba że jest to absolutnie potrzebne, przez CONFIG_AF_UNIX_OOB (5155cbcdbf03). Wzmocnić manage_oob() dodatkowymi sanity checks (np. pętlą aż unix_skb_len() > 0) i audytować inne protokoły socketów pod kątem podobnych założeń.
  • Sandboxing: Filtruj flagi MSG_OOB/MSG_PEEK w profilach seccomp lub w wyższych-level broker API (zmiana w Chrome 6711812 teraz blokuje renderer-side MSG_OOB).
  • Allocator defenses: Wzmocnienie randomizacji freelist w SLUB lub wymuszenie per-cache page coloring utrudniłoby deterministyczny recycling stron; ograniczenie maksymalnej liczby buforów pipe także zmniejsza niezawodność realokacji.
  • Monitoring: Eksponuj wysoką częstotliwość alokacji page-table lub nienormalne użycie pipe przez telemetrię — ten exploit zużywa duże ilości page tables i pipe buffers.

References

Tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks