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.

Photo by Karunakar Rayker

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.

$ sha1sum bash-4.3.tar.gz
45ac3c5727e7262334f4dfadecdf601b39434e84  bash-4.3.tar.gz

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.

$ sha1sum shell.c
6073520406d0a091ffe3c5d9c319b1306fabc5b9  shell.c

Alla linea 1738, abbiamo la chiamata che, secondo mio gusto personale, sarebbe stata più elegante senza quell’ifdef.

#if defined (RESTRICTED_SHELL)
  initialize_shell_variables (shell_environment, privileged_mode||restricted||running_setuid);
#else
  initialize_shell_variables (shell_environment, privileged_mode||running_setuid);
#endif

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.

$ sha1sum variables.c
bafda97b905da97c2a01ee296bc1522cb6eba8a8  variables.c

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.

extern char **environ;

/* Variables used here and defined in other files. */
extern int posixly_correct;
extern int line_number, line_number_base;
extern int subshell_environment, indirection_level, subshell_level;
extern int build_version, patch_level;
extern int expanding_redir;
extern int last_command_exit_value;
extern char *dist_version, *release_status;
extern char *shell_name;
extern char *primary_prompt, *secondary_prompt;
extern char *current_host_name;
extern sh_builtin_func_t *this_shell_builtin;
extern SHELL_VAR *this_shell_function;
extern char *the_printed_command_except_trap;
extern char *this_command_name;
extern char *command_execution_string;
extern time_t shell_start_time;
extern int assigning_in_environment;
extern int executing_builtin;
extern int funcnest_max;

#if defined (READLINE)
extern int no_line_editing;
extern int perform_hostname_completion;
#endif

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.

for (string_index = 0; string = env[string_index++]; )

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.

for (string_index = 0; string = env[string_index]; string_index++)

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 ‘=’.

while ((c = *string++) && c != '=')
      ;

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.

$ man strtok

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.

strcpy (temp_string, name);
temp_string[char_index] = ' ';
strcpy (temp_string + char_index + 1, string);

if (posixly_correct == 0 || legal_identifier (name))
  parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST);

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!

comments powered by Disqus