AF_UNIX MSG_OOB UAF & SKB-आधारित kernel primitives

Tip

AWS हैकिंग सीखें और अभ्यास करें:HackTricks Training AWS Red Team Expert (ARTE)
GCP हैकिंग सीखें और अभ्यास करें: HackTricks Training GCP Red Team Expert (GRTE) Azure हैकिंग सीखें और अभ्यास करें: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks का समर्थन करें

TL;DR

  • Linux >=6.9 introduced a flawed manage_oob() refactor (5aa57d9f2d53) for AF_UNIX MSG_OOB handling. Stacked zero-length SKBs ने उस लॉजिक को बायपास कर दिया जो u->oob_skb को क्लियर करता है, इसलिए एक सामान्य recv() out-of-band SKB को free कर सकता था जबकि pointer लाइव रहा, जिससे CVE-2025-38236 हुआ।
  • Re-triggering recv(..., MSG_OOB) dangling struct sk_buff का dereference कर देता है। MSG_PEEK के साथ, path unix_stream_recv_urg() -> __skb_datagram_iter() -> copy_to_user() एक स्थिर 1-बाइट arbitrary kernel read बन जाती है; बिना MSG_PEEK के primitive UNIXCB(oob_skb).consumed को offset 0x44 पर increment करता है, यानी reallocated object के offset 0x40 में रखे किसी भी 64-bit value के upper dword में +4 GiB जोड़ देता है।
  • order-0/1 unmovable pages को drain करके (page-table spray), एक SKB slab page को buddy allocator में force-free करके, और उसी physical page को pipe buffer के रूप में reuse करके, exploit controlled memory में SKB metadata बनाता है ताकि dangling page पहचान सके और read primitive को .data, vmemmap, per-CPU, और page-table regions में pivot करे, usercopy hardening के बावजूद।
  • वही page बाद में ताज़ा cloned thread के top kernel-stack page के रूप में recycle किया जा सकता है। CONFIG_RANDOMIZE_KSTACK_OFFSET एक oracle बन जाता है: pipe_write() ब्लॉक होने के दौरान stack layout को probe करके, attacker तब तक इंतज़ार करता है जब तक spilled copy_page_from_iter() length (R14) offset 0x40 पर नहीं आता, फिर +4 GiB increment फायर करके stack value को corrupt कर देता है।
  • self-looping skb_shinfo()->frag_list UAF syscall को kernel space में घुमाता रखता है जब तक एक सहयोगी thread copy_from_iter() को stall न कर दे (एक ऐसे VMA पर mprotect() के माध्यम से जिसमें single MADV_DONTNEED hole हो)। लूप तोड़ने पर increment उसी समय रिलीज़ होता है जब stack target live होता है, bytes argument को inflate कर देता है ताकि copy_page_from_iter() pipe buffer page के पार अगले physical page में लिख दे।
  • read primitive से pipe-buffer PFNs और page tables की मॉनिटरिंग करके, attacker यह सुनिश्चित करता है कि अगला page एक PTE page है, OOB copy को arbitrary PTE writes में बदल देता है, और unrestricted kernel read/write/execute प्राप्त कर लेता है। Chrome ने renderers से MSG_OOB ब्लॉक करके पहुंच को कम किया (6711812), और Linux ने logic flaw को 32ca245464e1 में फिक्स किया तथा feature को optional बनाने के लिए CONFIG_AF_UNIX_OOB जोड़ा।

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

