Stack Buffer Overflows in 2025 — A Practitioner’s Guide

TL;DR

We’ll build intuition for stack frames, show how a tiny bug becomes control of EIP/RIP, then layer in modern exploitation techniques (ret2libc/ROP, stack pivots) and hardening (canaries, DEP/NX, ASLR, CFG/CET, toolchain flags, sanitizers). Short, dense, and field-ready.

What a stack buffer overflow really is

A stack buffer overflow is an out-of-bounds write on the stack that corrupts adjacent data (saved EBP/RBP, saved return address, stack canary, locals). It’s distinct from “heap overflows” (different allocator primitives) and from “overreads” (info leaks). On 32-bit we usually talk EIP/EBP; on 64-bit, RIP/RBP.

Impact buckets - Availability: crash via smashed frame - Integrity: redirect control flow (overwrite saved return address / structured metadata) - Confidentiality: turn a small write into a big leak (e.g., format string → canary/ASLR bypass)


Stack frame anatomy (x86 & x64, fast but precise)

x86 (cdecl)
Typical prologue:

push ebp
mov  ebp, esp
sub  esp, <locals>

Layout (high → low): arguments → return address → saved EBP → locals. Access is stable via [ebp+offset] / [ebp-offset].

x64 notes worth your time
- System V AMD64 (Linux/Unix): RSP/RBP, 16-byte stack alignment at call boundaries, a 128-byte red zone below RSP (non-Windows) used by leaf functions. ROP chains must keep alignment or certain calls crash.
- Windows x64: shadow space (32 bytes) reserved by caller before call, common gadgets rely on it. CFG + CET are more prevalent here.

Mental model The stack grows downward. The saved return address sits just above the saved frame pointer. Anything that writes past a local array can trample those fields.


Minimal vulnerable program → from crash to control

Vulnerable sample (Linux)

// build: gcc vuln.c -o vuln -fno-stack-protector -z execstack -no-pie
#include <stdio.h>
#include <string.h>

void greet(void) { puts("hello"); }

void vuln(void) {
    char buf[64];
    puts("Say something:");
    gets(buf);            // classic overflow: no bounds check
    printf("%s\n", buf);
}

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

Workflow (compact, reproducible)

  1. Crash & measure offset
    • Generate a cyclic pattern (e.g., gef/peda): run, feed 300 bytes, note EIP/RIP at crash, compute offset (the exact index that overwrites return address).
  2. Confirm control
    • Build payload padding + new_ret. Choose an address that’s unmistakable (e.g., greet or a dummy 0x41414141 to prove reach).
  3. Turn NX to your advantage
    • Instead of raw shellcode, go ret2libc (e.g., system("/bin/sh")). On x64 SysV: chain pop rdi; ret → pointer to "/bin/sh"systemexit.
  4. Deal with ASLR/PIE
    • Compile -no-pie or leak first (format string, uninitialized read). With PIE/ASLR, you generally need an info leak to derive libc/base addresses.
  5. Polish
    • Clean alignment (x64 often wants ret sleds to keep 16-byte alignment).
    • Prefer short gadgets (fewer bad chars, fewer side effects).

Code-reuse under modern defenses

DEP/NX blocks executing injected bytes on the stack. So we reuse what’s there:

Windows quick hits - Call VirtualProtect to mark RWX, then jump into your buffer. - Respect shadow space, or your call chain misbehaves. - Expect CFG (Control Flow Guard) and increasingly CET (shadow stacks).


Real-world case study: CVE-2017-11882 (Equation Editor)

Why it’s a great teaching example:
- It’s a stack-based overflow in an old Office component (EQNEDT32) shipped for years.
- The binary lacked modern hardening in many deployments, making control-flow hijack straightforward.
- Attackers used crafted Office documents to trigger the bug and execute arbitrary code.

Takeaways
- Legacy components lag behind platform defenses.
- Once you have EIP/RIP control, the rest is standard playbook: find ROP gadgets or call Win32 APIs to run payloads.
- Defense in depth matters: even a single old component can sink the ship.


Secure coding patterns (pragmatic)

Unsafe → safer (and why)

Risky API Safer pattern (examples) Why it’s better
gets, strcpy, strcat, plain sprintf fgets, snprintf, length-checked memcpy/memmove Enforces buffer bounds at the call site
strncpy as a “safe strcpy” Prefer snprintf or ensure manual NUL termination strncpy may omit '\0' and silently truncate
printf(user) printf("%s", user) Avoids format-string leaks/ writes
manual char arrays std::string / std::vector (C++), or Rust Safer ownership & automatic sizing

“Safe” APIs aren’t magic
Mis-sized destinations, wrong length calculations, or off-by-one mistakes still overflow.


Modern mitigations you’ll actually meet

Compiler / Linker
- Canaries: -fstack-protector-strong (GCC/Clang) or /GS (MSVC).
- PIE/ASLR: -fPIE -pie (ELF) + OS ASLR; /DYNAMICBASE (PE).
- RELRO: -Wl,-z,relro,-z,now to lock GOT early.
- _FORTIFY_SOURCE=2: inline bound checks for known sizes.
- CFGuard (/guard:cf) on Windows.

OS / CPU
- DEP/NX: non-executable stacks/heaps.
- ASLR: randomized bases for stack/heap/libs.
- CET / Shadow Stack: hardware-enforced return address integrity (increasingly common on modern Windows/Intel).
- CFG: limits indirect calls/jumps to valid targets.

Testing / Triage
- ASan/UBSan: catch OOB writes and use-after-… during tests.
- Fuzzing: libFuzzer, AFL++ to exercise edge cases.
- Crash policy: fail-fast on canary hit; rate-limit repeated faults (thwarts canary brute-force).

Reference hardening sets

# Linux hardening (release builds)
CFLAGS="-O2 -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE"
LDFLAGS="-Wl,-z,relro,-z,now -pie"

# Linux debug (sanitizers)
CFLAGS="-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer"
LDFLAGS="-fsanitize=address,undefined"

A compact exploitation checklist (from bug to shell)


Takeaway

Overflows are simple bugs that become exploits only when paired with precise layout knowledge and mitigation-aware chaining. Master the frame, measure the offset, plan your chain, and keep one eye on the defender’s toolchain.