Here's a crackme: it reads a key, mangles it through a few rounds of arithmetic,
and prints correct or nope. You could reverse the mangling by hand and
write an inverse. Or you can mark the input as an unknown, tell a symbolic
execution engine "find a path that reaches correct and avoids nope," and let
the constraint solver work out what bytes satisfy every comparison on the way.
- arch
- amd64-64-little
- check
- input -> arithmetic rounds -> strcmp against a computed target
- goal
- recover a key that prints "correct" without inverting the maths
mark the input unknown, name the two exits#
angr explores the binary's paths over symbolic values. You give it a symbolic
input, a find address (the print of correct) and an avoid address (the
failure print), and it searches for a state that reached find. Pull the
addresses out of the disassembly first:
|
explore walks both branches at every comparison, accumulating constraints
(input[3] ^ 0x5a == 0x31, …). The moment a state reaches find, the solver has
a system of equations with one satisfying assignment — your key — and
posix.dumps(0) prints the exact stdin that produced it.
the part the tutorials skip: when it won't#
Symbolic execution is not magic; it's a search, and the search blows up:
- Path explosion. Every input-dependent branch forks the state. A loop over
your 16 bytes with a branch inside is
2^16paths — minutes to never. Hook the hot function with aSimProcedure, orexploretoward an address past the loop, or constrain bytes up front (as above) to prune dead branches. - Crypto and hashes. A real hash (SHA-256, AES) produces constraints the SMT
solver can't invert in any human timescale — that's the point of a hash. If the
check is
sha256(key) == const, angr will grind forever; reverse it by hand or attack the key space instead. - Environment. Syscalls, threading,
rdtsc, and libc internals can derail the engine;auto_load_libs=FalseplusSimProcedurestubs for the noisy calls keeps it on the part you actually care about.
use it as a scalpel, not a hammer
The win isn't "throw angr at the whole binary." It's: reverse far enough to
find the one function that does the check, proj.factory.call_state straight
into it with symbolic args, and let the solver finish the last mile. Minutes of
reversing to save hours, instead of waiting on a state explosion you could have
pruned.
When the check is pure arithmetic over your input, angr feels like cheating — you describe the destination and it derives the path. The skill that matters is recognising the cases where the solver can't help, and not burning an afternoon watching the active-state count climb.