Feed the Magical Goat (Battelle)

Reversing challenge from Battelle showcasing angr’s file simulation feature!

Table of contents

  1. Reversing
  2. Angr Solve

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.

The function first opens "chow.down" and assigns the stream to eax. The following conditional checks if the operation was not successful via checking the file descriptor in eax. If it wasn't, the program closes the file descriptor, unlinks the binary (deletes it), prints the outro and calls exit. From here on I will refer to this blob as the fail block. Assuming this conditional was false, the file is allocated onto the heap at eax_2. The next conditional checks if eax_2 is 0x40, if true a hint is printed, both the elf and chow.down are deleted and the chunk is freed, followed by a fail block. The next conditional returns the pointer to the file contents and is the path I have to follow in order to continue program execution. It checks if the more than 0xf bytes were read.

Returning to main, multiple conditions are checked against various offsets of the file content. If code execution continues without a conditional being true, the flag is printed using these file content offsets, of which I assume were operated on by the functions in the conditions.

Looking at just one of the functions reveals that it is quite complicated.

Manually reversing these functions would be significantly detremental to my mental health, so instead I'll use symbolic execution to find an execution path that leads to the flag print and what the file contents need to be in order for this path to execute. Angr is a symbolic execution engine for python that utilizes microsoft's SMT z3 solver and a simulation manager to manage execution states. It is also capable of file system emulation. Using this feature will be simpler than alternative methods of symbol placement, such as directly injecting into memory.

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.

FaultPoint

FaultPoint.com is a personal blog maintained by elbee.


Feed the Magical Goat (Battelle)

By Dylan, 2023-03-23