
Estrattore di firmware per microprocessori CH55x
L'estrattore di firmware CH55x viene utilizzato per leggere il firmware dai circuiti integrati CH55x. I dispositivi non dispongono di un supporto integrato per la lettura diretta del firmware tramite il bootloader. Tuttavia, esiste una funzionalità per verificare il contenuto del firmware 8 byte alla volta rispetto a dati forniti. Questa funzionalità è vulnerabile a un classico attacco basato sui tempi ed è ciò che viene sfruttato qui per consentire l'estrazione del firmware da questi dispositivi. Il bootloader è accessibile sia tramite USB che UART. Questo estrattore di firmware funziona solo tramite UART, perché la latenza più elevata della USB renderebbe difficile questo attacco. I chip testati sono CH552 e CH554, con versioni del bootloader 2.4 e 2.5. La soluzione hardware per l'estrattore di firmware si basa su una STM32 Blue Pill, perché sono facilmente reperibili, economiche e hanno le prestazioni necessarie.
Il bootloader è stato precedentemente estratto dal dispositivo e il suo protocollo di comunicazione è stato sottoposto a reverse engineering. Di seguito sono riportati un comando di verifica approssimativamente corretto e la funzione di verifica utilizzata nel bootloader. Sono presenti alcuni controlli che richiedono che la lunghezza sia un multiplo di 8, che l'indirizzo sia allineato a un confine di 8 byte, che l'indirizzo sia inferiore a 0x3800 e che non ci siano errori di verifica precedenti. L'ultimo controllo implica che sia necessario un riavvio del CH55x dopo ogni verifica non riuscita.
Possiamo notare che la funzione di verifica restituisce immediatamente il controllo quando un byte della verifica fallisce. Questo significa che più byte sono corretti, più tempo impiegherà la funzione di verifica. Questo è il classico esempio di un attacco basato sui tempi che può essere sfruttato.
unsigned char verifycmd[] = {
// 0x57, 0xab, // UART magic not included to verify function
0xa6, // Verify command
5 + len, // Constant 5 plus length of data to verify
0, // Unused
addr_low, // Low byte of address
addr_high, // High byte of address
0, 0, 0, // Unused
0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, // Data to verify against
checksum
}
unsigned char verify(unsignec char *cmdbuffer)
{
static char prev_verify_error;
unsigned char len = cmdbuffer[1]-5
unsigned short addr = cmdbuffer[3] + cmdbuffer[4] << 8;
if (len & 0x07 || addr & 0x07 || addr > 0x3800 || prev_verify_error) {
return 0xfe;
}
for (int i=0; i < len; i++) {
// Key can be set through bootloader, and CBYTE[] means code memory
if(key[i & 0x07] ^ cmdbuffer[8+i] ^ CBYTE[addr+i]) {
prev_verify_error = 1;
return 0xf5;
}
}
return 0;
}
Attraverso tentativi ed errori abbiamo scoperto che ogni byte corretto estende il tempo di esecuzione della funzione di verifica di circa 4,2 µs. Il baud rate utilizzato dal bootloader per la comunicazione UART è 57600 (indipendentemente da quanto letto altrove), il che significa che la trasmissione di un bit richiede circa 17,4 µs. La relazione tra questi due tempi è importante, perché sembra che la risposta venga inviata con un jitter di anch'esso ~17 µs. Cerchiamo quindi di distinguere risposte che differiscono di 4,2 µs nel tempo di risposta dalla funzione di verifica, ma che possono differire fino a 17 µs a causa del jitter UART (temporizzazione dell'orologio). Questo sembra un compito difficile, ma è possibile realizzarlo con metodi statistici.
Possiamo determinare se un byte è corretto o meno provando a verificarlo più volte e registrando i risultati. Supponiamo, ad esempio, che il tempo più breve possibile per ricevere una risposta se il primo byte è sbagliato sia 30 µs. Quindi, con il jitter UART, possiamo aspettarci un tempo di risposta massimo di 30 µs + 17,4 µs = 47,4 µs per un primo byte non valido. Allo stesso tempo, un primo byte valido e un secondo byte non valido sposterebbero questo "intervallo" da 34,2 µs a 51,6 µs. Aggiungendo alcuni margini, possiamo concludere che il primo carattere non era valido se il tempo di risposta è inferiore a ~33 µs. Analogamente, possiamo affermare che il primo carattere era valido se il tempo di risposta è superiore a 48 µs. Questa è la base dell'attacco basato sui tempi utilizzato per estrarre il firmware.
Questo non è uno strumento completamente automatico per l'estrazione del firmware: sarà necessario modificare il codice sorgente e ricompilare (usando VS Code con PlatformIO). Il motivo principale è che le caratteristiche temporali esatte per ogni byte variano tra le diverse configurazioni e dovranno essere calibrate. Il processo di calibrazione potrebbe essere automatizzato, ma non era l'obiettivo di questo progetto. La calibrazione principale viene effettuata tramite la variabile prober_limits. Ad esempio, prober_limits[0] contiene i limiti per il byte 0 degli 8 byte da verificare. Se il tempo di risposta è inferiore a .invalid_under_time, sappiamo che il byte non era valido. Se è superiore a .valid_over_time, sappiamo che il byte era corretto. C'è anche un .min_delta che consente di passare al byte successivo prima di sapere con certezza se il byte è corretto o meno.
struct ProberByteLimits prober_limits[8] = {
{
// Byte 0
.invalid_under_time = 33,
.valid_over_time = 50,
.min_delta = 30
}, // ...
Per trovare i valori corretti da usare, si consiglia di impostare .invalid_under_time a 0, .valid_over_time a, ad esempio, 100, e .min_delta può essere mantenuto a 30. Questo significa che il prober non riuscirà a trovare il primo byte corretto, ma l'avanzamento verrà stampato sulla UART del PC host. Si vedrà qualcosa di simile a:
[0x0000]=0x01? min=31 max=47 tries=63
min=31 max=48 tries=127
min=31 max=48 tries=191
min=30 max=48 tries=255
min=30 max=48 tries=319
Supponendo che il primo carattere testato non sia valido, possiamo usare questi valori min/max con un offset di 1 o 2 per determinare i limiti appropriati da utilizzare. Ad esempio, in precedenza, potremmo impostare .invalid_under_time a 33 e .valid_over_time a 50. Il prober dovrebbe quindi provare diversi valori finché non trova uno valido, che apparirebbe come sotto:
...
[0x0000]=0x7d? min=31 max=39 tries= 7
[0x0000]=0x02? min=40 max=56 tries= 7
[0x0001]=0x01? min=35 max=52 tries=34
Vediamo che l'ultimo tentativo all'indirizzo 0x0000 ha avuto un tempo di risposta massimo di 56 µs, il che significa che quello era il byte corretto. Il prober passa quindi al byte successivo e continua il processo. Si noti che tutti i limiti degli 8 byte devono essere calibrati separatamente, ma una volta fatto, funzionerà per l'intera memoria. Quando sono stati trovati 8 byte corretti, li stamperà in formato ihex:
:0800000002002932ffffffff9f
Registrando l'output UART su un file di testo, è possibile usare grep per ^: ed estrarre l'intero contenuto della memoria in formato ihex.
Di seguito è mostrato un esempio di circuito per l'estrazione del firmware. Due transistor permettono di spegnere l'alimentazione del CH55x dalla Blue Pill. Questo è consigliato rispetto all'utilizzo del solo reset software, perché il reset software funziona solo quando il CH55x è in modalità bootloader (e il bootloader ha un timeout dopo il quale avvia il codice applicativo). I resistori sulla UART verso il CH55x sono inclusi perché sospetto che le pull-up interne del CH55x possano alimentarlo dalla UART anche quando l'alimentazione è spenta. Il resistore da 10k tra V33 e P3.6 è necessario per mettere il CH55x in modalità bootloader. Sulla Blue Pill, PA11 è collegato al pin RX della UART per poter misurare il tempo di risposta usando il timer 1.

Calibrando e utilizzando questo strumento, è possibile estrarre il firmware dei dispositivi CH55X. Il processo di estrazione non è veloce, ma estrarrà i 14 kb in uno o due giorni. Dipende un po' dalla calibrazione e da quanto la tabella delle frequenze utilizzata corrisponde all'assembler effettivo del firmware. Purtroppo, il codice sorgente è un po' caotico. Ho creato questo strumento perché avevo bisogno del firmware da un dispositivo CH554 e, ora che l'ho ottenuto, non c'è un vero motivo per lavorare ulteriormente sullo strumento stesso. Tuttavia, dovrebbe rivelarsi utile nel caso qualcuno abbia bisogno di estrarre firmware da dispositivi CH55x.