Perché i programmi scritti in C e C ++ sono così spesso vulnerabili agli attacchi di overflow?

131

Quando guardo gli exploit degli ultimi anni relativi alle implementazioni, vedo che molti di loro provengono da C o C ++ e molti di questi sono attacchi di overflow.

  • Heartbleed era un buffer overflow in OpenSSL;
  • Recentemente, è stato rilevato un bug in glibc che consentiva overflow del buffer durante la risoluzione DNS;

sono solo quelli che riesco a pensare subito, ma dubito che questi siano gli unici che A) siano per software scritto in C o C ++ e B) sono basati su un buffer overflow.

Specialmente riguardo al bug di glibc, ho letto un commento che afferma che se questo fosse accaduto in JavaScript invece che in C, non ci sarebbe stato un problema. Anche se il codice fosse solo compilato in Javascript, non sarebbe stato un problema.

Perché C e C ++ sono così vulnerabili agli attacchi di overflow?

    
posta Nzall 23.02.2016 - 15:37
fonte

8 risposte

169

C e C ++, contrariamente alla maggior parte delle altre lingue, tradizionalmente non controllano gli overflow. Se il codice sorgente dice di mettere 120 byte in un buffer da 85 byte, la CPU lo farà felicemente. Questo è legato al fatto che mentre C e C ++ hanno una nozione di array , questa nozione è solo in fase di compilazione. Al momento dell'esecuzione, ci sono solo dei puntatori, quindi non esiste un metodo runtime per verificare l'accesso di un array per quanto riguarda la lunghezza concettuale di tale array.

Al contrario, la maggior parte degli altri linguaggi ha una nozione di array che sopravvive in fase di runtime, in modo che tutti gli accessi agli array possano essere controllati sistematicamente dal sistema di runtime. Questo non elimina gli overflow: se il codice sorgente richiede qualcosa di assurdo come scrivere 120 byte in una matrice di lunghezza 85, non ha ancora senso. Tuttavia, questo attiva automaticamente una condizione di errore interno (spesso un'eccezione, ad esempio ArrayIndexOutOfBoundException in Java) che interrompe l'esecuzione normale e non consente al codice di procedere. Ciò interrompe l'esecuzione e spesso implica la cessazione dell'elaborazione completa (il thread muore), ma normalmente impedisce lo sfruttamento oltre un semplice denial-of-service.

Fondamentalmente, gli exploit di overflow del buffer richiedono che il codice esegua l'overflow (lettura o scrittura oltre i limiti del buffer accessibile) e per continuare a fare cose oltre quell'overflow. La maggior parte dei linguaggi moderni, contrariamente a C e C ++ (e alcuni altri come Forth o Assembly), non consentono realmente l'overflow e sparano al colpevole. Da un punto di vista della sicurezza questo è molto meglio.

    
risposta data 23.02.2016 - 15:48
fonte
56

Si noti che c'è un certo numero di ragionamenti circolari coinvolti: i problemi di sicurezza sono spesso collegati a C e C ++. Ma quanto di ciò è dovuto alle debolezze intrinseche di questi linguaggi, e quanto è dovuto al fatto che quelle sono semplicemente le lingue in cui la maggior parte dell'infrastruttura del computer è scritta in?

C è pensato per essere "un passo avanti rispetto all'assemblatore". Non ci sono limiti di controllo diversi da ciò che hai implementato tu stesso, per spremere l'ultimo ciclo di clock dal tuo sistema.

C ++ offre vari miglioramenti rispetto a C, la più rilevante per la sicurezza sono le sue classi contenitore (ad esempio <vector> e <string> ) che consentono di gestire i dati senza dover gestire manualmente anche la memoria. Tuttavia, poiché evoluzione di C invece di un linguaggio completamente nuovo, comunque anche fornisce la meccanica manuale di gestione della memoria di C, quindi se insisti a riprenderti il piede, C ++ non fa nulla per impedirti di farlo.

Quindi, perché cose come SSL, binding o kernel OS sono ancora scritte in queste lingue?

Perché possono modificare direttamente la memoria, il che li rende particolarmente adatti per un certo tipo di applicazioni a basso livello ad alte prestazioni (come crittografia, ricerche di tabelle DNS, driver hardware ... o Java VM, per quella materia ;-)).

Quindi, se un software rilevante per la sicurezza viene violato, la possibilità di essere scritta in C o C ++ è alta, semplicemente perché la maggior parte del software relativo alla sicurezza è scritto in C o C ++, di solito per ragioni storiche e / o prestazionali. E se è scritto in C / C ++, il vettore di attacco primario è il sovraccarico del buffer.

Se fosse una lingua diversa, sarebbe un vettore di attacco diverso, ma sono sicuro che ci sarebbero anche violazioni della sicurezza.

Sfruttare il software C / C ++ è più facile che sfruttare, per esempio, il software Java. Allo stesso modo in cui lo sfruttamento di un sistema Windows è più facile di sfruttare un sistema Linux: il primo è ubiquitario, ben compreso (ovvero i ben noti vettori di attacco, come trovarli e come sfruttarli), e molte persone cercano cercando per exploit in cui il rapporto ricompensa / sforzo è elevato.

Ciò non significa che quest'ultimo sia intrinsecamente sicuro (saf er , forse, ma non sicuro ). Significa che - essendo il bersaglio più difficile con minori benefici - i Bad Boys non stanno ancora sprecando più tempo a disposizione.

    
risposta data 23.02.2016 - 17:12
fonte
37

In realtà "heartbleed" è stato non proprio un buffer overflow. Per rendere le cose più "efficienti", hanno messo molti buffer più piccoli in un unico grande buffer. Il grande buffer conteneva dati da vari client. Il bug leggeva byte che non doveva leggere, ma in realtà non leggeva i dati al di fuori di quel grande buffer. Una lingua che ha verificato il sovraccarico del buffer non l'avrebbe impedito, perché qualcuno si è messo in mezzo o impedito a tali controlli di trovare il problema.

    
risposta data 23.02.2016 - 16:46
fonte
25

In primo luogo, come altri hanno già detto, C / C ++ è talvolta caratterizzato come un macro assemblatore glorificato: è pensato per essere "vicino al ferro", come linguaggio per la programmazione a livello di sistema.

Quindi, ad esempio, il linguaggio mi permette di dichiarare una matrice di lunghezza zero come segnaposto quando, in effetti, può rappresentare una sezione di lunghezza variabile in un pacchetto di dati o l'inizio di una regione di lunghezza variabile nella memoria che è usato per comunicare con un pezzo di hardware.

Sfortunatamente significa anche che C / C ++ è pericoloso nelle mani sbagliate; se un programmatore dichiara un array di 10 elementi e poi scrive nell'elemento 101, il compilatore lo compilerà felicemente, il codice eseguirà felicemente, cestinando qualunque cosa si trovi in quella posizione di memoria (codice, dati, stack, chissà.)

In secondo luogo, C / C ++ è idiosincratico. Un buon esempio sono le stringhe, che sono fondamentalmente array di caratteri. Ma ogni costante di stringa porta un carattere di terminazione aggiuntivo, invisibile. Questa è stata la causa di innumerevoli errori in quanto (soprattutto, ma non esclusivamente) i programmatori inesperti spesso non riescono ad allocare quel byte in più necessario per terminare il nulla.

In terzo luogo, C / C ++ è piuttosto vecchio. Il linguaggio è nato in un momento in cui gli attacchi esterni a un sistema software erano praticamente inesistenti. Ci si aspettava che gli utenti fossero fidati e cooperativi, non ostili, poiché il loro obiettivo era quello di far funzionare il programma, non di mandarlo in crash.

