Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
A stack buffer overflow is the archetypal memory-corruption bug: a program copies attacker-influenced data into a fixed-size local buffer without bounding the length, and the write runs off the end of the buffer into adjacent stack memory. Because the call stack stores not just data but *control* data — most importantly the saved return address that the ret instruction will pop into the instruction pointer — an overflow that reaches far enough lets an attacker overwrite where the function returns to. That is the pivot from a crash (a segmentation fault) to control (the CPU executing an address of the attacker’s choosing).
This bug class is old — the 1988 Morris worm and the 1996 Phrack article “Smashing the Stack for Fun and Profit” both turn on it — yet it remains relevant because C and C++ still ship functions with no length awareness (gets, strcpy, strcat, sprintf, unbounded scanf("%s")), and because the modern defenses that were built in response (NX, ASLR, stack canaries, PIE, RELRO) are each defeatable in the wrong configuration. Understanding the primitive precisely — what gets overwritten, in what order, and which mitigation stops which step — is the foundation for every later technique such as ret2libc and ROP.
Attack Prerequisites
A stack overflow is exploitable to code execution when the following line up. Not all are strictly required for a crash or a limited overwrite, but full control generally needs them:
- A write past a stack buffer’s bounds — an unbounded copy (
gets,strcpy,memcpywith an attacker-controlled length,readinto a too-small buffer) driven by input the attacker supplies. - Enough overflow length to reach the saved return address (and, on many layouts, the saved frame pointer that sits just below it).
- No intact stack canary between the buffer and the saved return address, or a way to leak/avoid it — otherwise
__stack_chk_failaborts beforeretexecutes. - A way to know or reach a useful target address — either the binary/libc is at a known address (no PIE / no ASLR), or you have an information leak to defeat ASLR, or the overflow itself only needs relative control.
How It Works
On x86-64 (System V ABI), a typical function prologue pushes the caller’s base pointer (push rbp; mov rbp, rsp) and then subtracts from rsp to reserve space for locals. The stack grows *down* (toward lower addresses), but a char buffer is written *up* (toward higher addresses). So a local char buf[64] lives below the saved rbp, which lives below the saved return address at [rbp+8]. Writing more than 64 bytes into buf marches upward through the saved rbp and into the saved return address. When the function reaches its epilogue (leave; ret), ret pops that (now attacker-controlled) 8-byte value into rip, and execution continues wherever the attacker wrote.
The classic 1990s exploit filled the buffer with shellcode and overwrote the return address with a stack address pointing back into that shellcode (often via a NOP sled to absorb address imprecision). Two mitigations broke that specific recipe. DEP/NX (the No-eXecute page permission bit) marks the stack non-executable, so even with rip pointing at your shellcode the CPU faults on the first instruction fetch — this is what pushes attackers to code-reuse techniques like ret2libc and ROP. ASLR randomizes the base addresses of the stack, heap, mmap/libc, and (with PIE) the executable itself, so you can no longer hardcode the address of your shellcode or of a libc function; you need an information leak first.
The most targeted mitigation for this exact bug is the stack canary (also called a stack cookie or, in GCC, -fstack-protector). The compiler inserts a random guard value between the local buffers and the saved rbp/return address in the prologue, and re-checks it in the epilogue before ret. On Linux/glibc x86-64 the canary is read from thread-local storage at %fs:0x28; a mismatch calls __stack_chk_fail, which prints “stack smashing detected” and aborts. A linear overflow that reaches the return address must pass *through* the canary, so it will corrupt it and trip the check — unless the attacker can leak the canary value first (e.g. via a separate format-string or read primitive) and splice the correct value back into the payload, or the overflow is non-linear (an index write) that skips over it.
Vulnerable Code: What Makes It Exploitable
The canonical offender is gets, which reads a line with no bound whatsoever — there is no argument that could limit it, which is why it was removed from C11. Any input longer than the buffer overflows the stack:
#include <stdio.h>
void vuln(void) {
char buf[64]; // 64-byte local; saved RIP sits above it
gets(buf); // VULNERABLE: no length limit at all
printf("got: %s\n", buf);
}
int main(void) {
vuln(); // returns THROUGH the overwritten saved RIP
return 0;
}
Cstrcpy is the same defect with a source that merely has to be longer than the destination. Here a fixed 64-byte stack buffer receives an attacker-controlled argv[1] of arbitrary length:
#include <string.h>
void copy_name(char *input) {
char name[64];
strcpy(name, input); // VULNERABLE: copies until input's NUL,
// not until name is full
}
// copy_name(argv[1]) with a 200-byte argv[1] overruns 'name',
// clobbering the saved frame pointer and saved return address.
CTo build a working exploit you compile a deliberately weakened target so the mechanics are visible before layering mitigations back on. This disables the canary, disables PIE, and marks the stack executable purely for study:
# Study build: no canary, no PIE, executable stack, no NX warnings
gcc -fno-stack-protector -no-pie -z execstack -g -o vuln vuln.c
# Inspect the mitigations actually present in a binary:
checksec --file=./vuln
# RELRO / STACK CANARY / NX / PIE columns tell you what you must defeat.BashWalkthrough: From Crash to Controlled RIP
First, find the exact offset from the start of the buffer to the saved return address. A De Bruijn (cyclic) pattern makes this deterministic: send it, let the program crash, read the value that landed in rip/rsp, and look it up. pwntools provides cyclic:
# Generate a non-repeating pattern and feed it to the crashing binary.
python3 -c 'from pwn import *; print(cyclic(200).decode("latin1"))' > pat
gdb -q ./vuln
pwndbg> run < pat
# Program received signal SIGSEGV.
pwndbg> cyclic -l $rsp # or feed the faulting value; prints the offset
# e.g. -> 72 (64-byte buf + 8-byte saved rbp = 72 to the saved RIP)BashWith the offset known (72 here), the payload is offset bytes of filler followed by the 8-byte target address in little-endian. Against the study build (executable stack, known address) you can jump to shellcode placed in the buffer; the more portable demonstration is simply to prove rip control by redirecting to an existing function such as a win() symbol. pwntools handles the packing and I/O:
from pwn import *
e = ELF('./vuln')
io = process('./vuln')
offset = 72
target = e.symbols['win'] # or a stack address to shellcode
payload = b'A' * offset
payload += p64(target) # overwrites saved RIP -> win()
io.sendline(payload)
io.interactive() # if win() spawns a shell, you're inPythonOn x86-64 one practical wrinkle appears the moment you call into glibc: functions such as system execute SSE instructions (movaps) that fault unless rsp is 16-byte aligned at the call. If your redirect lands on an odd number of 8-byte words you will see a crash *inside* libc rather than a shell; the fix is to prepend a single bare ret gadget to re-align the stack. This is the bridge to the next article: once NX is on, you can no longer run bytes on the stack, and control of rip must be spent on chaining existing code.
Note: The saved frame pointer matters even when you do not fully reach the return address. A partial overflow that overwrites only the low byte(s) of the saved
rbpis the classic “off-by-one”/frame-pointer overwrite: on the *next* return the corruptedrbpshifts the stack frame, which can hand you control one call later. Alignment and the exact buffer-to-saved-RIP distance also depend on compiler padding — always measure the offset with a cyclic pattern rather than assumingsizeof(buf).
Opsec: A failed overflow is loud: canary trips emit “* stack smashing detected *” and a SIGABRT, and repeated SIGSEGVs on a network service produce crash loops, core dumps, and (on modern Linux) systemd-coredump / ABRT records. When brute-forcing an offset or a partial address against a forking server, expect noisy logs and consider that each crash may reseed or not reseed ASLR depending on whether the server re-execs.
Detection and Defense
The reliable fixes are to stop the out-of-bounds write at the source and to keep every compiler and OS mitigation enabled so that any residual bug is much harder to weaponize:
- Eliminate the unbounded APIs — never use
gets; replacestrcpy/strcat/sprintfwith length-bounded forms (strncpywith explicit NUL termination,snprintf, or safer wrappers) and bound everyread/scanf. - Stack canaries — build with
-fstack-protector-strong(or-fstack-protector-all); it detects linear overwrites of the return address at epilogue time. - DEP/NX — ensure the stack and heap are non-executable (do not link with
-z execstack); this alone removes the shellcode-on-stack path. - ASLR + PIE + Full RELRO — compile position-independent (
-fPIE -pie), keep kernel ASLR on (/proc/sys/kernel/randomize_va_space = 2), and use-Wl,-z,relro,-z,nowso the GOT is read-only, denying easy follow-on targets. - Modern hardening —
_FORTIFY_SOURCE=2/3swaps in bounds-checked builtins at compile time; compiler/kernel shadow-stack schemes (Intel CET / shadow stack) validate the return address independently of the canary. - Detection — compile-time
-Wall -Wextraand static analyzers flag the sinks; at runtime, ASan/Valgrind catch overflows in testing, and crash-telemetry (core dumps, ABRT, EDR memory-violation events) surfaces exploitation attempts in production.
Real-World Impact
Stack overflows have driven landmark incidents for over three decades: the 1988 Morris worm spread partly through a stack overflow in the BSD fingerd daemon (which used gets), and network-facing C services have produced a steady stream of remotely triggerable overflows since. They remain common in embedded firmware, IoT, and legacy C/C++ codebases where the vulnerable string functions persist and where canaries, NX, or ASLR are frequently missing or weakly implemented. On such targets the crash-to-control path described here is often still nearly this direct.
Conclusion
A stack buffer overflow turns a length mistake into control of the saved return address, and therefore of rip. The mechanics are precise and worth internalizing: locals sit below the saved frame pointer and return address, the write grows toward them, and ret is the instruction that converts a corrupted stack word into hijacked execution. Each modern mitigation blocks a specific step — canaries guard the return address, NX blocks stack shellcode, ASLR/PIE hide the addresses — which is exactly why real exploitation chains them together with leaks and code reuse, the subject of the next articles.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments