AF_UNIX MSG_OOB UAF & SKB-based kernel primitives

Tip

Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks

TL;DR

  • Το Linux >=6.9 εισήγαγε μια ελαττωματική refactor του manage_oob() (5aa57d9f2d53) για τον χειρισμό του AF_UNIX MSG_OOB. Στοιβαγμένα SKB μηδενικού μήκους παρέκαμψαν τη λογική που καθαρίζει το u->oob_skb, οπότε ένα κανονικό recv() μπορούσε να απελευθερώσει το out-of-band SKB ενώ ο δείκτης παρέμενε ενεργός, οδηγώντας σε CVE-2025-38236.
  • Η επανενεργοποίηση του recv(..., MSG_OOB) αποαναφέρεται το κρεμασμένο struct sk_buff. Με MSG_PEEK, το μονοπάτι unix_stream_recv_urg() -> __skb_datagram_iter() -> copy_to_user() γίνεται σταθερή ανάγνωση 1 byte στον kernel· χωρίς MSG_PEEK το primitive αυξάνει το UNIXCB(oob_skb).consumed στη μετατόπιση 0x44, δηλαδή προσθέτει +4 GiB στο ανώτερο dword οποιασδήποτε 64-bit τιμής τοποθετημένης στη μετατόπιση 0x40 μέσα στο επανακατανεμημένο αντικείμενο.
  • Με το να εξαντλήσει τις unmovable σελίδες order-0/1 (page-table spray), να αναγκάσει την απελευθέρωση μιας σελίδας slab SKB στον buddy allocator, και να επαναχρησιμοποιήσει τη φυσική σελίδα ως pipe buffer, το exploit ψεύδει τα SKB metadata σε ελεγχόμενη μνήμη για να εντοπίσει τη κρεμασμένη σελίδα και να μεταστρέψει το read primitive στο .data, το vmemmap, per-CPU και περιοχές page-table παρά την hardening του usercopy.
  • Η ίδια σελίδα μπορεί αργότερα να ανακυκλωθεί ως η κορυφαία σελίδα kernel-stack ενός πρόσφατα cloned thread. Το CONFIG_RANDOMIZE_KSTACK_OFFSET γίνεται oracle: με διερεύνηση της διάταξης του stack ενώ το pipe_write() μπλοκάρεται, ο επιτιθέμενος περιμένει μέχρι το spilled μήκος του copy_page_from_iter() (R14) να βρεθεί στη μετατόπιση 0x40, και στη συνέχεια πυροδοτεί την αύξηση +4 GiB για να καταστρέψει την τιμή στο stack.
  • Μια αυτο-επαναλαμβανόμενη skb_shinfo()->frag_list κρατάει το UAF syscall να περιστρέφεται στον kernel μέχρι ένα συνεργαζόμενο thread να σταματήσει το copy_from_iter() (μέσω mprotect() πάνω σε ένα VMA που περιέχει μια μονή τρύπα MADV_DONTNEED). Η διάρρηξη του βρόχου απελευθερώνει την αύξηση ακριβώς όταν ο stack στόχος είναι ενεργός, φουσκώνοντας το όρισμα bytes έτσι ώστε το copy_page_from_iter() να γράψει πέρα από τη σελίδα pipe buffer στην επόμενη φυσική σελίδα.
  • Παρακολουθώντας τα PFN του pipe-buffer και τους πίνακες σελίδων με το read primitive, ο επιτιθέμενος διασφαλίζει ότι η επόμενη σελίδα είναι σελίδα PTE, μετατρέπει το OOB copy σε αυθαίρετες εγγραφές PTE και αποκτά απεριόριστο kernel read/write/execute. Το Chrome μείωσε τη δυνατότητα εκμετάλλευσης εμποδίζοντας το MSG_OOB από renderers (6711812), και το Linux διόρθωσε το λογικό σφάλμα στο 32ca245464e1 και εισήγαγε το CONFIG_AF_UNIX_OOB για να κάνει το feature προαιρετικό.

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

