Shellshock, quando il codice è sotto nafta
Un paio di settimane fa eravamo nel pieno del bubbone #shellshock, tutti ad applicare patch, tutti con il fiato sul collo del proprio capo da una parte ed un occhio ai bollettini CVE che sembravano non finire mai dall’altra.
Adesso la calma è tornata su Internet, complice anche la quarta ondata di leak di celebrità nude LINK, torniamo ad uno stato di presunta sicurezza. Siamo tutti al sicuro fino al prossimo break-in.
Oggi non parliamo dell’exploit, ma parliamo della causa. Perché #shellshock? Perché la #bash si è rivelata così vulnerabile? Robert Graham ha scritto questo bellissimo post che lo spiega nel dettaglio. Io ve lo racconto in italiano cercando, se riesco, di dare qualche spunto in più. Sappiate che tutti i #kudos vanno a lui.
Il problema, in breve
Per chi si fosse perso anche il post dove automatizzo il test e in generale si fosse perso tutte le ultime due settimane di racconti isterici di script CGI in frantumi, la farò veramente breve.
Bash non sanitizza il codice quando si dichiara una variabile d’ambiente. E’ possibile quindi dichiarare una variabile d’ambiente che contenga una funzione vuota ed è possibile accodare un comando arbitrario dopo il separatore dei comandi, il ;. La shell prende quella dichiarazione, e una volta raggiunto il separatore lo onora, termina la variabile ed esegue il comando arbitrario per noi.
Ricordiamoci tutti gli script CGI che ad esempio usano alcuni campi della richiesta http, questi sono candidati ideali per provare a vedere se il sysadmin ha aggiornato tutti i pacchetti o meno.
Entrino gli imputati: le variabili d’ambiente
In questa analisi, sto prendendo in considerazione il codice sorgente nel file bash-4.3.tar.gz con il seguente checksum.
Come abbiamo detto il problema è localizzato durante l’inizializzazione delle variabili d’ambiente. Le variabili d’ambiente sono inizializzate nel file shell.c, primo imputato in questo processo.
Alla linea 1738, abbiamo la chiamata che, secondo mio gusto personale, sarebbe stata più elegante senza quell’ifdef.
Visto che a seconda della definizione della costante RESTRICTED_SHELL cambia solo un flag passato in or mentre si costruisce il secondo parametro della chiamata, senza che cambi il prototipo della funzione stessa, sarebbe stato molto più elegande valorizzare il parametro restricted a 1 o 0 rispettivamente.
Veniamo poi al file variables.c, riga 318 dove è dichiarata la funzione incriminata.
Ci sono la bellezza di 24 variabili globali in questo file. Al mio professore di programmazione alla Statale, probabilmente non sarebbe mai venuta voglia di firmarmi il libretto con un codice del genere.
24 variabili globali in un file mi dicono che quel codice è un PITA da manutenere e probabilmente a livello di architettura e disegno sono state seguite strade diverse, in momenti storici diversi e da team di sviluppo diversi e che per passare informazioni utili alle routine in questo file si è dovuto ricorrere a 24 variabili condivise per non rompere tutto. Dal mio punto di vista il commento è questo codice è attaccato con lo sputo, funziona ma non sappiamo come.
Dalla riga 329 compresa è la fiera dell’aritmentica dei puntatori.
Partiamo con un ciclo for dove incremento l’indice nel punto stesso dove valuto la condizione per rimanere nel for. Se l’indice fosse cambiato da qualche altra parte nel codice, avrei avuto un brutto side effect e tanti mal di testa.
L’indice viene comunque incrementato dopo il test, quindi il fatto di scrivere il for così o scriverlo nella forma più formalmente corretta, non ha impatti.
Questo codice, serve data una variabile d’ambiente a cercare il carattere di ‘=’. Assegno alla variabile c, un indirizzo di memoria che varia ad ogni iterazione, fino a quando non arrivo al ‘=’.
Ma un’occhiata alla man page di strtok, non gliela vogliamo dare? Sul mio mac, si parla di un’implementazione basata su FreeBSD 3.0. Di sicuro fare un refactor per portare questo ciclo negli anni 2000 poteva essere una buona idea visto che i mezzi ora ci sono.
La vulnerabilità però è a linea 362. La routeine parse_and_execute, definita nel file evalstring.c alla riga 189, non fa altro che eseguire il comando specificato nel primo parametro, la variabile temp_string nel nostro caso.
Il problema è che temp_string è costruita con delle strcpy che sappiamo essere insicure by design, in quanto non controllano né la dimensione del buffer di destinazione prima di eseguire la copia (il 90% dei buffer overflow li vai a trovare con grep su strcpy & similia), né eventuali caratteri speciali, come il ‘;’ che separa due comandi shell.
Quando temp_string è passata alla parse_and_execute, il valore del comando da eseguire non è stato validato ed è sotto controllo dell’attaccante che si può sbizzarrire ad lanciare comandi arbitrari grazie al fatto che nessuno ha validato il suo input prima di passarlo ad una routine che lancia una shell.
Off by one
La mancata validazione dell’input è la causa di praticamente il 99% della magagne con cui combattiamo ogni giorno, dal cross site scripting, al buffer overflow. I punti dove il nostro codice legge input dall’esterno sono i punti dove un attaccante può interagire, ne segue che devono essere i punti più protetti, o quantomeno, i punti dove metto più attenzione. Io non posso permettermi, in nome di non so quale ottimizzazione, di non validare un input di un utente prima di memorizzarlo, soprattutto come in questo caso se devo passarlo in esecuzione ad una shell.
Un’espressione regolare, una strtok o una strchr alla ricerca di ‘;’ avrebbero mitigato l’attacco con successo con una penalizzazione in termini di tempo di esecuzione del tutto trascurabile.
Pensaci la prossima volta che leggi input dall’esterno. Vuoi che la prossima applicazione alla ribalta sia la tua?
Enjoy!
Vuoi aiutarmi a portare avanti il progetto Codice Insicuro con una donazione? Fantastico, allora non ti basta che premere il pulsante qui sotto.
Supporta il progetto