Reversing challenge from Battelle showcasing angr’s file simulation feature!
Table of contents
Feed the Magical Goat (Battelle)
The following is a writeup for a reverse engineering challenge made by Battelle as one of their cyber career challenges. This challenge explores the use of angr and it’s ability to emulate file systems for the use of symbolic data. If you are unfamiliar with Angr and the concept of symbolic execution, I made a YouTube video exploring and explaining this which I (obviously) highly recommend you watch.
Part 1: Reversing
A zip file containing a 32-bit, unstripped ELF is provided as part of the challenge. Running the binary outputs a bunch of text and then ends with the binary deleting itself.
Starting a Binja project and looking through the strings reveals the following: - File operations - A filename - A flag format string (Character by character, flag is likely calculated within the binary)Viewing main, a function is called which interacts with what is likely the expected file called give_offering
.
Part 2: I’m Angry FS
The following is my solve script:
import angr,claripy,sys
p = angr.Project("./billygoat")
s = p.factory.blank_state(addr=0x8048f46)
symbol = claripy.BVS('file',8*0xf)
f = angr.storage.SimFile("chow.down", content=symbol)
s.fs.insert("chow.down",f)
def win(state): # Check stdout for "flag{" and print flag
out = str(state.posix.dumps(sys.stdout.fileno()))
if "flag{" in out:
print("Flag: flag"+out.split("flag")[1][:-3])
return "flag{" in out
simgr = p.factory.simulation_manager(s)
simgr.explore(find=win, avoid=0x80490ce)
print(b"Input: "+simgr.found[0].posix.closed_fds[0][1].concretize())
Let’s step through it to understand it better.
The first few lines create the Angr project, create the initial state which starts in give_offering
(0x8048f46) and creates a symbol whose size is based on the constraint within that function.
p = angr.Project("./billygoat")
s = p.factory.blank_state(addr=0x8048f46)
symbol = claripy.BVS('file',8*0xf)
Next a SimFile object is created with the name “chow.down” and whose content is the symbolic data. It is then inserted into the simulated file system.
f = angr.storage.SimFile("chow.down", content=symbol)
s.fs.insert("chow.down",f)
Moving on a function to check for a valid state is created. It checks for the substring “flag{” in the stdout and prints it. Then the simulation manager is created and explored with find set to this function and avoid set to the fail block.
def win(state): # Check stdout for "flag{" and print flag
out = str(state.posix.dumps(sys.stdout.fileno()))
if "flag{" in out:
print("Flag: flag"+out.split("flag")[1][:-3])
return "flag{" in out
simgr = p.factory.simulation_manager(s)
simgr.explore(find=win, avoid=0x80490ce)
The last line is interesting and I initially had to get help with this as Angr has some issues with managing file descriptors. Essentially its purpose is to print out the symbolic content of the file that led to the success block.
print(b"Input: "+simgr.found[0].posix.closed_fds[0][1].concretize())
Looking at the source of Angr’s posix can help clarify this line a bit better. A deep copy of the SimState is created and the the closed_fds copy is a list of the super object’s closed_fds, which is also a list. This line accesses the right file descriptor and patches the input together using concretize
.
With that, the challenge is solved.