unix_stream_read_generic() αναμένει κάθε SKB που επιστρέφεται από το manage_oob() να έχει unix_skb_len() > 0. Μετά το 93c99f21db36, το manage_oob() παρέκαμψε το μονοπάτι καθαρισμού skb == u->oob_skb κάθε φορά που πρώτα αφαιρούσε ένα SKB μηδενικού μήκους που είχε μείνει από το recv(MSG_OOB). Η επακόλουθη διόρθωση (5aa57d9f2d53) προχώρησε από το πρώτο SKB μηδενικού μήκους στο skb_peek_next() χωρίς να ελέγξει ξανά το μήκος. Με δύο διαδοχικά SKB μηδενικού μήκους, η συνάρτηση επέστρεψε το δεύτερο άδειο SKB· το unix_stream_read_generic() το παρέκαμψε χωρίς να καλέσει ξανά το manage_oob(), έτσι το πραγματικό OOB SKB αφαιρέθηκε από τη σειρά και απελευθερώθηκε ενώ το 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 επανακατανεμηθεί σε μνήμη ελεγχόμενη από τον επιτιθέμενο (ή σε ένα ελεγχόμενο alias όπως μια pipe page), κάθε recv(MSG_OOB | MSG_PEEK) αντιγράφει ένα byte από μια αυθαίρετη διεύθυνση kernel που επιτρέπεται από __check_object_size() στον χώρο του χρήστη χωρίς crash. Κρατώντας το MSG_PEEK ενεργό διατηρείται ο dangling pointer για απεριόριστες αναγνώσεις.
  2. Constrained write: Όταν το MSG_PEEK είναι απενεργοποιημένο, το UNIXCB(oob_skb).consumed += 1 αυξάνει το 32-bit πεδίο στη μετατόπιση 0x44. Σε 0x100-ευθυγραμμισμένες SKB allocations αυτό βρίσκεται τέσσερα bytes πάνω από μια 8-byte ευθυγραμμισμένη λέξη, μετατρέποντας το primitive σε μια αύξηση +4 GiB της λέξης που φιλοξενείται στη μετατόπιση 0x40. Για να μετατραπεί αυτό σε kernel write απαιτείται η τοποθέτηση μιας ευαίσθητης 64-bit τιμής σε εκείνη τη μετατόπιση.