Ecco perché la libreria standard C / C ++ contiene molte funzioni intrinsecamente insicure. Prendi strcpy (), per esempio. Copierà felicemente qualsiasi cosa fino a quando un carattere null terminante. Se non trova un carattere nullo terminante, continuerà a copiare finché l'inferno si blocca, o più probabilmente, finché non sovrascrive qualcosa di vitale e il programma si blocca. Questo non era un problema nei bei vecchi tempi, quando, non ci si aspettava che un utente entrasse in un campo riservato, ad esempio, a un codice postale, 16000 caratteri illeggibili seguito da un set di byte appositamente progettato che doveva essere eseguito dopo che lo stack è stato cestinato e il processore ha ripreso l'esecuzione all'indirizzo sbagliato.

Solo per essere sicuri, C / C ++ non è l'unico linguaggio idiosincratico là fuori. Altri sistemi hanno comportamenti idiosincratici diversi, ma possono essere altrettanto negativi. Prendi linguaggi di programmazione back-end come PHP e quanto è facile scrivere codice che consente l'iniezione SQL.

Alla fine, se diamo ai programmatori i potenti strumenti di cui hanno bisogno per svolgere il loro lavoro, ma senza un'adeguata formazione e consapevolezza dell'ambiente di sicurezza, le cose brutte accadranno indipendentemente dal linguaggio di programmazione utilizzato.

    
risposta data 24.02.2016 - 02:49
fonte
4

Probabilmente toccherò alcune cose che alcune delle altre risposte hanno già detto ... ma ... trovo che la domanda stessa sia errata e "vulnerabile".

Come richiesto, la domanda sta assumendo molto senza comprendere i problemi sottostanti. C / C ++ non sono "più vulnerabili" rispetto ad altre lingue. Piuttosto, mettono la maggior parte della potenza dei dispositivi di elaborazione e la responsabilità di usare quel potere, direttamente nelle mani del programmatore. Quindi, la realtà della situazione è che molti programmatori scrivono codice vulnerabile allo sfruttamento e dal momento che C / C ++ non fa di tutto per proteggere il programmatore da se stessi come fanno alcuni linguaggi, il loro codice è più vulnerabile. Questo non è un problema di C / C ++, poiché i programmi scritti in linguaggio assembly avrebbero gli stessi problemi, per esempio.

Il motivo per cui tale can programmazione di basso livello è così vulnerabile è perché fare cose come il controllo dei limiti di array / buffer può diventare molto dispendioso dal punto di vista computazionale e molto spesso non necessario quando si programma in modo difensivo. Immagina, per esempio, che tu stia scrivendo il codice per alcuni dei principali motori di ricerca, che deve elaborare migliaia di miliardi di record di database in un batter d'occhio, così l'utente non si annoierà o si sentirà frustrato mentre "Caricamento pagina ..." È visualizzato. Non vuoi che il tuo codice continui a controllare i limiti dell'array / buffer ogni volta che passi attraverso il loop; Anche se è necessario un nanosecondo per eseguire tale controllo, cosa banale se si elaborano solo dieci record, è possibile aggiungere fino a molti secondi o minuti quando si esegue il ciclo di miliardi o di trilioni di record.

Quindi, ci si può "fidare" del fatto che l'origine dati (ad esempio, il "bot web" che esegue la scansione dei siti Web e inserisce i dati nel database) abbia già controllato i dati. Questo non dovrebbe essere un assunto irragionevole; Per un programma tipico, si desidera controllare i dati su input , quindi il codice che elabora può funzionare alla massima velocità. Anche molte librerie di codici seguono questo approccio. Alcuni addirittura documentano che si aspettano che il programmatore abbia già controllato i dati prima di chiamare le funzioni della libreria per agire sui dati.

Sfortunatamente, tuttavia, molti programmatori non programmano in modo difensivo e presumono che i dati debbano essere validi e entro limiti / parametri sicuri. E questo è ciò che viene sfruttato dagli aggressori.

Alcuni linguaggi di programmazione sono progettati in modo tale da provare a proteggere il programmatore da tali scarse pratiche di programmazione inserendo automaticamente ulteriori verifiche nel programma generato, che il programmatore non ha scritto esplicitamente nel proprio codice. Ancora una volta, questo va bene quando si esegue solo il ciclo del codice alcune centinaia di volte o meno. Ma quando stai attraversando miliardi o trilioni di iterazioni, si aggiungono lunghi ritardi nell'elaborazione dei dati, che possono diventare inaccettabili. Quindi è un compromesso quando si sceglie quale lingua utilizzare per una particolare parte di codice, e quanto spesso e dove si controllano condizioni potenzialmente pericolose / sfruttabili all'interno dei dati.

    
risposta data 26.02.2016 - 00:53
fonte
2

