Implicazioni sulla sicurezza di trascurare il byte extra per la terminazione NULL in array C / C ++

20

Considerare : l'inglese è la mia seconda lingua.

Nel podcast Security Now! episodio 518 ( HORNET: A Fix for TOR? ), al 27:51 Steve Gibson cita un esempio di codice vulnerabile in C / C ++:

"[...] one of them [problems with vulnerable code] is creating a new array of a certain size [...]. And the fix is 'of a certain size + 1'. So, [...] it [the vulnerable code] was just one byte too short. Probably a NULL terminator, so that when you fill the array with size objects, you would have one extra byte of NULL that would guarantee NULL termination, and that would prevent that string from being overrun. But that's not what the coder did: they'd forgotten the '+ 1' [...]"

Capisco cosa intende dire: quando si crea un array, è necessario consentire un byte aggiuntivo per il byte di terminazione NULL. Quello che vorrei ottenere con questo post è ottenere un puntatore per ulteriori ricerche sull'impatto di avere un array il cui ultimo byte non è il byte terminator; Non capisco tutte le implicazioni di tale negligenza e come ciò possa portare a un exploit. Quando dice che ha la terminazione NULL

"would prevent that string from being overrun",

la mia domanda è "come viene superato nei casi in cui il carattere di terminazione NULL viene trascurato?".

Capisco che questo è un argomento enorme e quindi non imporre alla comunità una risposta troppo completa. Ma se qualcuno fosse abbastanza gentile da fornire alcuni suggerimenti per ulteriori letture, sarei molto riconoscente e felice di andare a fare la ricerca da solo.

    
posta RoraΖ 29.07.2015 - 12:54
fonte

4 risposte

21

Vulnerabilità legata alla terminazione stringa

Dopo aver riflettuto su questo aspetto, l'utilizzo di strncpy() è probabilmente il modo più comune (che posso immaginare) che possa creare errori di terminazione nulli. Poiché generalmente le persone pensano alla lunghezza del buffer come non compreso strncpy(a, "0123456789abcdef", sizeof(a)); . Quindi vedrai qualcosa di simile al seguente:

a

Supponendo che char a[16] sia inizializzato con a la stringa strlen(a) non sarà terminata con null. Quindi, perché questo è un problema? Bene, in memoria ora hai qualcosa come:

30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66 
e0 f3 3f 5a 9f 1c ff 94 49 8a 9e f5 3a 5b 64 8e

Senza un terminatore null le funzioni di stringa standard non conoscono la lunghezza del buffer. Ad esempio, 0x00 continuerà a contare finché non raggiunge un string.h byte. Quando è questo, chi lo sa? Ma ogni volta che lo trova restituirà una lunghezza molto più grande del buffer; diciamo 78. Vediamo un esempio:

int main(int argc, char **argv) {
    char a[16];

    strncpy(a, "0123456789abcdef", sizeof(a));

    ... lots of code passes, functions are called...
    ... we finally come back to array a ...

    do_something_with_a(a);
}

void do_something_with_a(char *a) {
    int a_len = 0;
    char new_array[16];

    // Don't know what the length of the 'a' string is, but it's a string so lets use strlen()!
    a_len = strlen(a);

    // Gonna munge the 'a' string, so lets copy it first into new_array
    strncpy(new_array, a, a_len);
}

Hai appena scritto 78 byte su una variabile alla quale sono assegnati solo 16 byte.

Buffer Overflow

Si verifica un overflow del buffer quando vengono scritti più dati su un buffer rispetto a quelli allocati per quel buffer. Non è diverso per una stringa, tranne per il fatto che molte delle funzioni a_len fanno affidamento su questo byte null per segnalare la fine di una stringa. Come abbiamo visto sopra.

Nell'esempio abbiamo scritto 78 byte in un buffer che è solo assegnato per 16. Non solo, ma è una variabile locale. Il che significa che il buffer è stato allocato nello stack. Ora, con gli ultimi 66 byte scritti, hanno appena sovrascritto 66 byte dello stack.

Se scrivi abbastanza dati oltre la fine di quel buffer, sovrascrivi l'altra variabile locale do_something_with_a() (anche se non va bene se la usi in seguito), qualsiasi puntatore a frame stack che è stato salvato nello stack e poi indirizzo di ritorno della funzione. Ora sei davvero andato e hai rovinato tutto. Perché ora l'indirizzo di ritorno è qualcosa di completamente sbagliato. Quando viene raggiunta la fine di %code% , accadono cose brutte.

Ora possiamo aggiungere un ulteriore all'esempio sopra.

void do_something_with_a(char *a, char *new_a) {
    int a_len = 0;
    char new_array[16];

    // Don't know what the length of the 'a' string is, but it's a string so
    // lets use strlen()!
    a_len = strlen(a);

    // 
    // By the way, copying anything based on a length that's not what you
    // initialized the array with is horrible horrible coding.  But it's
    // just an example.
    //
    // Gonna munge the 'a' string, so lets copy it first into new_array
    strncpy(new_array, a, a_len);

    // 'a_len' was on the stack, that we just blew away by writing 66 extra 
    // bytes to the 'new_array' buffer.  So now the first 4 bytes after 16
    // has now been written into a_len.  This can still be interpreted as
    // a signed int.  So if you use the example memory, a_len is now 0xe0f33f5a
    //
    // ... did some more munging ...
    //
    // Now I want to return the new munged string in the *new_a variable
    strncpy(new_a, new_array, a_len);

    // Everything burns

}