unix_stream_read_generic() यह मानता है कि manage_oob() द्वारा लौटाया गया हर SKB का unix_skb_len() > 0 होगा। 93c99f21db36 के बाद, manage_oob() ने उस skb == u->oob_skb cleanup path को स्किप कर दिया जब भी उसने पहला recv(MSG_OOB) द्वारा छोड़ा गया zero-length SKB हटाया। बाद के फिक्स (5aa57d9f2d53) ने पहले zero-length SKB से skb_peek_next() की ओर आगे बढ़ना जारी रखा बिना लंबाई को फिर से जाँचे। दो लगातार zero-length SKBs के साथ, function दूसरे empty SKB को लौटाती थी; unix_stream_read_generic() ने उसे फिर से कॉल किए बिना स्किप कर दिया, इसलिए असली OOB SKB dequeue और free हो गया जबकि u->oob_skb अभी भी उस पर पॉइंट कर रहा था।

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() द्वारा एक्सपोज़ प्रिमिटिव्स

  1. 1-byte arbitrary read (repeatable): state->recv_actor() अंततः copy_to_user(user, skb_sourced_addr, 1) करता है। यदि dangling SKB attacker-controlled memory में पुन:आवंटित हो जाता है (या किसी controlled alias जैसे pipe page में), तो हर recv(MSG_OOB | MSG_PEEK) __check_object_size() द्वारा अनुमति प्राप्त किसी भी arbitrary kernel एड्रेस से एक बाइट user space में बिना क्रैश किए कॉपी कर देता है। MSG_PEEK चालू रखने से dangling pointer अपरिमित पढ़नों के लिए बरकरार रहता है।
  2. Constrained write: जब MSG_PEEK clear होता है, तो UNIXCB(oob_skb).consumed += 1 offset 0x44 पर मौजूद 32-bit फ़ील्ड को इन्क्रीमेंट करता है। 0x100-एलाइंड SKB allocations पर यह 8-बाइट एलाइन वाले शब्द से चार बाइट ऊपर बैठता है, जिससे प्रिमिटिव उस ऑफ़सेट 0x40 पर होस्ट किए गए शब्द में +4 GiB इन्क्रीमेंट में बदल जाता है। इसे kernel write में बदलने के लिए उस ऑफ़सेट पर कोई संवेदनशील 64-bit वैल्यू पॉज़िशन करनी होगी।

Arbitrary read के लिए SKB page को फिर से आवंटित करना

  1. Drain order-0/1 unmovable freelists: एक बड़ा read-only anonymous VMA मैप करें और पेज-टेबल अलोकेशन मजबूर करने के लिए हर पेज को fault कराएँ (order-0 unmovable)। लगभग RAM का ~10% page tables से भरने पर subsequent skbuff_head_cache allocations fresh buddy pages खींचते हैं जब order-0 lists खत्म हो जाती हैं।
  2. Spray SKBs और एक slab page अलग करें: दर्जनों stream socketpairs का उपयोग करें और हर socket पर सैंकड़ों छोटे संदेश queue करें (~0x100 bytes प्रति SKB) ताकि skbuff_head_cache भरा जाए। चुने हुए SKBs को free करके target slab page पूरी तरह attacker control में ले आएँ और उसके struct page refcount को emerging read primitive से मॉनिटर करें।
  3. Slab page को buddy allocator को लौटाएँ: पेज पर मौजूद हर object को free करें, फिर अतिरिक्त allocations/frees करके page को SLUB per-CPU partial lists और per-CPU page lists से बाहर धकेलें ताकि वह buddy freelist पर order-1 पेज बन जाए।
  4. Pipe buffer के रूप में पुन:आवंटन: सैकड़ों pipes बनाएं; हर pipe कम से कम दो 0x1000-बाइट data पेज रिज़र्व करता है (PIPE_MIN_DEF_BUFFERS)। जब buddy allocator एक order-1 पेज स्प्लिट करता है, तो एक आधा freed SKB पेज को reuse करता है। यह पता लगाने के लिए कि कौन सा pipe और किस offset ने oob_skb को alias किया है, fake SKBs में यूनिक marker bytes लिखें जो pipe पेजों में फैले हों और तब तक बार-बार recv(MSG_OOB | MSG_PEEK) कॉल करें जब तक marker वापस न आ जाए।
  5. Stable SKB layout तैयार करें: aliased pipe page को एक fake struct sk_buff से भरें जिनके data/head pointers और skb_shared_info स्ट्रक्चर arbitrary kernel एड्रेसों की ओर इशारा करते हों। चूंकि x86_64 पर copy_to_user() के अंदर SMAP डिसेबल रहता है, user-mode एड्रेस staging buffers के रूप में तब तक उपयोग हो सकते हैं जब तक kernel pointers ज्ञात न हों।
  6. usercopy हार्डनिंग का ध्यान रखें: कॉपी .data/.bss, vmemmap एंट्रियों, per-CPU vmalloc रेंजेज़, अन्य थ्रेड्स के kernel stacks, और direct-map पेजेज़ के खिलाफ काम करती है जो उच्च-ऑर्डर folio सीमाओं को पार नहीं करते। .text या specialized caches के खिलाफ पढ़ने पर __check_heap_object() अस्वीकृति पर बस -EFAULT लौटता है बिना प्रोसेस को मारने के।

