By Manny Fernandez

October 21, 2026

The Anatomy of a Buffer Overflow Attack

A buffer overflow is one of the oldest bugs in computing, and somehow it is still with us. The 1988 Morris Worm used one. So did Code Red, SQL Slammer, and a long parade of CVEs that are still being published today. If you have ever wondered what people actually mean when they say “an attacker overflowed a buffer and took control of the machine,” this post walks through it from the ground up, no assembly degree required.

The goal here is understanding, not weaponization. We will look at why the bug exists, what it corrupts, and how modern systems defend against it, which is the part that actually matters when you are hardening something.

First, what is a buffer?

A buffer is just a fixed-size box of memory that a program sets aside to hold some data: a username, a filename, a packet of network traffic, whatever. When a developer writes something like “reserve room for 16 characters,” the program carves out exactly 16 bytes and hands back the address of the first one.

The box has a size. That is the whole story, and also the whole problem. The box knows how big it is, but in older languages like C and C++, the act of copying data into it does not automatically check whether the data fits. If you pour 40 bytes into a 16-byte box, the language will happily let you, and the extra 24 bytes land wherever they land.

That “wherever they land” is the heart of a buffer overflow. The spillover does not vanish. It overwrites whatever happened to be sitting in memory right next to the box.

Where buffers live: the stack

To understand why the spillover is dangerous, you need a rough mental model of the stack.

Every time a program calls a function, it creates a little scratchpad of memory called a stack frame. That frame holds the function’s local variables (including our buffers) and some critical bookkeeping. The most important piece of bookkeeping is the return address: a pointer that says “when this function finishes, jump back to here and keep running.”

Think of it like a bookmark. Function A calls function B. Before handing control to B, the program writes down “when you’re done, come back to this line in A.” B does its work, looks at the bookmark, and control flows back to exactly the right spot. This happens millions of times a second and it is completely invisible when everything behaves.

