Perché è così difficile rendere C meno soggetto a buffer overflow?


23

Sto facendo un corso al college, dove uno dei laboratori deve eseguire exploit buffer overflow sul codice che ci danno. Questo varia da semplici exploit come la modifica dell'indirizzo di ritorno per una funzione su uno stack per tornare a una funzione diversa, fino al codice che modifica lo stato di un registro / memoria dei programmi ma poi ritorna alla funzione che hai chiamato, il che significa che il la funzione che hai chiamato è completamente ignara dell'exploit.

Ho fatto qualche ricerca su questo, e questo tipo di exploit viene usato praticamente ovunque ovunque ora, in cose come l' esecuzione di homebrew su Wii e il jailbreak illimitato per iOS 4.3.1

La mia domanda è: perché questo problema è così difficile da risolvere? È ovvio che questo è uno dei principali exploit utilizzati per hackerare centinaia di cose, ma sembra che sarebbe abbastanza facile da risolvere semplicemente troncando qualsiasi input oltre la lunghezza consentita e semplicemente disinfettando tutti gli input che prendi.

EDIT: Un'altra prospettiva che vorrei prendere in considerazione le risposte: perché i creatori di C non risolvono questi problemi reimplementando le librerie?

Risposte:


35

Hanno corretto le librerie.

Qualsiasi libreria standard moderni C contiene varianti più sicuri strcpy, strcat, sprintf, e così via.

Sui sistemi C99 - che è la maggior parte degli Unix - li troverai con nomi come strncate snprintf, la "n" indica che serve un argomento delle dimensioni di un buffer o un numero massimo di elementi da copiare.

Queste funzioni possono essere utilizzate per gestire molte operazioni in modo più sicuro, ma a posteriori la loro usabilità non è eccezionale. Ad esempio alcune snprintfimplementazioni non garantiscono che il buffer sia terminato con null. strncatrichiede un numero di elementi da copiare, ma molte persone passano erroneamente la dimensione del buffer dest.

Su Windows, si trova spesso il strcat_s, sprintf_s, il suffisso "_s" che indica "sicuro". Anche questi si sono fatti strada nella libreria standard C in C11 e forniscono un maggiore controllo su ciò che accade in caso di overflow (troncamento vs. asserzione per esempio).

Molti fornitori offrono ancora più alternative non standard come asprintfnella libc GNU, che assegnerà automaticamente un buffer della dimensione appropriata.

L'idea che si possa "solo correggere C" è un malinteso. Correggere C non è il problema - ed è già stato fatto. Il problema sta risolvendo decenni di codice C scritto da programmatori ignoranti, stanchi o affrettati, o codice che è stato portato da contesti in cui la sicurezza non contava in contesti in cui la sicurezza lo fa. Nessuna modifica alla libreria standard può risolvere questo codice, sebbene la migrazione a compilatori e librerie standard più recenti possa spesso aiutare a identificare automaticamente i problemi.


11
+1 per indirizzare il problema sui programmatori, non sulla lingua.
Nicol Bolas,

8
@Nicol: Dire "il problema [è] i programmatori" è ingiustamente riduzionista. Il problema è che per anni (decenni) C ha reso più semplice la scrittura di codice non sicuro rispetto al codice sicuro, in particolare poiché la nostra definizione di "sicuro" si è evoluta più velocemente di qualsiasi standard linguistico e che quel codice è ancora in circolazione. Se vuoi provare a ridurlo a un singolo nome, il problema è "1970-1999 libc", non "i programmatori".

1
È ancora responsabilità dei programmatori utilizzare gli strumenti che hanno ora per risolvere questi problemi. Prendi una mezza giornata o giù di lì e sfoglia il codice sorgente per queste cose.
Nicol Bolas,

1
@Nicol: Sebbene banale rilevare un potenziale overflow del buffer, spesso non è banale essere certi che sia una vera minaccia, e meno banale capire cosa dovrebbe accadere se il buffer dovesse mai essere traboccato. La gestione degli errori è / non è stata spesso presa in considerazione, non è possibile implementare "rapidamente" un miglioramento poiché è possibile modificare il comportamento di un modulo in modi imprevisti. Lo abbiamo appena fatto in una base di codice legacy multi-milioni di righe e, sebbene valga la pena esercitarsi, è costato molto tempo (e denaro).
Mattnz,

