AF_UNIX MSG_OOB UAF & SKB-based kernel primitives

Tip

AWS Hacking’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin

TL;DR

  • Linux >=6.9, AF_UNIX MSG_OOB işleme için yapılan hatalı manage_oob() refactor’ı (5aa57d9f2d53) getirdi. Üst üste binmiş sıfır-uzunluklu SKB’ler, u->oob_skb’yi temizleyen mantığı atlatıyordu; böylece normal bir recv() out-of-band SKB’yi serbest bırakırken işaretçi canlı kalıyor ve bu durum CVE-2025-38236’a yol açıyordu.
  • recv(..., MSG_OOB) yeniden tetiklendiğinde sarkık (dangling) struct sk_buff dereference ediliyordu. MSG_PEEK ile yol unix_stream_recv_urg() -> __skb_datagram_iter() -> copy_to_user() kalıcı 1-baytlık rastgele kernel okumasına dönüşüyordu; MSG_PEEK olmadan primitive, yeniden tahsis edilen nesnenin offset 0x40 içindeki herhangi bir 64-bit değerin üst dword’una offset 0x44’te +4 GiB ekleyen UNIXCB(oob_skb).consumed değerini arttırıyordu.
  • order-0/1 unmovable sayfaları boşaltarak (page-table spray), bir SKB slab sayfasını buddy allocator içine force-free ederek ve fiziksel sayfayı pipe buffer olarak yeniden kullanarak, exploit kontrollü bellekte SKB metadata’sı sahteleyip sarkık sayfayı tespit ediyor ve read primitive’i .data, vmemmap, per-CPU ve page-table bölgelerine pivotluyor; bu, usercopy hardening’e rağmen mümkün oluyor.
  • Aynı sayfa daha sonra yeni clone’lanmış bir thread’in en üst kernel-stack sayfası olarak geri dönüştürülebiliyor. CONFIG_RANDOMIZE_KSTACK_OFFSET bir oracle haline geliyor: attacker, pipe_write() bloklanmışken stack düzenini probe ederek, spilled copy_page_from_iter() uzunluğu (R14) offset 0x40’a denk gelene kadar bekliyor, sonra +4 GiB arttırmayı tetikleyip stack değerini bozuyor.
  • Kendini döngüye alan skb_shinfo()->frag_list, UAF syscall’un kernel alanında dönmesini sağlıyor; bu süre boyunca işbirlikçi bir thread copy_from_iter()’i (tek bir MADV_DONTNEED boşluğu içeren bir VMA üzerinde mprotect() ile) duraklatıyor. Döngüyü kırmak, arttırmayı tam olarak stack hedefi canlı olduğunda serbest bırakıyor; böylece bytes argümanı şişiyor ve copy_page_from_iter() pipe buffer sayfasını aşıp sonraki fiziksel sayfaya yazıyor.
  • Read primitive ile pipe-buffer PFN’lerini ve page table’ları izleyerek, attacker sonraki sayfanın bir PTE sayfası olduğundan emin oluyor, OOB copy’yi arbitrary PTE write’larına çeviriyor ve sınırsız kernel read/write/execute elde ediyor. Chrome, renderer’lardan MSG_OOB’u engelleyerek erişimi kısıtladı (6711812), Linux ise hatalı mantığı 32ca245464e1 ile düzeltti ve özelliği isteğe bağlı yapmak için CONFIG_AF_UNIX_OOB’u ekledi.

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

unix_stream_read_generic() her manage_oob() tarafından döndürülen SKB’nin unix_skb_len() > 0 olmasını bekliyor. 93c99f21db36 sonrası, manage_oob() ilk olarak recv(MSG_OOB) tarafından geride bırakılmış sıfır-uzunluklu bir SKB’yi kaldırdığında skb == u->oob_skb temizleme yolunu atlıyordu. Sonraki düzeltme (5aa57d9f2d53) yine uzunluğu yeniden kontrol etmeden ilk sıfır-uzunluklu SKB’den skb_peek_next()’e geçti. İki ardışık sıfır-uzunluklu SKB olduğunda, fonksiyon ikinci boş SKB’yi döndürdü; unix_stream_read_generic() bunu tekrar manage_oob() çağırmadan atladı, böylece gerçek OOB SKB dequeue edilip free edilirken u->oob_skb hâlâ ona işaret ediyordu.

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