Reallocating the SKB page for arbitrary read

  1. Drain order-0/1 unmovable freelists: Mapάρετε ένα τεράστιο read-only anonymous VMA και faultάρετε κάθε σελίδα για να αναγκάσετε allocation page-tables (order-0 unmovable). Γεμίζοντας ~10% της RAM με page tables εξασφαλίζει ότι οι επόμενες skbuff_head_cache allocations θα πάρουν φρέσκες buddy σελίδες όταν εξαντληθούν οι order-0 λίστες.
  2. Spray SKBs and isolate a slab page: Χρησιμοποιήστε δεκάδες stream socketpairs και ουράροντας εκατοντάδες μικρά μηνύματα ανά socket (~0x100 bytes ανά SKB) για να γεμίσετε το skbuff_head_cache. Αποδεσμεύστε επιλεγμένα SKBs για να οδηγήσετε μια στοχευμένη slab σελίδα εξ ολοκλήρου υπό τον έλεγχο του επιτιθέμενου και παρακολουθήστε το struct page refcount της μέσω του αναδυόμενου read primitive.
  3. Return the slab page to the buddy allocator: Ελευθερώστε κάθε αντικείμενο στη σελίδα, μετά εκτελέστε αρκετές επιπλέον allocations/frees ώστε να ωθήσετε τη σελίδα έξω από τις SLUB per-CPU partial λίστες και τις per-CPU page λίστες ώστε να γίνει μια order-1 σελίδα στη buddy freelist.
  4. Reallocate as pipe buffer: Δημιουργήστε εκατοντάδες pipes· κάθε pipe δεσμεύει τουλάχιστον δύο 0x1000-byte data pages (PIPE_MIN_DEF_BUFFERS). Όταν ο buddy allocator σπάσει μια order-1 σελίδα, το ένα μισό επαναχρησιμοποιεί την ελευθερωμένη SKB σελίδα. Για να βρείτε ποιο pipe και ποιο offset aliased το oob_skb, γράψτε μοναδικά marker bytes μέσα σε fake SKBs αποθηκευμένα σε όλη τη pipe page και εκτελέστε επαναλαμβανόμενα recv(MSG_OOB | MSG_PEEK) μέχρι να επιστραφεί ο marker.
  5. Forge a stable SKB layout: Γεμίστε την aliased pipe page με ένα fake struct sk_buff του οποίου τα data/head pointers και η δομή skb_shared_info δείχνουν σε αυθαίρετες διευθύνσεις kernel ενδιαφέροντος. Επειδή το x86_64 απενεργοποιεί SMAP μέσα σε copy_to_user(), διευθύνσεις user-mode μπορούν να χρησιμεύσουν ως staging buffers μέχρι να γίνουν γνωστοί οι kernel pointers.
  6. Respect usercopy hardening: Η copy επιτυγχάνει απέναντι σε .data/.bss, vmemmap entries, per-CPU vmalloc ranges, stacks άλλων threads του kernel, και direct-map σελίδες που δεν διασχίζουν όρια υψηλότερης τάξης folio. Οι αναγνώσεις απέναντι σε .text ή εξειδικευμένα caches που απορρίπτονται από __check_heap_object() απλώς επιστρέφουν -EFAULT χωρίς να σκοτώσουν τη διαδικασία.

Introspecting allocators with the read primitive

  • Break KASLR: Διαβάστε οποιοδήποτε IDT descriptor από το fixed mapping στο CPU_ENTRY_AREA_RO_IDT_VADDR (0xfffffe0000000000) και αφαιρέστε το γνωστό handler offset για να ανακτήσετε τη βάση του kernel.
  • SLUB/buddy state: Παγκόσμια .data σύμβολα αποκαλύπτουν τις βάσεις των kmem_cache, ενώ τα vmemmap entries εκθέτουν τα flags τύπου κάθε σελίδας, pointer freelist, και την owning cache. Σκανάροντας τα per-CPU vmalloc segments εντοπίζονται struct kmem_cache_cpu instances ώστε η επόμενη διεύθυνση allocation των κλειδιών cache (π.χ. skbuff_head_cache, kmalloc-cg-192) να γίνει προβλέψιμη.
  • Page tables: Αντί να διαβάσετε mm_struct (μπλοκάρεται από usercopy), περπατήστε το global pgd_list (struct ptdesc) και ταιριάξτε το τρέχον mm_struct μέσω cpu_tlbstate.loaded_mm. Μόλις γίνει γνωστό το root pgd, το primitive μπορεί να διασχίσει κάθε page table για να αντιστοιχίσει PFNs για pipe buffers, page tables, και kernel stacks.

