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:
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.
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.
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.
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.
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:
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:
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.
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 '/'.
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:
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.
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!
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#6540. If you have a challenge you would like me to try, let me know and I'll give it a shot! I'll see you all next time!
Peace out! ✌🏾