Penso che i miei commenti spieghino praticamente tutto. Ma alla fine hai scritto una quantità enorme di dati in un array molto probabilmente pensando che stai scrivendo solo 16 byte. A seconda di come si manifesta questa vulnerabilità, ciò potrebbe portare allo sfruttamento tramite l'esecuzione di codice in modalità remota.

Questo è un esempio molto ingegnoso di scarsa codifica, ma puoi vedere come le cose possono aumentare rapidamente se non stai attento quando lavori con la memoria e copia i dati. Il più delle volte la vulnerabilità non è così ovvia. Con programmi di grandi dimensioni hai così tanto da fare che la vulnerabilità potrebbe non essere facile da individuare, e potrebbe essere innescata dal codice di chiamate a più funzioni.

Per ulteriori informazioni su come funzionano gli overflow del buffer .

E prima che qualcuno lo menzioni, ho ignorato l'endianess quando ho fatto riferimento alla memoria per motivi di semplicità

Ulteriori letture

Descrizione completa della vulnerabilità
Voce di enucleazione comune (CWE)
Presentazione delle stringhe di codifica protetta (download PDF automaticamente)
Università di Pittsburgh - Secure Coding C / C ++: Vulnerabilità delle stringhe (PDF)

    
risposta data 29.07.2015 - 14:04
fonte
2

Sono a rischio di essere ridondante aggiungendo un'altra risposta, ma penso che le risposte esistenti potrebbero non rispondere pienamente a ciò che stai chiedendo. In una vulnerabilità di buffer overflow tradizionale (in particolare della varietà basata su stack), si tenta di sovrascrivere il puntatore del frame sullo stack per fare in modo che l'esecuzione passi al codice di exploit quando la funzione corrente tenta di tornare.

Ovviamente non funzionerà se l'unica cosa che (l'attaccante) può far scrivere al programma oltre la fine del buffer è un byte zero. Potenzialmente, puoi causare il crash del programma in questo modo facendo in modo che provi a saltare a un indirizzo non valido, ma è solo un DoS e non l'esecuzione di codice remoto.

Tuttavia, si consideri che si ottiene che il programma scriva una stringa di lunghezza 16 in un buffer da 16 byte che chiameremo "A", in modo tale che il byte null superi. Quindi il programma sovrascrive quel byte null con qualcosa che non è \ 0, quindi la stringa A non viene terminata con null. Se poi il programma ti invierà il contenuto di A, leggerà oltre la fine di A, dando potenzialmente accesso a tutti i tipi di informazioni segrete. Heartbleed ha utilizzato questo tipo di divulgazione di informazioni per rubare le chiavi private, il che è piuttosto serio.

A questo punto la stringa A è effettivamente più lunga del programmatore previsto. Non è troppo difficile immaginare che il programmatore si basi su A come una stringa da 16 byte e copiandolo altrove, superando potenzialmente altri buffer di molto più di un byte. Questo potrebbe quindi essere utilizzato per eseguire codice arbitrario.

    
risposta data 06.08.2015 - 16:33
fonte
1

Come indicato nella risposta, i buffer overflow sono una probabile vulnerabilità se un programmatore non termina una stringa di caratteri con un byte NULL. La ragione è che la maggior parte delle funzioni di stringa lo assumono e continuerà fino a quando non incontrano uno zero. Se sei fortunato, l'errore è già abbastanza grave da ottenere un errore di errore di segmentazione nelle prime fasi dello sviluppo, in modo da poter eseguire il debug e correggere il problema. Tuttavia, con molti bug un fallimento questo ovvio si verificherà solo in condizioni speciali. Spesso un utente malintenzionato può sfruttare il modo in cui il programma si comporta e, a seconda dei particolari della vulnerabilità, sfruttarlo per leggere i contenuti della memoria che dovrebbero essere nascosti o copiare i dati dall'input dell'utente in aree di memoria che l'utente non intendeva per controllare, ecc. Se il buffer si trova nello stack, quest'ultimo exploit può essere utilizzato per iniettare il codice e per sovrascrivere l'indirizzo di ritorno memorizzato nello stack frame situato in un indirizzo superiore (su x86) nello stack. Alcuni sistemi operativi hanno protezioni, come segmenti di memoria non eseguibili, ma questo è sicuramente qualcosa su cui il programmatore non vuole fare affidamento:)

I programmatori che sono nuovi alla programmazione in linguaggi come C possono trovare le pratiche per evitare questi problemi difficili o soggetti a errori, ma alla fine diventa una seconda natura, sebbene sia ancora possibile fare un errore. Praticando il più possibile, ho programmato in C per circa 7 anni, e ho ancora bisogno di correggere un errore di volta in volta.