Recycling the SKB page as the top kernel-stack page

  1. Αποδεσμεύστε ξανά την ελεγχόμενη pipe page και επιβεβαιώστε μέσω vmemmap ότι το refcount της επιστρέφει στο μηδέν.
  2. Αμέσως allocate τέσσερις βοηθητικές pipe pages και μετά αποδεσμεύστε τες σε αντίστροφη σειρά ώστε η LIFO συμπεριφορά του buddy allocator να είναι ντετερμινιστική.
  3. Καλέστε clone() για να δημιουργήσετε ένα helper thread· οι Linux stacks είναι τέσσερις σελίδες στο x86_64, οπότε οι τέσσερις πιο πρόσφατα ελευθερωμένες σελίδες γίνονται το stack του, με την τελευταία ελευθερωμένη σελίδα (η πρώην SKB page) στη μεγαλύτερη διεύθυνση.
  4. Επαληθεύστε μέσω page-table walk ότι το top stack PFN του helper thread ισούται με το ανακυκλωμένο SKB PFN.
  5. Χρησιμοποιήστε το arbitrary read για να παρατηρήσετε τη διάταξη του stack ενώ κατευθύνετε το thread σε pipe_write(). Το CONFIG_RANDOMIZE_KSTACK_OFFSET αφαιρεί ένα τυχαίο 0x0–0x3f0 (ευθυγραμμισμένο) από το RSP ανά syscall· επαναλαμβανόμενες εγγραφές συνδυασμένες με poll()/read() από άλλο thread αποκαλύπτουν πότε ο writer μπλοκάρει με το επιθυμητό offset. Όταν τύχει, το spilled copy_page_from_iter() bytes όρισμα (R14) κάθεται στη μετατόπιση 0x40 μέσα στην ανακυκλωμένη σελίδα.

Placing fake SKB metadata on the stack

  • Χρησιμοποιήστε sendmsg() σε AF_UNIX datagram socket: ο kernel αντιγράφει το user sockaddr_un σε ένα stack-resident sockaddr_storage (έως 108 bytes) και τα ancillary data σε άλλο on-stack buffer πριν το syscall μπλοκάρει περιμένοντας χώρο στην ουρά. Αυτό επιτρέπει τη φύτευση μιας ακριβούς fake SKB δομής στη μνήμη του stack.
  • Εντοπίστε πότε ολοκληρώθηκε η αντιγραφή παρέχοντας ένα 1-byte control message τοποθετημένο σε μια unmapped user σελίδα· το ____sys_sendmsg() θα το faultάρει μέσα, οπότε ένα helper thread που κάνει polling με mincore() σε εκείνη τη διεύθυνση μαθαίνει πότε η προοριζόμενη σελίδα είναι παρούσα.
  • Το μηδενικοποιημένο padding από το CONFIG_INIT_STACK_ALL_ZERO συμπληρώνει βολικά πεδία που δεν χρησιμοποιούνται, ολοκληρώνοντας ένα έγκυρο SKB header χωρίς επιπλέον εγγραφές.

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

  • Forgeάρετε skb_shinfo(fakeskb)->frag_list ώστε να δείχνει σε ένα δεύτερο fake SKB (αποθηκευμένο σε μνήμη user που ελέγχεται από τον επιτιθέμενο) που έχει len = 0 και next = &self. Όταν το skb_walk_frags() επαναλαμβάνει αυτή τη λίστα μέσα σε __skb_datagram_iter(), η εκτέλεση περιστρέφεται επ’ αόριστον γιατί ο iterator δεν φτάνει ποτέ σε NULL και το copy loop δεν προχωρά.
  • Κρατήστε το recv syscall να τρέχει μέσα στον kernel αφήνοντας το δεύτερο fake SKB να κάνει self-loop. Όταν είναι ώρα να πυροδοτήσετε την αύξηση, απλώς αλλάξτε τον next pointer του δεύτερου SKB από user space σε NULL. Το loop τερματίζει και το unix_stream_recv_urg() εκτελεί αμέσως UNIXCB(oob_skb).consumed += 1 μία φορά, επηρεάζοντας όποιο αντικείμενο βρίσκεται αυτήν τη στιγμή στη ανακυκλωμένη stack page στη μετατόπιση 0x40.

