
Il dispositivo in questione è qualsiasi SSD della serie MX500. Questi SSD sono controllati da un microcontrollore Sillicon-Motion SM2259 (i lotti più vecchi avevano un controller più vecchio, Sillicon-Motion SM2258, ma il focus principale di questo documento è il più recente).
SM2259 è un microcontrollore SATA 6Gb/s a 4 canali che monta una CPU a 32 bit little-endian basata sull'architettura ARC.
Osservando l'ultimo firmware applicabile alla data di questo documento, che è M3CR046, sono stati identificati e confermati alcuni problemi sia staticamente che dinamicamente.
Tutti i problemi sono stati identificati nel meccanismo di aggiornamento del firmware del controller, che corrisponde al gestore del microcontrollore del comando ATA PIO DOWNLOAD-MICROCODE (0x92), in particolare nella logica che scarica il firmware utilizzando il metodo degli offset, che corrisponde ai sottocomandi 0x03 e 0x0E.
Tutti i bug trattati in questo documento sono stati verificati su un Crucial MX500 500GB SSD (CT500MX500SSD1), controller SM2259H-AC con firmware M3CR046 e chip flash NY112, utilizzando un PC con CPU x86_64.
Il codice del firmware è mappato all'indirizzo base 0x80020000, e il gestore ATA vulnerabile si trova all'indirizzo 0x80024A9C. Una versione decompilata della funzione può essere trovata in resources/download_microcode_handler.c per comodità.
Per chi preferisce non affrontare i dettagli tecnici e capire la conclusione, si prega di consultare la sezione FAQ in basso.
Poiché M3CR046 contiene più immagini firmware tra cui quella appropriata viene scelta dal meccanismo di aggiornamento del firmware (forse in base ai chip flash effettivamente utilizzati o ad altre caratteristiche hardware), questo documento tratterà le specifiche della prima variante firmware (poiché questo è il firmware supportato sul nostro drive specifico, e quindi abbiamo potuto testare solo quella variante). Tuttavia, i bug presentati in questo documento sembrano applicarsi a tutte le varianti firmware, ma potrebbero esserci alcune differenze nei dettagli pratici durante la riproduzione.
Questo problema si riferisce ai casi in cui il primo chunk inviato ha una dimensione maggiore di 0x200 settori. Se diamo un'occhiata all'interno del gestore dei comandi ATA, in particolare alla logica eseguita quando la dimensione del chunk è maggiore della dimensione del settore e quando il chunk è il primo inviato:

Questo imposta alcune variabili in base all'offset successivo (che nel nostro caso, poiché abbiamo inviato un solo chunk finora, è la lunghezza del chunk in settori) e a una variabile chiamata lower_bound_fw_offset che è l'offset del blocco (cioè offset in granularità di settori) all'interno dell'immagine di download di input in cui ci si aspetta che si trovi la nostra immagine firmware. Questo è un valore hardcoded per variante firmware, che nel nostro caso (la prima variante), è uguale a 0.
In questo caso, si verifica un underflow quando si calcola il risultato della sottrazione per some_index, facendo sì che some_index sia alto fino a 0xFFFF. Questo è un comportamento inaspettato, poiché in base alla logica che sposta i dati al buffer di download:

Osserviamo che l'indirizzo di origine da cui i dati vengono copiati potrebbe non essere valido dato il valore inaspettato calcolato per some_index.
Quando testato dinamicamente inviando una richiesta di aggiornamento firmware con il primo chunk di dimensione maggiore di 0x200 settori, il controller si blocca e non invia nemmeno una risposta alla richiesta originale. Questo è coerente e facilmente riproducibile.
È probabile che ciò accada a causa di un riferimento non valido all'indirizzo di origine calcolato, che quindi innesca un'eccezione che causa il blocco del controller. Questo non è stato provato, ma è piuttosto una congettura che potrebbe spiegare il blocco.
L'immagine di download di input (per M3CR046) ha dimensione 0x242400 byte, e all'interno di questa immagine ci sono 3 immagini firmware interne di cui solo una viene infine scritta sulla flash dopo un processo di aggiornamento firmware, ciascuna di queste immagini ha dimensione 0xC0C00 byte (o 0x606 settori).
Ciò significa che quando il meccanismo di aggiornamento firmware estrae la copia firmware corretta dall'immagine di download di input, deve verificare che la sua dimensione non superi 0xC0C00 byte.
Il controller tenta effettivamente di farlo, ma ci sono alcuni casi limite che possono portare a comportamenti inaspettati. Diamo un'occhiata al seguente snippet (che condivide parte del codice con il bug precedente):