Read primitive से allocators का निरीक्षण

  • KASLR तोड़ना: fixed mapping पर CPU_ENTRY_AREA_RO_IDT_VADDR (0xfffffe0000000000) से कोई भी IDT descriptor पढ़ें और ज्ञात handler offset घटाकर kernel base पुनर्प्राप्त करें।
  • SLUB/buddy स्थिति: Global .data symbols kmem_cache बेस दिखाते हैं, जबकि vmemmap एंट्रीज़ हर पेज के type flags, freelist pointer, और owning cache उजागर करती हैं। per-CPU vmalloc segments स्कैन करने से struct kmem_cache_cpu इंस्टेंस मिलते हैं ताकि key caches (जैसे skbuff_head_cache, kmalloc-cg-192) के अगले allocation addresses अनुमानित हो जाएँ।
  • Page tables: mm_struct पढ़ने के बजाय (usercopy द्वारा ब्लॉक), global pgd_list (struct ptdesc) को वॉक करके और cpu_tlbstate.loaded_mm के द्वारा वर्तमान mm_struct से मैचे करके root pgd ज्ञात करें। एक बार root pgd ज्ञात होने पर primitive हर पेज टेबल को ट्रैवर्स कर सकती है ताकि pipe buffers, page tables, और kernel stacks के PFNs मैप किए जा सकें।

SKB पेज को top kernel-stack पेज के रूप में रिसायकल करना

  1. Controlled pipe page को फिर से free करें और vmemmap से पुष्टि करें कि उसका refcount फिर से शून्य हो गया है।
  2. तुरंत चार helper pipe पेज allocate करें और फिर उन्हें reverse order में free करें ताकि buddy allocator का LIFO व्यवहार deterministic बने।
  3. clone() कॉल करके एक helper थ्रेड स्पॉन करें; x86_64 पर Linux stacks चार पेज होते हैं, इसलिए चार सबसे हाल में freed पेज उसका stack बन जाते हैं, जिसमें आखिरी freed पेज (पूर्व SKB पेज) उच्चतम एड्रेसेज़ पर होता है।
  4. Page-table walk द्वारा सत्यापित करें कि helper थ्रेड का top stack PFN recycled SKB PFN के समान है।
  5. Arbitrary read का उपयोग करके stack layout देखें और थ्रेड को pipe_write() में निर्देशित करें। CONFIG_RANDOMIZE_KSTACK_OFFSET प्रत्येक syscall पर RSP से एक रैंडम 0x0–0x3f0 (aligned) घटाता है; repeated writes और दूसरे थ्रेड से poll()/read() मिलाकर पता चलता है जब writer वांछित offset के साथ block हो। किस्मत से, spilled copy_page_from_iter() का bytes argument (R14) recycled page के अंदर offset 0x40 पर आ सकता है।

Stack पर fake SKB metadata रखने का तरीका

  • AF_UNIX datagram socket पर sendmsg() का उपयोग करें: kernel user sockaddr_un को stack-resident sockaddr_storage (अधिकतम 108 bytes) में और ancillary data को syscall के ब्लॉक होने से पहले एक अन्य on-stack buffer में कॉपी करता है। इससे stack मेमोरी में सटीक fake SKB संरचना प्लांट करना संभव होता है।
  • तब पता लगाएँ कि copy कब खत्म हुई है: एक 1-बाइट control message दें जो कि unmapped user page में स्थित हो; ____sys_sendmsg() इसे fault करके लाएगा, तो उस address पर mincore() पोल कर रहा helper थ्रेड यह जान लेता है कि destination page कब मौजूद है।
  • CONFIG_INIT_STACK_ALL_ZERO से मिलने वाला zero-initialized padding अनावश्यक फ़ील्ड भर देता है, जिससे अतिरिक्त लेखन के बिना एक वैध SKB हेडर बन जाता है।

self-looping frag list के साथ +4 GiB इन्क्रीमेंट का टाइमिंग

  • skb_shinfo(fakeskb)->frag_list को दूसरे fake SKB (attacker-controlled user memory में रखा हुआ) की ओर पॉइंट करने के लिए बनाएं, जिसकी len = 0 और next = &self हो। जब skb_walk_frags() यह लिस्ट __skb_datagram_iter() के अंदर iterate करती है, तो iterator कभी NULL तक नहीं पहुँचता और copy loop बिना प्रगति के अनंतकाल तक घूमता रहता है।
  • recv syscall को kernel के अंदर चलते रहने दें ताकि दूसरा fake SKB self-loop करे। जब इन्क्रीमेंट फ़ायर करना हो, तो बस दूसरे SKB का next pointer user space से NULL में बदल दें। लूप निकल जाता है और unix_stream_recv_urg() तुरंत UNIXCB(oob_skb).consumed += 1 को execute करता है, जो उस ऑब्जेक्ट को प्रभावित करेगा जो वर्तमान में recycled stack page के offset 0x40 पर है।

