#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
char *gets(char *);
void complete_level() {
printf("Congratulations, you've finished stack3 :-) Well done!\n");
exit(0);
}
void start_level() {
char buffer[64];
gets(buffer);
}
int main(int argc, char **argv) {
printf("Welcome to stack4\n");
// la sfida si chiama stack4, ma per noi è il terzo caso di studio
start_level();
}
C
Come al solito, c'è un buffer e una funzione vulnerabile: gets()
.
Notiamo che la funzione complete_level
non viene chiamata dalla funzione start_level
invocata dal main
.
Per cui quando si entra nella funzione start_level
viene allocato spazio sullo stack per un array di caratteri. Sappiamo che prima di entrare nella funzione start_level
viene salvato sullo stack il vecchio valore del frame pointer e il return address. Possiamo provare a riempire il buffer e accedere al campo di return address e inserire l'indirizzo in cui viene eseguita la funzione complete_level
.
complete_level
Eseguiamo il debugger gdb
e gli passiamo il programma e richiediamo la stampa dell'indirizzo della funzione complete_level
:
Adesso abbiamo l'indirizzo (non in formato little endian) della funzione complete_level
.
Il prossimo passo è eseguire il codice e controllare lo stato della memoria, ovviamente non ci basta eseguire il codice normalmente, poiché non abbiamo modo di dare uno sguardo alla memoria. Usiamo sempre gdb
.
Quello che facciamo è disassemblare
la funzione start_level
.
Anzitutto possiamo notare, che viene messo ebp
sullo stack: viene salvato il frame pointer.ebp
viene fatto puntare a esp
.esp
viene esteso due volte, una volta di 64 byte (0x48
) e un'altra di 12 byte (0xc
).
Poi vediamo che vi è l'istruzione lea -0x48(%ebp), %eax
, la funzione lea
calcola un indirizzo a partire dall'operando -0x48(%ebp)
in questo caso, non fa altro che calcolare l'indirizzo che viene fuori quando si sottraggono 0x48
byte a ebp
e lo mette in eax
.
Tale indirizzo viene poi messo sullo stack e viene chiamata gets
per cui il punto da cui si cominciano a copiare i byte che prende gets
partono proprio da ebp - 0x48 (72 byte)
, ricordiamo che da quel punto i byte saranno copiati andando verso indirizzi più alti, quindi verso sopra. Proprio sopra quei 72 byte, c'è il vecchio frame pointer, e sopra di esso ci dovrebbe essere il return address posizionato sullo stack dal main
. Quindi sappiamo che ebp
si trova a 72 byte di distanza dall'inizio del buffer, inoltre sappiamo che nel punto in cui punta attualmente ebp
c'è il vecchio valore del frame pointer e che sopra di esso +4 byte
dovrebbe essere il return address, per cui il return addresso dovrebbe trovare a 76 byte
di distanza dal punto in cui comincia il buffer.
Per verificare dove si trova tale indirizzo, eseguiamo il debug di questo programmino con gdb
, prepariamo un file da dare in pasto al programma.
Piazziamo un breakpoint proprio alla fine della funzione start_level
, guardando l'immagine che riguarda il suo disassemblamento, ci riferiamo alla riga <+21>
Adesso continuiamo l'esecuzione utilizzando sempre il comando run
, ma per passare in input anche il file creato prima utilizziamo il comando nel seguente modo:
Per avere una maggiore sicurezza del calcolo fatto prima, usiamo gdb
, per vedere dove si trova ebp
:
questo non basta a confermare il calcolo fatto precedentemente, allora stampiamo lo stato della memoria a parte da esp
(la cima dello stack):
come abbiamo detto più volte, l'inizio del buffer sta più in basso, mentre la fine sta più in alto.gdb
ci ha detto che ebp
si trova a 0xbfffef38
. Come si vede viene indicata una linea di memoria 0xbfffef30
, poi i 4 byte adiacenti, poi gli altri 4 byte adiacenti e così via. Proprio in quella linea dopo 8 byte 0xbfffef38
c'è ebp
, i quattro byte adiacenti all'indirizzo di ebp
è l'indirizzo di old_ebp
quindi dopo c'è il return address. Dopo l'ultimo 0x41414141
(riquadro più a destra, in rosso), ci sono 3 gruppetti di 4 byte che in totale sono 12 byte
. Il buffer è 64 byte
, per cui 64 + 12 = 76
, dobbiamo riempire il buffer e in più altri 12 byte, dopo di questo piazzeremo l'indirizzo della funzione complete_level
in little endian usando python, creiamo il file:
nello script che abbiamo creato abbiamo scelto di utilizzare due locazioni in cui mettere il return address, una dopo 72 byte e l'altra dopo 76, il motivo è che lo stack potrebbe essere più corto di quanto effettivamente quando viene debuggato con gdb
.