Se il chunk corrente ha dimensione maggiore di 0x200 settori e non è il primo chunk nella sequenza, allora 0x200 settori (0x40000 byte) verranno copiati alla volta. Quindi, c'è un controllo il cui scopo è troncare i byte in eccesso dal numero di byte da copiare se la dimensione totale dell'immagine firmware supera higher_bound_fw_offset (che nel nostro caso è 0x606 settori, poiché la dimensione del firmware dovrebbe essere esattamente questa).
Questa logica ha senso complessivamente, ma c'è un difetto: se l'ultimo chunk inviato fa sì che l'offset successivo diventi troppo alto, tale che il numero di byte in eccesso supera 0x200 settori (o 0x40000 byte), allora curr_bytes_to_copy ottiene un valore "negativo", che underflow a circa ~4GB (~0xFFFFFFFF). Come abbiamo visto prima, questa variabile viene utilizzata per determinare il numero di byte da trasferire al buffer di download.
Se diamo un'occhiata all'interno di r_maybe_some_efficient_data_transfer, vediamo il seguente pezzo di codice:

Il che significa che la dimensione della copia viene troncata a 32MB (dalla dimensione originale di copia di circa ~4GB), ma è ancora un numero grande che potrebbe anche causare un comportamento indefinito se l'intervallo di memoria che inizia a 0x40000000 ha dimensione inferiore a 32MB.
Quando testato dinamicamente inviando chunk ATA per arrivare a un offset di 0x600, e poi inviando un chunk grande di dimensione 0x207 settori per innescare l'underflow, il controller si blocca ancora una volta, probabilmente a causa di un accesso alla memoria non valido durante la copia.
Questo bug è più interessante del precedente, perché anche se non abbiamo una sovrascrittura controllata (ma piuttosto una grande sovrascrittura che possibilmente innesca un'eccezione che blocca il controller), se la funzione che sposta i dati al buffer di download riesce effettivamente a trasferire così tanti dati prima del crash (sovrascrivendo l'intervallo di memoria che si trova subito dopo il buffer di download nella memoria principale), allora forse il comportamento del gestore delle eccezioni può essere alterato in base ai dati sovrascritti. Ciò può accadere, ad esempio, se il gestore delle eccezioni legge un puntatore dall'area sovrascritta e poi salta ad esso (questo caso specifico non è particolarmente probabile, ma con ulteriori ricerche, qualcosa del genere potrebbe essere scoperto).
Come detto, la dimensione dell'immagine di download è di 0x242400 (o 0x1212 settori). Il firmware verifica che la dimensione totale dell'immagine trasferita non superi questa dimensione verificando che l'offset successivo non superi 0x1212 settori. Questo controllo ha senso, ma il calcolo dell'offset successivo è difettoso:

Se l'offset corrente è 0x600 settori, e il prossimo comando ATA da elaborare è di dimensione abbastanza grande (diciamo 0xFC00 settori, che è consentito dallo standard ATA), allora l'offset successivo si avvolge, in modo tale che il suddetto controllo non funzioni correttamente:

O in altre parole, nel caso normale, il meccanismo di aggiornamento firmware resetta la sua macchina a stati e restituisce un errore, ma se inviamo un chunk molto grande, continueremo a elaborarlo. Il seguente snippet di codice mostra come viene effettuato il trasferimento:

