Lo scopo di questo articolo è spiegare come scrivere shellcode per un sistema Linux a 64 bit e come realizzare un exploit basato sul buffer overflow.

Le informazioni fornite in questo articolo sono a scopo esclusivamente didattico. L'autore non si assume alcuna responsabilità nel caso di utilizzo illecito di queste conoscenze.

 

1. Il programma vulnerabile

Nel nostro esempio scriveremo un semplice programma che accetta una stringa come parametro, crea un buffer di 100 caratteri nello stack e vi copia la stringa sopracitata. Senza controllarne la lunghezza.

#include  
int main(int argc, char *argv[])
{
char buffer[100];
if(argc<2)
return -1;
strcpy(buffer, argv[1]);
return 0;
} 

 

Come si può facilmente prevedere, quando una stringa di lunghezza superiore a 100 caratteri viene passata al programma, altre porzioni di memoria vengono sovrascritte. Se sovrascriviamo quelle giuste, con le istruzioni appropriate, possiamo far fare al programma quello che vogliamo.

 

2. Lo shellcode

Lo scopo del nostro exploit è di far stampare al programma vulnerabile la stringa hello sullo schermo. Il nostro primo passo consiste quindi nel creare una sequenza di istruzioni macchina che esegui questo compito.

2.1. Scrivere il programma in Assembly

Scriviamo un programma in x64 assembly che stampi una riga su schermo.

 

jmp short ender
starter:
xor rax, rax
xor rdi, rdi
xor rsi, rsi
xor rdx, rdx
mov al, 1
mov dil, 1
pop rsi
mov dl, 5
syscall
xor eax, eax
mov al, 60
xor ebx,ebx
syscall
ender:
call starter
db 'hello'

Avrete notato che l'esecuzione salta subito alla penultima riga, per poi tornare alla seconda. Questo trucco ci permette di ottenere l'indirizzo della stringa hello. Non appena chiameremo la funzione push, questa ci restituerà un puntatore alla stringa.

Il programma poi azzera i registri che verranno usati in seguito.

Ora bisogna stampare la stringa sullo schermo. Per questo serve la chiamata di sistema write.

Le chiamate di sistema sono elencate in un header chiamato unisdt. Nel nostro caso la chiamata write ha il numero 1.

La chiamata di sistema avviene attraverso l'istruzione syscall.

Il registro rax contiene il numero della chiamata.

Il registro rbx dice a write dove scrivere, nel nostro caso stdout (1).

Il registro rdx contiene la lunghezza della stringa.

Le ultime tre righe di codice terminano la schellcode.

Viene scritto solo sui byte meno significativi dei registri per evitare byte nulli nello shellcode. Questo accorgimento è di cruciale importanza, come diventerà più chiaro in seguito.

Infatti lo shellcode dovrà essere passato al programma vulnerabile come se fosse una stringa. Un eventuale byte nullo verrebbe interpretato come la fine della stringa, e lo shellcode completo non potrebbe essere iniettato nel programma vulnerabile.

2.2. Testare il programma

shellcode.jpg

Ok, funziona.

 

 

2.2. Ricavare il codice macchina

disass2.jpg

 

Ora abbiamo lo shellcode. Si noti l'assenza di byte nulli.