Fondamentalmente i programmatori sono persone pigre (incluso me stesso). Fanno cose come usare gets () invece di fgets () e definire i buffer di I / O nello stack e non guardare abbastanza per i modi in cui la memoria potrebbe essere sovrascritta involontariamente (in modo non intenzionale per il programmatore, intenzionalmente per l'hacker:).

    
risposta data 23.02.2016 - 18:37
fonte
1

Esiste una grande quantità di codice C esistente che consente la scrittura non controllata dei buffer. Alcuni di questi sono nelle biblioteche. Questo codice è potenzialmente pericoloso se uno stato esterno può modificare la lunghezza scritta, e solo altrimenti molto pericoloso.

Esiste una quantità maggiore di codice C esistente che limita la scrittura ai buffer. Se l'utente di detto codice fa un errore matematico e lascia che sia scritto più di quanto dovrebbe, questo è sfruttabile come sopra. Non esiste alcuna garanzia in fase di compilazione che la matematica sia eseguita correttamente.

C'è anche una grande quantità di codice C esistente che legge in offset fuori dalla memoria. Se l'offset non è verificato come valido, questo può perdere informazioni.

Il codice C ++ è spesso usato come linguaggio di alto livello per l'interoperabilità con C, quindi sono seguiti molti concetti di C e sono comuni errori di comunicazione con le API C.

Gli stili di programmazione C ++ che impediscono tali sovraccarichi esistono, ma ci vuole solo 1 errore per consentire loro di accadere.

Inoltre, il problema dei puntatori penzolanti, in cui le risorse di memoria vengono riciclate e il puntatore ora punta alla memoria con una durata / struttura diversa da quella originale, consente alcuni tipi di exploit e perdite di informazioni.

Questo tipo di errori - errori "fencepost", errori "pointer dangling" - sono così comuni e così difficili da eliminare completamente, che molte lingue sono state sviluppate con sistemi progettati esplicitamente per impedire da loro.

Non sorprendentemente, in linguaggi progettati per eliminare questi errori, questi errori non si verificano quasi altrettanto spesso. Si verificano ancora a volte: o il motore che esegue la lingua ha il problema, oppure viene impostata una situazione manuale che corrisponde all'ambiente del caso C / C ++ (riutilizzo di oggetti in un pool, utilizzando un comune buffer comune suddiviso per consumatore, ecc. ). Ma poiché questi usi sono più rari, il problema si verifica meno spesso.

Ogni allocazione dinamica, ogni utilizzo di buffer, in C / C ++ corre questi rischi. E essere perfetti non è raggiungibile.

    
risposta data 26.02.2016 - 22:41
fonte
0

Le lingue più usate (Java e Ruby, ad esempio) compilano il codice che viene eseguito in una VM. La VM è progettata per separare il codice macchina, i dati e in genere lo stack. Ciò significa che le normali operazioni di linguaggio non possono modificare il codice o reindirizzare il flusso di controllo (a volte ci sono API speciali che possono farlo, ad esempio per il debug).

C e C ++ di solito vengono compilati direttamente nella lingua macchina nativa della CPU - questo offre vantaggi in termini di prestazioni e flessibilità, ma significa che il codice errato può sovrascrivere la memoria o lo stack del programma e quindi eseguire istruzioni non nel programma originale.

Questo si verifica in genere quando un buffer è (forse deliberatamente) sovraccarico in C ++. In Java o Ruby, al contrario, un sovraccarico del buffer causerà immediatamente un'eccezione e non può (eccetto i bug VM) sovrascrivere il codice o modificare il flusso di controllo.

    
risposta data 27.02.2016 - 00:07
fonte

Leggi altre domande sui tag