Come scrivere shellcode

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:

  • prima che un programma venga eseguito deve essere caricato in memoria e il suo ambiente d'esecuzione deve essere pronto. Queste operazioni vengono effettuate dal loader del sistema operativo che è responsabile di riservare la memoria (per esempio stack e heap), copiare il programma in memoria, invocare il dynamic linker in modo che punti alle librerie necessarie. Se uno di questi passi appena descritti viene saltato, il programma non funzionerà bene. In un attacco di buffer overflow il programma malevolo non è caricato in memoria dal sistema operativo, ma è caricato direttamente mediante copia in memoria.
  • Terminatori di stringhe nel codice: funzioni come 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
    • ci sono due NULL, che sono degli zero
    • anche lo zero in happy[0] potrebbe diventare uno zero nel file binario in base al compilatore utilizzato

Non 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:
Pasted image 20230911162530.png

La shellcode che useremo è la seguente, è già commentata con il significato di ciascun valore (rispetto al linguaggio assembler che rappresentano):
Pasted image 20230911162726.png

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:

  • usando lo xor poniamo 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 stringa

Pasted image 20230911163636.png

Passo 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:
Pasted image 20230911165246.png

Seguendo i passi sopra descritti, riusciamo a scrivere un codice assembly del genere:

Per qualche motivo il libro inverte gli operandi delle istruzioni

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:

  • le stringhe devono essere lunghe 4 byte
  • che possono essere scritte tra apici, purché siano al massimo di 4 byte
  • che vanno inserite in ordine inverso, se la stringa è /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 output
    Il risultato è il seguente:
    Pasted image 20230926151035.png
    ovviamente non si capisce nulla.

Per 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.
Pasted image 20230926151430.png
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.