userfaultfd के बिना copy_from_iter() को स्टाल करना

  • एक विशाल anonymous RW VMA मैप करें और उसे पूरी तरह fault कराएँ।
  • madvise(MADV_DONTNEED, hole, PAGE_SIZE) से एक single-page hole बनाएं और उस address को iov_iter में रखें जो write(pipefd, user_buf, 0x3000) के लिए उपयोग हो रहा है।
  • समानांतर में, दूसरे थ्रेड से पूरे VMA पर mprotect() कॉल करें। syscall mmap write lock लेता है और हर PTE को वॉक करता है। जब pipe writer hole पर पहुँचता है, तो page fault handler mmap lock पर block हो जाता है जो mprotect() द्वारा होल्ड किया गया है, और इस तरह copy_from_iter() एक deterministic पॉइंट पर रुक जाता है जबकि spilled bytes वैल्यू recycled SKB पेज पर होस्ट किए गए stack segment पर रहती है।

इन्क्रीमेंट को arbitrary PTE writes में बदलना

  1. इन्क्रीमेंट फ़ायर करें: frag loop को तब रिलीज़ करें जब copy_from_iter() स्टाल हो ताकि +4 GiB इन्क्रीमेंट bytes वैरिएबल पर लगे।
  2. copy को overflow करें: जब fault resume होता है, copy_page_from_iter() मानता है कि वह >4 GiB कॉपी कर सकता है current pipe page में। वैध 0x2000 bytes (दो pipe buffers) भरने के बाद यह एक और iteration चलाता है और शेष user data को उस physical पेज में लिख देता है जो pipe buffer PFN के बाद आता है।
  3. adjacency सेट करें: allocator telemetry का उपयोग करके buddy allocator को मजबूर करें कि वह target pipe buffer पेज के ठीक बाद एक process-owned PTE पेज रखे (उदाहरण के लिए, pipe पेजों को alternate करें और नई virtual रेंजेज़ को छुएँ ताकि page-table allocation ट्रिगर हो जब तक PFNs एक ही 2 MiB pageblock में संरेखित न हो जाएँ)।
  4. page tables overwrite करें: अतिरिक्त 0x1000 bytes user data में इच्छित PTE एंट्रीज़ एन्कोड करें ताकि OOB copy_from_iter() पड़ोसी पेज को attacker-चुने हुए एंट्रीज़ से भर दे, जिससे kernel physical memory के RW/RWX user mappings मिलें या मौजूदा एंट्रीज़ rewrite होकर SMEP/SMAP डिसेबल कर दी जाएँ।

Mitigations / hardening ideas

  • Kernel: 32ca245464e1479bfea8592b9db227fdc1641705 लागू करें (properly revalidates SKBs) और AF_UNIX OOB को तब तक disable करने पर विचार करें जब तक वह सख्ती से आवश्यक न हो, CONFIG_AF_UNIX_OOB (5155cbcdbf03) के माध्यम से। manage_oob() को अतिरिक्त sanity checks (उदा., loop until unix_skb_len() > 0) के साथ हार्डन करें और समान अनुमानों के लिए अन्य socket protocols का ऑडिट करें।
  • Sandboxing: seccomp प्रोफाइल्स या higher-level broker APIs में MSG_OOB/MSG_PEEK फ्लैग्स को फ़िल्टर करें (Chrome change 6711812 अब renderer-side MSG_OOB को ब्लॉक करता है)।
  • Allocator defenses: SLUB freelist randomization को मजबूत करना या per-cache page coloring लागू करना deterministic page recycling को जटिल बनाएगा; pipe buffer counts का pipeline-limiting भी reallocation reliability घटाता है।
  • Monitoring: high-rate page-table allocation या असामान्य pipe उपयोग को telemetry के माध्यम से उजागर करें—यह exploit बड़े पैमाने पर page tables और pipe buffers जलाता है।

References

Tip

AWS हैकिंग सीखें और अभ्यास करें:HackTricks Training AWS Red Team Expert (ARTE)
GCP हैकिंग सीखें और अभ्यास करें: HackTricks Training GCP Red Team Expert (GRTE) Azure हैकिंग सीखें और अभ्यास करें: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks का समर्थन करें