Le tecniche di verifica del programma potrebbero impedire il verificarsi di bug del genere Heartbleed?


9

Sulla questione dell'insetto Heartbleed, Bruce Schneier scrisse nel suo Crypto-Gram del 15 aprile: "Catastrofico" è la parola giusta. Sulla scala da 1 a 10, questo è un 11. ' Ho letto diversi anni fa che un kernel di un determinato sistema operativo è stato rigorosamente verificato con un moderno sistema di verifica del programma. Pertanto, è possibile impedire che si verifichino bug del genere Heartbleed attraverso l'applicazione delle tecniche di verifica del programma oggi o è ancora irrealistico o addirittura impossibile?


2
Ecco un'interessante analisi di questa domanda di J. Regehr.
Martin Berger,

Risposte:


6

Per rispondere alla tua domanda nel modo più conciso, sì, questo errore potrebbe essere stato potenzialmente rilevato da strumenti di verifica formale. In effetti, la proprietà "non invia mai un blocco più grande della dimensione del hearbeat inviato" è abbastanza semplice da formalizzare nella maggior parte dei linguaggi di specifica (ad es. LTL).

Il problema (che è una critica comune contro i metodi formali) è che le specifiche che usi sono scritte dagli umani. In effetti, i metodi formali non fanno altro che spostare la sfida della caccia ai bug dalla ricerca dei bug alla definizione di quali siano i bug. Questo è un compito difficile.

Inoltre, verificare formalmente il software è notoriamente difficile a causa del problema dell'esplosione dello stato. In questo caso, è particolarmente rilevante, poiché molte volte per evitare l'esplosione dello stato, sottraggiamo i limiti. Ad esempio, quando vogliamo dire "ogni richiesta è seguita da una sovvenzione, entro 100000 passi", abbiamo bisogno di una formula molto lunga, quindi la astraggiamo alla formula "ogni richiesta è infine seguita da una sovvenzione".

Pertanto, nel caso del cuore, anche mentre si tenta di formalizzare i requisiti, il limite in questione avrebbe potuto essere sottratto, determinando lo stesso comportamento.

Per riassumere, potenzialmente questo errore avrebbe potuto essere evitato usando metodi formali, ma avrebbe dovuto esserci un essere umano che specificava in anticipo questa proprietà.


5

I controllori di programmi commerciali come Klocwork o Coverity potrebbero essere stati in grado di trovare Heartbleed poiché si tratta di un "relativamente dimenticato" errore di controllo dei limiti ", che è uno dei problemi principali per cui sono stati progettati. Ma esiste un modo molto più semplice: utilizzare tipi di dati astratti opachi che sono stati testati per essere liberi da sovraccarico del buffer.

Esistono numerosi tipi di dati astratti "stringa sicura" disponibili per la programmazione C. Quello con cui ho più familiarità è Vstr . L'autore, James Antill, ha una grande discussione sul perché hai bisogno di una stringa di tipo di dato astratto con i propri costruttori / metodi di fabbrica e anche un elenco di altri tipi di dati astratti di stringa per C .


2
Coverity non trova Heartbleed, vedi questa analisi di John Regehr.
Martin Berger,

Bel link! Dimostra la vera morale della storia: la verifica del programma non può compensare astrazioni mal progettate (o inesistenti).
Wandering Logic,

2
Dipende da cosa intendi per verifica del programma. Se intendi l'analisi statica, allora sì, questa è sempre un'approssimazione, come conseguenza diretta del teorema di Rice. Se si verifica il comportamento completo in un procuratore di teorema interattivo, si ottiene una garanzia in ghisa che il programma soddisfa le sue specifiche, ma è estremamente laborioso. E hai ancora il problema che le tue specifiche potrebbero essere errate (vedi ad esempio l'esplosione di Ariane 5).
Martin Berger,

1
@MartinBerger: Coverity lo trova ora .
Ripristina Monica - M. Schröder il

4

Se si considera come una "  tecnica di verifica del programma  " la combinazione di controllo di runtime e fuzzing, , questo particolare bug potrebbe essere stato rilevato .

Il fuzzing corretto farà sì che l'ormai famigerato memcpy(bp, pl, payload);leggere oltre il limite del blocco di memoria plappartenga. In linea di principio, il controllo dei limiti di runtime può catturare tali accessi e, in pratica, in questo caso particolare, anche una versione di debug mallocche si preoccupa di controllare i parametri memcpyavrebbe fatto il lavoro (non c'è bisogno di confondere con la MMU qui) . Il problema è che è necessario eseguire test di fuzzing su ogni tipo di pacchetto di rete.


1
Sebbene sia vero in generale, IIRC, nel caso di OpenSSL gli autori hanno implementato la propria gestione della memoria interna in modo tale che era molto meno probabile memcpyche colpisse il vero confine della (grande) regione originariamente richiesta dal sistema malloc.
William Price,

Sì, nel caso di OpenSSL come era al momento del bug, memcpy(bp, pl, payload)avrebbe dovuto verificare i limiti utilizzati dalla mallocsostituzione di OpenSSL , non il sistema malloc. Ciò esclude il controllo automatico dei limiti a livello binario (almeno senza una profonda conoscenza della mallocsostituzione). Deve esserci una ricompilazione con la procedura guidata a livello di sorgente utilizzando, ad esempio, le macro C che sostituiscono il token malloco qualunque sostituzione OpenSSL utilizzata; e sembra che abbiamo bisogno dello stesso con memcpytranne trucchi MMU molto intelligenti.
fgrieu,

