Questo argomento è stato trattato in secureProgramming


Spiegazione della vulnerabilità

#include <string.h>
 
char *duplicate_string(char *s){
  int len = strlen(s);
  char *p = (char *) malloc (len + 1);
  if(p != NULL)
    strcpy(p, s);
  return p;
}

Il codice mostrato presenta una grave imperfezione che può condurre ad un buffer overflow. In particolare, vi è una dimenticanza per quel che riguarda la funzione strlen(): questa restituisce il tipo size_t, che varia in base all’architettura (su una CPU a 64 bit, size_t è un unsigned int a 64 bit). Salvare il risultato di strlen() in una variabile di tipo int, che può non essere a 64 bit (solitamente è a 32 bit anche se la piattaforma di esecuzione è a 64 bit; viene garantito solo che sia almeno un intero con segno a 16 bit): questa differenza causa il troncamento dell’output quando si supera il valore massimo rappresentabile dal tipo int: provando a salvare il valore , quello che verrà salvato è solamente e si proverà a scrivere una stringa di caratteri in un buffer per solo 10 caratteri, causando un (heap) buffer overflow, come anticipato.

Possibili conseguenze causate da questa vulnerabilità includono:

  • crash del programma (Denial of Service);
  • allocazione di un buffer sottodimensionato (underallocation of a buffer) e quindi heap buffer overrun;
  • controllo errato dell’indice più grande di un buffer, causando stack buffer overflow;
  • alterazione della logica del programma tale da passare controlli di validazione con dati non validi e permettendo altri attacchi.

Solo operazioni aritmetiche?

La vulnerabilità può presentarsi anche quando non sono coinvolte operazioni aritmetiche: è sufficiente che un valore di un certo tipo sia oggetto di cast verso un tipo diverso, con un intervallo di rappresentabilità minore (o che tale valore sia associato ad una variabile con quel dato tipo).

Linguaggi affetti dalla vulnerabilità

I linguaggi più affetti da questa vulnerabilità sono C e i suoi derivati (C++ e Objective-C) a causa di alcuni fattori:

  • l’overflow in operazioni intere non è rilevato, ma processato in maniera silenziosa con un wrap around;
  • la debolezza può essere sfruttata causando un buffer overrun;
  • il linguaggio fa cast implicito e silenzioso di un tipo intero più grande verso uno più piccolo (64 bit 32 bit);
  • mischiare interi con e senza segno può causare effetti non desiderati in maniera silenziosa.

Anche altri linguaggi non effettuano un controllo per l’overflow di interi, come Java. Altri linguaggi, invece, includono meccanismi che rendono non possibile questo errore:

  • Python non limita la rappresentazione degli interi a un numero fisso di bit;
  • Ada consente allo sviluppatore di dichiarare un intervallo di valori accettabili per ogni tipo intero e il compilatore inserisce dei controlli a run-time per rilevare overflow sul risultato delle operazioni.

Altri esempi della debolezza

TInfo *alloc_buffer(int n) {
  int size=sizeof(TInfo)*n; // THIS CAN OVERFLOW!
  return (TInfo *)malloc(size);
}

Con l’attaccante che può controllare il valore di n, può fare in modo che il programma allochi un buffer più piccolo del necessario.


int check_string_characters(char *s) {
int i, n=strlen(s); // IMPLICIT CAST!
for(i=0; i<n; i++) // // not executed if n<=0
  if (!check_character(s[i]))
    return 0;
  return 1;
}

Con l’attaccante che può controllare la stringa s, se questa è da caratteri, n sarà 0 e il controllo non sarà effettuato.


int check_size(int requested_size) {
  return (requested_size<=MAX_BUFFER_SIZE);
} // Always true if requested_size<0 !
 
