Simple usage
import angr
import claripy
When you’re playing with angr, inevitably at the beginning you’ll have to load a binary of some form, you can do this the following way:
project_name = angr.Project("./<binary_name>")
Now angr works by using a lot of states which it steps through and investigates. To load our initial state we use the following command:
state = proj.factory.entry_state()
There are a few ways to load binaries, as one would imagine. We can add these as arguments to the state.
# To start execution from specific address (reduce runtime)
state = proj.factory.entry_state(addr=0xdeadbeef)
# Specify which architecture to use
state = proj.factory.entry_state(arch="amd64")
# Specify what to send with stdin
state = proj.factory.entry_state(stdin="test_stdin_string\n")
# How to send data with argv
state = proj.factory.entry_state(args=["./<binary_name>", argv1])
# Use unicorn engine! This is often a good way to go abouts
state = proj.factory.entry_state(add_options=angr.options.unicorn)
To actually change the state, or use it in anyway, a simulation manager is needed:
simgr = project_name.factory.simulation_manager(state)
Now to ACTUALLY change the state ( :D ), you need to use the simgr. You can do a few things:
# Step one instruction at a time
simgr.step()
# Find specific address, avoid another
simgr.explore(find=0xdeadbabe, avoid=0xcafebeef)
Solving and bitvectors
Python integers don’t have the same semantics as words on a CPU, e.g. wrapping on overflow, so we work with bitvectors.
.BVV = concrete BIT VECTOR VALUE
.BVS = BIT VALUE SYMBOLIC
db = state.solver.BVV(0xdeadbeef, 64) # Bitvector which angr uses
state.solver.eval(db) # Python int representative of said bit vector
# Solving equation for an integer
x = state.solver.BVS("x", 64)
state.solver.add(x > 4)
state.solver.add(x <= 5)
print(f"\n[+] Trying to solve for x, satisfiable? {state.satisfiable()}\nAnswer is {state.solver.eval(x)}")
# Solving for a character (one byte)
n = state.solver.BVS("n", 8)
state.solver.add(n >= 0x41)
state.solver.add(n < 0x7f)
state.solver.add(n ^ 0x10 == 0x76)
solution = state.solver.eval(n)
print(f"\n[+] Trying to find the character that xored with 10 is 0x76.\nFound: {hex(solution)}")
Flag?
Using angr for CTF style challenges, a bit of reverse engineering knowledge is a must. For example you can try to find out the length of the flag, and use this, to setup a symbolic bit vector array of flag characters. Imagine the following pseudocode:
int flag(){
if (strlen(flag) != 25)
{
return -1;
}
else
{
...
win();
}
}
We know that the flag is 25 bytes. So we can set this as a constraint:
flag_chars = [claripy.BVS("flag_%d" % i, 8) for i in range(25)]
flag = claripy.Concat(*flag_chars)
Firstly we make the array of flag_chars, we set each flag_%d to have a size of 8 bits, which is a byte, which is a character. We can use this as an input to stdin in state:
_state(stdin=flag)
Now running we can explore for a success string:
simgr.explore(find=lambda s: b"Success string" in s.posix.dumps(1))
if "found" in str(simgr):
s = simgr.found[0]
print(s.posix.dumps(0)) #Flag
print(s.posix.dumps(1)) #Input
Assuming we know that the flag’s last character is “q” we can also set this as a constraint:
state.solver.add(flag_chars[24] == ord("q"))
Appendix - Further reading
[¹] - Cheatsheet: https://github.com/bordig-f/angr-strategies/blob/master/angr_strategies.md
[²] - Constraint Solving and Symbolic Execution: https://flagbot.ch/lesson5.pdf
[³] - Documentation: https://angr.io/
[⁴] - Video by John Hammond: https://www.youtube.com/watch?v=RCgEIBfnTEI