security|March 26, 2026|12 min read

Buffer Overflow Attacks — How Memory Corruption Actually Works

TL;DR

Buffer overflows corrupt adjacent memory by writing past a buffer's boundary. Stack overflows overwrite the return address to hijack execution. Heap overflows corrupt metadata or adjacent objects. Modern defenses (ASLR, NX/DEP, stack canaries) make exploitation harder but not impossible — ROP chains bypass NX, and info leaks bypass ASLR. The real fix is memory-safe languages like Rust.

Buffer Overflow Attacks — How Memory Corruption Actually Works

Buffer overflows are the oldest and most consequential vulnerability class in computing. The Morris Worm in 1988 used a buffer overflow. Code Red in 2001 — buffer overflow. Heartbleed in 2014 — buffer over-read. EternalBlue (WannaCry ransomware) in 2017 — buffer overflow.

After 35+ years, we’re still writing code that lets attackers corrupt memory and take over systems. This article explains exactly how that happens, at the memory level, with real code.

What Is a Buffer Overflow?

A buffer is a contiguous block of memory allocated to hold data — an array, a string, a struct. A buffer overflow occurs when a program writes more data to a buffer than it can hold. The excess data spills into adjacent memory, corrupting whatever lives there.

In C and C++, there are no automatic bounds checks. If you allocate 64 bytes and write 128, the language lets you. The hardware doesn’t stop you. The OS doesn’t stop you (usually). You just wrote over whatever came after your buffer in memory.

What lives “after your buffer” depends on where the buffer was allocated — the stack or the heap. That determines what gets corrupted and how an attacker can exploit it.

The Stack — How Functions Work in Memory

To understand stack buffer overflows, you need to understand how the stack works.

Every time a function is called, a stack frame is created containing:

  1. Arguments passed to the function
  2. Return address — where to jump back when the function returns
  3. Saved base pointer — the caller’s frame pointer
  4. Local variables — including any buffers declared in the function

The stack grows downward in memory (from high addresses to low). But buffers fill upward (from low to high). This means a buffer overflow in a local variable writes upward toward the return address.

Stack Buffer Overflow

Stack Buffer Overflow — Step by Step

Here’s a vulnerable C program:

#include <stdio.h>
#include <string.h>

void vulnerable_function(char *input) {
    int authenticated = 0;
    char buffer[64];

    // VULNERABLE: no bounds checking
    strcpy(buffer, input);

    if (authenticated) {
        printf("Access granted!\n");
        // ... give admin access ...
    }
}

int main(int argc, char *argv[]) {
    if (argc > 1) {
        vulnerable_function(argv[1]);
    }
    return 0;
}

The stack layout when vulnerable_function is called:

High addresses
┌──────────────────────────┐
│  argv[1] (pointer)       │  ← function argument
├──────────────────────────┤
│  Return Address          │  ← where to go after function returns
├──────────────────────────┤
│  Saved EBP               │  ← caller's base pointer
├──────────────────────────┤
│  authenticated (int = 0) │  ← local variable
├──────────────────────────┤
│  buffer[64]              │  ← local buffer (64 bytes)
│  ...                     │
│  (grows upward →)        │
└──────────────────────────┘
Low addresses

Attack 1: Variable Overwrite

If the attacker provides 65+ bytes of input, strcpy writes past buffer[64] into authenticated:

# 64 bytes of 'A' fill the buffer, 65th byte overwrites 'authenticated'
./vulnerable $(python3 -c "print('A' * 68)")
# Output: Access granted!

The authenticated variable (which was 0) is now overwritten with 0x41414141 (“AAAA”), which is non-zero, so the if (authenticated) check passes.

Attack 2: Return Address Overwrite

More dangerous: overflow enough to reach the return address.

buffer[64]  →  64 bytes of padding
authenticated →  4 bytes of padding
saved EBP    →  4 bytes of padding
return addr  →  4 bytes the attacker controls

Total: 64 + 4 + 4 = 72 bytes of padding, then the next 4 bytes overwrite the return address.