4
@NicolBolas: Non sono sicuro del tipo di negozio in cui lavori, ma l'ultimo posto in cui ho scritto C per l'uso in produzione ha richiesto la modifica del documento di progettazione dettagliato, la revisione, la modifica del codice, la modifica del piano di test, la revisione del piano di test, l'esecuzione di un completo test di sistema, revisione dei risultati del test, quindi nuova certificazione del sistema presso il sito del cliente. Questo è per un sistema di telecomunicazione in un altro continente scritto per un'azienda che non esiste più. L'ultima volta che ho saputo, la fonte era in un archivio RCS su un nastro QIC che dovrebbe essere ancora leggibile, se è possibile trovare un'unità nastro adatta.
TMN,

19

Non è proprio inesatto affermare che C sia in realtà "soggetto a errori" in base alla progettazione . A parte alcuni gravi errori come gets, il linguaggio C non può davvero essere in nessun altro modo senza perdere la caratteristica principale che attira le persone in C in primo luogo.

C è stato progettato come linguaggio di sistema per agire come una sorta di "assemblaggio portatile". Una caratteristica importante del linguaggio C è che, a differenza dei linguaggi di livello superiore, il codice C spesso è molto simile al codice macchina effettivo. In altre parole, di ++isolito è solo incun'istruzione e spesso puoi avere un'idea generale di cosa farà il processore in fase di esecuzione guardando il codice C.

Ma l'aggiunta del controllo implicito dei limiti aggiunge un sacco di sovraccarico extra - sovraccarico che il programmatore non ha richiesto e potrebbe non desiderare. Questo sovraccarico va ben oltre l'archiviazione aggiuntiva richiesta per memorizzare la lunghezza di ciascun array o le istruzioni aggiuntive per controllare i limiti dell'array su ogni accesso dell'array. Che dire dell'aritmetica del puntatore? O se hai una funzione che accetta un puntatore? L'ambiente di runtime non ha modo di sapere se quel puntatore rientra nei limiti di un blocco di memoria legittimamente allocato. Per tenerne traccia, avresti bisogno di una seria architettura di runtime in grado di verificare ogni puntatore rispetto a una tabella di blocchi di memoria attualmente allocati, a quel punto stiamo già entrando nel territorio di runtime gestito in stile Java / C #.


12
Onestamente quando le persone chiedono perché C non sia "sicuro", mi chiedo se si lamenterebbero che l'assemblea non è "sicura".
Ben Brocka,

5
Il linguaggio C è molto simile all'assemblaggio portatile su una macchina PDP-11 di Digital Equipment Corporation. Allo stesso tempo, le macchine Burroughs avevano il controllo dei limiti di array nella CPU, quindi erano davvero facili da ottenere direttamente dai programmi. Controlli di array su hardware
resiste

15

Credo che il vero problema non è che questi tipi di insetti sono difficili da risolvere, ma che sono così facili da fare: se si utilizza strcpy, sprintfe amici nel (apparentemente) modo più semplice che il lavoro può, allora probabilmente avete ha aperto la porta per un overflow del buffer. E nessuno lo noterà finché qualcuno non lo sfrutta (a meno che tu non abbia ottime recensioni di codice). Ora aggiungi il fatto che ci sono molti programmatori mediocri e che sono sotto la pressione del tempo per la maggior parte del tempo - e hai una ricetta per il codice che è così pieno di buffer overflow che sarà difficile risolverli tutti semplicemente perché c'è così tanti di loro e si nascondono così bene.


3
Non hai davvero bisogno di "ottime recensioni di codice". Hai solo bisogno di vietare sprintf, o ridefinire # sprintf in qualcosa che usa sizeof () ed errori sulla dimensione di un puntatore, o ecc. Non hai nemmeno bisogno di revisioni del codice, puoi fare questo tipo di cose con SCM commit ganci e grep.

1
@JoeWreschnig: generalmente sizeof(ptr)è 4 o 8. Questa è un'altra limitazione C: non c'è modo di determinare la lunghezza di un array, dato solo il puntatore ad esso.
MSalters,

@MSalters: Sì, un array di int [1] o char [4] o qualunque cosa possa essere un falso positivo, ma in pratica non gestirai mai buffer di quella dimensione con quelle funzioni. (Non sto parlando teoricamente qui - Ho lavorato su una grande base di codice C per quattro anni che ha usato questo approccio. Non ho mai raggiunto il limite dello sprint in un carattere [4].)

