*Questo argomento è stato trattato in secureProgramming *


Spiegazione della debolezza

#include <stdio.h>
int main() {
  char name[100];
  printf("What is your name? ");
  scanf("%s", name);
  printf("Hello, %s\n", name);
  return 0;
}

Analizzando il frammento di codice, sorge una preoccupazione: cosa accadrebbe se il nome fosse più lungo di 99 caratteri?

La risposta è che i byte extra sarebbero scritti oltre la fine del buffer, sovrascrivendo qualsiasi informazione vi sia lì, a prescindere da cosa questa rappresenti, potenzialmente alternando il comportamento del programma. Nel caso più grave, può accadere che l’informazione sovrascritta possa essere un’informazione di controllo, come ad esempio l’indirizzo di ritorno sullo stack, lasciando all’attaccante la possibilità di eseguire codice arbitrario.

Possibile attacco

L’attacco parte dal fatto che il codice macchina è una “semplice” sequenza di byte, dunque un attaccante può semplicemente inserire i byte che rappresentano le istruzioni in in linguaggio macchina che l’attaccante vuole eseguire.

Quando il main ritorna, l’esecuzione non riprenderà dal chiamante del main (ad esempio, il runtime di supporto fornito dal compilatore C), ma dal frammento di codice fornito dall’attaccante. Tipicamente, questo codice è in grado di eseguire una shell (per questo è chiamato shellcode) sul sistema operativo in grado di eseguire i comandi decisi dall’attaccante.

In alcuni casi, può capitare che l’attaccante non possa inserire il codice macchina nello stack a causa di un meccanismo di protezione; in questo caso, l’attaccante può comunque alterare l’indirizzo di ritorno e provocare la chiamata di una funzione di libreria di scelta con i parametri adeguati per perseguire il suo scopo: un esempio è la libreria system, che può essere usata per eseguire un comando della shell.

In questo caso, quando il main ritorna, l’esecuzione riprenderà dalla libreria selezionata, con i parametri scelti dall’attaccante.

Variante dell’attacco: utilizzo dell’heap

Se non si usa lo stack ma l’heap, l’attacco è reso più complicato ma comunque possibile.

Funzionamento dell’heap (malloc e free)

L’heap è una struttura simile a una lista doppiamente concatenata, i cui nodi sono rappresentati da blocchi di memoria. Ciascuno è preceduto da un’intestazione che contiene delle informazioni di controllo usata dalla funzione free per liberare i blocchi alla fine del loro utilizzo, come dimensione del blocco, puntatore ai blocchi precedenti e successivi etc.

Quando un blocco viene liberato, la funzione free scriverà qualcosa nell’intestazione per aggiungere il blocco all’elenco dei blocchi disponibili.

Facendo in modo che un buffer vada in overflow nell’heap, l’attaccante può alterare l’intestazione del blocco successivo. Quando il blocco verrà liberato, la funzione free scriverà qualcosa nell’indirizzo indicato dall’attaccante; più precisamente, si parla della scrittura di una word in una posizione a scelta dell’attaccante, cioè una write-what-where condition^[CWE-123.]. Soprattutto se questo attacco è ripetuto, si permette all’attaccante l’esecuzione di codice arbitrario, ad esempio cambiando il contenuto dell’indirizzo di ritorno o il contenuto di un puntatore a funzione.

Linguaggi affetti dalla debolezza

I linguaggi maggiormente affetti da questa vulnerabilità sono C e i suoi derivati (C++ e Objective-C). La motivazione cardine dietro questo fenomeno è rappresentata dal fatto che il C non svolge un bound check sugli array, a sua volta causata dal fatto che, in fase di compilazione, l’array diventa un puntatore al primo elemento e, dunque, qualsiasi informazione sulla dimensione viene persa quando l’array è passato a una funzione - a meno che il programmatore non passi questo valore esplicitamente mediante un altro parametro.

L’aspetto è esacerbato dal fatto che, in C, la rappresentazione di una stringa non include direttamente un’informazione sulla lunghezza della stringa.

Anche altri linguaggi sono affetti:

  • Assembly, in cui non c’è nessun controllo applicato dal linguaggio, ma più raro oggi;
  • linguaggi ad alto livello - Java o Python, per esempio - non consentono un accesso diretto alla memoria e, per questo, sono più sicuri. Tuttavia, è un errore considerarli completamente sicuri, poiché spesso utilizzano delle librerie scritte in C/C++ all’interno delle quali questa vulnerabilità può nascondersi.

Cosa evitare

Alcune funzioni aprono la porta a questa debolezza; si annoverano: gets, strcat, strcpy, sprintf. La motivazione risiede nel fatto che queste funzioni non controllano la dimensione del buffer su cui scrivono.

Un’altra funzione risiede in come il buffer viene allocato:

int *dont_do_this(int n) {
    int *buffer=(int *)malloc(n); // Should have been n*sizeof(int)
    if (buffer==NULL)
        return NULL;
    int i;
    for(i=0; i<n; i++)
        scanf("%d", &buffer[i]);
    return buffer;
}

