JAKET · jaket.site · writeups

Dreadful Battle

The Challenge

This was a reverse engineering challenge from the 2022 SpaceHeroes CTF.

Raven Beak was able to steal the flag from the Galactic Federation HQ! Help Samus defeat him and retrieve the flag, but make sure to stay on your toes and never let your guard down…

This challenge winds up being a fairly simple crackme challenge with a fun Metroid theme. Our goal is to pick the correct sequence of attacks to defeat Raven Beak and save the world (?). I don’t know, I’ve never played Metroid.

Dynamic Analysis

Running the program, it looks like we’re supposed to input some sort of action for Samus and we either deal damage or get damaged based off of our choice.

The challenge has a few quirks if you poke around. I found that if you enter 23 characters, it will think you won the game. Entering even more characters juices up the health points of the characters. None of these methods will actually lead to the flag, so cheesing the challenge is unfortunately not an option.

Static Analysis

Let’s take a look at the binary in Ghidra. This block in main is interesting – it looks like that the outcome of the game is determined by the return value of the Battle function.

Within the battle function, we can begin to decipher the control flow of the game. Two variables immediately stand out, as their initialized values are 10 and 14, the same values we see above in our dynamic analysis as starting health. Coincidence? I think not. We see these values used in a condition for a loop.

The values decrement according to the return value of another function: TurnAction, which is called in every iteration of the loop, likely to determine who should take damage. If either of these variables hit zero, the loop will finish. At this point if Samus’ health is zero, a zero is returned to indicate a loss. Otherwise, a one is returned to indicate victory.

As for the meat of this game loop, it prompts the user to enter an action for Samus. This action string and the loop’s counter (I’ll call it the round number) are used in a subsequent call to TurnAction.

Here we’ve got a switch case using the round number that will execute a series of obstacle functions on the action string. Let’s focus on just the first case for now, as they are all pretty similar.

Note above that the first thing the each case does is build a string on the stack. This is more clear if we look at the assembly:

Interestingly, after our action buffer has been passed through a series of obstacle functions, it is compared to this pre-built string. If they’re the same, the function will return a one to indicate that RavenBeak should take damage. Otherwise, as stated before, it will return a zero and damage Samus. So, if we can figure out how to get the strings to match post-obstacles, we’ll be able to ensure that RavenBeak is damaged.

Looking at the obstacles, we note that all of them iterate through the string, changing each character with some sort of operation as they go. We have one that adds the loop counter to the existing character, one that subtracts the counter before adding seven, and one that XORs the character based on the counter.

Obstacle 1

Obstacle 2

Obstacle 3

We know the desired end string, the sequence the obstacles are called in, and the operations performed. What if we just took the end string and passed it through the obstacles in reverse order, while also flipping all of the operations along the way? This will give us the action string we must enter in order to get to the desired end string. We can do this for every round/case to, as they all follow the same general process, just with different ending strings and obstacle sequences.

Solution

I’m going to make a Python script to automate this whole process. First, let’s make functions for each of the three obstacles. All we’re really doing here is copying the original obstacle, except flipping each of the operations. This entails turning our pluses into minuses, minuses into pluses, and or XORs into… well, XORs.

def obstacle_one(prev_str):
    next_str = ""
    for i,s in enumerate(prev_str):
        next_str += chr(ord(s) - i)
    return next_str

def obstacle_two(prev_str):
    next_str = ""
    for i,s in enumerate(prev_str):
        next_str += chr(ord(s) ^ i)
    return next_str

def obstacle_three(prev_str):
    next_str = ""
    for i,s in enumerate(prev_str):
        next_str += chr(ord(s) + i + 7)
    return next_str

This next part is a bit tedious, but I’m going to next make a several lists of the sequences that contain the order in which to run the obstacle for each round. Keep in mind, we want to do them in the reverse order in which they appear. So for round one, our sequence will look like so:

r1_sequence = ['two', 'two', 'one', 'three']

The reason I put all of these in a list is so that we don’t have to call each obstacle individually. To accomplish this, we’ll also need a function to iterate through each round’s sequences and call the correct obstacle in the list by its name (which is a string). We can accomplish this by using some Python magic:

def battle(sequence, action):
    for seq in sequence:
        action = globals()['obstacle_'+seq](action)
    print(action)

Due to little-endianess and the janky way the strings are built, I made a little helper function to convert the targeted end string so that it will process in the correct order for our functions:

def convert(prev_str):
    converted_str = ""
    i = len(prev_str)-1
    while i > 0:
        hex_num = prev_str[i-1] + prev_str[i]
        converted_str += chr(int(hex_num, 16))
        i -= 2
    return converted_str
r1_str = convert("5e606b5a613c")

Now we have everything that we need: obstacle functions, the sequence of the obstacles, and the desired end string that is used in the comparison. Now for each round, we just simply invoke the battle function, using the round’s sequences and strings as arguments:

battle(r1_sequence, r1_str)
battle(r2_sequence, r2_str)
battle(r3_sequence, r3_str)
battle(r4_sequence, r4_str)
battle(r5_sequence, r5_str)
battle(r6_sequence, r6_str)
battle(r7_sequence, r7_str)

Run the program and you should get the correct actions for Samus to win! I’m a little slow, so at first I missed the fact that the pieces of the flag were printed out to the screen after each round.

$ python3 solver.py | ./Battle
It looks like Raven Beak stole the flag from the Galactic Federation HQ 
while everything was off the grid, you have to stop him!

Battle 1: The Face off!


Round: 1
SamusHP = 10
RavenBeakHP = 14
What will Samus do?: Charge

Round: 2
SamusHP = 10
RavenBeakHP = 12
What will Samus do?: _Beee

Round: 3
SamusHP = 10
RavenBeakHP = 10
What will Samus do?: eee

Round: 4
SamusHP = 10
RavenBeakHP = 8
What will Samus do?: eeeeaa

Round: 5
SamusHP = 10
RavenBeakHP = 6
What will Samus do?: aaa

Round: 6
SamusHP = 10
RavenBeakHP = 4
What will Samus do?: a

Round: 7
SamusHP = 10
RavenBeakHP = 2
What will Samus do?: m!}
Commander: Congratulations Samus,I may be wrong, but did you retrieve the flag?

Flag: shctf{Charge_Beeeeeeeeeeaaaaaam!}

Full Solve Script

def convert(target_str):
    converted_str = ""
    i = len(target_str)-1
    while i > 0:
        hex_num = target_str[i-1] + target_str[i]
        converted_str += chr(int(hex_num, 16))
        i -= 2
    return converted_str

def obstacle_one(prev_str):
    next_str = ""
    for i,s in enumerate(prev_str):
        next_str += chr(ord(s) - i)
    return next_str

def obstacle_two(prev_str):
    next_str = ""
    for i,s in enumerate(prev_str):
        next_str += chr(ord(s) ^ i)
    return next_str

def obstacle_three(prev_str):
    next_str = ""
    for i,s in enumerate(prev_str):
        next_str += chr(ord(s) + i + 7)
    return next_str

def battle(sequence, action):
    for seq in sequence:
        action = globals()['obstacle_'+seq](action)
    print(action)

def main():
    r1_sequence = ['two', 'two', 'one', 'three']
    r2_sequence = ['three', 'one', 'two', 'one']
    r3_sequence = ['three', 'one', 'two', 'three']
    r4_sequence = ['two', 'three', 'one', 'three']
    r5_sequence = ['two', 'three', 'one', 'one', 'three']
    r6_sequence = ['two', 'two', 'one', 'three']
    r7_sequence = ['one', 'two', 'one', 'one']

    r1_str = convert("5e606b5a613c")
    r2_str = convert("756e5a653b")
    r3_str = convert("5757525f36")
    r4_str = convert("645759516144")
    r5_str = convert("6266515f34")
    r6_str = convert("5e606b5a613c")
    r7_str = convert("316b766b46")
    
    battle(r1_sequence, r1_str)
    battle(r2_sequence, r2_str)
    battle(r3_sequence, r3_str)
    battle(r4_sequence, r4_str)
    battle(r5_sequence, r5_str)
    battle(r6_sequence, r6_str)
    battle(r7_sequence, r7_str)

if __name__ == "__main__":
    main()