Preparazione della shellcode:
# include <stdio.h>
int main(){
char* happy[2];
happy[0] = "/bin/sh";
happy[1] = NULL;
execve(happy[0], happy, NULL);
}
C
L'idea più semplice è quella di compilare il codice sopra e salvarlo come input di un file che dobbiamo passare ad un programma, in modo da creare un file malevolo. Questo non funzionerà per diversi motivi:
strcpy()
si fermano quando uno zero viene trova nella stringa sorgente: quando compiliamo il codice sopra ci sono almeno 3 zeri (terminatori di stringa):\0
al fine di /bin/sh
NULL
, che sono degli zerohappy[0]
potrebbe diventare uno zero nel file binario in base al compilatore utilizzatoNon possiamo dunque utilizzare programmi C per generare codice malevolo. Per cui bisogna scriverli direttamente in assembler. La parte centrale di una shellcode è l'uso della system call execve()
per eseguire /bin/sh
. Per utilizzare la system call abbiamo bisogno di quattro registri:
eax
: deve contenere 11, che è il numero per la chiamata di sistema execve()
ebx
: deve contenere l'indirizzo del comando in stringa (ad esempio /bin/sh
)ecx
: deve contenere l'indirizzo dell'array; nel nostro caso, il primo elemento dell'array punta alla stringa "/bin/sh
", mentre il secondo è NULL
, ovvero zero (che indica la fine dell'array)edx
: deve contenere l'indirizzo delle variabili d'ambiente che vogliamo passare al nuovo programma. Possiamo impostarle a zero, visto che non abbiamo bisogno di passare nessuna variabili d'ambiente.Inizializzare questi registri non è difficile. Per esempio è più complesso il fatto che, per impostare ebx
abbiamo bisogno di conoscere l'indirizzo della stringa /bin/sh
, possiamo metterla sullo stack usando il buffer overflow, ma non saremmo in grado di dire dove si trova con certezza. Per evitare di "indovinare" la posizione della stringa sullo stack, possiamo utilizzare lo stack pointer (esp
), invece di copiare la stringa con il buffer overflow, possiamo dinamicamente pusharla sullo stack, in tal modo, potremmo ottenere l'indirizzo della stringa grazie a esp
. Per far funzionare bene il codice, non vi deve essere alcuno zero, non dobbiamo avere zero nel codice, ma possiamo generarli dinamicamente. Ci sono diversi modi, per esempio per mettere zero nel registro eax
, possiamo usare l'istruzione mov
, ma questo causerà l'apparizione di uno zero nel codice, allora un altro modo è usare: xor eax, eax
, che mette in xor due registri uguali, il che restituisce zero.
Prima di andare oltre è necessario essere coscienti di come funziona lo stack prima e dopo aver ritornato il valore di una funzione:
La shellcode che useremo è la seguente, è già commentata con il significato di ciascun valore (rispetto al linguaggio assembler che rappresentano):
Adesso riga per riga vediamo di capire come funziona.
Passo 1: trovare l'indirizzo della stringa /bin/sh
e impostarlo in ebx
.
Per trovare l'indirizzo della stringa, dobbiamo fare push di questa stringa sullo stack. Dal momento che lo stack cresce da indirizzi più alti a indirizzi più bassi, e che possiamo solamente fare push di 4 byte alla volta, abbiamo bisogno di dividere la stringa in 3 pezzetti, ciascuno di 4 byte. Trattandosi di little endian, faremo prima push dell'ultimo pezzo:
eax
a zero: xor eax, eax
push eax
, mettiamo zero sullo stack, questo marca la fine della stringa /bin/sh
push 0x68732f2f
, mettiamo //sh
sullo stack (lo slash doppio è usato perché è necessario che ciascuna istruzione sia di 4 byte, il doppio slash è trattato da execve
come uno slash singolo). Se al posto di una shell /sh
avessimo avuto una shell /zsh
, allora l'istruzione era già di 4 byte, e non sarebbe servito aggiungere un ulteriore slash.push 0x6e69622f
, mettiamo /bin
sullo stack (è già 4 byte, non serve doppio slash). Adesso abbiamo sullo stack /bin//sh
, e in questo momento lo stack pointer esp
punta alla cima dello stack, dunque sta puntando all'inizio della stringa (come si vede nella figura in basso).mov esp, ebx
: facciamo puntare esp
a ebx
. Quindi abbiamo salvato l'indirizzo corrente di eps
in ebx
, salvando di fatto l'indirizzo della stringaPasso 2: trovare l'indirizzo dell'array happy[]
e impostarlo a ecx
.
Il prossimo passo consiste nel trovare l'indirizzo di happy[]
, che deve contenere due elementi, l'indirizzo di /bin/sh/
che si trova attualmente in ebx
e zero in posizione 1. Utilizzeremo la stessa tecnica per ottenere l'indirizzo dell'array: costruiremo l'array dinamicamente e poi utilizzeremo lo stack pointer per ottenerne l'indirizzo.
push eax
, mettiamo il secondo elemento di happy
sullo stack. Dal momento che contiene uno zero, semplicemente faremo push di eax
in questa posizione, poiché eax
è ancora zero (vedi inizio passo precedente).push ebx
, ebx
contiene l'indirizzo della stringa e quindi la posizione 0 di happy
. Array costruito.mov esp, ecx
, salviamo il valore di esp
in ecx
, così adesso ecx
, punta all'inizio dell'array happy
Passo 3: impostare edx
a zero.
Il registro edx
deve essere posto a zero. Possiamo utilizzare la tecnica dello xor, ma in modo da ridurre la dimensione del codice di un byte, possiamo sfruttare una istruzione diversa cdq
, questa istruzione imposta edx
a zero come effetto collaterale. Di base, copia il bit di segno del valore in eax
(che attualmente è zero), in ogni posizione di edx
(è proprio questa la funzione dell'istruzione: copiare il bit significativo di eax in edx).
Passo 4: invocare execve()
.
Sono necessarie due istruzioni per invocare questa system call. La prima è salvare il numero della chiamata di sistema in eax
. Il numero è 11 0x0b
in esadecimale.movb 0x0b, al
imposta al
a 11 (al
rappresenta gli 8 bit meno significativi di eax
, gli altri bit sono ancora zero dal passo 1).int 0x80
esegue la system call, l'istruzione int
vuol dire interrupt. Lo stato finale dello stack è mostrato di seguito:
Seguendo i passi sopra descritti, riusciamo a scrivere un codice assembly del genere:
Il libro inverte gli operandi delle istruzioni di x86 a 32 bit, il motivo è che probabilmente usa un pseudo-linguaggio assembly IA32. In istruzioni come: mv esp, ecx
il libro afferma che si sposta esp
in ecx
, ma in assembly x86, gli operandi sono invertiti mv ecx, esp
. Per questo motivo di seguito le istruzioni con mv
avranno gli operandi invertiti.
xor eax, eax
push eax
push "//sh"
push "/bin"
mov ebx, esp
push eax
push ebx
mov ecx, esp
cdq
mov al, 0x0b
int 0x80
Ricorda che:
/bin/sh
, allora prima metto sullo stack //sh
e poi /bin
A questo punto dobbiamo convertire il codice assembly in binario, per ottenere una shellcode, per fare questo utilizziamo un compilatore assembly nasm
.nasm -O0 -f win32 codice.asm -o codice.bin
-O0
: imposta una compilazione senza ottimizzazione-f win32
: imposta il formato x86-32bit
di compilazione-o codice.bin
nome del file di outputPer leggere questo file dobbiamo decompilarlo usando un altro tool objdump
:objdump -D -b binary -mi386 -Mintel code.bin
questo tool, ci mostrerà il codice assembly compilato. In fase di compilazione nasm
avrà aggiunto ulteriori dati al file come gli header e altre informazioni varie. Quello che dobbiamo fare è cercare all'interno del file decompilato da objdump
, la serie di istruzioni che corrispondono con il nostro codice malevolo.
Sopra si vede il risultato mostrato da objdump
, dopo qualche riga iniziale si trova il nostro codice. Sulla sinistra troviamo il valore esadecimale delle istruzioni, ovvero la nostra shellcode:
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80
che possiamo scrivere sul nostro file malevolo utilizzando python.