| Indice Articolo |
|---|
| Buffer overflow |
| Buffer overflow in lettura |
| A cosa possono servire gli overflow |
| Heap overflow |
| Tutte le pagine |
In questo articolo partendo da un semplice esempio in delphi viene mostrato il meccanismo del buffer overflow.
Buffer overflow in scrittura
Ecco il sorgente della piccola applicazione apparentemente innocua, che dove solo eseguire una procedura chiamata prova e in seguito stampare una frase a video.
program ovl1;
{$APPTYPE CONSOLE}
uses
SysUtils;
procedure prova(p:pchar);
var
buff: array[0..3] of char;
begin
StrCopy(buff,p);
end;
begin
prova('123');
writeln('test ok, premi invio per terminare');
readln;
exit;
writeln('overflow'); //qui in teoria non ci deve mai arrivare
readln;
end.
Vediamo di disassemblare il codice:
Corpo principale del programma
begin
00409134 55 push ebp
00409135 8BEC mov ebp,esp
00409137 83C4F0 add esp,-$10
0040913A A19CAA4000 mov eax,[$0040aa9c]
0040913F C60001 mov byte ptr [eax],$01
00409142 B8B08B4000 mov eax,$00408bb0
00409147 E8F0C1FFFF call @InitExe
prova('123');
0040914C B8B0914000 mov eax,$004091b0
00409151 E826FAFFFF call prova
writeln('test ok, premi invio per terminare');
00409156 A1F0A94000 mov eax,[$0040a9f0]
0040915B BABC914000 mov edx,$004091bc
00409160 E8D7B7FFFF call @Write0LString
00409165 E89EA4FFFF call @WriteLn
0040916A E86D9CFFFF call @_IOTest
readln;
0040916F A15CAA4000 mov eax,[$0040aa5c]
00409174 E877A2FFFF call @ReadLn
00409179 E85E9CFFFF call @_IOTest
exit;
0040917E EB28 jmp $004091a8
writeln('overflow'); //qui in teoria non ci deve mai arrivare
00409180 A1F0A94000 mov eax,[$0040a9f0]
00409185 BAE8914000 mov edx,$004091e8
0040918A E8ADB7FFFF call @Write0LString
0040918F E874A4FFFF call @WriteLn
00409194 E8439CFFFF call @_IOTest
readln;
00409199 A15CAA4000 mov eax,[$0040aa5c]
0040919E E84DA2FFFF call @ReadLn
004091A3 E8349CFFFF call @_IOTest
end.
004091A8 E883B2FFFF call @Halt0
Riportiamo anche la parte di codice relativa alla funzione prova
Funzione prova
begin
00408B7C 51 push ecx
StrCopy(buff,p);
00408B7D 8BD4 mov edx,esp
00408B7F 92 xchg eax,edx
00408B80 E8EFD7FFFF call StrCopy
end;
00408B85 5A pop edx
00408B86 C3 ret
E infine riportiamo il codice della funzione StrCopy
StrCopy:
00406374 29C2 sub edx,eax
00406376 A901000000 test eax,$00000001
0040637B 50 push eax
0040637C 740B jz $00406389
0040637E 0FB60C02 movzx ecx,[edx+eax]
00406382 8808 mov [eax],cl
00406384 85C9 test ecx,ecx
00406386 741B jz $004063a3
00406388 40 inc eax
00406389 0FB60C02 movzx ecx,[edx+eax]
0040638D 85C9 test ecx,ecx
0040638F 7414 jz $004063a5
00406391 0FB70C02 movzx ecx,[edx+eax]
00406395 668908 mov [eax],cx
00406398 83C002 add eax,$02
0040639B 81F9FF000000 cmp ecx,$000000ff
004063A1 77E6 jnbe $00406389
004063A3 58 pop eax
004063A4 C3 ret
004063A5 8808 mov [eax],cl
004063A7 58 pop eax
004063A8 C3 ret
Ora per spiegare cosa accade dovremo mostrare cosa accade esattamente nello stack, che è in pratica il nostro luogo del delitto. Proviamo a descrivere l'esecuzione del programma passo passo, magari un bel caffè può aiutare a rimanere lucidi. Saltiamo lo startup e iniziamo con l'esecuzione della funzione prova:
prova('123');
0040914C B8B0914000 mov eax,$004091b0
00409151 E826FAFFFF call prova
prima di passare alla funzione prova, viene caricato nel registro EAX l'indirizzo di memoria dove risiede la stringa ('123') da passare alla funzione prova. Dopo di che c'èe la solita chiamata ad una funzione, che comprende 2 operazioni :
- Salvataggio nello stack (push) del puntatore dell'istruzione successiva a quella della chiamata (EIP)
- L'incremento dello ESP ( puntatore alla cima dello stack)1
1Nota : lo stack è una parte della memoria virtuale che va da 0012C000 a 0012FFFF (4000h byte), il suo indirizzamento logico parte da un valore alto 0012FFFC che sarà la sua base fino al 0012C000 che è la sua estreamità superiore. La così detta cima dello stack è un puntatore che si muove all'interno di questo range e dato che la base è il valore più alto incrementare lo stack di fatto vuol dire decrementare l'indirizzo di questo puntatore. Quindi quando diciamo che vogliamo incrementare la pila di fatto decrementiamo il valore di questo puntatore.
Il registro ESP che ci indica la cima dello stack è 0012FFB0.
| Prima | Dopo | |||||
|---|---|---|---|---|---|---|
| EIP | 00409151 | 00408B7C | ||||
| ESP | 0012FFB0 | 0012FFAC | ||||
| Stack | ... 0012FFB0 0012FFB4 0012FFB8 0012FFBC ... |
... 7C91DCBA 0012FFE0 004040D0 0012FFC0 ... |
Pointer to next SEH record SE handler |
0012FFAC 0012FFB0 0012FFB4 0012FFB8 0012FFBC ... |
00409156 7C91DCBA 0012FFE0 004040D0 0012FFC0 ... |
RETURN dopo prova Pointer to next SEH record SE handler |
Come possiamo vedere nello stack è stata inserita una riga che ci sarà utile quando, terminata la funzione, dovremo ritornare al corpo principale del nostro codice.
Procediamo. Ora siamo dentro la funzione prova
begin
00408B7C 51 push ecx
A cosa serve questa istruzione? E' molto semplice mette il valore contenuto nel registro ECX dentro lo stack. Ma perche? Il valore conservato non ha nessuna importanza, ma questo è un modo come un altro di preservare dello spazio nello stack. In pratica esso rappresenta la mia variabile locale buff all'interno della procedura prova (buff: array[0..3] of char) ed essendo questa variabile di 4 byte essa corrisponde precisamente ad una cella di memoria dello stack.
Se avessimo imposto come grandezza del nostro buffer 5 caratteri la procedura avrebbe allocato 2 celle di memoria e probabilmente o usava l'istruzione di push per 2 volte (metodo indiretto) oppure spostava direttamente la cima dello stack incrementando il registro ESP (metodo diretto) con l'istruzione add esp,-$08 (ricordiamo che incrementare la pila significa decrementare ESP).
| Prima | Dopo | |||||
|---|---|---|---|---|---|---|
| EIP | 00408B7C | 00408B7D | ||||
| ESP | 0012FFAC | 0012FFA8 | ||||
| Stack | ... 0012FFAC 0012FFB0 0012FFB4 0012FFB8 0012FFBC |
... 00409156 7C91DCBA 0012FFE0 004040D0 0012FFC0 ... |
RETURN dopo prova Pointer to next SEH record SE handler |
0012FFA8 0012FFAC 0012FFB0 0012FFB4 0012FFB8 0012FFBC ... |
00408BB8 00409156 7C91DCBA 0012FFE0 004040D0 0012FFC0 ... |
Spazio allocato per buff RETURN dopo prova Pointer to next SEH record SE handler |
Proseguendo nella procedura prova troviamo la chiamata alla funzione StrCopy
StrCopy(buff,p);
00408B7D 8BD4 mov edx,esp
00408B7F 92 xchg eax,edx
00408B80 E8EFD7FFFF call StrCopy
Prima della chiamata vera e propria bisogna passare i parametri nella posizione corretta : StrCopy(buff,p)
allora ricapitolando in EAX avevamo salvato il puntatore p alla stringa '123', la variabile locale buffer invece era nello stack quindi il puntatore ad essa è proprio la cima dello stack conservata in ESP. Quindi recupero lo stack e lo metto in EDX, dopo di che li scambio di posto perché la funzione StrCopy si aspetta come prima parametro (EAX) il puntatore alla variabile buff e come secondo parametro (EDX) il puntatore a '123'. Infine chiamo la funzione StrCopy:
| Prima | Dopo | |||||
|---|---|---|---|---|---|---|
| EIP | 00408B7D | 00406374 | ||||
| ESP | 0012FFA8 | 0012FFA4 | ||||
| Stack | ... 0012FFA8 0012FFAC 0012FFB0 ... |
... 00408BB8 00409156 7C91DCBA ... |
RETURN dopo prova |
0012FFA4 0012FFA8 0012FFAC 0012FFB0 ... |
00408B85 00408BB8 00409156 7C91DCBA ... |
RETURN dopo Strcopy Spazio allocato per Buff RETURN dopo prova |
come accaduto per la procedura prova anche per la funzione StrCopy viene inserito nello stack l'indirizzo della funzione successiva e incrementato l'ESP.
Non sto a fare la descrizione di tutto quello che avviene all'interno della funzione StrCopy perché sarebbe veramente lungo ed inutile, ma focalizzeremo l'attenzione su alcuni passaggi.
La funzione StrCopy(A,B) non fa altro che copiare dei byte dalla posizione di memoria puntata da B alla posizione di memoria puntata da A, e si ferma quando incontra nella stringa B il byte di valore 00. Nel nostro caso A sarà il puntatore alla variabile buff nello stack e B sarà il puntatore p alla stringa '123'.
Diamo una occhiata a quello che accade nello stack all'inizio della funzione e successivamente quando finisce.
CPU Stack
Indirizzo Valore ASCII Commenti
0012FFA4 00408B85 @. ; RETURN dopo StrCopy
0012FFA8 00408BB8 @. ; BUFF
0012FFAC 00409156 V@. ; RETURN dopo Prova
dopo il procedimento
CPU Stack
Indirizzo Valore ASCII Commenti
0012FFA4 00408B85 @. ; RETURN dopo StrCopy
0012FFA8 00333231 123. ; BUFF
0012FFAC 00409156 V@. ; RETURN dopo Prova
Come avevamo previsto all'indirizzo 0012FFA8 dove risiedeva la variabile locale Buff sono stati copiati i byte della stringa "123", naturalmente in ordine inverso, infatti "1" = 31 , "2" = 32 , "3"=33 e 00 termina la stringa.
Terminata il processo di copia della stringa come ultima istruzione troviamo un RET. Il che significa che viene preso il valore contenuto nella cima dello stack (puntato da ESP) e usato come successivo indirizzo della prossima istruzione.
In pratica prendo il valore in cima allo stack e lo sostituisco col EIP :
| Prima | Dopo | |||||
|---|---|---|---|---|---|---|
| EIP | 004063A4 | 00408B85 | ||||
| ESP | 0012FFA4 | 0012FFA8 | ||||
| Stack | ... 0012FFA4 0012FFA8 0012FFAC 0012FFB0 ... |
... 00408B85 00333231 00409156 7C91DCBA ... |
RETURN dopo Strcopy BUFF RETURN dopo prova |
... 0012FFA8 0012FFAC 0012FFB0 ... |
... 00333231 00409156 7C91DCBA ... |
BUFF RETURN dopo prova |
La prossima istruzione sarà quindi quella puntata dall' EIP all'indirizzo 00408B85, dove ritroviamo la prosecuzione della procedura prova:
Procedura prova
...
end;
00408B85 5A pop edx
00408B86 C3 ret
Il RET della procedura di prova infine ci riporta al corpo principale del nostro programma ( indirizzo 00409156):
writeln('test ok, premi invio per terminare');
00409156 A1F0A94000 mov eax,[$0040a9f0]
0040915B BABC914000 mov edx,$004091bc
00409160 E8D7B7FFFF call @Write0LString
00409165 E89EA4FFFF call @WriteLn
0040916A E86D9CFFFF call @_IOTest
readln;
0040916F A15CAA4000 mov eax,[$0040aa5c]
00409174 E877A2FFFF call @ReadLn
00409179 E85E9CFFFF call @_IOTest
exit;
0040917E EB28 jmp $004091a8
writeln('overflow'); //qui in teoria non ci deve mai arrivare
00409180 A1F0A94000 mov eax,[$0040a9f0]
00409185 BAE8914000 mov edx,$004091e8
0040918A E8ADB7FFFF call @Write0LString
0040918F E874A4FFFF call @WriteLn
00409194 E8439CFFFF call @_IOTest
readln;
00409199 A15CAA4000 mov eax,[$0040aa5c]
0040919E E84DA2FFFF call @ReadLn
004091A3 E8349CFFFF call @_IOTest
end.
004091A8 E883B2FFFF call @Halt0
Tutto bellissimo, tutto perfetto e vissero tutti felici e contenti. Ma se invece di una stringa di 3 caratteri + il carattere terminazione avessi avuto una stringa più grande?
Bene se così fosse ci troveremo di fronte ad un buffer overflow in scrittura! infatti la funzione StrCopy avrebbe continuato a scrivere anche al di fuori dei limiti imposti dalla variabile Buff grande solamente 4 byte.
Proviamo a mettere una stringa più grande lasciando il buffer della stessa misura:
prova('12345');
Il resto del codice sarà identico ma vediamo cosa scrive e dove scrive la funzione StrCopy:
Diamo una occhiata a quello che accade nello stack all'inizio della funzione e successivamente quando finisce.
CPU Stack
Indirizzo Valore ASCII Commenti
0012FFA4 00408B85 @. ; RETURN dopo StrCopy
0012FFA8 00408BB8 @. ; BUFF
0012FFAC 00409156 V@. ; RETURN dopo Prova
dopo il procedimento
CPU Stack
Indirizzo Valore ASCII Commenti
0012FFA4 00408B85 @. ; RETURN dopo StrCopy
0012FFA8 34333231 1234 ; BUFF
0012FFAC 00400035 5.@. ; RETURN dopo Prova
La fase di copia è terminata solo di fronte al carattere nullo della stringa "12345" (31 32 33 34 35 00) che avevamo inserito ed ha proseguito ad occupare il buffer fino a sovrascrivere la cella di memoria successiva.
Questo cosa comporta? In generale non è prevedibile a cosa può portare un buffer overflow, dato che ogni programma è diverso da un altro, ma in questo caso noi possiamo notare come il rientro della funzione StrCopy avvenga normalmente, ma quando dovrà essere eseguito il RET della funzione prova al posto dell'indirizzo precedentemente memorizzato nello stack (00409156) il programma salterà al nuovo indirizzo 00400035.
Cosa si sia a quell'indirizzo non ci interessa, infatti potrebbe bloccarsi l'esecuzione o creare un errore e quindi provocare la terminazione del programma, ma quello che è importante è capire come invece potremo usare questo meccanismo a nostro favore.
Ritornando al nostro codice sorgente originario potremo fare in modo da utilizzare il buffer overflow in modo da scrivere un indirizzo a noi utile e quindi saltare in alla parte di codice desiderata. In particolare noi vorremo saltare la parte di codice relativa alla stama a video della frase "test ok, premi invio per terminare" ed andare alla funzione che stampa la scritta "overflow".
prova('123'); ->
writeln('test ok, premi invio per terminare');
readln;
exit;
->writeln('overflow'); //qui in teoria non ci deve mai arrivare
readln;
Per fare questo dovremo sapere l'indirizzo a cui saltare. Da debugger vediamo che questo è 00409180
writeln('overflow'); //qui in teoria non ci deve mai arrivare
00409180 A1F0A94000 mov eax,[$0040a9f0]
00409185 BAE8914000 mov edx,$004091e8
0040918A E8ADB7FFFF call @Write0LString
0040918F E874A4FFFF call @WriteLn
00409194 E8439CFFFF call @_IOTest
readln;
00409199 A15CAA4000 mov eax,[$0040aa5c]
0040919E E84DA2FFFF call @ReadLn
004091A3 E8349CFFFF call @_IOTest
end.
004091A8 E883B2FFFF call @Halt0
cosa dovremo scrivere come parola chiave? Beh i primi 4 caratteri sono ininfluenti dato che verrebbero inseriti nella variabile buff correttamente, ma gli altri andrebbero a sovrascrivere il nostro indirizzo target.
Noi vogliamo venga scritto 00 40 91 80 quindi consirando l'ordine con cui verranno inseriti la parola chiave sarà "1234"+char($80)+char($91)+char($40)+char($00)
Modifichiamo il programma inserendo il comando:
prova('1234'+char($80)+char($91)+char($40)+char($00));
Diamo una occhiata a quello che accade nello stack all'inizio della funzione e successivamente quando finisce.
CPU Stack
Indirizzo Valore ASCII Commenti
0012FFA4 00408B85 @. ; RETURN dopo StrCopy
0012FFA8 00408BB8 @. ; BUFF
0012FFAC 00409156 V@. ; RETURN dopo Prova
dopo il procedimento
CPU Stack
Indirizzo Valore ASCII Commenti
0012FFA4 00408B85 @. ; RETURN dopo StrCopy
0012FFA8 34333231 1234 ; BUFF
0012FFAC 00409180 @. ; RETURN dopo Prova
Lanciando il programma vedremo che a video non comparirà più la scritta solita ma la scritta "overflow".
Questo procedimento potrebbe essere sfruttato per bypassare una password o mandare in crash il sistema, ma un fine molto più ardito è quello di far eseguire uno "shellcode" all'interno del programma, magari al fine di prendere possesso della sistema.