4

L'uso di un linguaggio più rigoroso non si limita a spostare i post degli obiettivi dall'implementazione corretta all'ottenimento delle specifiche giuste. È difficile fare qualcosa che sia molto sbagliato ma logicamente coerente; ecco perché i compilatori catturano così tanti bug.

L'aritmetica del puntatore, come è normalmente formulata, non è corretta perché il sistema dei tipi in realtà non significa ciò che dovrebbe significare. Puoi evitare completamente questo problema lavorando in un linguaggio di raccolta dei rifiuti (l'approccio normale che ti fa anche pagare per l'astrazione). Oppure puoi essere molto più specifico su quali tipi di puntatori stai utilizzando, in modo che il compilatore possa rifiutare tutto ciò che è incoerente o semplicemente non può essere dimostrato corretto come scritto. Questo è l'approccio di alcune lingue come Rust.

I tipi costruiti sono equivalenti alle prove, quindi se scrivi un sistema di tipi che lo dimentica, tutti i tipi di cose vanno male. Supponiamo per un po 'che quando dichiariamo un tipo, in realtà intendiamo che stiamo affermando la verità su ciò che è nella variabile.

  • int * x; // Una falsa affermazione. x esiste e non punta a un int
  • int * y = z; // Vero solo se è dimostrato che z punta a un int
  • * (x + 3) = 5; // Vero solo se (x + 3) punta a un int nella stessa matrice di x
  • int c = a / b; // Vero solo se b è diverso da zero, come: "nonzero int b = ...;"
  • nullable int * z = NULL; // nullable int * non è uguale a un int *
  • int d = * z; // Una falsa affermazione, perché z è nullable
  • if (z! = NULL) {int * e = z; } // Ok perché z non è null
  • libera (y); int w = * y; // Falsa affermazione, perché y non esiste più in w

In questo mondo, i puntatori non possono essere nulli. Le dereferenze NullPointer non esistono e non è necessario verificare la nullità dei puntatori da nessuna parte. Invece, un "nullable int *" è un tipo diverso che può avere il suo valore estratto su null o su un puntatore. Ciò significa che nel punto in cui inizia l' assunzione non nulla , vai a registrare la tua eccezione o vai in giù un ramo null.

In questo mondo, non esistono nemmeno errori fuori campo. Se il compilatore non è in grado di provare che è nei limiti, prova a riscriverlo in modo che il compilatore possa provarlo. Se ciò non è possibile, dovrai inserire l'Assunta manualmente in quel punto; il compilatore potrebbe trovare una contraddizione in seguito.

Inoltre, se non è possibile avere un puntatore che non è inizializzato, non si avranno puntatori alla memoria non inizializzata. Se hai un puntatore alla memoria liberata, allora dovrebbe essere rifiutato dal compilatore. In Rust, ci sono diversi tipi di puntatori per rendere ragionevole aspettarsi questo tipo di prove. Esistono solo puntatori di proprietà (ovvero: nessun alias), puntatori a strutture profondamente immutabili. Il tipo di archiviazione predefinito è immutabile, ecc.

Esiste anche il problema di applicare una grammatica effettiva ben definita sui protocolli (che include i membri dell'interfaccia), per limitare l'area di input a esattamente ciò che è previsto. La cosa sulla "correttezza" è: 1) Sbarazzarsi di tutti gli stati indefiniti 2) Garantire la coerenza logica . La difficoltà di arrivarci ha molto a che fare con l'uso di utensili estremamente cattivi (dal punto di vista della correttezza).

Questo è esattamente il motivo per cui le due peggiori pratiche sono variabili globali e goto. Queste cose impediscono di porre condizioni pre / post / invarianti attorno a qualsiasi cosa. È anche il motivo per cui i tipi sono così efficaci. Man mano che i tipi diventano più forti (usando infine i Tipi dipendenti per tenere conto del valore reale), si avvicinano a essere prove costruttive di correttezza in se stessi; la compilazione di programmi incoerenti non riesce.

Tieni presente che non si tratta solo di errori stupidi. Si tratta anche di difendere la base di codice da infiltrati intelligenti. Ci saranno casi in cui devi rifiutare un invio senza una convincente prova generata dalla macchina di proprietà importanti come "segue il protocollo formalmente specificato".



1

la verifica del software automatizzata / formale è utile e può aiutare in alcuni casi, ma come altri hanno sottolineato, non è un proiettile d'argento. si potrebbe sottolineare che OpenSSL è vulnerabile in quanto open source e tuttavia utilizzato commercialmente e in tutto il settore, ampiamente utilizzato e non pesantemente rivisto prima della pubblicazione (ci si chiede se nel progetto ci siano anche sviluppatori pagati). il difetto è stato scoperto fondamentalmente tramite la revisione del codice post-rilascio e il codice è stato apparentemente rivisto prima del rilascio (si noti che probabilmente non c'è modo di tracciare chi ha eseguito la revisione del codice interno). il "momento insegnabile" con heartbleed (tra molti altri) è sostanzialmente una migliore revisione del codice idealmente prima di rilasciare esp di codice altamente sensibile, possibilmente meglio monitorato. forse OpenSSL sarà ora soggetto a più scrutinio.

più bkg dai media che ne dettagliano le origini:

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.