Sicurezza e affidabilità nei Bootloader

Benvenuti a un nuovo appuntamento con la Rubrica Firmware Reload di Elettronica Open Source. In questa Rubrica del blog troverete articoli tecnici della vecchia rivista cartacea Firmware, che contengono argomenti e temi passati ancora di interesse per Makers, Professionisti e Appassionati di elettronica. Sicurezza e affidabilità sono due aspetti importanti da tenere presente al fine di realizzare politiche corrette per l’upgrade del codice. In questo articolo vedremo le loro peculiarità e gli impatti sulle soluzioni adottate.

Introduzione

La sicurezza e l’affidabilità sono due concetti che rientrano nella sfera della sicurezza delle informazioni, anche se oggi il nostro pensiero si focalizza sull’obiettivo di mantenere riservate solo quelle personali o militari. Oggigiorno, a maggior ragione per via degli impatti economici, si avverte sempre con più preoccupazione la necessità che le informazioni si scambino utilizzando sistemi sicuri e non corrompibili, per l’ovvio motivo di contrastare la lotta alla pirateria informatica e di salvaguardare il ritorno degli investimenti. Questo aspetto è una questione di enorme importanza. Infatti, proviamo ad immaginare che cosa succederebbe se la procedura d’aggiornamento del firmware di un televisore non andasse a buon fine: da una parte, l’acquirente ha la necessità che l’intera procedura di caricamento sia sicura e affidabile, mentre, dall’altra parte, il produttore, con enormi impatti sulla sua immagine, ha il dovere di offrire una procedura che garantisca il buon esito dell’operazione.

Oggi, almeno per una grossa fetta del pianeta, viviamo in una società interconnessa con un’enorme disponibilità di dispositivi end-user che hanno la necessità di un loro costante aggiornamento e dove i dati interagiscono in modo continuo. Per tale ragione, quando parliamo di sicurezza delle informazioni, ci vogliamo riferire anche alle tecniche utilizzate per nascondere il contenuto dei messaggi stessi per impedire a chiunque di inserirsi e, per così dire, sniffare i dati durante l’aggiornamento sul campo. Qualunque programma che si occupa di preservare la sicurezza delle informazioni e di perseguire l’aggiornamento on-field utilizzando software dedicati persegue, in qualche misura, tre obiettivi fondamentali: disponibilità, integrità e riservatezza. Occorre fare però una premessa: ogni dispositivo elettronico, dal televisore alla lavatrice fino ad arrivare ad un cellulare, utilizza una memoria di tipo flash per conservare nel tempo il codice applicativo o i dati sensibili dell’utente. Da ciò si ricava che il programma che sovrintende al funzionamento del dispositivo può essere facilmente aggiornato anche per venire incontro alle esigenze di ogni utilizzatore.

Tale prerogativa è garantita ricorrendo ad un piccolo modulo software noto come bootloader: questo si preoccupa, all’avvio di una particolare condizione, di procedere all’aggiornamento del codice per sostituire il vecchio firmware del dispositivo. Un bootloader risiede sempre in una memoria non volatile al fine di garantire l’aggiornamento del dispositivo in qualsiasi situazione e condizione. Ogni bootloader deve possedere diverse caratteristiche, ossia offrire una policy che riesca ad identificare in maniera precisa il software da utilizzare e fornire strumenti che possano garantire l’integrità del codice da trasferire. In sostanza, il bootloader deve garantire specifiche politiche di security e di safety in una piccola porzione di codice visto che il costruttore non può occupare troppa memoria sottraendo quella disponibile dall’acquirente. Di solito, quando si intende aggiornare un firmware presente sul dispositivo, è necessario utilizzare un’apposita procedura che si attiva attraverso una condizione hardware o software.

Una volta attivata la condizione per l’upgrade, il bootloader è posto in esecuzione cercando di connettersi, se non dispone già in memoria della versione corretta, ad uno specifico host per scaricare il modulo corretto. In questo contesto un host può essere semplicemente un server dedicato o un PC connesso al target attraverso una linea seriale ad alta velocità. Il bootloader può connettersi, di solito, con l’host con un qualsiasi mezzo di comunicazione supportato dal target, ovvero una RS232, USB, CAN o altro. Secondo la letteratura in uso, una procedura del genere presenta due punti critici: o durante il trasporto del firmware dal produttore al cliente o durante il download sul dispositivo di destinazione. A questo proposito, la Figura 1 è un diagramma che mostra le differenti criticità presenti. Tipicamente, la procedura utilizzata per aggiornare il software residente (Figura 2) è composta da una serie di piccoli passi (Figura 1).