Stalling copy_from_iter() without userfaultfd

  • Mapάρετε μια γιγάντια anonymous RW VMA και faultάρετε την πλήρως.
  • Κάντε μια single-page τρύπα με madvise(MADV_DONTNEED, hole, PAGE_SIZE) και τοποθετήστε αυτή τη διεύθυνση μέσα στο iov_iter που χρησιμοποιείται για write(pipefd, user_buf, 0x3000).
  • Παράλληλα, καλέστε mprotect() στην ολόκληρη VMA από άλλο thread. Το syscall παίρνει το mmap write lock και περπατά κάθε PTE. Όταν ο pipe writer φτάσει την τρύπα, ο page fault handler μπλοκάρει πάνω στο mmap lock που κρατείται από το mprotect(), παγώνοντας το copy_from_iter() σε ένα ντετερμινιστικό σημείο ενώ το spilled bytes value βρίσκεται στο stack segment που φιλοξενείται από την ανακυκλωμένη SKB page.

Turning the increment into arbitrary PTE writes

  1. Fire the increment: Απελευθερώστε το frag loop ενώ το copy_from_iter() είναι σταματημένο ώστε η +4 GiB αύξηση να χτυπήσει τη μεταβλητή bytes.
  2. Overflow the copy: Μόλις το fault συνεχιστεί, το copy_page_from_iter() πιστεύει ότι μπορεί να αντιγράψει >4 GiB μέσα στην τρέχουσα pipe page. Αφού γεμίσει τα νόμιμα 0x2000 bytes (δύο pipe buffers), εκτελεί άλλη μια επανάληψη και γράφει τα υπόλοιπα δεδομένα χρήστη στην φυσική σελίδα που ακολουθεί το PFN του pipe buffer.
  3. Arrange adjacency: Χρησιμοποιώντας telemetry allocator, αναγκάστε τον buddy allocator να τοποθετήσει μια process-owned PTE page αμέσως μετά την στοχευόμενη pipe buffer page (π.χ. εναλλάσσοντας allocations pipe pages και αγγίζοντας νέα virtual ranges για να προκαλέσετε page-table allocation μέχρι να ευθυγραμμιστούν τα PFNs μέσα στο ίδιο 2 MiB pageblock).
  4. Overwrite page tables: Κωδικοποιήστε τις επιθυμητές εγγραφές PTE στα επιπλέον 0x1000 bytes των δεδομένων χρήστη ώστε το OOB copy_from_iter() να γεμίσει τη διπλανή σελίδα με εγγραφές που επιλέγει ο επιτιθέμενος, παρέχοντας RW/RWX user mappings φυσικής μνήμης του kernel ή επανεγγράφοντας υπάρχουσες εγγραφές ώστε να απενεργοποιηθούν SMEP/SMAP.

Mitigations / hardening ideas

  • Kernel: Εφαρμόστε το 32ca245464e1479bfea8592b9db227fdc1641705 (επαληθεύει σωστά ξανά τα SKBs) και εξετάστε την απενεργοποίηση του AF_UNIX OOB εντελώς εκτός αν είναι απολύτως απαραίτητο μέσω CONFIG_AF_UNIX_OOB (5155cbcdbf03). Σκληρύνετε το manage_oob() με πρόσθετους ελέγχους ορθότητας (π.χ., loop μέχρι unix_skb_len() > 0) και ελέγξτε άλλα socket πρωτόκολλα για παρόμοιες υποθέσεις.
  • Sandboxing: Φιλτράρετε τα flags MSG_OOB/MSG_PEEK σε seccomp profiles ή σε υψηλότερου επιπέδου broker APIs (η αλλαγή του Chrome 6711812 τώρα μπλοκάρει το renderer-side MSG_OOB).
  • Allocator defenses: Ενίσχυση της SLUB freelist randomization ή επιβολή per-cache page coloring θα περιέπλεκε την ντετερμινιστική ανακύκλωση σελίδων· ο περιορισμός του αριθμού των pipe buffers επίσης μειώνει την αξιοπιστία της επανακατανομής.
  • Monitoring: Εκθέστε υψηλό ρυθμό page-table allocation ή ασυνήθιστη χρήση pipe μέσω telemetry—αυτό το exploit καταναλώνει μεγάλες ποσότητες page tables και pipe buffers.

References

Tip

Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks