Reverse Engineering Challenge - Save Scooby

Reverse Engineering Challenge - Save Scooby

Introduction

Hello and welcome back! Today, we have a very important task! Scooby Doo is lost and needs our help to find his way back to his friends! We were given a binary from crackmes.one which we will reverse engineer to find Scooby. Come join me on this long arduous journey to find good old Scooby!

My intended audience for my walkthroughs are noob reverse engineers like myself. Therefore, I tend to be verbose in my walkthroughs. If you prefer, you can watch the corresponding YouTube video for this walkthrough here:

Optional Materials to Follow Along

If you want to follow along feel free to download the VM I provide. You can find instructions on importing the VM here. If you don't want to use my VM that's fine, my feelings won't be shattered. But you will at least need the binary. You can download the binary here. The binary comes in a password protected zip file. The password is crackmes.one.

You'll also need a disassembler. I recommend IDA or Ghidra. With all of that out of the way, let's get reversing!

Initial Triage

Disclaimer: This challenge will piss you off. Not because it is insanely difficult, quite the contrary. You'll know exactly what I mean when you get to the end. Here's a hint, a simple $ can go a long way😉.

Just as in our previous challenges, we start by running file on the binary. Running file provides the following output:

Running file on the save_scooby binary
Running file on the save_scooby binary

We see that we are dealing with a 64-bit binary that is not stripped. Let's take a look at the symbols using the nm command.

Viewing symbols for the save_scooby binary
Viewing symbols for the save_scooby binary

The only user defined function is main which isn't very interesting. However, we see the binary calls the getcwd function which gets the current working directory. Finally, let's run strings to see if we can discover any additional information.

Output of running strings on the save_scooby binary
Output of running strings on the save_scooby binary

We see some interesting strings: "Hi Scooby !!", "Where are you??", "You won a medal Scooby !!", and "Scooby Doobie Doo !! Not too easy." We can infer that Scooby Doo is lost and we have to find him. We can try the obvious solution and provide the current working directory.

Attempting to use the current working directory as the solution
The obvious solution is almost never the solution

Since we don't know what this binary is doing, let's go ahead and open this up in IDA Pro. Refer to my previous blog post for instructions on loading a binary in IDA Pro.

Save Scooby main function
Save Scooby main function

What I really like about IDA is it'll attempt to name variables with meaningful names. For example, we see that IDA named a variable buf which is much more useful than var_10. If we take a closer look, we can see this variable is being used in the call to the getcwd function. So, while buf is a meaningful variable name, it does not accurately depict this variable's purpose; storing the current working directory. So, let's rename the variable to cwd. In IDA, the hotkey for renaming variables is "n." We then see on lines 0x759 to 0x768, our cwd variable is used in the call the strlen and the output is then placed in var_10. So, let's go ahead and rename var_10, to cwd_len. Finally, we see var_4 is getting assigned to 0 and right after that we see a jmp instruction. This is most likely going to be our loop control variable, so let's rename var_4 to i. If you've been following along your IDA Pro should now look like this:

Updated Save Scooby main function
Updated Save Scooby main function

We can translate the above disassembly to the following C code:

cwd = getcwd(cwd, 0x1000);
cwd_len = strlen(cwd);
i = 0;

If we follow the jmp loc_820 instruction we see our i variable is compared with cwd_len as shown below:

Disassembly responsible for comparing our loop control variable with cwd_len.
Disassembly responsible for comparing our loop control variable with cwd_len.

As you can see, our loop control variable, i, is being compared with cwd_len. If i is less than cwd_len, we jump to loc_777 which holds the meat of the loop. Also, take note of the instruction at loc_81c. This increments our loop control variable by 1. Now, let's look at the contents of the loop. Fair warning, this is about to get very confusing. Hopefully the C code translation will aid my explanation. Double-click on loc_777 and it'll bring you to the beginning of the loop.

First part of the loop
First part of the loop

We start by indexing the cwd variable with the i variable. If you're wondering why this time we are not multiplying rax by 0x4 here. The multiplication isn't necessary because we are iterating through a char array. Since chars are only 1 byte we do not need the offset. So, eax now holds cwd[i] and we see it gets compared with 0x2f. IDA is nice enough to provide the alphanumeric equivalent of 0x2f which is the '/' character. If the current character is not a '/' you see we jump to another section of the code. That's what the jnz instruction dictates. If the result of the previous comparison results in a non-zero value (meaning the two values are not equal) then we jump to another section of code, in this case loc_79A. However, if the current character is a '/' character, we set the cwd[i] with 0x24 which IDA tells us is the '$' character. Let's add onto our previous C code with what we just learned:

cwd = getcwd(cwd, 0x1000);
cwd_len = strlen(cwd);
i = 0;
while (i < cwd_len)
{
	if(cwd[i] == '/')
    	cwd[i] = '$';
    else
    {
    	// do other stuff
    }
    i++;
}

Alright now let's look at the other stuff that happens if the character is not '/'.

Disassembly for loc_79A
Analyzing the else statement

This is where things get a little confusing. Hopefully, I am able to properly explain the following sections. In any case, let's step through this together! First, we see cwd[i] first gets compared with 0x60 (the '`' character). If cwd[i] is less than or equal to 0x60 we jump to loc_7DC. Let's assume that cwd[i] was greater than 0x60 and see what happens. If we make this assumption, that means we'll move past the previous check and get to another comparison on line 0x7B8. This time we are comparing cwd[i] with 0x7A. Notice that if cwd[i] is greater than 0x7A we jump to loc_7DC. This is the same location as the previous comparison if cwd[i] was less than or equal to 0x60. What we are seeing here is something like this:

if condition || condition
	do something
else
	do something else

How did I come up with this? Recall for an OR statement to be true, only one condition in the if statement needs to be true. So, this if statement is leading us to loc_7DC. We only need to perform the second check if the first check turns out to be false. If both statements are false, then we perform the else condition. We can see the else condition in the image above. Beginning on line 0x7BC is our else statement. You see we get cwd[i] again and store the value in EAX. Then, 0x1E is subtracted from EAX. That value is then placed back in cwd[i]. If that doesn't make sense hopefully the C code below will make it clearer. As of right now this is our understanding of the program including what we just learned:

cwd = getcwd(cwd, 0x1000);
cwd_len = strlen(cwd);
i = 0;
while (i < cwd_len)
{
	if(cwd[i] == '/')
    	cwd[i] = '$';
    else
    {
    	if ((cwd[i] <= 0x60) || (cwd[i] > 0x7A))
        	// do something
        else
        	cwd[i] -= 0x1E;
    }
    i++;
}

Alright, now let's figure out what "do something" is! Double-click on loc_7DC and that'll bring us to the following set of disassembly:

Analyzing the "do something" in the if statement
Analyzing the "do something" in the if statement

And we see another set of comparisons! Let's go through this step-by-step again. We see cwd[i] is first compared to 0x40. If cwd[i] is less then or equal to 0x40, we jump to loc_81C. As you can see, this is the end of the loop. You can see loc_81C is where we add 1 to i. Although it isn't shown here after that we would compare i with cwd_len and either continue looping or break out of the loop. So, in order to continue this iteration, i has to be greater than 0x40. So let's make that assumption and see what happens next. After we compare cwd[i] with 0x40, we compare cwd[i] with 0x5A. Again, we see if cwd[i] is greater than 0x5A, we jump to the end of the loop. So, in order to continue to the last section of code, cwd[i] has to be less than or equal to 0x5A. While before we had an "if condition || condition", here we have a "if condition && condition." Why is it "AND" this time? Because in order to reach the section of code we are interested in reaching, both comparisons have to fail. Kind of confusing right? It was tough to wrap my head around this as well, but what helps me understand it is we have to reach that block of code. So what we can do is flip this conditional statement to say the following, if ((cwd[i] > 0x40) && (cwd[i] <= 0x5A)). If both of these conditions hold, we will reach our interesting block of code which does the same thing as the previous block of code. The only difference here is we add 0x1E to cwd[i]. With this in mind, let's update our code again:

cwd = getcwd(cwd, 0x1000);
cwd_len = strlen(cwd);
i = 0;
while (i < cwd_len)
{
	if(cwd[i] == '/')
    	cwd[i] = '$';
    else
    {
    	if ((cwd[i] <= 0x60) || (cwd[i] > 0x7A))
        	if ((cwd[i] > 0x40) && (cwd[i] <= 0x5A))
            	cwd[i] += 0x1E;
        else
        	cwd[i] -= 0x1E;
    }
    i++;
}

Before we move on, let's take a moment to decipher what this section of code is trying to do. After all, our goal is not to simply reverse for the sake of reversing. We need to be able to understand what the author is trying to achieve.

First, we grab the current directory and store the length and current directory in variables cwd and cwd_len respectively. We then begin "encrypting" the cwd variable. All '/' characters are changed to '$' characters. That's pretty simple. Then we get to the else statement which has a nested if-else statement. The first part of the if statement, if ((cwd[i] <= 0x60), is a first check to determine whether cwd[i] falls within the range of uppercase letters. The hexadecimal values for all uppercase letters are less than or equal to 0x60. But what is the purpose of the OR case, (cwd[i] > 0x7A). This confused me for a little but it's actually to ensure that we only process alphabetic characters. If we only have the first check if (cwd[i] <= 0x60), then any value greater than 0x60 will fall into the else case which is not what the author wanted. The author wants to ensure that only alphabetic characters are "encrypted." Any other character will be left alone. This works because if you try and work it out manually, you'll never find a non-alphabetic character that will satisfy pass through the nested if statement. Furthermore, only lowercase letters will default to the else statement because the hex values for all lowercase letters are greater than 0x60 but less than or equal to 0x7A. Pretty much, the author wants to add 0x1E for uppercase characters and subtract 0x1E from lowercase characters. Any character that is non-alphabetic will  fail these checks.

Alright Jaybailey you hit this point home can you get on with the challenge now!? Yes, yes I can!

Wrapping Up!

Ok we left off here:

Right away we see a familiar string ("Hi Scooby !! Where are you??")gets printed to the screen. We see scanf is called with a format specifier of %s. IDA was nice enough to name our user input as s. However, this isn't a great variable name so let's rename it to user_input. We then see our user_input is passed to the strlen function and the result is stored in var_14. So we can rename var_14 to user_input_len. Finally, we see two variables, var_8 and var_C, are set to 0 before we jump to a different location. We can assume one of these variables will be the loop control variable. The other is not clear it's purpose so let's rename var_8 to j and var_c to is_0x0. After renaming the variables, jump to loc_8A2 so we can analyze the loop.

Our assumption appears to be correct. The variable j is used as the loop control variable. We see when j is greater than or equal to user_input_len, we break out of the loop. An astute reader might already recognize why this challenge pissed me off lol. But let's continue anyway. If j does not satisfy this condition, we then compare j to cwd_len and jump to loc_875 if j is less than cwd_len. Let's look at what happens in this loop.

In this section, we see cwd[j] is compared to user_input[j]. If they are equal we add 1 to j and perform the comparisons we discussed earlier. If they aren't equal, we set the variable is_0x0 to -1 and break out of the loop. So, this is a simple check here that simply compares each byte of the "encrypted" cwd to our user_input. So, our final C code looks something like this:

cwd = getcwd(cwd, 0x1000);
cwd_len = strlen(cwd);
i = 0;
while (i < cwd_len)
{
	if(cwd[i] == '/')
    	cwd[i] = '$';
    else
    {
    	if ((cwd[i] <= 0x60) || (cwd[i] > 0x7A))
        	if ((cwd[i] > 0x40) && (cwd[i] <= 0x5A))
            	cwd[i] += 0x1E;
        else
        	cwd[i] -= 0x1E;
    }
    i++;
}
j = 0;
is_0x0 = 0;
while (true)
{
	if ((j >= user_input_len) || (j >= cwd_len))
    	break;
    if (cwd[j] != user_input[j])
    {
    	is_0x0 = -1;
        break;
    }
    j++;
}
if (is_0x0 == 0)
	printf("You won a medal Scooby !!");
else
	printf("Scooby Dooby Doo!! Not too easy!");

If you watched my YouTube video I broke down the code a little differently but I think this is a little easier to understand and probably more accurate. If you're confused about this line if(( j >= user_input_len) || j >= cwd_len)) that's okay. It is a little confusing. I came up with this because if you look at the disassembly we can break out of the loop if j is greater than or equal to user_input_len OR if j is greater than or equal to cwd_len. Otherwise, we perform the check if cwd[j] and user_input[j] are equal or not.

Solve This Thing Already!!!

Ok now we fully understand the code we can easily create a keygen. The following Python script will solve this challenge:

import os

def encrypt(cwd):
        scooby = ""
        for x in cwd:
                if x == '/':
                        scooby += '$'
                else:
                        if x.isalpha() and x.isupper():
                                scooby += chr(ord(x) + 0x1e)
                        elif x.isalpha() and x.islower():
                                scooby += chr(ord(x) - 0x1e)
                        else:
                                scooby += x
        print scooby

if __name__ == '__main__':
        cwd = os.getcwd()
        encrypt(cwd)

This script does exactly what that "encrypt" section does. If you run this script in the same directory as the "save_scooby" binary you should get a valid string to provide the binary.

Solving the challenge
Solving the challenge

Ready to Get Pissed Off?

As I mentioned earlier, this challenge pissed me off. Let's take another look at this comparison:

if ((j >= user_input_len) || (j >= cwd_len)). What's interesting here is, we first check whether j is greater than or equal to user_input_len. If this holds then we break out of the loop without ever checking if j is greater than cwd_len. I'm not sure if this is an error on the author's part or if the author wanted to troll us. Since we know the first character will always be a '$', because all directories begin with a '/' character, we only have to provide '$' in order to win the medal. Don't believe me? Check this out!

Solving the challenge with only 1 character
Solving the challenge with only 1 character

Told ya so 😂

Conclusion

Doesn't that make your blood boil lol? It sure got me going! Anyway, that's it for this challenge. I hope you learned something new and enjoyed reading this post. Feel free to check out my YouTube channel and/or other blog posts! If you have any questions feel free to reach out to me on Twitter, Instagram, or Discord Jaybailey216#2566. If you have a challenge you would like me to try, let me know and I'll give it a shot!

Support your boy Jaybailey216

I truly hope you enjoyed this reading this blog and/or watching the YouTube video! My goal is to always provide quality content. If you want to support me even further, consider becoming a Patron! This is the best way to support me! Regardless if you decide to join or not, I'll see you all next time!

Peace out! ✌

Show Comments