Polimorfismo
Il termine “polimorfismo” deriva dal greco ed indica qualcosa che ha “molte forme”. L’espressione, riferita ad un Virus, descrive la sua capacità di mutare identità dietro continui processi crittografici, nascondendo il suo codice per non essere rilevato. Il Capitolo VII del Manifesto The Fallen Dreams, pone un interessante analogia: se ci pensate, anche noi adottiamo comportamenti “polimorfici” per raggiungere i nostri scopi. Cambiamo i nostri atteggiamenti in base ai nostri obiettivi, per piacere a qualcuno, per integrarci in gruppo… Ma la domanda più profonda a cui ci spinge questa riflessione è: ciò che abbiamo raggiunto con stratagemmi, fingendo di essere qualcuno o qualcos’altro, è davvero ciò che vogliamo?
In questa settima parte vedremo come creare una backdoor scrivendo da zero il suo Shellcode.
Che cos’è uno Shellcode
Il termine Shellcode viene utilizzato per descrivere un insieme di istruzioni in linguaggio Assembly, atte solitamente, ad eseguire una shell. La shell, in informatica, è il punto di dialogo fra l’utilizzatore ed il sistema operativo. Attraverso una shell è possibile impartire comandi, eseguire altri programmi, manipolare parametri e file di configurazione… La sua declinazione, solitamente, è il classico terminale testuale a riga di comando.
Uno Shellcode iniettato nel flusso di un normale programma, diventa un exploit. Alcuni tipi di Shellcode non richiedono la creazione di nuovi processi sulla macchina target e non hanno bisogno di ulteriore codice per la pulizia del processo vittima, in quanto possono ricorrere a librerie presenti all’interno del processo.
Cos’è il linguaggio Assembly (asm)?
Assembly è il linguaggio di programmazione più vicino al linguaggio macchina vero e proprio, per questo esiste una forte dicotomia fra le sue istruzioni e l’architettura della macchina. In una scala che parte dal linguaggio a livello più alto e arriva sino al codice macchina, eseguito dalla CPU, troveremmo: Python, C e C++ , Assembly , Codice Macchina. Uno dei vantaggi dell’utilizzare un linguaggio a basso livello è che non ha dipendenze, se non lo scrivere il programma specifico per quell’architettura.
Il nostro obiettivo finale sarà quindi quello di scrivere uno Shellcode in Assembly, per una macchina Linux con architettura a 64 bit.
HELLO “Hacker Journal”
Il nostro primo passo, sarà un classico: scriveremo un programma in assembly x64 che stamperà a video la scritta: “Hacker Journal“.
FASE I – I REGISTRI
Gli elementi chiave dell’Assembly sono i registri, cioè le celle di memoria che possono memorizzare determinati valori. La grandezza di un dato registro dipende dal processore e dal compilatore utilizzato (nel nostro caso 64 bit). Ogni registro assolve specifiche funzioni:
Registri
64-bit |
Tipologia | Scope |
rax | Volatile | Sono registri “general purpose” a 64 bit – Servono come sorgente o destinazione per qualunque operazione di trasferimento |
rbx | Non Volatile | Viene utilizzato come puntatore di base per l’accesso alla memoria |
rcx | Volatile | Viene utilizzato come contatore di cicli. |
rdx | Volatile | Sono registri “general purpose” a 64 bit – Servono come sorgente o destinazione per qualunque operazione di trasferimento |
rsi | Non Volatile | Sono registri “general purpose” a 64 bit – Servono come sorgente o destinazione per qualunque operazione di trasferimento |
rdi | Non Volatile | Sono registri “general purpose” a 64 bit – Servono come sorgente o destinazione per qualunque operazione di trasferimento |
rbp | Non Volatile | Contiene l’indirizzo di base dello stack |
rsp | – | Contiene l’indirizzo in cima allo stack |
Sebbene i registri “general purpose” possano essere usati per qualsiasi scopo, esistono delle istruzioni per sfruttarli in modo specifico, rendendo il loro utilizzo efficace e mirato.
FASE II – LE ISTRUZIONI
Come ogni linguaggio che si rispetti, anche l’Assembly ha le sue istruzioni. Vediamo quali ci occorre conoscere.
ADD: Addizione tra due registri, tra un numero e un registro, tra un’area di memoria e un registro, tra un’area di memoria e un numero.
SUB: Funzione inversa di ADD, effettua la sottrazione tra numeri, registri o valori. Stesso funzionamento di ADD.
INC / DEC: Incrementa di 1 o decrementa di 1.
JMP: Salta a un indirizzo.
MOV: Muove un valore dentro ad un registro o un registro dentro un altro registro, un valore dentro un’area di memoria o un registro dentro un’area di memoria.
PUSH / POP: Permettono di salvare un registro o il valore puntato da un registro nello stack, per poi ripristinarlo.
Lo stack è un’area di memoria contigua utilizzata per memorizzare dati. I singoli elementi sono gestiti in modalità Last In First Out (LIFO), vale a dire che l’ultimo oggetto inserito è il primo ad essere rimosso. Le due operazioni principali (come abbiamo visto) sono: push (aggiunge un elemento in cima allo stack) e pop (rimuove un elemento).
FASE III – IL CODICE
Utilizziamo un editor di testo e creiamo il nostro file Hello_HJ.asm .
Nell’immagine trovate il codice commentato e spiegato riga per riga.
FASE IV – FUNZIONAMENTO
Il Sistema Operativo controllerà i valori caricati nei registri (rax, rdi, rsi e rdx) e utilizzerà questi valori per determinare quali saranno le operazioni da eseguire.
Potremmo riassumere il nostro semplice algoritmo come segue:
- Qual è il nostro scopo? Scrivere dei dati: mov rax, 1
- Dove vogliamo scrivere questi dati? Vogliamo visualizzarli a video: mov rdi, 1
- Quali dati vogliamo scrivere? Vogliamo Scrivere il contenuto del messaggio (“Hacker Journal”): mov rsi, messaggio
Da notare che il comando syscall esegue una chiamata al sistema, richiedendo un servizio a livello kernel del sistema operativo in uso. Equivale, più o meno, a dire “esegui le istruzioni”!
FASE V – COMPILAZIONE
Possiamo finalmente compilare il programma ed eseguirlo.
Per farlo, ci serviremo di Netwide Assembler (NASM), un software per la scrittura di programmi Assembler x16/32/64 bit. Solitamente il software è già preinstallato sulla distribuzione standard di Kali Linux. Se così non fosse, sarà sufficiente digitare:
apt-get install nasm
Una volta installato, dovremo digitare il seguente comando:
nasm -felf64 Hello_HJ.asm
- nasm : comando
- -felf64 : La lettera “f” è l’opzione per il formato. “elf64” indica l’architettura x86-64 –Linux e la maggior parte delle varianti Unix
- asm : nome del nostro file .asm
Adesso possiamo compilare il nostro object file, appena ottenuto con il comando nasm.
Il comando ld, conosciuto anche come linkage editor, assume la funzione di “linker”, generando dal file .object il suo eseguibile:
ld Hello_HJ.o -o Hello_HJ
Non ci rimane che avviare il file eseguibile appena ottenuto:
./Hello_HJ
Creare la Reverse Shell in C
Prima di passare alla creazione della nostra Reverse Shell in Assemblerx64, abbiamo bisogno di alcuni punti di riferimento e dobbiamo capire come alcune funzioni e le loro chiamate al sistema si comportano in memoria, ad un livello più basso.
Per farlo, il primo passo è scrivere una Reverse Shell usando il linguaggio di programmazione C. Utilizziamo quindi il nostro editor di testo e creiamo il file rshell.c .
Non mi dilungherò molto nella spiegazione, perché nelle immagini potrete trovare il codice commentato riga per riga.
- Caricamento delle librerie di cui ci serviremo:
2. Utilizzeremo la funzione SOCKET per l’invio e la ricezione dei dati fra gli host:
3. A chi ci vogliamo connettere? Creiamo la struttura per passare IP e PORTA.
Per il test utilizzeremo la nostra macchina, quindi l’interfaccia loopback (localhost) IP. 0.0.1 e la porta 5555 :
4. Adesso, abbiamo bisogno di poter comunicare con il socket attraverso la nostra shell. Per farlo utilizzeremo DUP (Duplicate).
5. In ultima istanza, apriremo la nostra shell, eseguendola: /bin/sh
Ci siamo! Salviamo il nostro codice ed usciamo dall’editor di testo. A questo, punto dobbiamo compilarlo e testarlo.
Utilizziamo il compilatore gcc.
gcc rshell.c –o rshell
- gcc : il nostro comando
- c : il nome del nostro programma
- -o : esplicitiamo l’output…
- rshell: …creando il file rshell
Apriamo un secondo terminale e mettiamoci in ascolto utilizzando netcat:
nc –lvp 5555
- nc: invochiamo netcat
- -l: listening mode
- -v: verbose
- -p: porta
- 5555: porta su cui desideriamo metterci in ascolto
Torniamo adesso sul nostro primo terminale, dove abbiamo appena compilato il nostro codice C e lanciamo il nostro programma ./rshell .
A questo punto, dovremmo aver ottenuto la nostra Reverse Shell in locale. Provando a digitare qualche comando, ci assicureremo che tutto funzioni.
Non ci resta che da fare un’ultima cosa: vedere come il programma si comporta a basso livello.
Per lo scopo utilizziamo un utility di debugging conosciuta come strace.
Prima installiamola con: sudo apt-get install strace
Per eseguirla, digitiamo:
strace /../rshell
- strace : invochiamo il tool
- /../rshell : il percorso al nostro file eseguibile che desideriamo analizzare
La parte su cui concentrarci è quella relativa all’esecuzione delle funzioni utilizzate.
Nella prossima parte, infatti, vedremo come questi valori ci serviranno per tradurre il nostro codice C in Assembly.
Tradurre l’output del debugging
FASE I – ESADECIMALE
Il primo passo da fare sarà quello di tradurre i vari parametri numerici da decimale a esadecimale, nello specifico: IP, Porta e “/bin/sh”.
Per farlo, utilizziamo la funzione di python hex in questo modo:
$ python : lanciamo python
>>> hex(valore numerico)
>>> hex(ord(‘lettera’)
Una volta eseguito per tutti i valori, il risultato sarà il seguente:
- Porta: “5555” in hex = 0x15b3
- IP: “127.0.0.1” in hex = 0x7f000001
- “/” in hex = 0x2f
- “b” in hex = 0x62
- “i” in hex = 0x69
- “n” in hex = 0x6e
- “/” in hex = 0x2f
- “s” in hex = 0x73
- “h” in hex = 0x68
FASE II – LE ISTRUZIONI
Procediamo adesso con le istruzioni AF_INET, SOCK_STREAM e IPPROTO.
Per trovare il loro corrispettivo valore di chiamata, dovremo fare un:
grep –r –color [nomeistruzione]
Ad esempio, l’istruzione SOCK_STREAM la troviamo nel file:
/usr/include/x86_64-linux-gnu/bits/socket.h ed il suo valore corrispondente è 1.
Così per tutte le altre:
- AF_INET –> PF_INET (/usr/include/x86_64-linux-gnu/bits/socket.h) –> 2
- SOCK_STREAM –> SOCK_STREAM (/usr/include/x86_64-linux-gnu/bits/socket.h) –> 1
- IPPROTO_IP –> (/usr/include/linux/in.h) –> 0
FASE III – SYSCALL
L’ultimo step, prima d’iniziare a scrivere il nostro codice Assembly, è la traduzione delle SYSCALLS, ovvero le chiamate al sistema da parte del nostro programma, che come abbiamo visto, sono quattro: socket, connect, dup2 e execve .
Per farlo, abbiamo a disposizione diversi modi. Uno dei più semplici è utilizzare internet. All’ indirizzo: https://syscalls.w3challs.com/?arch=x86_64 possiamo cercare il nome della chiamata ed il suo equivalente in Assembly.
- socket –> RAX 0x29
- connect –> RAX 0x2a
- dup2 –> RAX 0x21
- execve –> RAX 0x3b
Finalmente siamo pronti per scrivere il nostro codice in Assemblyx64!
Il codice Assembler
Per scrivere il nostro codice, terremo come timone di rotta l’output del nostro strace, riscritto dopo le tre fasi.
Ad esempio la riga:
connect(3, {sa_ AF_INET , sin_port= 5555 ), sin_ inet_addr 127.0.0.1 “)}, 16) = 0
Diventerà:
0x2a ((2, 0x15b3,) , 0x7f000001) |
Nota: In Assembly i valori come la porta 5555, in esadecimale 0x15b3, dovranno essere passati in formato Endian. Ciò vale a dire che andranno inseriti al contrario: 5555 in esadecimale 0x15b3, in Endian 0xb315 .
Cominciamo.
Utilizziamo il nostro editor di testo per creare il file Arshellx64.asm .
Allo stesso modo, compiliamo il nostro file Arshellx64.asm, come abbiamo fatto per il file Assembler HELLO_HJ:
nasm -felf64 Arshellx64.asm
ld Arshellx64.o -o Arshellx64
Testiamo sempre il nostro codice Assembler, come già visto per la reverse shell scritta in C, che produrrà esattamente il medesimo risultato.
Bad Character/Byte
Prima di passare a scrivere il nostro Shellcode finale, dobbiamo controllare che nel codice non siano presenti “caratteri non validi”, che potrebbero comprometterne l’esecuzione. Esistono diverse tecniche, vi faccio solo un esempio.
Per vedere come si comporta il nostro codice, utilizzeremo un altro tool: objdump. Esso permette di disassemblare il codice per osservarlo durante la sua esecuzione in Assembly.
Digitiamo:
objdump –d Arshellx64
- objdump: il tool per disassemblare
- -d : opzione per disassemblare
- Arshellx64: il nome dell’eseguibile che vogliamo disassemblare
Come possiamo vedere, il nostro IP 127.0.0.1 potrebbe creare qualche problema allo Shellcode, generando diversi [00] o NULL .
Una tecnica da utilizzare per far fronte al problema, è quella dell’operatore booleano XOR. Nel nostro caso, ad esempio, potremmo modificare il nostro codice come segue:
Creare uno Shellcode
Adesso non ci resta che generare il nostro SHELLCODE.
In realtà, quando poco fa abbiamo utilizzato objdump per rimuovere i caratteri che potevano generare errori, ci siamo già imbattuti nel nostro Shellcode: il flusso del codice disassemblato nella colonna centrale. Volendo, potremmo quindi ricopiare a mano il nostro “flusso d’esecuzione”.
Ma prima, lavoriamo per ottimizzare il nostro codice ed estrarre dalla Reverse Shell in assemblyx64 esclusivamente la porzione di codice eseguibile. Per farlo, utilizzeremo il comando:
objcopy -O binary -j .text Arshellx64 Arshellx64.bin
- objcopy : comando
- -O : opzione per specificare la tipologia di output
- -j : opzione per copiare esclusivamente una data porzione del programma
- .text : la porzione di programma che vogliamo estrarre
- Arshellx64 : il nostro Assembly-file eseguibile
- bin : il nome del programma con l’estensione .bin che specifica il formato binario.
Se adesso listiamo i file nella directory, utilizzando ls –al, noteremo la ridotta dimensione del file .bin appena creato.
Diamo un occhio al nostro Shellcode visualizzando il dump in hex:
xxd Arshellx64.bin
Ci siamo quasi! Adesso, non ci resta che estrarre dal file binario il nostro Shellcode. Utilizziamo qualche riga di codice in python: apriamo ancora una volta il nostro editor di testo e creiamo il nostro script binToShell.py .
Salviamo il nostro script, usciamo e lanciamolo:
python3 binToShell.py Arshellx64.bin
Test the Shellcode
A questo punto, non ci resta che testare il nostro Shellcode. Per farlo, dovremo scrivere un breve programma in C, che ci permetta di lanciarlo. Apriamo sempre il nostro editor di testo e creiamo il file runShellcode.c , inserendo lo Shellcode che abbiamo appena estratto dal file .bin (figura precedente).
Infine, compiliamolo utilizzando sempre gcc, ma con qualche parametro in più per ottimizzare il nostro eseguibile:
gcc -fno-stack-protector -z execstack -static runShellcode.c –o THESHELLCODE
Non ci rimane che lanciare netcat (come visto precedentemente) e, mettendoci in ascolto sulla porta 5555, eseguire il nostro codice: THESHELLCODE . In questo modo, avremo finalmente ottenuto la nostra backdoor!
qemu-x86_64-static ./THESHELLCODE