Angr is a powerful binary analysis framework which has come in handy several times in CTFs. It does a huge amount of stuff, so I'm only going to cover the few things I've used it for.
Symbolic Execution
Angr can analyse binaries by inputting 'symbols' rather than literal text. What happens to these symbols (comparisons, transformations) is then recorded, allowing us to get a picture of what a binary is doing. The main usage I've gotten out of this is flag-checker challenges, where we are expected to enter a flag, our input goes through a series of transformations, and is compared against a constant to check if the entered value is correct. Here's a script I used to solve Beginner from Google CTF 2020.
import angr, claripytarget = angr.Project('a.out', auto_load_libs=False)input_len =15# Discovered with manual analysis at a glanceinp = [claripy.BVS('flag_%d'%i, 8)for i inrange(input_len)]# Define an array of 8 bit vectors ffor each char of the flagflag = claripy.Concat(*inp + [claripy.BVV(b'\n')])st = target.factory.full_init_state(args=["./a.out"], stdin=flag)# Create a simulation with our flag symbols as stdinfor k in inp: st.solver.add(k <0x7f) st.solver.add(k >0x20)# Add constraints that the characters should be printablesm = target.factory.simulation_manager(st)sm.run()y = []for x in sm.deadended:# Out of the simulations that exit, record# any that output SUCCESSifb"SUCCESS"in x.posix.dumps(1): y.append(x)#grab the first ouptutvalid = y[0].posix.dumps(0)print(valid)
This particular example isn't great, as it is basically a brute force, as I was just getting to learn angr. However, it shows how easy it makes it for challenges with light brute forcing.
Here's a better example: Beginner Rev from Fword CTF
import angrimport claripy #the solver engineproj = angr.Project("./welcome", auto_load_libs=False)sym_arg_size =0x10#Length in Bytes because we will multiply with 8 laterinp = [claripy.BVS('flag_%d'% i, 8 )for i inrange(sym_arg_size)]flag = claripy.Concat(*inp + [claripy.BVV(b'\n')])state = proj.factory.full_init_state(args=["./welcome"], stdin=flag)for byte in inp: state.solver.add(byte >=ord('0')) state.solver.add(byte <=ord('9'))# Input is specified to be a numbersimgr = proj.factory.simulation_manager(state)good =0x400000+0x12b2# Address of flag file being openedbad = [0x400000+0x1669,0x400000+0x167b]# Addresses of failure messages being printedsimgr.use_technique(angr.exploration_techniques.DFS())simgr.explore(find=good, avoid=bad)# Explore input that will end at the good while avoiding the badfound = simgr.found[0]print(found.solver.eval(flag, cast_to=bytes))# Cast our found input to bytes and print
This is a much more intelligent way of exploring the binary, and printed 1755121917194838 after only 20 seconds.