
Sfruttamento remoto del kernel tramite IPv6
Sfruttare da remoto il kernel tramite IPv6
CVE-2024-38063 - Sfruttare da remoto il kernel tramite IPv6 Marcus Hutchins
Dall'ultimo aggiornamento di Windows rilasciato il 13 agosto, mi sono immerso a fondo in tcpip.sys (il driver del kernel responsabile della gestione dei pacchetti TCP/IP). Una vulnerabilità con un punteggio CVSS di 9.8 nella parte più facilmente raggiungibile del kernel di Windows era qualcosa a cui non potevo semplicemente rinunciare. Non avevo mai approfondito IPv6 (o i driver che lo analizzano), quindi sapevo che cercare di fare reverse engineering di questa vulnerabilità sarebbe stato estremamente impegnativo, ma una buona esperienza di apprendimento.
Per la maggior parte, tcpip.sys è largamente non documentato. Sono riuscito a trovare un paio di write-up di exploit per bug più vecchi: qui, qui, e qui, ma poco altro. Quando il primo risultato della mia ricerca su Google in inglese è scritto in cinese, capisco immediatamente di essere ben al di là delle mie competenze e di andare incontro a un brutto periodo, ma dobbiamo imparare. Nonostante Google Traduttore faccia un lavoro mediocre, il post ha fornito alcune informazioni incredibilmente dettagliate su come funziona la frammentazione IPv6 e mi ha dato un buon vantaggio.
Più tardi, googlando alcuni nomi di funzioni, mi sono imbattuto in un'altra analisi della stessa vulnerabilità del 2021, scritta da Axel Souchet (noto come 0vercl0k), che approfondiva ancora di più gli internals di tcpip.sys e mi dava abbastanza informazioni per definire diverse strutture non documentate. La più semplice analisi di patch di sempre
Di solito, anche solo fare reverse engineering della patch per capire quale modifica al codice corrisponde alla vulnerabilità può richiedere giorni o addirittura settimane, ma in questo caso è stato immediato. È stato così facile, infatti, che più persone sui social media mi hanno detto che mi sbagliavo e che il bug era da un'altra parte. Ho davvero ascoltato loro e poi sprecato un'intera giornata a fare reverse engineering del driver sbagliato? Forse non lo sapremo mai.
C'era esattamente una modifica nell'intero file del driver, che si è rivelata essere proprio il bug.
Una panoramica bindiff di tcpip.sys prima e dopo l'installazione della patch.
Solo una singola funzione nell'intero driver è stata modificata. Di solito, potrei passare un'intera giornata a esaminare 20 o più modifiche di funzione solo per capire quale sia quella su cui concentrarmi, ma non questa volta.
Ipv6pProcessOptions() prima della patch.
Ipv6pProcessOptions() dopo la patch.
Non solo è stata modificata una singola funzione, ma una singola riga di codice.
La funzione dal nome estremamente lungo Feature_2660322619__private_IsEnabledDeviceUsage_3() è qualcosa che Microsoft a volte aggiunge per consentire rollback parziali delle patch. La chiamata verifica la presenza di un flag globale o di un'impostazione di registro che, se impostata, farà sì che la funzione restituisca false, con conseguente esecuzione del codice originale invece della versione patchata.
Il motivo per cui Microsoft fa questo è che le patch di sicurezza a volte rompono involontariamente le cose, quindi questa impostazione consente a un amministratore di rimuovere la patch da una singola vulnerabilità, senza disinstallare l'intero aggiornamento cumulativo mensile e indebolire drasticamente la sicurezza del sistema.
Tenendo conto di ciò, è chiaro che questa patch si limita a sostituire una chiamata a IppSendErrorList() con IppSendError(), dandoci un indizio che il problema riguarda una sorta di lista. Il diff di patch più facile di sempre (o così pensavo). Vulnerabilità opzionali, sfruttamento obbligatorio
Fare reverse engineering della patch per trovare il codice alterato è solo metà della sfida (o in questo caso meno dello 0,1%). Il resto del processo consiste nel fare reverse engineering di abbastanza della base di codice per capire cosa sta succedendo, capire che tipo di vulnerabilità è stata patchata, come costruire una richiesta per raggiungere il codice bersaglio e quale stato porta a una condizione sfruttabile.
La prima parte è abbastanza semplice. La modifica è in Ipv6pProcessOptions(), il che ci dice che si tratta di IPv6 e coinvolge l'elaborazione delle opzioni. Quindi, una rapida chiamata all'RFC ci dice esattamente cos'è un'opzione IPv6 e dove trovarne una.
Il layout dell'header delle opzioni di destinazione da Wikipedia.
Ok, bello. Quello che stiamo cercando sembra essere l'header delle opzioni di destinazione, che si trova direttamente dopo l'header IPv6 principale. Usiamo la libreria Python 'scapy' per costruire un pacchetto IPv6 di test.
Nota: Per mitigare gli attacchi DDoS che utilizzano indirizzi IP falsificati, Windows limita la capacità di costruire pacchetti IP grezzi. Per questo motivo, ho scelto di usare Linux per sviluppare il mio proof-of-concept. Mentre Linux permette agli utenti di costruire e inviare pacchetti grezzi di livello 2 e livello 3, richiede che lo script Python venga eseguito come root.
import sys import struct from scapy.all import *
def send_ipv6_option_packet(dest_ip): ethernet_header = Ether() ip_header = IPv6(dst=dest_ip) options_header = IPv6ExtHdrDestOpt() sendp(ethernet_header / ip_header / options_header)
if len(sys.argv) < 2: print('Use: python3 script.py <target_ipv6_address>') exit(-1)
send_ipv6_option_packet(sys.argv[1])
Dopo aver impostato un breakpoint su tcpip!Ipv6pProcessOptions, poi eseguito lo script, era chiaro che tutto ciò che serviva per raggiungere la funzione vulnerabile era inviare un pacchetto IPv6 con una struttura di opzioni vuota. Ho poi provato ad aggiungere alcune opzioni non valide alla struttura per vedere se potevo raggiungere la chiamata a IppSendErrorList().
Una breve revisione del codice ha indicato che quasi qualsiasi formattazione di opzione non valida poteva innescare la chiamata a IppSendErrorList. Quindi, ho deciso di usare l'opzione Jumbo Packet con una lunghezza non valida (inferiore a 65535 byte).
options_header = IPv6ExtHdrDestOpt(options=[Jumbo(jumboplen=0x1337)])
Quindi, cosa fa effettivamente IppSendErrorList()? Beh, il codice è piuttosto semplice.
L'intera funzione IppSendErrorList.
Il codice itera una lista concatenata e chiama IppSendError() su ogni elemento della lista. Ancora una volta, le stelle si sono allineate e le cose sono state facili finora. Se IppSendErrorList chiama semplicemente IppSendError per ogni elemento di una lista, e la patch sostituisce la chiamata a IppSendErrorList con IppSendError, allora il problema si verifica quando IppSendError viene chiamato su un elemento della lista diverso dal primo.
Quindi, di cosa è una lista questa, e come se ne crea una? Sta facendo una lista, la sta controllando... 52.567 volte
È qui che le cose sono passate da ovvie ad anormalmente difficili, anche se penso che gran parte di questo sia stato dovuto al fatto che una delle mie due cellule cerebrali disponibili era occupata a combattere una brutta infezione da covid. Ho perso un paio di giorni a capire parti del codice, addormentarmi, poi dimenticare ciò che avevo capito. L'intero processo ha richiesto più di una settimana di reverse engineering di parti di tcpip.sys per capire cosa stesse succedendo. Ma il blog post di Axel è stato estremamente utile.
Guardando le funzioni e la struttura che Axel ha fatto reverse engineering, e a quali altre funzioni vengono passate, è chiaro che l'unico argomento passato a Ipv6pProcessOptions() è la stessa struttura packet_t definita nell'articolo. Essenzialmente, il puntatore passato a Ipv6pProcessOptions, e iterato da IppSendErrorList, è una lista concatenata di pacchetti.
Quindi, ho impostato un breakpoint su Ipv6pProcessOptions() e ho ispezionato la lista.
L'elemento list->Next è NULL.