unix_stream_recv_urg() tarafından açığa çıkarılan ilkel işlemler

  1. 1 bayt keyfi okuma (tekrarlanabilir): state->recv_actor() nihayetinde copy_to_user(user, skb_sourced_addr, 1) yapar. Eğer dangling SKB saldırgan kontrollü belleğe (veya pipe page gibi kontrollü bir alias’a) yeniden tahsis edilirse, her recv(MSG_OOB | MSG_PEEK) __check_object_size() ile izin verilen rastgele bir kernel adresinden bir baytı kullanıcı alanına çökmeden kopyalar. MSG_PEEK açık tutulursa dangling pointer sınırsız okumalar için korunur.
  2. Kısıtlı yazma: MSG_PEEK kapalı olduğunda UNIXCB(oob_skb).consumed += 1 offset 0x44’teki 32-bit alanı artırır. 0x100 hizalı SKB tahsislerinde bu, 8 bayt hizalı bir kelimenin dört bayt üstünde yer alır; bu da ilgili offset 0x40’ta barınan kelimeyi +4 GiB artırma primitifine çevirir. Bunu kernel write’a dönüştürmek için hassas 64-bit değeri o offset’e yerleştirmek gerekir.

Keyfi okuma için SKB sayfasını yeniden tahsis etme

  1. Order-0/1 unmovable freelist’leri boşaltma: Büyük bir read-only anonymous VMA map’leyin ve her sayfayı fault ederek page-table tahsisini zorlayın (order-0 unmovable). Page table’larla RAM’in ~%10’unu doldurmak, sonraki skbuff_head_cache tahsislerinin order-0 listeleri tükendiğinde taze buddy sayfaları almasını sağlar.
  2. SKB spreyleme ve bir slab sayfası izole etme: Birkaç düzine stream socketpair kullanın ve her socket için yüzlerce küçük mesaj (~0x100 byte/ SKB) kuyruğa koyarak skbuff_head_cache’i doldurun. Seçilen SKB’leri free ederek hedef slab sayfayı tamamen saldırgan kontrolüne sokun ve ortaya çıkan okuma primitifiyle onun struct page refcount’unu izleyin.
  3. Slab sayfayı buddy allocator’e geri verme: Sayfadaki tüm objeleri free edin, sonra sayfayı SLUB per-CPU partial list’leri ve per-CPU page list’lerden iteleyecek kadar ilave allocation/free yapın ki sayfa buddy freelist’inde order-1 sayfa haline gelsin.
  4. Pipe buffer olarak yeniden tahsis etme: Yüzlerce pipe oluşturun; her pipe en az iki 0x1000-byte veri sayfası rezerv eder (PIPE_MIN_DEF_BUFFERS). Buddy allocator bir order-1 sayfayı böldüğünde, yarılardan biri freed SKB sayfasını yeniden kullanır. Hangi pipe ve hangi offset’in oob_skb ile alias ettiğini bulmak için pipe sayfalarının her yerine sahte SKB’lerde benzersiz marker byte’ları yazın ve marker dönene kadar tekrar eden recv(MSG_OOB | MSG_PEEK) çağrıları yapın.
  5. Stabil SKB düzeni oluşturma: Alias’lanan pipe sayfasını data/head pointer’ları ve skb_shared_info yapısı ilgilenilen rastgele kernel adreslerine işaret eden sahte struct sk_buff ile doldurun. Çünkü x86_64 copy_to_user() içinde SMAP’i devre dışı bırakır, kullanıcı mod adresleri kernel pointer’ları bilinene kadar staging buffer olarak kullanılabilir.
  6. usercopy sertleştirmesine dikkat edin: Kopya .data/.bss, vmemmap girişleri, per-CPU vmalloc aralıkları, diğer thread’lerin kernel stack’leri ve yüksek-order folio sınırlarını aşmayan direkt-map sayfalarına karşı başarılı olur. .text veya __check_heap_object() tarafından reddedilen özel cache’lere karşı okumalar sadece -EFAULT döndürür, process’i öldürmez.

Okuma primitifini kullanarak allocator’ları inceleme

  • KASLR’ı kırma: Sabit mapping’teki CPU_ENTRY_AREA_RO_IDT_VADDR (0xfffffe0000000000) adresinden herhangi bir IDT descriptor’ünü okuyun ve bilinen handler offset’ini çıkararak kernel base’i bulun.
  • SLUB/buddy durumu: Global .data sembolleri kmem_cache bazlarını açığa çıkarırken, vmemmap girişleri her sayfanın tip flag’lerini, freelist pointer’ını ve sahip cache’i gösterir. Per-CPU vmalloc segmentlerini taramak struct kmem_cache_cpu örneklerini ortaya çıkarır, böylece önemli cache’lerin (ör. skbuff_head_cache, kmalloc-cg-192) sonraki tahsis adresi öngörülebilir olur.
  • Page table’lar: mm_struct okumak yerine (usercopy tarafından engellenir), global pgd_list (struct ptdesc) içinde dolaşıp mevcut mm_struct’u cpu_tlbstate.loaded_mm ile eşleştirin. Root pgd bilindikten sonra, primitif pipe buffer’lar, page table’lar ve kernel stack’ler için PFN’leri eşlemek üzere her page table’ı gezebilir.

SKB sayfasını üst kernel-stack sayfası olarak yeniden kullanma

  1. Kontrol edilen pipe sayfasını tekrar free edin ve vmemmap ile onun refcount’unun sıfıra döndüğünü doğrulayın.
  2. Hemen dört yardımcı pipe sayfası allocate edin ve sonra buddy allocator’in LIFO davranışı deterministik olacak şekilde bunları ters sırada free edin.
  3. Bir yardımcı thread üretmek için clone() çağırın; Linux stack’leri x86_64’de dört sayfadır, bu yüzden en son free edilen dört sayfa onun stack’i olur ve son free edilen sayfa (eski SKB sayfası) en yüksek adreslerde yer alır.
  4. Yardımcı thread’in üst stack PFN’sinin recycled SKB PFN’si ile eşit olduğunu page-table walk ile doğrulayın.
  5. Arbitrary read’i kullanarak stack düzenini gözleyin ve thread’i pipe_write() içine yönlendirin. CONFIG_RANDOMIZE_KSTACK_OFFSET her syscall için RSP’den hizalanmış 0x0–0x3f0 rastgele bir değer çıkarır; tekrarlayan yazmalar ile diğer thread’ten poll()/read() kombinasyonu yazarın istenen offset ile bloke olduğu zamanı açığa çıkarır. Şanslıysanız, spilled copy_page_from_iter() bytes argümanı (R14) recycled sayfa içindeki offset 0x40’ta yer alır.

Stack üzerinde sahte SKB metadata’sı yerleştirme

  • AF_UNIX datagram socket üzerinde sendmsg() kullanın: kernel kullanıcı sockaddr_un’u stack-resident sockaddr_storage içine (maks 108 byte) ve ancillary datayı syscall bloke olana kadar başka bir on-stack buffer’a kopyalar. Bu, stack bellekte hassas bir sahte SKB yapısı yerleştirmeye izin verir.
  • Kopyanın ne zaman bittiğini, unmapped bir user sayfasında yer alan 1 baytlık control message vererek tespit edin; ____sys_sendmsg() bunu fault ettirir, böylece o adresi mincore() ile polling yapan yardımcı thread hedef sayfanın hazır olduğunu öğrenir.
  • CONFIG_INIT_STACK_ALL_ZERO’dan gelen sıfır-initialize edilmiş padding kullanılmayan alanları doldurarak ek yazma yapmadan geçerli bir SKB header’ı tamamlar.

Self-looping frag list ile +4 GiB artışı zamanlama

  • skb_shinfo(fakeskb)->frag_list’i ikinci bir sahte SKB’ye işaret edecek şekilde düzenleyin (attacker-kontrollü user bellek içinde) ve bunun len = 0 ve next = &self olmasını sağlayın. skb_walk_frags() bu listeyi __skb_datagram_iter() içinde iter’elediğinde iterator NULL’a ulaşmadığı için yürütme sonsuz döngüye girer ve kopya döngüsü ilerlemez.
  • İkinci sahte SKB’nin self-loop yapmasına izin vererek recv syscall’unu kernel içinde çalışır durumda bırakın. Artışı tetikleyeceğiniz zaman ikinci SKB’nin next pointer’ını user space’ten NULL yapın. Döngü çıkacak ve unix_stream_recv_urg() hemen UNIXCB(oob_skb).consumed += 1 çalıştırarak recycled stack sayfasında şu anki offset 0x40’taki nesneyi etkileyecektir.

userfaultfd olmadan copy_from_iter()’i durdurma

  • Devasa bir anonymous RW VMA map’leyin ve tamamen fault edin.
  • madvise(MADV_DONTNEED, hole, PAGE_SIZE) ile tek sayfalık bir delik açın ve bu adresi write(pipefd, user_buf, 0x3000) için kullanılan iov_iter içine koyun.
  • Paralel olarak, başka bir thread’ten tüm VMA üzerinde mprotect() çağırın. Bu syscall mmap write lock’u alır ve tüm PTE’leri gezer. Pipe writer delikteki sayfaya geldiğinde page fault handler mprotect() tarafından tutulan mmap lock’u bekler; bu, spilled bytes değeri recycled SKB sayfası tarafından host edilen stack segmentinde iken copy_from_iter()’i deterministik bir noktada duraklatır.

Artışı keyfi PTE yazmalarına dönüştürme

  1. Artışı tetikleyin: Frag loop’u copy_from_iter() duraklatılmışken serbest bırakın, böylece +4 GiB artış bytes değişkenine isabet etsin.
  2. Kopyayı taşırma: Fault tekrar devam edince, copy_page_from_iter() mevcut pipe sayfasına >4 GiB kopyalayabileceğini düşünür. Meşru 0x2000 byte’ı (iki pipe buffer) doldurduktan sonra bir iter daha yapar ve kalan kullanıcı verisini pipe buffer PFN’sinin ardından gelen fiziksel sayfaya yazar.
  3. Komşuluğu düzenleyin: Allocator telemetrisi kullanarak buddy allocator’in süreç-sahipli PTE sayfasını hedef pipe buffer sayfasının hemen sonuna yerleştirmesini zorlayın (ör. pipe sayfalarını allocate etmek ve yeni virtual aralıkları touch etmek arasında geçiş yaparak page-table tahsisini tetikleyin ta ki PFN’ler aynı 2 MiB pageblock içinde hizalansın).
  4. Page table’ları overwrite edin: OOB copy_from_iter()’in komşu sayfayı saldırganın seçtiği entry’lerle doldurması için ekstra 0x1000 byte user verisinde istenen PTE entry’lerini kodlayın; bu kernel fiziksel belleğinin RW/RWX user mapping’leri verilmesini veya mevcut entry’lerin SMEP/SMAP’i devre dışı bırakacak şekilde yeniden yazılmasını sağlayabilir.

Mitigations / hardening fikirleri

  • Kernel: 32ca245464e1479bfea8592b9db227fdc1641705 uygulanmalı (SKB’leri doğru şekilde yeniden doğrular) ve AF_UNIX OOB yalnızca gerçekten gerekliyse etkin olacak şekilde CONFIG_AF_UNIX_OOB ile devre dışı bırakma düşünülmelidir (5155cbcdbf03). manage_oob() ek sağlamlık kontrolleriyle (örn. unix_skb_len() > 0 olana dek döngü) sertleştirilmeli ve diğer socket protokolleri benzer varsayımlar için denetlenmelidir.
  • Sandboxing: seccomp profillerinde veya üst seviye broker API’lerinde MSG_OOB/MSG_PEEK bayraklarını filtreleyin (Chrome değişikliği 6711812 artık renderer tarafında MSG_OOB’u engelliyor).
  • Allocator savunmaları: SLUB freelist randomizasyonunu güçlendirmek veya per-cache page coloring uygulamak deterministik sayfa geri dönüşümünü zorlaştırır; pipe buffer sayısını sınırlamak da yeniden tahsis güvenilirliğini azaltır.
  • İzleme: Yüksek oranda page-table tahsisi veya anormal pipe kullanımı gibi durumları telemetri ile açığa çıkarın — bu exploit büyük miktarda page table ve pipe buffer tüketir.

References

Tip

AWS Hacking’i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking’i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin