This years DEFCON CTF qualifiers featured a section called crackme. The idea behind the questions in this section was to find correct input to a prompted executable. The added challenge was that each challenge was actually a series of ~200 binaries. Your goal was to automate the cracking of one and be able to extend it to all the rest.
Magic was one of the easy ones, but it shows off the power of angr in finding good code paths.
$ file 4245f48054debd4d1a4cc0e5bd704705bff1440607443b8c6fc5c342d067e93e
4245f48054debd4d1a4cc0e5bd704705bff1440607443b8c6fc5c342d067e93e: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, stripped
Give the binary a run:
So this was the standard prompt for this year's DEFCON. It asked for a code. If the code was correct, you'd get some positive feedback. If it was incorrect, you'd get nothing back. Notice the file command showed "/lib/ld-musl-x86_64.so.1". Usually this is something like ld-linux. Basically, this is just a drop-in replacement for the normal libc. There are various reasons for this, but from angr's perspective this could be important.
There was nothing interesting in ltrace of this execution. The only interesting things in strings was the string "sum is %ld", which we want to get to. Let's take a look at the control flow of an example main function.
The main function is pretty linear. We allocate some space, print a message, get input, then call a function (which I've labeled "do_checks"). Finally, we print out the "sum_is" string. Since we don't tend to get to the "sum_is" string, there must be checks in the function above that cause it to exit. Let's open up that function.
So this is a long linear function. We still haven't seen any branching, so let's open up a few examples of those call functions:
These are very simple functions. They look at the input (passed in as rdi) and check it against a value. Radare2 helpfully tells us that these values are actually printable ASCII in the comment line directly above the cmp statement. If you look at a few of these you can see that the checks are spelling out the correct characters. If the character does not match, control goes to the exit function. Otherwise, a value is set and it returns.
With this in mind, there are many different ways to approach solving these in an automated manner. Because the binary was so simple, I opted to simply have angr execute the whole thing for me. The game plan is simply to tell angr to execute past the checks function. If it gets that far, then we have the correct input and should ask angr for the input string.
There was one unintended gotcha on this. That was that angr didn't automatically load the SimProcedures for standard libc functions since the libc being used was musl. I chose to bypass this problem by simply calling hook_symbol for each function used. Now that the CTF is over, there will be a patch to angr to have it auto-load functions in this case in the future.
import angr, simuvex proj = angr.Project("4f49c8c63601a71d3ae23694f78951fd3c63ae9ce295e92cfe67378cb2c7e22f") # A winning path is one that prints out the "sum" string def win(p): try: return "sum" in p.state.posix.dumps(1) except: return False # Define the symbols we want to hook symbols = ['calloc','puts','fflush','fgets','printf'] # Hook them all with the normal SimProc for symbol in symbols: proj.hook_symbol(symbol,simuvex.SimProcedures['libc.so.6'][symbol]) pg = proj.factory.path_group() # Explore until we find a winning path pg.explore(find=win) # Print out the winner print(pg.found.state.posix.dumps(0).rstrip("\n\x00"))
That's it. It's not the most efficient thing in the world, but it will find the solution to any given binary in about 5-10 seconds. Looping through that and submitting the flag to the service is an exercise left to the reader. :-)
The flag is: a color map of the sun sokemsUbif
Update: Since this was posted, angr added support to automatically hook those functions. So the hook statements are no longer needed. This makes the final solution as follows:
import angr, simuvex proj = angr.Project("4f49c8c63601a71d3ae23694f78951fd3c63ae9ce295e92cfe67378cb2c7e22f") # A winning path is one that prints out the "sum" string def win(p): try: return "sum" in p.state.posix.dumps(1) except: return False pg = proj.factory.path_group() # Explore until we find a winning path pg.explore(find=win) # Print out the winner print(pg.found.state.posix.dumps(0).rstrip("\n\x00"))