# Overwrite return address with 0xDEADBEEF
./vulnerable $(python3 -c "print('A' * 72 + '\xef\xbe\xad\xde')")
# Segfault — but the CPU tried to jump to 0xDEADBEEF

If the attacker points the return address to their own shellcode (also injected via the buffer), they get arbitrary code execution.

Classic Shellcode Injection

In the pre-mitigation era, the attack was straightforward:

┌─────────────────────────────────────────────────┐
│ NOP sled (0x90 × N) │ Shellcode │ Padding │ RET │
└─────────────────────────────────────────────────┘
                                              │
                                              ▼
                                    Points back to NOP sled
// Classic x86 Linux shellcode — spawns /bin/sh (45 bytes)
// This is the payload that goes into the buffer
"\x31\xc0\x50\x68\x2f\x2f\x73\x68"
"\x68\x2f\x62\x69\x6e\x89\xe3\x50"
"\x53\x89\xe1\xb0\x0b\xcd\x80"

The NOP sled (0x90 = no-operation) is a landing pad — the return address doesn’t need to be exact, just anywhere in the NOP sled, and execution slides down to the shellcode.

Heap Buffer Overflow

The heap is where dynamically allocated memory lives (malloc, new, calloc). Heap overflows are harder to exploit but equally dangerous.

#include <stdlib.h>
#include <string.h>

struct user {
    char name[64];
    int is_admin;
};

void process_user(char *input) {
    // Heap-allocated struct
    struct user *u = malloc(sizeof(struct user));
    u->is_admin = 0;

    // VULNERABLE: no bounds check
    strcpy(u->name, input);

    if (u->is_admin) {
        printf("Admin access granted to %s\n", u->name);
    }

    free(u);
}

Because name and is_admin are adjacent in the struct, overflowing name overwrites is_admin:

# 64 bytes fill name, next bytes overflow into is_admin
./heap_vuln $(python3 -c "print('A' * 68)")
# Output: Admin access granted to AAAA...

Heap Metadata Corruption

More advanced heap exploits target the heap allocator’s internal metadata. malloc uses linked lists to track free chunks:

Heap layout:
┌──────────────────┐
│ chunk header      │ ← size, prev_size, fd/bk pointers
├──────────────────┤
│ user data (64B)   │ ← your buffer
├──────────────────┤
│ chunk header      │ ← next chunk's metadata
├──────────────────┤
│ user data         │ ← next allocation
└──────────────────┘

If an overflow corrupts the next chunk’s header — particularly the fd (forward) and bk (back) pointers — the attacker can trick free() into writing an arbitrary value to an arbitrary address. This is the classic unlink exploit.

Integer Overflow Leading to Buffer Overflow

Some of the most subtle memory bugs come from integer overflows in size calculations:

void process_items(int count, char *data) {
    // Integer overflow: if count = 1073741824 (2^30)
    // then count * 4 = 4294967296 = 0 (wraps on 32-bit)
    size_t total_size = count * sizeof(int);

    // Allocates 0 bytes (or very small buffer)
    int *buffer = malloc(total_size);

    // But copies count * 4 bytes — massive overflow
    memcpy(buffer, data, count * sizeof(int));
}
count = 1073741824  (0x40000000)
count * 4 = 4294967296 (0x100000000) → wraps to 0 on 32-bit

malloc(0) returns a small valid pointer
memcpy copies 4GB of data into a tiny buffer → catastrophic overflow

Safe pattern:

// Check for overflow BEFORE the multiplication
if (count > SIZE_MAX / sizeof(int)) {
    // Overflow would occur — reject
    return -1;
}
size_t total_size = count * sizeof(int);
int *buffer = malloc(total_size);
if (!buffer) return -1;

Or use compiler builtins:

size_t total_size;
if (__builtin_mul_overflow(count, sizeof(int), &total_size)) {
    return -1; // Overflow detected
}

Format String Attacks

Not technically a buffer overflow, but a closely related memory corruption bug. When user input is passed directly as the format string to printf:

// VULNERABLE — user controls the format string
printf(user_input);

// SAFE — user input is an argument, not the format
printf("%s", user_input);

If user_input is "%x %x %x %x", printf reads values off the stack (information leak). If user_input contains %n, printf writes to memory — the number of bytes printed so far is written to an address on the stack.

Input: "AAAA%08x.%08x.%08x.%08x.%n"

printf reads 4 stack values with %08x
Then %n writes the number of bytes printed to the address 0x41414141 (AAAA)

With careful construction, attacker gets arbitrary memory write.

Buffer Overflow Types

Modern Defenses

The exploit techniques above work on unprotected systems. Modern operating systems and compilers implement multiple layers of defense.

Defense Layers

Defense 1: Stack Canaries (Stack Protector)

The compiler inserts a random value (“canary”) between the buffer and the return address. Before the function returns, it checks if the canary was modified.

Stack with canary:
┌──────────────────┐
│  Return Address   │
├──────────────────┤
│  Saved EBP        │
├──────────────────┤
│  CANARY (random)  │ ← checked before return
├──────────────────┤
│  Local variables  │
├──────────────────┤
│  Buffer[64]       │
└──────────────────┘

If buffer overflow corrupts the canary:
  → Function detects it before returning
  → Process aborts with "*** stack smashing detected ***"
  → Attacker never gets code execution

Enabled by default in GCC and Clang:

# Compile with stack protection (default in most distros)
gcc -fstack-protector-strong -o program program.c

# Compile WITHOUT protection (for testing/education only)
gcc -fno-stack-protector -o program program.c

# Maximum protection
gcc -fstack-protector-all -o program program.c

Bypass: If the attacker can leak the canary value (via a separate info leak vulnerability or a format string bug), they can include the correct canary in their overflow payload.

Defense 2: NX Bit / DEP (Data Execution Prevention)

The CPU marks memory pages as either writable or executable — never both. The stack and heap are marked non-executable.

Without NX:
  Stack: READ + WRITE + EXECUTE  → Attacker injects shellcode, executes it

With NX:
  Stack: READ + WRITE            → Attacker injects shellcode, CPU refuses to execute
  Code:  READ + EXECUTE          → Legitimate code runs normally
# Check NX status on Linux
readelf -l ./program | grep "GNU_STACK"
# RW  = NX enabled (no E flag)
# RWE = NX disabled (executable stack)

Bypass: Return-Oriented Programming (ROP)

ROP is the modern technique for exploiting buffer overflows on NX-protected systems. Instead of injecting new code, the attacker chains together small fragments (“gadgets”) of existing executable code — from libc, the program itself, or other loaded libraries.

Normal stack overflow:
  buffer → [shellcode] → [return to shellcode]

ROP chain:
  buffer → [padding] → [addr of gadget1] → [addr of gadget2] → [addr of gadget3] → ...

Each gadget ends with a RET instruction, which pops the next address
off the stack and jumps to it — chaining gadgets together.

Example: calling system("/bin/sh") via ROP:

1. Find gadget: pop rdi; ret    (loads argument into rdi register)
2. Find address of "/bin/sh" string in libc
3. Find address of system() in libc

ROP chain:
  [padding × 72]
  [addr of "pop rdi; ret"]   ← gadget 1
  [addr of "/bin/sh"]        ← argument loaded into rdi
  [addr of system()]         ← called with rdi = "/bin/sh"

Tools like ropper and ROPgadget automate finding gadgets in binaries.

Defense 3: ASLR (Address Space Layout Randomization)

ASLR randomizes the base addresses of the stack, heap, and shared libraries on each execution:

# Run the same program twice — addresses differ each time
$ ./show_addresses
Stack:  0x7ffd3a821000
Heap:   0x55a8c2f41000
libc:   0x7f8b3c200000

$ ./show_addresses
Stack:  0x7ffc91a05000
Heap:   0x561e8f320000
libc:   0x7f2a4d100000

The attacker can’t hardcode addresses in their exploit because they change every run.

# Check ASLR status on Linux
cat /proc/sys/kernel/randomize_va_space
# 0 = disabled
# 1 = stack and libraries randomized
# 2 = full randomization (stack, heap, mmap, libraries)