Here is the layout that matters, simplified:

  [ caller's data        ]   <- further up in memory
  [ saved return address ]   <- the "come back here" bookmark
  [ saved frame pointer  ]
  [ our buffer (16 bytes)]   <- data gets copied in here

Notice what sits just above our buffer: the return address. Hold that thought.

The bug in one picture

Here is the classic shape of a vulnerable function. This is illustrative, not a working exploit, it is the pattern that shows up in textbooks and secure-coding courses:

void handle_input(char *user_supplied) {
    char buf[16];
    strcpy(buf, user_supplied);   // copies until it hits a null byte
    // ... use buf ...
}

The flaw is strcpy. It copies characters from the source into buf and keeps going until it reaches the end of the source, not the end of buf. It has no idea the box is only 16 bytes. It was never told.

  • Feed it "hello" and everything is fine. Five bytes into a sixteen-byte box.
  • Feed it 40 bytes and the first 16 fill the box, then the next bytes keep writing upward into the saved frame pointer and the saved return address.

That is the overflow. The program did not crash at the moment of the bad copy. It happily corrupted its own bookmark and carried on, none the wiser.

Why a corrupted bookmark is a takeover

Now the payoff. When handle_input finishes, the CPU does what it always does: it reads the return address off the stack and jumps there.

Except the return address is no longer the legitimate bookmark. It got overwritten by the tail end of the oversized input. So the CPU jumps to whatever value those spillover bytes happened to form.

This is the pivot point that turns a memory bug into a security incident:

  • Best case: the overwritten address is garbage, points at unmapped memory, and the program crashes. That is a denial-of-service, annoying but contained.
  • Worst case: an attacker who controls the input controls those exact bytes, which means they control where the CPU jumps next. They have redirected the program’s execution. From there, decades of technique exist to turn “I control the instruction pointer” into running attacker-chosen behavior.

The crucial idea, the one to take away: data got treated as control. Input that was supposed to be a harmless string of characters reached into the machinery that decides what the program does next. The boundary between “stuff the user typed” and “instructions the CPU follows” was never enforced, so an attacker erased it.

It is not always the stack

Stack overflows are the classic teaching example because the return address makes the consequence so vivid. But the same root cause (writing past the end of an allocation) shows up in other places:

  • Heap overflows corrupt dynamically allocated memory and the hidden bookkeeping the allocator keeps between chunks. Different path, identical principle.
  • Integer overflows often lead to buffer overflows: a size calculation wraps around to a tiny number, a buffer is allocated too small, and the copy that follows overflows it.
  • Off-by-one errors write just a single byte too far. On the right architecture, one byte in the right place is enough.

Same disease, different organ.

How we defend against it

This is the part worth internalizing, because you can influence most of these in real systems. Modern defense is layered: no single mitigation is bulletproof, but stacked together they turn “reliable remote code execution” into “usually just a crash.”

1. Stack canaries

The compiler places a secret, random value, a canary, on the stack just below the return address. Before the function returns, it checks whether the canary is still intact. An overflow that reaches the return address has to trample the canary on its way there, so the altered value trips the check and the program aborts safely instead of jumping to attacker bytes. Named after the canary in the coal mine. Enabled with compiler flags like -fstack-protector-strong.

2. Non-executable memory (DEP / NX)

Historically attackers wrote their own code into the buffer and jumped to it. The NX bit (No-eXecute), marketed as DEP on Windows, lets the OS mark memory regions as data-only. The stack is data, so the CPU refuses to execute anything there. If the attacker’s injected bytes sit in a non-executable region, the jump fails. This killed the simplest form of the attack outright.

3. Address Space Layout Randomization (ASLR)

To hijack execution you usually need to know where things live in memory. ASLR shuffles the addresses of the stack, the heap, and loaded libraries every time the program runs. The attacker can no longer hardcode an address, because it is different on every launch. Combined with NX, this forces attackers into far more fragile, situational techniques.

4. Safe functions and bounds checking

The cleanest fix is the oldest advice: never copy without knowing the limit.

  • Replace strcpy with strncpy / strlcpy, sprintf with snprintf, and so on. The “n” variants take a maximum length.
  • Validate input length at the boundary, before it ever reaches a fixed buffer.
  • Treat every external input as hostile until proven otherwise.

5. Memory-safe languages

The most durable answer is to remove the footgun entirely. Languages like Rust, Go, Java, C#, and Python check array bounds automatically (or prove them safe at compile time). An out-of-bounds write becomes a clean, catchable error instead of silent memory corruption. This is why so much new systems software is being written in Rust, and why initiatives like CISA’s memory-safety roadmap push vendors in that direction. You cannot overflow a buffer that refuses to be overflowed.

6. Compiler and OS hardening, for free

Modern toolchains bundle a lot of this. Turning on -D_FORTIFY_SOURCE=2, -fstack-protector-strong, and building as a Position Independent Executable (PIE) gives you canaries, fortified libc calls, and full ASLR coverage with no code changes. If you ship C or C++, these should be non-negotiable defaults in your build.

The practitioner takeaway

A buffer overflow is not magic. It is one small failure, a copy that did not check its destination size, with an outsized blast radius because of what happens to sit next to the buffer in memory. The spillover corrupts a control value, and control values are supposed to be sacred.

Everything defensive flows from two instincts:

  1. Enforce the boundary between data and control. Length-check at every input edge. Assume the input is adversarial.
  2. Stack your mitigations. Canaries, NX, ASLR, fortified builds, and memory-safe languages each close a different door. Attackers need all the doors open at once; you only need to keep one shut.

If you are auditing your own code, the fastest win is to grep for the unbounded copy functions and the places where external input meets a fixed-size buffer. That is where this bug lives, and it has been living there, patiently, since 1988.

Recent posts

  • If you've spent any time configuring user authentication on... Full Story

  • DNS is one of those technologies that quietly underpins... Full Story

  • BGP issues on FortiGate firewalls usually trace back to... Full Story

  • Every time your laptop talks to your router, a... Full Story

  • If you've spent any time configuring NAT on a... Full Story

  • If you have spent any time configuring firewall policies... Full Story

  • High availability on FortiGate is one of those features... Full Story

  • If you've configured SD-WAN on a FortiGate, you've almost... Full Story

  • FortiLink is the management protocol that turns a FortiSwitch... Full Story

  • FortiSwitches are pretty rock solid from Mean Time Between... Full Story

  • This is a quicky tip.  Have you ever gone... Full Story

  • DNS is one of those quiet pieces of internet... Full Story

  • This article is an updated version of the previous... Full Story

  • You will add ns2 as a secondary (slave) BIND9... Full Story

  • In the process of deploying my lab, I needed... Full Story

  • RFC 8805, used to be known as Self-Correcting IP... Full Story

  • Years back, I wrote an article about certificate pinning. ... Full Story

  • FortiGates have the ability to send alerts to Microsoft... Full Story

  • In this post, I am going to walk through... Full Story

  • Troubleshooting VoIP on a FortiGate can feel like trying... Full Story

  • Prior to FortiOS 7.0, there were three commands to... Full Story

  • In this post, I am going to go over... Full Story

  • What we are going to do:  We are going... Full Story

  • Choosing between FGCP (FortiGate Clustering Protocol) and FGSP (FortiGate... Full Story

  • Creating a VLAN on macOS (The "Pro" Move) A... Full Story

  • This blog post explores the logic behind how macOS... Full Story

  • Pretty Fly for a Wi-Fi Tell My Wi-Fi Love... Full Story

  • Part of my daily gig is creating BoMs (Bill-of-Materials)... Full Story

  • ICMP introduces several security risks, but careful filtering, rate... Full Story

  • The command diag debug application dhcps -1 enables full... Full Story

  • In the world of FortiOS, execute tac report is... Full Story

  • LLDP; What is it The Link Layer Discovery Protocol... Full Story

  • What it actually does When you run diagnose fdsm... Full Story

  • Monkey Bites are bite-sized, high-impact security insights designed for... Full Story

  • I have run macOS in macOS with Parallels but... Full Story

  • Don't be confused with my other FortiNAC posts where... Full Story

  • This is the third session in a multi-part article... Full Story

  • Today I was configuring key-based authentication on a FortiGate... Full Story

  • Netcat, often called the "Swiss Army knife" of networking,... Full Story

  • At its core, IEEE 802.1X is a network layer... Full Story

  • In case you did not see the previous FortiNAC... Full Story

  • This is our 5th session where we are going... Full Story

  • Now that we have Wireshark installed and somewhat configured,... Full Story

  • The Philosophy of Packet Analysis Troubleshooting isn't about looking... Full Story

  • No fluff. Just the config that works. Executive Summary... Full Story

  • FortiGate DDNS: config system ddns Explained No fluff. Just... Full Story

  • Executive Summary Objective: Get you from zero to writing... Full Story