figura 1 - flusso del programma.

                                                                        Figura 1: Flusso del programma

 

figura 2 - possibile scenario.

Figura 2: Possibile scenario

Per prima cosa, una volta che il produttore ha rilasciato una nuova versione del firmware (1), questo (2) deve essere distribuito agli interessati. L’acquirente potrebbe avere l’onere di attivare la condizione d’avvio del bootloader, magari pigiando un particolare tasto o connettendosi ad un sito dedicato (3): in caso di richiesta del cliente, il bootloader viene attivato (4) inizializzando, per prima cosa, il canale di comunicazione e, in seguito, si pone in ascolto sulla linea. Il costruttore o il fornitore del codice invia infine al bootloader (5), magari dopo una breve e articolata sessione iniziale, il nuovo codice. Al termine della procedura di upload, il bootloader si preoccupa di rifare uno startup scrivendo il nuovo checksum per perfezionare la sostituzione del vecchio codice (6). In caso di successo dell’operazione, magari a seguito di un controllo della coerenza dei dati attraverso un checksum dedicato, il bootloader viene inibito e il dispositivo è così pronto a gestire la nuova applicazione (7).

In realtà, ogni costruttore ha una propria procedura per l’upgrade del codice. Per esempio, un costruttore potrebbe assegnare il compito di aggiornare il firmware residente all’applicazione principale. Chiaramente, la procedura che abbiamo descritto offre diversi vantaggi; infatti, una siffatta procedura permette di ottenere una maggiore versatilità dell’applicazione evitando di realizzare moduli software in modo differente, uno per ogni singola famiglia di dispositivi. Quali sono le regole che deve rispettare un buon bootloader? O meglio, a quali situazioni critiche è maggiormente suscettibile? Certamente, in una visione globale, e come messo in evidenza in precedenza, la procedura per l’upgrade del software è molto sensibile durante la fase di acquisizione della nuova versione del codice e durante l’aggiornamento stesso in memoria flash. A questo riguardo, nella Figura 1 si mostrano tutte queste particolarità ponendo l’accento sui diversi aspetti, per così dire, certificativi coinvolti. Infatti, dal punto di vista del costruttore è necessario garantire che la procedura d’aggiornamento rispetti i vincoli di safety e di security secondo le accezioni del contesto software. Infatti, la safety è direttamente relazionabile alla garanzia di evitare situazioni catastrofiche che causano, ad esempio, morte, ferite, malattie, il danneggiamento, la perdita di apparati o addirittura danni per l’ambiente.

Per aumentare la safety in caso di guasto, occorre rilevare il guasto e adottare le opportune misure per portare il sistema in uno stato fail-safe. Nel nostro contesto, come mette in evidenza la figura proposta, la safety è un parametro che si presenta in fase di trasmissione del codice. La security, infine, è posta in relazione alla prevenzione degli accessi non autorizzati e/o alle manipolazioni delle informazioni, ovvero rientrano in questo contesto le politiche di privacy, integrità e autenticità. Per privacy si intende l’impossibilità di leggere le informazioni o risalire al suo significato originario: le informazioni non possono essere lette da utenti o da dispositivi non autorizzati. Al contrario, l’autenticità permette di verificare il proprietario del firmware; infatti, grazie all’autenticità è possibile risalire in modo univoco al proprietario del codice: una questione di enorme importanza se parliamo di firmware che deve sovraintendere a particolari applicazioni di sicurezza. Infine, l’integrità è utile per rilevare una modifica del codice o dei dati dell’applicazione. Senza alcun tipo di funzione di sicurezza, un firmware sarà soggetto a tutti gli attacchi in materia di privacy, d’integrità o d’autenticità. Pertanto, sono necessarie alcune tecniche per impedire un uso improprio del firmware fornito dal costruttore.

POSSIBILI SOLUZIONI - SAFETY

Diversi costruttori hanno adottato diverse tecniche che permettono di offrire dei buoni livelli di safety; infatti, c’è chi utilizza un particolare Communication Protocol Stack al fine di elevare il grado di safety delle comunicazioni. A questo riguardo, il costruttore cerca di puntare sulla reliability per assicurare l’incorruttibilità dei dati scambiati in fase di acquisizione delle informazioni. In un classico Protocol Stack, la reliability è tipicamente implementata a livello di trasporto; infatti, per sopperire a questa funzionalità si preferisce utilizzare particolari tecniche quali l’error detection/correction code o il block numbering e anche ricorrendo al cosiddetto packet acknowledgement. In particolare, lo scopo del block numbering (Figura 3) è quello di evitare la perdita di pacchetti o l’arrivo di due pacchetti in modo non congruo. Questo particolare meccanismo è molto apprezzato quando si cerca di trasferire dei file.

figura 3 - block numbering.

Figura 3: Block numbering

Così come pone in evidenza il nome, nel block numbering si associa una sequenza di numeri ad ogni pacchetto che si trasmette: ogni pacchetto è incrementato di uno per ogni blocco. In questo modo, il ricevitore può tracciare ogni pacchetto permettendo di conoscere situazioni anomale, ovvero, se si è saltato un pacchetto (si riceve il #3 e il #5, ma non il #5) o è presente una ricezione fuori ordine (si riceve il #3 prima del #2). A questo riguardo, la Figura 3 ne mostra il suo funzionamento. Al contrario, con il packet acknowledgement ogni volta che il trasmettitore invia un pacchetto si aspetta l’acknowledge del ricevitore prima di inviare il pacchetto successivo. Se il trasmettitore non riceve nulla entro un tempo prestabilito, questi considera perso il pacchetto e lo ritrasmette (Figura 4).

figura 4 - Packet management.

Figura 4: Packet management

Un’altra tecnica è quella del memory partitioning. L’idea che sta alla base di questa politica è quella di avere, in ogni momento, una copia del firmware di lavoro in memoria. Grazie alla sua presenza è possibile, in caso di insuccesso dell’operazione, ritornare nella situazione precedente. Uno dei problemi più importanti nella trasmissione delle informazioni è quello di fare e gestire una corretta error detection; infatti, nella maggior parte dei casi si preferisce utilizzare un Cyclic Redundancy Check. A tal proposito, il Listato 1 presenta un possibile esempio dove, in ragione di un dato in ingresso pari ad un flusso binario di 1101 si deve ottenere un code word pari a 11011101000110101101; infatti, dall’algoritmo polinomiale generatore 10001000000100001 si deve ottenere un checksum di 1101000110101101 pari a un code word finale di 11011101000110101101. Il Listato mostra anche che cosa succederebbe se volessimo inserire un errore.

#include<stdio.h>
#include<string.h>
#define N strlen(g)
char t[28],cs[28],g[]=”10001000000100001”;
int a,e,c;
void xor(){
for(c = 1;c < N; c++)
cs = (( cs == g)?’0’:’1’);
}
void crc(){
for(e=0;e<N;e++)
cs[e]=t[e];
do{
if(cs[0]==’1’)
xor();
for(c=0;c<N-1;c++)
cs=cs;
cs=t[e++];
}while(e<=a+N-1);
}
int main()
{
printf(“\nEnter data : “);
scanf(“%s”,t);
printf(“\n————————————————————“);
printf(“\nGeneratng polynomial : %s”,g);
a=strlen(t);
for(e=a;e<a+N-1;e++)
t[e]=’0’;
printf(“\n————————————————————“);
printf(“\nModified data is : %s”,t);
printf(“\n————————————————————“);
crc();
printf(“\nChecksum is : %s”,cs);
for(e=a;e<a+N-1;e++)
t[e]=cs[e-a];
printf(“\n————————————————————“);
printf(“\nFinal codeword is : %s”,t);
printf(“\n————————————————————“);
printf(“\nTest error detection 0(yes) 1(no)? :
“);
scanf(“%d”,&e);
if(e==0)
{
do{
printf(“\nEnter the position where error
is to be inserted : “);
scanf(“%d”,&e);
}while(e==0 || e>a+N-1);
t[e-1]=(t[e-1]==’0’)?’1’:’0’;
printf(“\n————————————————————“);
printf(“\nErroneous data : %s\n”,t);
}
crc();
for(e=0;(e<N-1) && (cs[e]!=’1’);e++);
if(e<N-1)
printf(“\nError detected\n\n”);
else
printf(“\nNo error detected\n\n”);
printf(“\n————————————————————\n”);
return 0;
}
</APPNOTES>
Listato 1 - Cyclic Redundancy Check

POSSIBILI SOLUZIONI – SECURITY

Anche in questo caso si è cercato di approntare diverse tecniche che permettono di rispettare i vincoli di security del sistema. Per rispettare l’integrità del codice si cerca di ricorrere a procedure di hash (Figura 5); infatti, l’obiettivo dell’hashing è quello di ricavare una specie di firma digitale del firmware.

figura 5 - firmaware hashing.

Figura 5: Firmware hashing

Ricordiamo che esiste una differenza sostanziale tra un testo crittografato e il suo valore di hash; infatti, con l’hashing e con la crittografia vogliamo riferirci a due modi per proteggere le informazioni: la crittografia consente di ripristinare il testo originale in seguito tramite la decodifica, al contrario, con l’hashing intendiamo riassumere il testo in una breve impronta che non può essere decrittata. Con questo vogliamo dire che con la sola impronta hashing, non c’è alcun modo di sapere esattamente quale testo è stato riassunto anche se un determinato testo genera sempre la stessa impronta. Ricordiamo che una funzione hashing è una funzione matematica che converte un input di dimensione variabile, ovvero in un vasto dominio, in una sequenza di bit di lunghezza fissa in un dominio più ristretto. In crittografia si utilizza una chiave pubblica o privata per cifrare non il documento originale ma l’output della funzione di hashing applicata al documento, e il risultato dell’hashing prende il nome di digest. Possiamo ricordare che esempi di algoritmi di hashing sono MD5, message digest 5 e SHA, o secure hash algorithm.

Ad ogni modo, ricordiamo che la sicurezza di un sistema non è mai assoluta, ma semmai si cerca di incidere sul costo che un avversario deve affrontare per superarla. Il costo è inteso sia nei termini economici, ma soprattutto nel nostro caso in termini temporali, come il tempo di calcolo necessario per trovare una soluzione al problema computazionale che costituisce la barriera di sicurezza. In altre parole, la sicurezza di un algoritmo di cifratura è direttamente proporzionale al tempo speso da un attaccante a violare l’algoritmo stesso, ovvero fino a quando non riesca a provare a fattorizzare il dato numero. In questo modo, il livello di sicurezza, oltre ad essere espresso in termini temporali, viene anche misurato come probabilità. In tal caso si sottintende un meccanismo d’attacco a tentativi, in cui l’attaccante non conosce un algoritmo in grado di calcolare direttamente la soluzione del problema in questione, ma è in possesso invece di un algoritmo che è in grado di stabilire l’esattezza o meno di una data soluzione. In questo caso quindi si misura la sicurezza calcolando la probabilità che un avversario ha di “indovinare” la soluzione corretta. Esprimendo questa probabilità come potenza di 2, l’opposto dell’esponente risulterà invece essere lo stesso livello di sicurezza espresso in bit, si veda a questo proposito la Tabella 1.

Tabella 1 - Ipotesi di calcolo del livello di sicurezza

Tabella 1: Ipotesi di calcolo del livello di sicurezza

Al fine di rispettare i vincoli di integrità e autenticità si ricorre a due metodi interessanti, ovvero si cerca di introdurre la cosiddetta Digital Signature (Figura 6) e Message Authentication Codes (Figura 7). La firma digitale si basa su un assunto importante; infatti, dal momento che un valore hash può essere calcolato senza particolare difficoltà, la soluzione di un approccio del genere è quella di ricorrere ad un sistema che riesca a coniugare l’hash con un sistema di cifratura a chiave pubblica. Dalla Figura 6 si nota che si parte da un digest che racchiude il firmware del costruttore utilizzando una funzione hash e solo in seguito si applica una cifratura utilizzando il metodo a chiave pubblica. In questo modo si ottiene una firma digitale, simile alle firme utilizzate nella vita quotidiana. Infatti, ricordiamo che la crittografia a chiave pubblica (o asimmetrica) si basa sull’utilizzo di due chiavi: il produttore utilizza la propria chiave privata (segreta) per crittografare la firma, mentre il dispositivo utilizza la chiave pubblica corrispondente per decodificarlo.

Poiché solo la chiave privata può cifrare dati, nessuno tranne il fabbricante può produrre la firma, ma chiunque può verificare la firma con la chiave pubblica del costruttore. Al contrario, con il Message Authentication Codes, o MACs, si ottiene la stessa cosa, ovvero una firma digitale ricorrendo alla crittografia a chiave privata, Figura 7. I moderni algoritmi di crittografia a chiave privata sono per lo più i cifrari a blocchi (per esempio, lavorano su un blocco di dati di dimensione fissa) al contrario di cifrari a flusso (che funzionano su un flusso di dati). La crittografia a chiave privata si affida a una sola chiave segreta, questa è condivisa tra il produttore e i dispositivi.

figura 6 - Digital Signature.

Figura 6: Digital Signature

 

figura 7 - Message Authentication Code Verification.

Figura 7: Message Authentication Code Verification

Scarica subito una copia gratis

Scrivi un commento

Seguici anche sul tuo Social Network preferito!

Send this to a friend