void process_inputs() {
  unsigned int req_size=from_outside();
  if (check_size(req_size)) // IMPLICIT CAST!
     happily_fill_the_buffer(req_size);

Se gli interi sono a 32 bit e req_size vale , req_sizeè convertito in un valore negativo dal cast da unisgned a signed e può bypassare il controllo.

Rilevare la debolezza

Per rilevare la debolezza, è necessario controllare le espressioni usate per calcolare la dimensione di un buffer o i limiti di un array, specialmente se l’espressione proviene dall’esterno.

Inoltre è opportuno abilitare e controllare tutti i warning del compilatore sui cast da un tipo più grande a uno più piccolo, inclusi cast impliciti quando si passano o si ricevono valori con le funzioni.

È da considerarsi sospetto anche codice dove un oggetto non è espresso usando il tipo size_t.

Rimuovere la debolezza

Allocazione di un buffer con controllo dell’overflow

Viene riportato un esempio di allocazione di un buffer con controllo dell’overflow. Supponendo di avere:

int buffer_size = header_size + n * element_size

con n che può essere controllato dall’esterno e tutte le variabili sono int.

Per controllare che non si vada in overflow non è sufficiente svolgere:

if (buffer_size <= INT_MAX)

in quanto la moltiplicazione potrebbe causare overflow e quindi rendere sempre vera la condizione del controllo.

È necessario, invece, fare:

if (n <= (INT_MAX - header_size) / element_size)

dove INT_MAX rappresenta il valore massimo rappresentabile. Qui si sottrae prima il valore dell’intestazione e la divisione restituisce quanti valori è possibile inserire nello spazio residuo del buffer. Se n è più piccolo o uguale al risultato, allora è possibile inserire nuovi elementi. Se n è più grande, serve più spazio del disponibile e dunque si andrebbe in overflow.

Utilizzo di size_t e dimensione degli elementi

Ogni volta che è necessario controllare la dimensione di un oggetto, è necessario utilizzare il tipo size_t per salvare il valore. Sempre.

Un esempio:

Item *alloc_vector(int n) {
   if (n<=0 || n>MAX_N)
      return NULL;
    int item_size=sizeof(Item);
    size_t size=n*item_size;
      // Is this correct? No!
    return (Item *)malloc(size);
}

Questo codice è errato poiché, con un n sufficientemente grande, è possibile fare in modo che si verifichi un overflow nella moltiplicazione. La misura adeguata da prendere è la seguente:

Item *alloc_vector(int n) {
  if (n<=0 || n>MAX_N)
    return NULL;
  size_t size=n*sizeof(Item);
  // Works correctly even if
  // MAX_N*sizeof(Item)>=INT_MAX
  return (Item *)malloc(size);
}

cioè quella di usare il tipo size_t (implicitamente quando si usa sizeof()) per fare in modo che la moltiplicazione sia svolta nel dominio del size_t.

Cast espliciti e controllo prima di un downcast

Il seguente codice è errato: avviene un cast implicito, con potenziale wraparound, rendendo il controllo inutile:

long compute_something(char *a_string);
 
void do_some_stuff() {
  ...
  int n=compute_something(some_input_data);
  // Bad! Implicit downcast can wrap around!
  if (n<INT_MIN || n>INT_MAX)
    // Bad! Useless check after downcast
    print_error_message();
  else {
  /* Use the value of n... */
  }
}

La soluzione è quella di evitare cast implicito ed effettuare un downcast solo dopo aver effettuato il controllo:

long compute_something(char *a_string);
 
void do_some_stuff() {
  ...
  long res=compute_something(some_input_data);
  // No downcast here!
  if (res<INT_MIN || res>INT_MAX)
    // Check value before the downcast
    print_error_message();
  else {
    int n=(int)res; // Now the downcast is ok!
    /* Use the value of n... */
  }
}

Misure extra

Alcuni compilatori includono opzioni per genere delle “traps” su overflow degli interi; ad esempio, gcc ha l’opzione -ftrapv 1.

Alcuni SAST includono una limitata abilità nel rilevare possibili overflow di interi.

Sarebbe opportuno implementare l’aritmetica tra interi in un modo sicuro, usando librerie come SafeInt (C e C++) e IntegerLib per C++; tuttavia, l’utilizzo di queste librerie richiede cambiamenti significativi al codice e può comportare una riduzione delle prestazioni.

Footnotes

  1. Sebbene sia un’opzione utile, può rompere la compatibilità col codice esistente.