
Exploit proof-of-concept per tre vulnerabilità nel meccanismo di aggiornamento del firmware SSD Crucial MX500, che consentono buffer overflow e potenziale esecuzione di codice tramite comandi ATA.
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).