Ricordiamo a questo punto che se il numero di settori da trasferire è maggiore di 0x200 settori e il chunk corrente non è il primo, allora 0x200 settori vengono copiati alla volta nel buffer di download. Questo è molto interessante, perché significa che possiamo copiare circa 0x200 settori (o 0x40000 byte) oltre il buffer di download, sovrascrivendo i dati nella memoria principale. Ad esempio, se l'offset corrente è 0x605 settori e forniamo una dimensione del chunk di 0xF9FB settori, allora __next_offset ottiene il valore di 0 a causa dell'avvolgimento. L'indice di origine da cui inizia la copia è 0, e curr_bytes_to_copy ottiene il valore di 0x40000. Poiché siamo attualmente all'offset 0x605 settori, allora g_blocks_copied ottiene il valore di 0x605. Poiché l'offset corrente è effettivamente valido (e anche il successivo), l'operazione di copia nel buffer di download viene attivata, causando una massiccia sovrascrittura di poco meno di byte oltre la fine del buffer di download.
Questa è una primitiva potente che consente un overflow del buffer del controller molto più controllato (che non blocca immediatamente il controller come nei casi precedenti) e può portare all'esecuzione di codice con una certezza molto maggiore rispetto al bug precedente (ma comunque, sono necessarie ulteriori ricerche su cosa esattamente sia posizionato dopo il buffer di download nella memoria principale per determinare le caratteristiche dello sfruttamento).
Tutti questi bug sono stati verificati su una macchina Ubuntu 22.04 64-bit utilizzando il driver SCSI standard di Linux sull'interfaccia SG_IO. Va sottolineato che per riprodurre il Bug #3 con questo driver specifico, le pagine enormi devono essere abilitate e una singola pagina da 1GB deve essere allocata per la richiesta grande. La ragione è che apparentemente questo driver richiede che l'intera richiesta ATA sia in blocchi di memoria fisica contigua. Poiché la richiesta ha dimensione vicina a ~30MB, le pagine da 2MB non sono sufficienti, e quindi le pagine da 1GB sono la dimensione successiva (e ultima) disponibile sul nostro sistema di test.
Tuttavia, va anche notato che ciò non significa che questo sia un passo necessario per innescarlo, perché forse ci sono altre soluzioni alternative che consentono di inviare richieste ATA grandi che non abbiamo ancora trattato. Abilitare le pagine enormi è stato semplicemente il percorso più veloce per confermare questo bug. Oltre a questo, l'unico prerequisito richiesto per innescare tutti questi bug sono i permessi necessari per inviare pacchetti ATA (tipicamente, accesso root al PC che comunica con il controller).
Il codice sorgente che riproduce tutti i suddetti bug è fornito come parte di questo repository. Per il Bug #1 e Bug #2, il comportamento atteso è che il drive si blocchi fino al prossimo ciclo di alimentazione. Per il Bug #3, il codice sorgente fornito non blocca necessariamente il controller, ma esegue una grande sovrascrittura oltre il buffer di download.
Come detto, poiché i bug sono stati verificati su una macchina Ubuntu 22.04 64-bit, il processo di compilazione deve essere effettuato su una macchina simile. Non ci sono garanzie per altre distribuzioni o sistemi operativi.
Per compilare, esegui quanto segue nella directory radice del progetto:
cmake -B build && make
Il processo di compilazione produce 3 binari, tutti disponibili nella directory build con i nomi CVE_MX500_BUG_1, CVE_MX500_BUG_2 e CVE_MX500_BUG_3, che corrispondono ai file sorgente che innescano rispettivamente Bug #1, Bug #2 e Bug #3.
Ogni binario prevede di ricevere il percorso del dispositivo dell'SSD MX500 e deve essere eseguito con privilegi di root. Ad esempio:
sudo ./build/CVE_MX500_BUG1 /dev/sda
Dipende dall'obiettivo finale di un potenziale attaccante. Se tutto ciò che vuole è l'accesso completo in lettura/scrittura allo storage del tuo drive, allora essere all'interno del tuo PC è già sufficiente. Tuttavia, cosa succede se quell'attaccante vuole fare qualche passo in più? Se il FW di un drive è firmato digitalmente, allora il Bug #3 può permettere a un attaccante di bypassare la verifica della firma del firmware, consentendo all'attaccante di inserire un payload dannoso nel firmware del drive. Una volta all'interno, tale payload è nascosto molto bene, sopravvive alla formattazione del drive e può persino assicurarsi di sopravvivere agli aggiornamenti del firmware del controller. Cosa potrebbe effettivamente fare tale payload è al di là dello scopo di questo documento, quindi non verrà discusso.
È probabile che la risposta sia un GRANDE NO. La quantità di R&D necessaria per effettuare effettivamente un tale attacco è molto alta, e sarebbe (MOLTO probabilmente) possibile solo da attori minacciosi molto seri. A meno che tu non sia ricercato dai governi, è estremamente improbabile che questo ti colpisca in qualche modo.
Il fornitore non ha risposto a molteplici email riguardanti questi problemi nel corso di mesi. Perché un CVE venga effettivamente pubblicato, deve essere fornito un link pubblico al CNA assegnatore. Purtroppo, inviare loro le informazioni privatamente non è così che funziona.
I bug menzionati in questo documento sono stati scoperti originariamente a maggio 2024. Micron è stato contattato più volte da allora (tramite il loro indirizzo email ufficiale per la sicurezza), e non c'è stata risposta da parte loro. MITRE è stato informato a luglio 2024, e un CVE è stato assegnato ad agosto 2024. Alla fine di agosto 2024, questo repository è stato reso pubblico (alcuni giorni dopo che il CVE è stato approvato da MITRE).
Poiché i firmware M3CR04X precedenti a M3CR046 non sono più disponibili per il download, non è chiaro se siano interessati, ma se dovessi indovinare, direi di sì. Per quanto riguarda versioni ancora più vecchie, ad esempio M3CR033, in base all'analisi statica, sembra che esistano bug molto simili.
Il controller in questione, SM2259, è integrato anche in SSD di altri fornitori. È possibile che i fornitori modifichino qualche parte del codice del firmware, ma direi anche che è decisamente possibile che questi bug (o molto simili) siano presenti anche in SSD di altri fornitori.
Questo CVE è stato pubblicato da MITRE. È stato anche analizzato da NVD con un punteggio CVSS 3.0 di 6.7 (medio).
Se hai identificato inesattezze o errori nella descrizione o hai difficoltà a riprodurre questi bug, contattami a log1kxd at gmail.com.
0x40000