| name | angr-binary-analysis |
| description | How to use angr for binary analysis, CTF challenges, and reverse engineering. Use this skill whenever the user mentions binary analysis, CTF challenges, reverse engineering, symbolic execution, angr, finding passwords in binaries, bypassing checks, or analyzing compiled programs. Make sure to use this skill for any task involving binary exploitation, symbolic execution, or automated binary analysis, even if the user doesn't explicitly mention 'angr'. |
Angr Binary Analysis Skill
A comprehensive guide to using angr for binary analysis, CTF challenges, and reverse engineering tasks.
Quick Start Template
import angr
import claripy
import sys
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state()
simulation = project.factory.simgr(initial_state)
def is_successful(state):
stdout = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout
def should_abort(state):
stdout = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout
simulation.explore(find=is_successful, avoid=should_abort)
if simulation.found:
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
else:
raise Exception('Could not find the solution')
if __name__ == '__main__':
main(sys.argv)
Core Techniques
1. Reaching Specific Addresses
Use when you know the target address in the binary:
simulation.explore(find=0x804867d, avoid=0x080485A8)
if simulation.found:
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()))
2. Working with Registers
When input is stored in registers after scanf:
start_address = 0x80488d1
initial_state = project.factory.blank_state(addr=start_address)
password0 = claripy.BVS('password0', 32)
password1 = claripy.BVS('password1', 32)
password2 = claripy.BVS('password2', 32)
initial_state.regs.eax = password0
initial_state.regs.ebx = password1
initial_state.regs.edx = password2
if simulation.found:
solution_state = simulation.found[0]
solution0 = solution_state.solver.eval(password0)
solution1 = solution_state.solver.eval(password1)
solution2 = solution_state.solver.eval(password2)
print(f'{solution0} {solution1} {solution2}')
3. Working with Stack Values
When scanf stores values on the stack:
start_address = 0x8048697
initial_state = project.factory.blank_state(addr=start_address)
initial_state.regs.ebp = initial_state.regs.esp
password0 = claripy.BVS('password0', 32)
password1 = claripy.BVS('password1', 32)
padding_length_in_bytes = 8
initial_state.regs.esp -= padding_length_in_bytes
initial_state.stack_push(password0)
initial_state.stack_push(password1)
4. Working with Static Memory (Global Variables)
When input is stored in global memory:
start_address = 0x8048606
initial_state = project.factory.blank_state(addr=start_address)
password0 = claripy.BVS('password0', 8*8)
password1 = claripy.BVS('password1', 8*8)
password2 = claripy.BVS('password2', 8*8)
password3 = claripy.BVS('password3', 8*8)
initial_state.memory.store(0xa29faa0, password0)
initial_state.memory.store(0xa29faa8, password1)
initial_state.memory.store(0xa29fab0, password2)
initial_state.memory.store(0xa29fab8, password3)
if simulation.found:
solution_state = simulation.found[0]
solution0 = solution_state.solver.eval(password0, cast_to=bytes).decode()
solution1 = solution_state.solver.eval(password1, cast_to=bytes).decode()
5. Working with Dynamic Memory (Malloc)
When input is stored in malloc'd memory:
password0 = claripy.BVS('password0', 8*8)
password1 = claripy.BVS('password1', 8*8)
fake_heap_address0 = 0x4444444
pointer_to_malloc_memory_address0 = 0xa79a118
initial_state.memory.store(
pointer_to_malloc_memory_address0,
fake_heap_address0,
endness=project.arch.memory_endness
)
initial_state.memory.store(fake_heap_address0, password0)
6. File Simulation
When the binary reads from a file:
start_address = 0x80488db
initial_state = project.factory.blank_state(addr=start_address)
filename = 'password.txt'
symbolic_file_size_bytes = 64
password = claripy.BVS('password', symbolic_file_size_bytes * 8)
password_file = angr.storage.SimFile(filename, content=password)
initial_state.fs.insert(filename, password_file)
if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.solver.eval(password, cast_to=bytes).decode()
print(solution)
7. Adding Constraints
When you want to add manual constraints to avoid expensive branching:
address_to_check = 0x8048671
simulation.explore(find=address_to_check)
if simulation.found:
solution_state = simulation.found[0]
constrained_address = 0x804a050
constrained_size = 16
constrained_bv = solution_state.memory.load(constrained_address, constrained_size)
expected_value = 'BWYRUBQCMVSBRGFU'.encode()
solution_state.add_constraints(constrained_bv == expected_value)
print(solution_state.posix.dumps(sys.stdin.fileno()))
8. Hooking Functions
Hook a Single Call
check_address = 0x80486b8
instruction_length = 5
@project.hook(check_address, length=instruction_length)
def skip_check(state):
input_address = 0x804a054
input_length = 16
input_string = state.memory.load(input_address, input_length)
expected = 'XKSPZSJKJYQCQXZV'.encode()
state.regs.eax = claripy.If(
input_string == expected,
claripy.BVV(1, 32),
claripy.BVV(0, 32)
)
Hook a Function (SimProcedure)
class ReplacementCheckEquals(angr.SimProcedure):
def run(self, to_check, length):
input_string = self.state.memory.load(to_check, length)
expected = 'WQNDNKKWAWOLXBAC'.encode()
return claripy.If(
input_string == expected,
claripy.BVV(1, 32),
claripy.BVV(0, 32)
)
project.hook_symbol('check_equals_WQNDNKKWAWOLXBAC', ReplacementCheckEquals())
9. Simulating scanf with Multiple Parameters
class ReplacementScanf(angr.SimProcedure):
def run(self, format_string, param0, param1):
scanf0 = claripy.BVS('scanf0', 32)
scanf1 = claripy.BVS('scanf1', 32)
self.state.memory.store(param0, scanf0, endness=project.arch.memory_endness)
self.state.memory.store(param1, scanf1, endness=project.arch.memory_endness)
self.state.globals['solutions'] = (scanf0, scanf1)
project.hook_symbol('__isoc99_scanf', ReplacementScanf())
if simulation.found:
solution_state = simulation.found[0]
stored_solutions = solution_state.globals['solutions']
solution = ' '.join(map(str, map(solution_state.solver.eval, stored_solutions)))
print(solution)
10. Static Binaries
For statically compiled binaries, manually hook libc functions:
project.hook(0x804ed40, angr.SIM_PROCEDURES['libc']['printf']())
project.hook(0x804ed80, angr.SIM_PROCEDURES['libc']['scanf']())
project.hook(0x804f350, angr.SIM_PROCEDURES['libc']['puts']())
project.hook(0x8048d10, angr.SIM_PROCEDURES['glibc']['__libc_start_main']())
11. Simulation Managers
Use Veritesting to merge similar states and reduce branching:
simulation = project.factory.simgr(initial_state, veritesting=True)
simulation = project.factory.simgr(initial_state)
simulation.use_technique(angr.exploration_techniques.Veritesting())
Common Patterns
Finding Passwords
- Simple stdin input: Use
entry_state() and posix.dumps(sys.stdin.fileno())
- Multiple scanf values: Hook
__isoc99_scanf with SimProcedure
- File input: Use
SimFile with symbolic content
- Register/stack/memory: Use
blank_state() at address after input
Performance Tips
- Avoid expensive loops: Add constraints before char-by-char comparisons
- Use Veritesting: Merges similar states to reduce branching
- Hook expensive functions: Replace slow checks with symbolic comparisons
- Start after input: Use
blank_state() to skip input handling
Debugging
print(f"State address: {hex(state.addr)}")
print(f"State registers: eax={hex(state.regs.eax)}")
print(f"Found: {len(simulation.found)}")
print(f"Active: {len(simulation.active)}")
print(f"Deadended: {len(simulation.deadended)}")
Troubleshooting
| Problem | Solution |
|---|
| Too many branches | Use Veritesting or add constraints |
| scanf with multiple params | Hook __isoc99_scanf with SimProcedure |
| Static binary | Manually hook libc functions |
| Memory address unknown | Disassemble binary to find addresses |
| Slow execution | Hook expensive functions, use constraints |
References