Bypass: ASLR is defeated by information leaks. If the attacker can read a single pointer from memory (via format string, partial overwrite, or side channel), they can calculate the base address of libc and build their ROP chain dynamically.

Defense 4: PIE (Position-Independent Executable)

ASLR randomizes libraries but not the main executable by default. PIE makes the executable itself position-independent, so its base address is also randomized.

# Compile with PIE (default on modern Linux)
gcc -pie -fPIE -o program program.c

# Check if binary is PIE
file ./program
# ... ELF 64-bit LSB pie executable ...    ← PIE enabled
# ... ELF 64-bit LSB executable ...         ← not PIE

Defense 5: RELRO (Relocation Read-Only)

Marks the Global Offset Table (GOT) as read-only after linking, preventing GOT overwrite attacks:

# Full RELRO
gcc -Wl,-z,relro,-z,now -o program program.c

# Check RELRO status
checksec --file=./program

The Dangerous Functions

These C functions are the primary source of buffer overflows. Know them, avoid them, and flag them in code reviews:

// NEVER USE — no bounds checking
gets(buffer);                    // Reads unlimited input
strcpy(dest, src);               // Copies until null terminator
strcat(dest, src);               // Appends until null terminator
sprintf(dest, fmt, ...);         // Formats without size limit
scanf("%s", buffer);             // Reads unlimited string

// USE INSTEAD — with explicit size limits
fgets(buffer, sizeof(buffer), stdin);
strncpy(dest, src, sizeof(dest) - 1);   // Still needs manual null termination!
strncat(dest, src, sizeof(dest) - strlen(dest) - 1);
snprintf(dest, sizeof(dest), fmt, ...);
scanf("%63s", buffer);           // Width specifier limits input

Even the “safe” versions have gotchas:

// strncpy does NOT null-terminate if src is too long
char dest[64];
strncpy(dest, user_input, 64);
// If user_input >= 64 chars, dest is NOT null-terminated!
dest[63] = '\0'; // Always do this manually

// Better: use strlcpy (BSD/macOS) or explicit logic
size_t len = strlen(src);
if (len >= sizeof(dest)) len = sizeof(dest) - 1;
memcpy(dest, src, len);
dest[len] = '\0';

Real-World Buffer Overflow Exploits

Morris Worm (1988)

The first internet worm exploited a buffer overflow in fingerd on Unix systems. The gets() function in the finger daemon had no bounds checking. The worm overflowed a 512-byte buffer, overwrote the return address, and injected code that spread the worm to other machines.

Code Red (2001)

Exploited a buffer overflow in Microsoft IIS’s Index Service. A long URL request overflowed a buffer in idq.dll, giving the worm control of the web server. Infected 359,000 servers in under 14 hours.

Heartbleed (2014)

Technically a buffer over-read (not overflow). OpenSSL’s heartbeat extension didn’t validate the payload length field. An attacker could request up to 64KB of server memory per heartbeat, leaking private keys, session tokens, and passwords. Affected ~17% of all HTTPS servers.

// Simplified Heartbleed bug
// Client sends: "I'm sending 5 bytes: HELLO"
// But lies:     "I'm sending 65535 bytes: HELLO"

// Vulnerable code allocates response based on CLAIMED length
unsigned char *response = malloc(claimed_length);
memcpy(response, payload, claimed_length);
// Copies 65535 bytes but only 5 were real payload
// The other 65530 bytes are whatever was adjacent in memory

EternalBlue / WannaCry (2017)

Buffer overflow in Windows SMBv1 (srv.sys). The NSA-developed exploit was leaked and weaponized as the WannaCry ransomware. Infected 200,000+ systems across 150 countries, causing billions in damage.

Writing Overflow-Resistant C Code

If you must write C/C++ (kernel modules, embedded systems, performance-critical code), follow these patterns:

// 1. Always validate buffer sizes before copy
int safe_copy(char *dest, size_t dest_size, const char *src, size_t src_len) {
    if (src_len >= dest_size) {
        return -1; // Would overflow
    }
    memcpy(dest, src, src_len);
    dest[src_len] = '\0';
    return 0;
}

// 2. Use safe integer arithmetic for sizes
#include <stdint.h>
void *safe_alloc_array(size_t count, size_t element_size) {
    if (count > 0 && element_size > SIZE_MAX / count) {
        return NULL; // Integer overflow
    }
    return malloc(count * element_size);
    // Or just use calloc() — it does this check internally
}

// 3. Prefer calloc over malloc for arrays
// calloc checks for integer overflow internally
int *array = calloc(count, sizeof(int)); // Safe
int *array = malloc(count * sizeof(int)); // Might overflow

// 4. Zero buffers after use (prevents info leaks)
void handle_password(const char *password) {
    char local_copy[128];
    strncpy(local_copy, password, sizeof(local_copy) - 1);
    local_copy[sizeof(local_copy) - 1] = '\0';

    // ... use the password ...

    // Clear sensitive data before returning
    explicit_bzero(local_copy, sizeof(local_copy));
    // Do NOT use memset — compiler may optimize it away
}

Compiler Flags for Maximum Protection

# GCC/Clang — production hardening flags
gcc \
  -fstack-protector-strong      \  # Stack canaries
  -D_FORTIFY_SOURCE=2           \  # Buffer overflow checks in libc
  -fPIE -pie                    \  # Position-independent executable
  -Wl,-z,relro,-z,now           \  # Full RELRO (read-only GOT)
  -Wl,-z,noexecstack            \  # Non-executable stack
  -Wall -Wextra -Werror         \  # Treat all warnings as errors
  -Wformat=2                    \  # Format string warnings
  -Wformat-security             \  # Format security warnings
  -Warray-bounds                \  # Array bounds warnings
  -fsanitize=address            \  # AddressSanitizer (dev/test only)
  -o program program.c

-D_FORTIFY_SOURCE=2 is particularly powerful — it replaces unsafe functions like strcpy, memcpy, and sprintf with checked versions at compile time:

// With -D_FORTIFY_SOURCE=2, this:
char buf[64];
strcpy(buf, user_input);

// Becomes (approximately):
char buf[64];
__strcpy_chk(buf, user_input, 64); // Aborts if copy exceeds 64 bytes

Memory-Safe Languages — The Real Fix

All the mitigations above are defense-in-depth for an inherently unsafe memory model. The actual solution is to stop using languages that allow raw memory access without bounds checking.

// Rust — buffer overflow is impossible
fn main() {
    let mut buffer = vec![0u8; 64];

    // This would panic at runtime — index out of bounds
    // buffer[100] = 42;

    // Safe: Rust checks bounds automatically
    if let Some(element) = buffer.get_mut(100) {
        *element = 42;
    } else {
        println!("Index out of bounds — handled safely");
    }

    // Strings are always bounds-checked
    let name = String::from("Hello");
    // name.as_bytes()[100]; // Would panic — caught at runtime
}
// Go — runtime bounds checking
func main() {
    buffer := make([]byte, 64)

    // This panics with: "runtime error: index out of range"
    // buffer[100] = 42

    // Safe: check length first
    if index < len(buffer) {
        buffer[index] = 42
    }

    // Or use append — automatically grows
    buffer = append(buffer, moreBytesFromUser...)
}

The industry is moving this direction. Microsoft reports that 70% of their CVEs are memory safety bugs. Google reports similar numbers for Chrome. The Linux kernel is gradually adopting Rust for new modules. Android rewrites in Rust have had zero memory safety vulnerabilities.

Buffer Overflow Security Checklist

CODE LEVEL:
[ ] No use of gets(), strcpy(), strcat(), sprintf(), scanf("%s")
[ ] All buffer operations have explicit size limits
[ ] Integer arithmetic checked for overflow before allocation
[ ] Format strings are never user-controlled
[ ] Buffers zeroed after holding sensitive data (explicit_bzero)
[ ] Arrays bounds-checked before access
[ ] Strings explicitly null-terminated after truncated copy

