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)
- 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).
- Generate a cyclic pattern (e.g.,
- Confirm control
- Build payload
padding + new_ret
. Choose an address that’s unmistakable (e.g.,greet
or a dummy0x41414141
to prove reach).
- Build payload
- Turn NX to your advantage
- Instead of raw shellcode, go ret2libc (e.g.,
system("/bin/sh")
). On x64 SysV: chainpop rdi; ret
→ pointer to"/bin/sh"
→system
→exit
.
- Instead of raw shellcode, go ret2libc (e.g.,
- 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.
- Compile
- Polish
- Clean alignment (x64 often wants
ret
sleds to keep 16-byte alignment). - Prefer short gadgets (fewer bad chars, fewer side effects).
- Clean alignment (x64 often wants
Code-reuse under modern defenses
DEP/NX blocks executing injected bytes on the stack. So we reuse what’s there:
-
ret2libc: call existing libc routines. Canonical chain:
[padding][pop rdi; ret][&"/bin/sh"][system][exit]
-
ROP/JOP: stitch short gadgets to set registers, flip protections (
mprotect
on Linux,VirtualProtect
on Windows), or pivot the stack to a controlled area. - Stack pivot: move RSP to a big attacker buffer (
xchg rsp, rax
,leave; ret
,mov rsp, rdx
, etc.) when the overflowed frame is too cramped.
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)
- Find the bug: overflow, but also look for leaks (format string, uninitialized, OOB-read).
- Crash with a pattern → compute exact offset to saved return address.
- Prove control: redirect to a known function/sentinel.
- Plan the chain: ret2libc or ROP; account for alignment/shadow space; prep a pivot if needed.
- Bypass mitigations: leak → defeat ASLR/PIE; use ROP (DEP); keep canary intact or leak it first.
- Stabilize: get a shell/reverse TCP; clean up handles; avoid noisy crashes.
- Document: root cause, reproducer, mitigations, and a minimal patch (length checks, safer API, tests).
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.