Use this file to discover all available pages before exploring further.
angr is a powerful tool for solving CTF challenges that involve reverse engineering and finding hidden flags. This guide shows you practical workflows used by Shellphish and other CTF teams.
Start by creating an angr project for your target binary:
import angr# Load with auto_load_libs=False for faster analysisproject = angr.Project("./challenge", auto_load_libs=False)
Use auto_load_libs=False for CTF challenges to speed up analysis. Most challenges don’t need full library support.
2
Identify target and avoid addresses
Find the addresses where you want to reach (success) or avoid (failure):
# Address that prints the flag or indicates successfind_addr = 0x400844# Address(es) that indicate failureavoid_addrs = [0x400850, 0x400860]
You can use tools like IDA, Ghidra, or objdump to identify these addresses.
3
Set up symbolic execution
Configure symbolic input (stdin, files, or arguments):
# Create initial state with symbolic stdinstate = project.factory.entry_state( stdin=angr.SimFile('/dev/stdin', size=100))# Or use symbolic argumentsstate = project.factory.entry_state( args=['./challenge', angr.claripy.BVS('arg1', 8*32)])
Here’s a complete script based on the defcamp_r100 challenge:
import angrproject = angr.Project("./r100", auto_load_libs=False)# Hook the address that prints the flag@project.hook(0x400844)def print_flag(state): print("FLAG SHOULD BE:", state.posix.dumps(0)) project.terminate_execution()project.execute()
This example uses angr’s hooking mechanism to intercept execution at the flag-printing function and extract the input that led there.
When you know the execution trace (from a debugger or tracer):
import angrproject = angr.Project("./traced_binary", auto_load_libs=False)state = project.factory.entry_state()# Define the known execution tracetrace = [0x400500, 0x400510, 0x400520, 0x400530]simgr = project.factory.simulation_manager(state)for addr in trace: # Force execution to follow the trace simgr.step(until=lambda sm: any(s.addr == addr for s in sm.active)) # Prune states not on the trace simgr.stash(filter_func=lambda s: s.addr != addr, from_stash='active', to_stash='pruned')# Solve for the input that produces this traceif simgr.active: solution = simgr.active[0].posix.dumps(0) print("Input:", solution)
def is_successful(state): """Custom condition to find states that strcpy from controlled input""" # Check if we're calling strcpy if state.addr == 0x080483e4: # strcpy call site # Check if source argument is symbolic (we control it) src_ptr = state.regs.esi # Second argument (source) if state.solver.symbolic(src_ptr): return True return Falsesimgr.explore(find=is_successful)
# Start from a specific point instead of entrystate = project.factory.blank_state(addr=0x400500)# Manually set up registers/memory if neededstate.regs.rdi = 0x1000
# Use exploration techniques to limit state explosionfrom angr import exploration_techniques as etsimgr.use_technique(et.DFS()) # Depth-first search# orsimgr.use_technique(et.Explorer( find=target, avoid=bad_addrs, num_find=1))