COMPILE TIME:
[ ] -fstack-protector-strong enabled
[ ] -D_FORTIFY_SOURCE=2 enabled
[ ] PIE enabled (-fPIE -pie)
[ ] Full RELRO (-Wl,-z,relro,-z,now)
[ ] Non-executable stack (-Wl,-z,noexecstack)
[ ] All warnings enabled and treated as errors

RUNTIME:
[ ] ASLR enabled (randomize_va_space=2)
[ ] AddressSanitizer used in testing/CI
[ ] Fuzzing with AFL or libFuzzer in CI pipeline
[ ] Static analysis (Coverity, CodeQL, clang-tidy)

STRATEGIC:
[ ] New components written in memory-safe languages (Rust, Go)
[ ] C/C++ modules isolated via sandboxing (seccomp, namespaces)
[ ] Critical parsing code fuzz-tested continuously

Buffer overflows have been the single most exploited vulnerability class for 35 years. The mitigations are good. The tooling is better than ever. But the only complete fix is to stop giving programs unchecked access to raw memory. Write new code in Rust or Go. Audit existing C/C++ with fuzzing and static analysis. And know how these attacks work — because the software you depend on was almost certainly written before anyone cared.

Related Posts

Format String Vulnerabilities — The Read-Write Primitive Hiding in printf()

Format String Vulnerabilities — The Read-Write Primitive Hiding in printf()

Format string vulnerabilities are unique in the exploit world. Most memory…

XSS and CSRF Explained — The Complete Guide with Real Attack Examples and Defenses

XSS and CSRF Explained — The Complete Guide with Real Attack Examples and Defenses

XSS and CSRF have been in the OWASP Top 10 for over a decade. They’re among the…

OWASP Top 10 (2021) — Every Vulnerability Explained with Code

OWASP Top 10 (2021) — Every Vulnerability Explained with Code

The OWASP Top 10 is the industry standard for web application security risks. If…

Software Security in the AI Era: How to Write Secure Code When AI Writes Code Too

Software Security in the AI Era: How to Write Secure Code When AI Writes Code Too

In 2025, 72% of professional developers used AI-assisted coding tools daily. By…

SQL Injection: The Complete Guide to Understanding, Preventing, and Detecting SQLi Attacks

SQL Injection: The Complete Guide to Understanding, Preventing, and Detecting SQLi Attacks

SQL injection has been on the OWASP Top 10 since the list was created in 200…

Building a Vulnerability Detection System That Developers Actually Use

Building a Vulnerability Detection System That Developers Actually Use

Here’s a stat that should concern every security team: 73% of developers say…

Latest Posts

Staff Engineer Study Plan for MAANG Interviews — The Complete 12-Week Roadmap

Staff Engineer Study Plan for MAANG Interviews — The Complete 12-Week Roadmap

If you’re a Senior Engineer (L5) preparing for Staff (L6+) roles at MAANG…

XSS and CSRF Explained — The Complete Guide with Real Attack Examples and Defenses

XSS and CSRF Explained — The Complete Guide with Real Attack Examples and Defenses

XSS and CSRF have been in the OWASP Top 10 for over a decade. They’re among the…

OWASP Top 10 (2021) — Every Vulnerability Explained with Code

OWASP Top 10 (2021) — Every Vulnerability Explained with Code

The OWASP Top 10 is the industry standard for web application security risks. If…

HTTP Cookies Security — Everything Developers Get Wrong

HTTP Cookies Security — Everything Developers Get Wrong

Cookies are the single most important mechanism for web authentication. Every…

Format String Vulnerabilities — The Read-Write Primitive Hiding in printf()

Format String Vulnerabilities — The Read-Write Primitive Hiding in printf()

Format string vulnerabilities are unique in the exploit world. Most memory…

Software Security in the AI Era: How to Write Secure Code When AI Writes Code Too

Software Security in the AI Era: How to Write Secure Code When AI Writes Code Too

In 2025, 72% of professional developers used AI-assisted coding tools daily. By…