5
@ Black Jack: la maggior parte dei programmatori non sono stupidi: se li costringi a passare la dimensione, passeranno quella giusta. È solo che la maggior parte non supererà la dimensione a meno che non sia forzata. È possibile scrivere una macro che restituirà la lunghezza di un array se è statica o di dimensioni automatiche, ma errori se viene fornito un puntatore. Quindi devi # definire sprintf per chiamare snprintf con quella macro che dia le dimensioni. Ora hai una versione di sprintf che funziona solo su array con dimensioni note e forza il programmatore a chiamare snprintf con una dimensione specificata manualmente altrimenti.

1
Un semplice esempio di tale macro sarebbe #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / (sizeof(a) != sizeof(void *))che innescherà un tempo di compilazione dividere per zero. Un altro intelligente che ho visto per la prima volta in Chromium è #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / !(sizeof(a) % sizeof((a)[0]))che scambia la manciata di falsi positivi con alcuni falsi negativi - sfortunatamente è inutile per char []. Puoi usare varie estensioni del compilatore per renderlo ancora più affidabile, ad esempio blogs.msdn.com/b/ce_base/archive/2007/05/08/… .

7

È difficile correggere gli overflow del buffer perché C non fornisce praticamente strumenti utili per risolvere il problema. È un difetto di linguaggio fondamentale che i buffer nativi non forniscono protezione ed è praticamente, se non completamente, impossibile sostituirli con un prodotto superiore, come ha fatto C ++ con std::vectore std::array, ed è difficile persino in modalità debug trovare buffer overflow.


13
"Language flaw" è un'affermazione terribilmente distorta. Che le biblioteche non fornissero il controllo dei limiti era un difetto; che la lingua non è stata una scelta consapevole per evitare spese generali. Tale scelta fa parte di ciò che consente std::vectordi implementare in modo efficiente costrutti di livello superiore . E vector::operator[]fa la stessa scelta per la velocità rispetto alla sicurezza. La sicurezza vectorderiva dal rendere più facile il trasporto delle dimensioni, che è lo stesso approccio adottato dalle moderne librerie C.

1
@Charles: "C non fornisce alcun tipo di buffer a espansione dinamica come parte della libreria standard." No, questo non ha nulla a che fare con questo. Innanzitutto, C li fornisce tramite realloc(C99 consente anche di dimensionare array di stack utilizzando una dimensione determinata dal tempo di esecuzione ma costante tramite qualsiasi variabile automatica, quasi sempre preferibile a char buf[1024]). In secondo luogo, il problema non ha nulla a che fare con l'espansione dei buffer, ha a che fare con il fatto che i buffer abbiano o meno dimensioni con essi e controllino tali dimensioni quando vi si accede.

5
@Joe: Il problema non è tanto che gli array nativi sono rotti. È impossibile sostituirli. Per cominciare, vector::operator[]fa il controllo dei limiti in modalità debug - qualcosa che gli array nativi non possono fare - e in secondo luogo, non c'è modo in C di scambiare il tipo di array nativo con uno che può fare il controllo dei limiti, perché non ci sono modelli e nessun operatore sovraccarico. In C ++, se vuoi passare da T[]a std::array, puoi praticamente scambiare un typedef. In C, non c'è modo di raggiungerlo, e non c'è modo di scrivere una classe con funzionalità equivalenti, per non parlare dell'interfaccia.
DeadMG

3
@Joe: Solo che non può mai essere dimensionato staticamente e non puoi mai renderlo generico. È impossibile scrivere qualsiasi libreria in C che svolga lo stesso ruolo std::vector<T>e lo std::array<T, N>faccia in C ++. Non ci sarebbe modo di progettare e specificare alcuna libreria, nemmeno una Standard, che potesse farlo.
DeadMG

1
Non sono sicuro di cosa intendi per "non può mai essere dimensionato staticamente". Dato che userei quel termine, non std::vectorpuò mai essere dimensionato staticamente. Per quanto riguarda il generico, puoi renderlo generico come C deve essere buono - un piccolo numero di operazioni fondamentali sul vuoto * (aggiungere, rimuovere, ridimensionare) e tutto il resto scritto in modo specifico. Se hai intenzione di lamentarti del fatto che C non ha generici in stile C ++, questo è ben al di fuori dell'ambito della gestione sicura del buffer.

7

Il problema non è con la C la lingua .

IMO, l'unico grande ostacolo da superare è che C è semplicemente insegnato male . Decenni di cattive pratiche e informazioni errate sono stati istituzionalizzati nei manuali di riferimento e negli appunti delle lezioni, avvelenando le menti di ogni nuova generazione di programmatori sin dall'inizio. Agli studenti viene fornita una breve descrizione delle funzioni di I / O "facili" come gets1 o, scanfquindi, lasciate ai propri dispositivi. Non viene detto dove o come tali strumenti possono fallire o come prevenirli. Non gli viene detto di usare fgetsestrtol/strtodperché quelli sono considerati strumenti "avanzati". Quindi vengono scatenati nel mondo professionale per provocare il loro caos. Non che molti dei programmatori più esperti conoscano meglio, perché hanno ricevuto la stessa educazione cerebrale. È esasperante. Vedo così tante domande qui e su Stack Overflow e su altri siti in cui è chiaro che la persona che pone la domanda viene insegnata da qualcuno che semplicemente non sa di cosa sta parlando , e ovviamente non puoi semplicemente dire "il tuo professore ha torto", perché è un professore e tu sei solo un ragazzo su Internet.

E poi hai la folla che disdegna qualsiasi risposta a cominciare, "beh, secondo lo standard linguistico ..." perché stanno lavorando nel mondo reale e secondo loro lo standard non si applica al mondo reale . Posso avere a che fare con qualcuno che ha una cattiva educazione, ma chiunque insista a essere ignorante è solo una rovina per l'industria.

Non ci sarebbero problemi di buffer overflow se la lingua fosse insegnata correttamente con un'enfasi sulla scrittura di codice sicuro. Non è "difficile", non è "avanzato", sta solo facendo attenzione.

Sì, questo è stato un rant.


1 Che, per fortuna, è stato finalmente strappato dalle specifiche del linguaggio, anche se rimarrà per sempre in 40 anni di codice legacy.


1
Anche se per lo più sono d'accordo con te, penso che tu sia ancora un po 'ingiusto. Ciò che consideriamo "sicuro" è anche una funzione del tempo (e vedo che sei stato uno sviluppatore di software professionale molto più a lungo di me, quindi sono sicuro che ne hai familiarità). Tra dieci anni qualcuno avrà questa stessa conversazione sul perché tutti nel 2012 hanno usato le implementazioni della tabella hash in grado di DoS, non sapevamo nulla della sicurezza? Se c'è un problema nell'insegnamento, è un problema che ci concentriamo troppo sull'insegnamento della "migliore" pratica, e non che la stessa migliore pratica si evolva.

1
E siamo onesti. Si potrebbe scrivere codice sicuro con solo sprintf, ma questo non significa che la lingua non era difettoso. C era difettoso ed è difettoso - come qualsiasi lingua - ed è importante ammettere quei difetti in modo da poter continuare a risolverli.

@JoeWreschnig - Anche se sono d'accordo con il punto più ampio, penso che ci sia una differenza qualitativa tra le implementazioni della tabella hash in grado di DoS e sovraccarichi del buffer. Il primo può essere attribuito alle circostanze che si evolvono intorno a te, ma il secondo non ha scuse; sovraccarichi del buffer sono errori di codifica, punto. Sì, C non ha protezioni per le lame e ti taglierà se sei negligente; possiamo discutere se questo è un difetto nella lingua o no. Questo è ortogonale al fatto che pochissimi studenti sono dati qualsiasi istruzioni di sicurezza quando stanno imparando la lingua.
John Bode,

5

Il problema è tanto quello della miopia gestionale che quello dell'incompetenza del programmatore. Ricordate, un'applicazione 90.000-line ha bisogno solo di un'operazione insicuro per essere completamente insicura. È quasi al di là delle possibilità che qualsiasi applicazione scritta sulla gestione delle stringhe fondamentalmente insicure sia perfetta al 100%, il che significa che sarà insicura.

Il problema è che i costi di insicurezza non sono addebitati al destinatario giusto (la società che vende l'app non dovrà quasi mai rimborsare il prezzo di acquisto) o non sono chiaramente visibili al momento in cui vengono prese le decisioni ("Dobbiamo spedire a marzo non importa cosa! "). Sono abbastanza certo che se considerassi i costi e i costi a lungo termine per i tuoi utenti piuttosto che per i profitti della tua azienda, scrivere in C o in lingue correlate sarebbe molto più costoso, probabilmente così costoso che è chiaramente la scelta sbagliata in molti campi in cui oggigiorno la saggezza convenzionale dice che è una necessità. Ma ciò non cambierà a meno che non venga introdotta una responsabilità software molto più rigorosa, che nessuno nel settore desidera.


-1: incolpare la gestione come radice di tutti i mali non è particolarmente costruttiva. Ignorando la storia un po 'meno. La risposta è quasi redenta dall'ultima frase.
Mattnz,

La responsabilità del software più severa potrebbe essere introdotta dagli utenti interessati alla sicurezza e disposti a pagare per questo. Probabilmente, potrebbe essere introdotto con severe sanzioni per le violazioni della sicurezza. Una soluzione basata sul mercato funzionerebbe se gli utenti fossero disposti a pagare per la sicurezza, ma non lo sono.
David Thornley,

4

Uno dei grandi poteri dell'uso di C è che ti consente di manipolare la memoria come preferisci.

Uno dei maggiori punti deboli dell'uso di C è che ti consente di manipolare la memoria come preferisci.

Esistono versioni sicure di tutte le funzioni non sicure. Tuttavia, i programmatori e il compilatore non applicano rigorosamente il loro uso.


2

perché i creatori di C non risolvono questi problemi reimplementando le librerie?

Probabilmente perché C ++ lo ha già fatto ed è retrocompatibile con il codice C. Quindi, se vuoi un tipo di stringa sicuro nel tuo codice C, devi solo usare std :: string e scrivere il tuo codice C usando un compilatore C ++.

Il sottosistema di memoria sottostante può aiutare a prevenire gli overflow del buffer introducendo blocchi di guardia e controllandone la validità - quindi a tutte le allocazioni sono aggiunti 4 byte di "fefefefe", quando questi blocchi vengono scritti, il sistema può lanciare un wobbler. Non è garantito per impedire una scrittura della memoria, ma mostrerà che qualcosa è andato storto e deve essere riparato.

Penso che il problema sia che le vecchie routine strcpy etc sono ancora presenti. Se fossero stati rimossi a favore di strncpy ecc., Sarebbe stato d'aiuto.


1
La rimozione completa di strcpy ecc. Renderebbe ancora più difficili i percorsi di aggiornamento incrementale, il che a sua volta comporterebbe il mancato aggiornamento del personale. Adesso puoi passare a un compilatore C11, quindi iniziare a utilizzare le varianti di _s, quindi vietare le varianti di non-s, quindi correggere l'utilizzo esistente, in qualsiasi periodo di tempo praticamente praticabile.

-2

È semplice capire perché il problema di overflow non è stato risolto. C era difettoso in un paio di aree. All'epoca quei difetti erano visti come tollerabili o addirittura come una caratteristica. Ora, decenni dopo, quei difetti non sono risolvibili.

Alcune parti della comunità di programmatori non vogliono tappare quei buchi. Guarda tutte le guerre di fiamma che ricominciano da stringhe, array, puntatori, raccolta dei rifiuti ...


5
LOL, risposta terribile e sbagliata.
Heath Hunnicutt,

1
Per spiegare perché questa è una cattiva risposta: C ha davvero molti difetti, ma consentire buffer overflow ecc. Ha ben poco a che fare con loro, ma con i requisiti linguistici di base. Non sarebbe possibile progettare un linguaggio per fare il lavoro di C e non consentire overflow del buffer. Parti della comunità non vogliono rinunciare alle capacità che C consente loro, spesso con buone ragioni. Ci sono anche divergenze su come evitare alcuni di questi problemi, dimostrando che non abbiamo una completa comprensione della progettazione del linguaggio di programmazione, niente di più.
David Thornley,

1
@DavidThornley: si potrebbe progettare un linguaggio per fare il lavoro di C ma farlo in modo che i normali modi idiomatici di fare le cose consentirebbero almeno a un compilatore di controllare gli overflow del buffer in modo ragionevolmente efficiente, nel caso in cui il compilatore scelga di farlo. C'è un'enorme differenza tra la memcpy()disponibilità e il fatto che siano solo mezzi standard per copiare in modo efficiente un segmento di array.
supercat,
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.