Tre vulnerabilità RCE (Remote Code Execution) nel runtime RPC
Analisi riassuntiva
Il ricercatore di Akamai Ben Barnea ha individuato tre importanti vulnerabilità nel runtime RPC di Microsoft Windows a cui ha assegnato i codici CVE-2023-24869, CVE-2023-24908e CVE-2023-23405, tutte con un punteggio base di 8,1.
Le vulnerabilità possono portare all'esecuzione di codice remoto. Poiché la libreria del runtime RPC viene caricata su tutti i server RPC, che sono comunemente usati dai servizi Windows, tutte le versioni di Windows (Desktop e Server) sono interessate.
Le vulnerabilità sono overflow interi presenti nelle tre strutture di dati utilizzate dal runtime RPC.
Le vulnerabilità sono state segnalate in modo responsabile a Microsoft e descritte nella Patch Tuesday di marzo 2023.
Introduzione
L'MS-RPC è un protocollo ampiamente usato nelle reti Windows, su cui si basano molti servizi e applicazioni. In quanto tali, le vulnerabilità presenti nel protocollo MS-RPC possono condurre a serie conseguenze. L'anno scorso, l'Akamai Security Intelligence Group ha condotto una ricerca sull'MS-RPC, nell'ambito della quale sono state rilevate e sfruttate varie vulnerabilità, realizzati strumenti per la ricerca e stilati alcuni elementi interni del protocollo non documentati.
Anche se alcuni blog precedenti si erano focalizzati sulle vulnerabilità presenti nei servizi, questo blog esaminerà le vulnerabilità nel runtime RPC, ossia il "motore" dell'MS-RPC. Queste vulnerabilità sono simili ad una vulnerabilità individuata a maggio 2022.
Un modello di overflow di intero
Le tre vulnerabilità recentemente osservate presentano un tema comune: esistono tutte a causa dell'inserimento di un overflow di intero nelle tre strutture di dati:
SIMPLE_DICT (un dizionario che salva solo i valori)
SIMPLE_DICT2 (un dizionario che salva chiavi e valori)
Coda
Tutte queste strutture di dati vengono implementate tramite un array dinamico che cresce ogniqualvolta si riempie. Questa operazione viene effettuata allocando il doppio della memoria assegnata all'array corrente. Questa allocazione dipende dall'overflow di intero.
La Figura 1 illustra un codice decompilato dal runtime RPC. Viene mostrato il processo di inserimento nella struttura SIMPLE_DICT e la riga di codice vulnerabile (evidenziata) in cui è possibile attivare l'overflow di intero.
Esplorazione di una vulnerabilità
Per attivare una vulnerabilità, dobbiamo comprendere la sua causa sottostante, scoprire se esiste un flusso verso la funzione vulnerabile e quanto tempo serve per attivarla.
Per brevità, descriveremo una delle tre vulnerabilità: quella presente nella struttura di dati della coda. Poiché gli altri overflow di intero sono simili per natura, l'analisi condotta nelle sezioni riportate di seguito può essere condotta in modo intercambiabile.
Descrizione dell'overflow di intero
Una coda è una semplice struttura di dati FIFO (First In, First Out). Una coda nel runtime RPC viene implementata tramite una struttura contenente un array di voci della coda, la capacità corrente e la posizione dell'ultimo elemento nella coda.
Quando si aggiunge una nuova voce alla coda (purché vi sia un posto disponibile), tutti gli elementi si spostano in avanti nell'array per consentire di aggiungere il nuovo elemento all'inizio dell'array. Quindi, aumenta la posizione dell'ultimo elemento nella coda.
Quando si verifica una rimozione nella coda, l'ultimo elemento viene eliminato e la sua posizione diminuisce (Figura 2).
Come accennato in precedenza, la vulnerabilità si verifica con l'inserimento di una nuova voce. Se l'array dinamico è pieno, il codice si comporta nel modo riportato di seguito.
Viene assegnato un nuovo array con le seguenti dimensioni:
Capacità corrente * 2 * dimensioni della (voce della coda)I vecchi elementi vengono copiati nel nuovo array
I vecchi elementi vengono cancellati dall'array
La capacità viene raddoppiata
Per un sistema a 32 bit, l'overflow si verifica calcolando le dimensioni del nuovo array:
La coda viene riempita con 0x10000000 (!) elementi.
Si verifica un'espansione. Vengono calcolate le dimensioni della nuova allocazione: 0x10000000 * 16. Poiché questo valore è eccedente, le dimensioni della nuova allocazione sono pari a 0.
Viene allocato un array di lunghezza pari a zero.
Il codice copia i vecchi elementi nel nuovo array, che determina una copia lineare più grande.
Nei sistemi a 64 bit, questa vulnerabilità non è sfruttabile perché si verificano problemi con un'allocazione enorme. In tal caso, il codice esce in modo corretto senza attivare una scrittura fuori dai limiti. Nonostante i sistemi a 64 bit siano immuni a questo problema, sono invece vulnerabili agli altri overflow di intero (nelle strutture SIMPLE_DICT e SIMPLE_DICT2).
Flusso di codice
Una connessione RPC viene rappresentata mediante la classe OSF_SCONNECTION. Ogni connessione può gestire più chiamate dal client (OSF_SCALL), ma in un dato momento è consentito eseguire una sola chiamata sulla connessione, mentre le altre chiamate vengono messe in coda.
Pertanto, OSF_SCONNECTION::MaybeQueueThisCall, un'interessante funzione che utilizza una coda, viene richiamata come parte di una nuova chiamata arrivata sulla connessione. In tal caso, la coda viene utilizzata per "mettere in attesa" le chiamate in entrata durante l'elaborazione di un'altra chiamata.
Pertanto, una coda viene riempita in un modo controllato dall'utente (inviando chiamate dal client una dopo l'altra), ma questa funzione impone un requisito: una chiamata è attualmente elaborata dalla connessione. In altre parole, se vogliamo riempire la coda, dobbiamo avere una chiamata che richiede del tempo per essere completata. Durante l'elaborazione della chiamata, verranno avviate nuove chiamate che andranno a riempire la coda.
Quale tipo di funzione richiede più tempo per essere completata?
La migliore opzione, in tal senso, è una funzione a "ciclo infinito".
La seconda migliore opzione è una vulnerabilità di coercizione dell'autenticazione perché il server effettua la connessione per noi, pertanto noi avremo il controllo sul tempo di risposta.
Una terza migliore opzione è rappresentata da una funzione complessa con una logica complicata o una funzione che elabora grandi quantità di dati e, pertanto, richiede molto tempo per il suo completamento.
Abbiamo deciso di utilizzare la nostra vulnerabilità di coercizione dell'autenticazione.
Il tempo richiesto per l'attivazione
Finora, abbiamo compreso cosa serve per riempire la coda e come farlo. Tuttavia, ora emerge un'altra importante domanda: è un'operazione pratica?
Abbiamo il minimo controllo sulla variabile in cui si verifica l'overflow di intero (possiamo solo incrementarne uno alla volta), in modo analogo all'overflow del conteggio riferimenti. Questo tipo di overflow di intero è peggiore marginalmente rispetto agli overflow di intero in cui due variabili da noi interamente controllate vengono aggiunte o moltiplicate oppure in cui è possibile controllare in qualche modo le dimensioni aggiunte (ad es., le dimensioni del pacchetto).
Come accennato in precedenza, dobbiamo allocare 0x10000000 (~268 milioni) elementi. Si tratta di un numero enorme.
Se cerco di attivare la vulnerabilità sul mio computer, otterrò un valore di circa 15 - 20 chiamate messe in coda al secondo. Pertanto, ci vorrebbero circa 155 giorni per attivare la vulnerabilità su un computer! Si prevede di causare un maggior numero di chiamate messe in coda al secondo. C'è un motivo per cui il runtime RPC è così lento? È costituito da più thread?
Eravamo partiti dalla convinzione secondo cui più thread elaborano e mettono in coda diverse chiamate contemporaneamente sulla stessa connessione. Dopo varie operazioni di decompilazione, abbiamo compreso che, in realtà, il flusso è leggermente diverso.
Gestione dei pacchetti MS-RPC
Prima di effettuare una chiamata, il codice crea un nuovo thread (se necessario) e richiama OSF_SCONNECTION::TransAsyncReceive. TransAsyncReceive tenta di ricevere una richiesta sulla stessa connessione, quindi invia la richiesta al nuovo thread (richiamando CO_SubmitRead).
L'altro thread prende la chiamata da TppWorkerThread e la porta a ProcessReceiveComplete, che richiama MaybeQueueThisCall per inserire SCALL in coda, quindi la propaga e tenta di ricevere una nuova richiesta su questa connessione.
Pertanto, anche se possiamo eseguire più thread, in realtà solo un thread viene utilizzato per la connessione. Ciò significa che non possiamo aggiungere chiamate alla coda contemporaneamente da più thread.
I "resti" del pacchetto
Abbiamo provato ad individuare vari modi per effettuare più chiamate al secondo in modo da minimizzare il tempo necessario per attivare la vulnerabilità. Durante la decompilazione del codice ricevente, abbiamo notato che, se la lunghezza di un pacchetto è maggiore dell'effettiva richiesta RPC nel pacchetto, il runtime RPC salva il promemoria. In seguito, quando verifica la presenza di nuove richieste, non usa immediatamente il socket, ma controlla prima se sono presenti "resti" del pacchetto e, in tal caso, fa partire una nuova richiesta dai questi "resti".
Ciò ci ha consentito di inviare un minor numero di pacchetti, ciascuno dei quali contiene il massimo numero di richieste. Il numero di chiamate messe in coda al secondo è rimasto relativamente invariato quando abbiamo tentato di eseguire proprio questa operazione, pertanto non sembra averci aiutato.
Riepilogo
Nonostante la bassa probabilità che avevamo previsto di sfruttare queste vulnerabilità, le abbiamo comunque aggiunte all'elenco delle importanti vulnerabilità riscontrate nel nostro ultimo anno di ricerca sul protocollo MS-RPC. È importante ricordare che anche le vulnerabilità più difficili da sfruttare rappresentano comunque un'opportunità per un criminale competente (e paziente).
Anche se l'MS-RPC esiste da vari decenni, presenta ancora delle vulnerabilità che aspettano di essere rilevate.
Speriamo che questo studio possa incoraggiare altri ricercatori ad esaminare l'MS-RPC e la superficie di attacco che presenta. Desideriamo ringraziare Microsoft per la rapida risposta nella gestione di questi problemi.
Il nostro archivio GitHub è pieno di strumenti e tecniche in grado di aiutarvi ad iniziare.