Vai al contenuto
Zyberum Cyber Security Firm
Menu
Secure CodingEmbedded

Secure coding in C embedded: cinque bug che troviamo sempre

Lunghezze non verificate, integer overflow, format string, casualità debole e residui di debug: le cinque classi di bug più frequenti nel firmware e come evitarle.

Zyberum Security Team · Pubblicato il · 7 min di lettura

Il firmware è quasi tutto C, e il C fa esattamente quello che gli dite di fare. Quando facciamo review di codice embedded o reverse engineering di un dispositivo, ricompaiono sempre le stesse cinque classi di bug. Nessuna è nuova. Tutte funzionano ancora.

1. Il campo lunghezza di cui vi siete fidati

Arriva un messaggio con un campo lunghezza, e il codice copia quel numero di byte:

void handle_frame(const uint8_t *frame) {
    uint8_t payload[64];
    uint8_t len = frame[1];
    memcpy(payload, &frame[2], len);   /* len è controllato dall'attaccante */
}

Se len può valere 200, la copia scrive ben oltre il buffer. Su un microcontrollore senza protezione della memoria questo significa spesso esecuzione diretta di codice.

Correzione: verificate ogni lunghezza rispetto alla dimensione della destinazione e al numero di byte effettivamente ricevuti, prima di usarla.

if (len > sizeof(payload) || len > received - 2) {
    return ERR_LENGTH;
}

2. Aritmetica intera che va in overflow

I controlli di lunghezza stessi falliscono quando l’aritmetica va in overflow:

uint16_t total = header_len + body_len;   /* va in overflow a 65535 */
if (total > sizeof(buffer)) return ERR;

Con header_len = 65530 e body_len = 10, total diventa 4 e il controllo viene superato. I confronti tra valori con segno e senza segno causano lo stesso tipo di sorpresa.

Correzione: verificate ogni operando prima della somma, usate tipi più ampi per i risultati intermedi e trattate come errori i warning del compilatore sulle conversioni di segno.

3. Format string e altre scorciatoie

Scrivere direttamente nei log dati esterni è ancora frequente nel codice diagnostico:

printf(device_name);        /* sbagliato */
printf("%s", device_name);  /* corretto */

Alla stessa famiglia appartengono strcpy, sprintf e gets. Non hanno idea di quanto sia grande la destinazione.

Correzione: vietate le funzioni senza limite di lunghezza nelle vostre linee guida di codifica e fate fallire la build quando compaiono.

4. Casualità che non è casuale

Token di sessione, valori di challenge per l’accesso diagnostico, nonce per la cifratura: tutti richiedono numeri imprevedibili. Li troviamo regolarmente generati con rand(), con l’uptime come seed, oppure presi da un generatore hardware non inizializzato.

Se un attaccante può prevedere la challenge, il miglior algoritmo che le sta dietro non serve a nulla.

Correzione: usate il generatore hardware di numeri realmente casuali del vostro MCU, controllatene i flag di stato e, se vi servono molti valori, alimentate con esso un generatore deterministico adeguato.

5. Residui di debug

I risultati più produttivi spesso non sono affatto bug, ma funzionalità che nessuno ha rimosso:

  • una console seriale con una shell di root
  • un comando diagnostico nascosto che salta l’autenticazione
  • chiavi di test e password predefinite nell’immagine di produzione
  • messaggi di errore dettagliati che rivelano indirizzi di memoria

Correzione: rendete esplicita la configurazione della build di produzione, esaminate che cosa cambia rispetto alla build di sviluppo e controllate l’immagine consegnata, non l’albero dei sorgenti.

Che cosa aiuta davvero

Regole e strumenti sono necessari, ma non sostituiscono le persone che ragionano come attaccanti:

  1. Linee guida di codifica (MISRA C, SEI CERT C) fatte rispettare dall’analisi statica in CI.
  2. Fuzzing per ogni parser e protocol handler. La maggior parte dei bug descritti sopra emerge nel giro di poche ore.
  3. Code review concentrata sui punti in cui entrano i dati esterni.
  4. Formazione sul vostro codice. Gli sviluppatori ricordano il bug che hanno sfruttato in prima persona.

È attorno a questo che ruotano il nostro lavoro di sviluppo sicuro e la nostra formazione sul secure coding.

FAQ

Domande frequenti

MISRA C basta per avere codice sicuro?

MISRA C è una buona base perché elimina molte fonti di undefined behaviour. È stato però scritto per la safety, non pensando agli attaccanti. Per la security aggiungete SEI CERT C, code review con la mentalità dell’attaccante e fuzzing di ogni interfaccia che accetta dati esterni.

Dovremmo passare a Rust?

Per i nuovi componenti che elaborano input non attendibili, i linguaggi memory-safe eliminano intere classi di bug e meritano di essere presi in considerazione. La maggior parte del firmware conterrà C ancora per molti anni, quindi secure coding e review in C restano necessari.

Come troviamo questi bug nel codice esistente?

Combinate tre cose: analisi statica per i pattern evidenti, fuzzing per parser e protocol handler e review manuale del codice che gestisce input esterni, autenticazione e aggiornamenti.

ChiamateciPrenota una call

Scegliete l’orario più comodo

Apri in una nuova scheda