Un buon modo per esercitarsi è allocare un array di caratteri, memset dell'intero array con un carattere ASCII non stampabile diverso da 0, 1 va bene. Usa la tua funzione di libreria standard di scelta per copiare alcune stringhe nell'array, ovviamente se il programma si arresta in modo errato. Altrimenti, basta usare un ciclo di base per iterare su ogni elemento dell'array e stampare il valore numerico, verificare che lo 0 sia dove dovrebbe essere, se usi printf, si fermerà in quel punto della stringa. Trovo che questo sia un buon modo per sperimentare per trovare le differenze tra le funzioni, ad es. strcat, strncat, strcpy, strlcpy, strlcpy, strlcat, sprint, ecc. Raccomanderei di usare strlcpy e strlcat su strncpy e strncat per la maggior parte delle cose, sono molto più facili e meno inclini agli errori.

Un altro consiglio, se sembra difficile convalidare il tuo algoritmo nella tua testa, immagina che stai facendo la stessa cosa con un input estremamente piccolo. Per le stringhe, immagina di eseguire l'operazione su una stringa con solo spazio per 1 carattere più il byte NULL. Questo rende facile vedere molte proprietà di stringhe di caratteri che altrimenti richiedono più lavoro sul cervello. Ad esempio, è necessario allocare un array con 2 elementi, anche se è necessario memorizzare un singolo carattere. Il secondo elemento, str [1], ovviamente deve essere 0. Strlen riferirà che la lunghezza della stringa è 1. Ora puoi tranquillamente generalizzare per sapere che strlen (str) è sempre l'indice del byte NULL (assumendolo è terminato NULL ovviamente :), allo stesso modo strlen (str) - 1 dove > 0 è sempre l'indice dell'ultimo carattere nella stringa. La quantità di memoria necessaria per la stringa e il byte NULL è sempre strlen (str) + 1.

Un'ultima cosa. È importante notare che le stringhe terminate con NULL sono solo una convenzione. Ci sono state e sono molte possibili alternative. È richiesto solo se si utilizzano funzioni che presuppongono che il byte NULL indichi il punto in memoria in cui dovrebbe smettere di fare cose. Questo è il caso delle funzioni stringa in libc. È possibile scrivere le proprie funzioni di stringa che memorizzano la lunghezza anteposta all'inizio della stringa. Al costo di una certa complessità aggiuntiva introdotta dal tipo puning richiesto per stringhe con lunghezze superiori a 255 caratteri e mantenendo questo numero ogni volta che la stringa viene aggiornata, questo approccio ha il vantaggio di trovare invece la lunghezza della stringa in O (1) tempo di O (n). È inoltre possibile memorizzare un puntatore della stringa e il valore della lunghezza in una struttura, anche se questo non può essere generalizzato in modo chiaro alle stringhe che non si trovano nell'heap. La maggior parte dei programmatori probabilmente ti dirà semplicemente che dovresti rimanere con le rappresentazioni di stringa standard per la maggior parte delle cose, e probabilmente hanno ragione. Ma se è il tuo codice, chi dovrebbe dirti cosa fare, è la tua macchina di calcolo universale (almeno un'approssimazione finita), esplora il panorama del calcolo e rendi la tua sandbox e divertiti!

    
risposta data 29.07.2015 - 19:50
fonte
1

Questo sta fluttuando nelle risposte sopra, ma credo che dovrebbe essere esplicitato. La gestione dell'array di caratteri di C / C ++ ha un numero di potenziali rischi off-by-one ... Esempi:

""  // a zero length string requiring one byte of storage
    // in memory:  00

"Hi."  // a length 3 string requiring four bytes of storage
       // in memory:  48 69 2e 00
"Hi."[3]  // is the 00, the characters in a string and a string array are indexed starting at 0, to wit
"Hi."[0]  // is the 'H'.

char foo[3]  // a length three character array requiring three bytes of storage
     bar[4]  // a length four character array requiring four bytes of storage

strncpy(foo, "Hi.", 3)  // copies three characters from a length three string to a length three character array.  
                        // The result is not a string because the null is not copied.

strcpy(foo, "Hi.")  // copies four characters from a length three string to a length three character array
                    // This causes overrun of the array.
                    // It writes 00 on whatever (if anything) is allocated next in storage.

strcpy(bar, "Hi.")  // copies four characters from a length three string to a length four character array.
                    // This works/is safe (enough).

Quindi

  • una lunghezza di tre stringhe contiene quattro caratteri.
  • una lunghezza di tre stringhe non rientra in una lunghezza di tre caratteri array
  • copiare tre caratteri da una lunghezza di tre stringhe non copia la stringa
  • se mystring è una stringa di lunghezza n , mystring [ n ] è la terminazione di 00. Di conseguenza, si può ragionare brevemente (o non affatto) che copiando fino a il carattere n -th copierà 00.

Oppure, per riassumere, questo è progettato al massimo per causare errori off-by-one.

    
risposta data 30.07.2015 - 01:25
fonte

Leggi altre domande sui tag