come in questo caso, in cui anziché allocare lo spazio corretto (la dimensione di ciascuno degli n elementi, moltiplicata per n), si alloca direttamente n.

Rilevare la debolezza

La prima cosa da fare è quella di controllare ed eliminare l’uso di funzioni di librerie standard non sicure, come strcpy, gets, scanf con %s etc.

Unsafe functionSafe function
getsfgets
strcpystrncpy
strcatstrncat

Controllare l’utilizzo delle funzioni non è l’unico modo con cui si elimina la debolezza, in quanto può presentarsi con qualsiasi operazione sugli array, di qualsiasi tipo. In C++ può essere utile rappresentare le stringhe con la classe string, anziché come char * e, analogamente, la classe vector anziché gli array C-like tradizionali.

La debolezza può nascere anche da utilizzo errato di funzioni di librerie standard sicure^[Effettuano un controllo sulla dimensione prima di procedere con l’operazione.], utilizzandole cioè con parametri errati. L’esempio è:

// UTILIZZO IMPROPRIO DI:
// char *strncpy(char *dest, char *src, size_t dest_size)
 
char buf[100];
strncpy(buf, source, strlen(source)+1); // should have passed sizeof(buf)

Se i SAST possono aiutare con l’individuazione di funzioni considerate “non sicure”, spesso non possono comprendere che qualcosa è errato, come nel caso appena presentato. Esistono però dei SAST che forniscono delle annotazioni al codice, in grado di conferire informazioni aggiuntive non inferibili dal linguaggio, attraverso le quali si possono rilevare più problemi. Un esempio:

void do_something(/* $$size_is(size1) */ char *buf1, int size1, /* $$size_is(size2) */ char *buf2, int size2) {
    strncpy(buf1, buf2, size2);
    // Should have been size1
}

Uno strumento incluso nei DAST è il Fuzz testing (fuzzing) che può aiutare a rilevare questa debolezza. L’idea su cui si basano è che i dati in input sono modificati randomicamente, scegliendo con una certa distribuzione di probabilità^[I dati non sono totalmente casuali, per evitare che il programma rifiuti i dati. In più, le trasformazioni dipendono dal tipo di dato; nel contesto di una stringa, possono essere aggiunti, rimossi o spostati alcuni caratteri.] tra diverse trasformazioni per controllare se il programma crasha o va in freeze.

Extra defensive measures

Misure aggiuntive possono essere utilizzate per rimuovere o mitigare gli effetti potenziali di questa debolezza, implementate da librerie di sistema, dal compilatore o dal sistema operativo. L’elenco include:

  • tecnica del canarino (the canary technique);
  • stack e heap senza permessi di esecuzione (non-executable stack and heap);
  • randomizzazione del layout dello spazio degli indirizzi (address space layout randomization).

Tecnica del canarino

La tecnica del canarino si basa sull’utilizzo di un valore canarino (canary value), messo dopo lo stack. Prima di ritornare dalla funzione, l’integrità del valore canarino viene controllata, causando la terminazione del programma se viene riportata la corruzione dello stack (e, allo stesso modo, per l’heap). Questo processo viene realizzato da codice inserito automaticamente dal compilatore.

Stack e heap senza permessi di esecuzione

Sulle architetture che supportano la paginazione di memoria, le entry nella tabella delle pagine includono un bit (NX bit - No eXecute) che consente la generazione di un segnale trap se la CPU effettua il fetch di una istruzione da una pagina con il bit NX impostato a 1, con il supporto della MMU.

Con questa tecnica rimane, però, possibile realizzare l’attacco con ritorno a una funzione di libreria e l’attaccante può inserire il codice da qualche altra parte, usando un’altra vulnerabilità, e usare il buffer overrun per saltarci.

Con il bit NX, alcuni sistemi operativi implementano anche la policy W^X(Write-XOR-Execute), in cui ogni pagina su cui è possibile scrivere ha il bit NX impostato. Questa policy richiede il supporto del linker, che deve essere in grado di dividere dati e codice. Inoltre, possono esserci dei problemi con i programming che usano la tecnica della compilazione Just-In-Time^[Avviene per la natura intrinseca del processo di JIT compilation: l’esito di questo processo è la creazione di codice macchina, direttamente in memoria, all’esecuzione del programma. Quindi, viene richiesto dall’interprete un’area di memoria su cui è possibile contemporaneamente scrivere, per salvare il codice appena compilato ed eseguire delle istruzioni, cioè quelle appena ottenute.].

Address Space Layout Randomization (ASLR)

La tecnica dell’ASLR consiste, ogni volta che il programma viene eseguito, nel sistema operativo che sceglie a caso dove le parti di ogni programma (stack, global data, heap, librerie dinamiche…) sono posizionate nello spazio degli indirizzi logici. In questo modo si rende più difficile per l’attaccante di localizzare l’indirizzo a cui saltare (come nel caso di un attacco di tipo ritorno a funzione di libreria).

L’attaccante però può ancora realizzare degli attacchi usando altre fonti di informazione (come il branch target predictor e l’MMU) per scoprire la posizione randomizzata dall’area bersaglio dell’attacco.