Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/angr/angr/llms.txt

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.

Overview

angr excels at CTF challenges because it can:
  • Automatically find paths to specific program states
  • Solve complex input constraints symbolically
  • Handle obfuscated or packed binaries
  • Scale from simple crackmes to complex reversing challenges

Basic CTF Challenge Workflow

1

Load the binary

Start by creating an angr project for your target binary:
import angr

# Load with auto_load_libs=False for faster analysis
project = 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 success
find_addr = 0x400844

# Address(es) that indicate failure
avoid_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 stdin
state = project.factory.entry_state(
    stdin=angr.SimFile('/dev/stdin', size=100)
)

# Or use symbolic arguments
state = project.factory.entry_state(
    args=['./challenge', angr.claripy.BVS('arg1', 8*32)]
)
4

Run symbolic execution

Use the simulation manager to explore paths:
simgr = project.factory.simulation_manager(state)
simgr.explore(find=find_addr, avoid=avoid_addrs)

if simgr.found:
    solution_state = simgr.found[0]
    flag = solution_state.posix.dumps(0)  # stdin
    print("Flag:", flag)

Real-World Example: Simple Flag Finder

Here’s a complete script based on the defcamp_r100 challenge:
import angr

project = 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.

Advanced CTF Techniques

Constraining Input with Custom Conditions

For challenges that require specific input formats:
import angr
import claripy

project = angr.Project("./challenge", auto_load_libs=False)

# Create symbolic input with constraints
flag = claripy.BVS('flag', 8 * 32)
state = project.factory.entry_state(stdin=flag)

# Constrain to printable ASCII
for i in range(32):
    byte = flag.get_byte(i)
    state.solver.add(byte >= 0x20)  # Space
    state.solver.add(byte <= 0x7e)  # Tilde

simgr = project.factory.simulation_manager(state)

Using Unicorn for Performance

For challenges with heavy computation, enable Unicorn engine:
state = project.factory.entry_state(
    add_options=angr.options.unicorn
)
# Standard symbolic execution (slower)
state = project.factory.entry_state()
simgr = project.factory.simulation_manager(state)
simgr.explore(find=target_addr)

Handling Self-Modifying Code

For packed or obfuscated binaries (like the TUMCTF zwiebel challenge):
import angr

project = angr.Project("./packed_binary", auto_load_libs=False)

# Enable self-modifying code support
state = project.factory.entry_state(
    add_options={
        angr.options.STRICT_PAGE_ACCESS,
    },
    remove_options={
        angr.options.LAZY_SOLVES,
    }
)

# Use Unicorn for the unpacking stub
state.options.add(angr.options.unicorn)
Self-modifying code analysis is very slow without Unicorn. Expect execution times in hours for complex packers.

Exploration Techniques

Guided Symbolic Tracing

When you know the execution trace (from a debugger or tracer):
import angr

project = angr.Project("./traced_binary", auto_load_libs=False)
state = project.factory.entry_state()

# Define the known execution trace
trace = [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 trace
if simgr.active:
    solution = simgr.active[0].posix.dumps(0)
    print("Input:", solution)

Custom Find Conditions

For more complex success criteria:
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 False

simgr.explore(find=is_successful)

Working with Different Input Sources

# Symbolic stdin
state = project.factory.entry_state(
    stdin=angr.SimFile('/dev/stdin', size=100)
)

# Extract solution
if simgr.found:
    solution = simgr.found[0].posix.dumps(0)
    print("Input:", solution)

Performance Optimization Tips

1

Use aggressive pruning

# Limit the number of active states
simgr.explore(
    find=target,
    avoid=bad_addrs,
    n=1  # Stop after finding one solution
)
2

Enable Unicorn engine

state.options.add(angr.options.unicorn)
Can provide 10-100x speedup for concrete execution segments.
3

Avoid unnecessary library loading

project = angr.Project("./binary", auto_load_libs=False)
4

Use targeted exploration

# Start from a specific point instead of entry
state = project.factory.blank_state(addr=0x400500)

# Manually set up registers/memory if needed
state.regs.rdi = 0x1000

Common CTF Challenge Patterns

Pattern 1: Password Checker

import angr
import claripy

project = angr.Project("./password_checker", auto_load_libs=False)

# Symbolic password
password = claripy.BVS('password', 8 * 32)
state = project.factory.entry_state(stdin=password)

# Find "Access Granted" path
simgr = project.factory.simulation_manager(state)
simgr.explore(find=lambda s: b"Access Granted" in s.posix.dumps(1))

if simgr.found:
    solution = simgr.found[0].solver.eval(password, cast_to=bytes)
    print("Password:", solution.rstrip(b'\x00'))

Pattern 2: Serial/License Key Validation

import angr

project = angr.Project("./keygen", auto_load_libs=False)

# Many keygens check the return value of validation function
validation_func = 0x401000
call_site = 0x401234

state = project.factory.call_state(
    validation_func,
    claripy.BVS('serial', 8 * 16)
)

simgr = project.factory.simulation_manager(state)
simgr.explore(find=lambda s: s.regs.eax != 0)  # Non-zero = valid

if simgr.found:
    serial = simgr.found[0].solver.eval(state.regs.rdi, cast_to=bytes)
    print("Valid serial:", serial)

Pattern 3: Crypto/Hash Reversal (Partial)

import angr
import claripy

# For simple crypto that angr can solve
project = angr.Project("./crypto_check", auto_load_libs=False)

flag = claripy.BVS('flag', 8 * 32)
state = project.factory.entry_state(args=['./crypto_check', flag])

# Constrain to known prefix
state.solver.add(flag.get_bytes(0, 5) == b'flag{')
state.solver.add(flag.get_byte(31) == ord('}'))

simgr = project.factory.simulation_manager(state)
simgr.explore(find=success_addr, avoid=fail_addr)

if simgr.found:
    solution = simgr.found[0].solver.eval(flag, cast_to=bytes)
    print("Flag:", solution)
For strong cryptography, angr may timeout. Use it to reduce the keyspace, then brute-force the remainder.

Troubleshooting

State Explosion

If you have too many active states:
# Use exploration techniques to limit state explosion
from angr import exploration_techniques as et

simgr.use_technique(et.DFS())  # Depth-first search
# or
simgr.use_technique(et.Explorer(
    find=target,
    avoid=bad_addrs,
    num_find=1
))

Timeout Issues

import signal

def timeout_handler(signum, frame):
    raise TimeoutError("Symbolic execution timed out")

signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(300)  # 5 minute timeout

try:
    simgr.explore(find=target)
except TimeoutError:
    print("Timed out, trying with Unicorn...")
    state.options.add(angr.options.unicorn)
    simgr = project.factory.simulation_manager(state)
    simgr.explore(find=target)

Further Resources