Solving a Simple C++ Crackme

Solving a Simple C++ Crackme

Introduction

Hello there! Someone from the crackmes.one discord channel asked for some help with this challenge. Although they ended up solving it on their own (good for them!) I also solved it and thought I should share my solution. This challenge is fairly simple but it was written in C++. C++ binaries always give me a little bit of trouble, but I think I learned a lot during this challenge and I hope you will too! As always, you can check out my video walkthrough on my YouTube channel.

Reverse Engineering Challenge — First Ever Crackme Video Walkthough

Optional Materials to Follow Along

If you'd like to follow along you can download the Kali VM I used. It has all of the tools I'll use throughout this post including the binary. It's stored in /home/kali/reverse_engineering/crackmes/abso_general. If you'd rather use your own VM, I understand, you can download the binary here. The binary comes in a password protected zip file, the password is "crackmes.one" without the quotes. I'd also recommend you download a disassembler like IDA or Ghidra. I'll be using Ghidra in this post. Alright, with all of that out of the way, let's get reversing.

Initial Triage

We start by running file on the binary.

File output

We see this is a 64-biy binary, dynamically linked, and we see that it is not stripped. Let's go ahead and look at the symbols with the nm command.

Symbols output
Symbols output

We see a bunch of symbols here. A lot of these are C++ artifacts and we don't necessarily have to worry about them. We do see a user-defined function called boringFunc. We also see there are a bunch of words and letters after the function name. This tells us that this function takes in a string as a parameter. Everything else here isn't very useful or interesting to us so let's go ahead and run strings. Fair warning, the author used some pretty harsh language lol.

Strings output
Strings output

We see what looks like a prompt, an error message and a success message. The error message is a little on the harsher side but we'll ignore that. Let's go ahead an open this binary up in Ghidra!

Static Analysis in Ghidra

Part of the main function disassembled
Part of the main function disassembled

The function starts off with normal function prolog stuff. We then see the stack cookie get stored on the stack. These are the two instructions at address 0x0010134A-0x00101353. We then see a local variable local_88 get passed to the basic_string function. This is how C++ strings are instantiated. I don't fully understand the details, but the basic_string constructor has a few different versions. This particular version is the default constructor that creates an empty string. With this in mind, we can rename local_88 to empty_string until we learn its actual purpose. After this string is created, we see another interesting function. A variable local_89 gets passed to the allocator function. Now, this isn't actually a variable we particularly care about. You'll primarily see these allocators when a string is being initialized to some value. After this allocator is created, we see yet another variable local_68. This variable will hold an actual string value. This variable, a string (1,)=8(), and the allocator are passed to the basic_string constructor. As you might be able to guess, this will initialize the string local_68 to 1,)=8(. Let's rename local_68 to buffer.

More of the main function
More of the main function

Here we see the allocator local_89 gets destroyed. This is pretty normal. The allocator usually gets destroyed right after the basic_string function is called. I'm not entirely sure what the allocator is used though. After the allocator is destroyed, we see our buffer and another string, ;18,w are passed to the += operator. This may look weird, but remember that strings are actually classes. The += operator is defined for integer types, so in this case, += was overridden for string objects. That's probably not the right terminology but I hope you get the idea. So, this is essentially concatenating the string ;18,w with buffer. We then see the prompt is printed out. The LEA instruction at address 0x001013b7 is a little strange. We see this __TMC_END__ this being placed in the RDI register. If we double-click on this value we can see that this actually holds a reference to cout.

cout reference
cout reference

So if we were translating the disassembly as it is presented to C++ it would look like: <<(cout, "Enter key: "). However, we know the actual C++ written was cout << "Enter key: "; This is pretty par for the course from what I've seen with C++ code. Everything just looks weird. Anyway, we then see our empty_string being passed to the >> operator along with the function cin. So, we can rename empty_string yet again to user_input.

So far, our pseudo C++ will look something like this:

string user_input;
string buffer = "1,)=8(";
buffer += ";18,w";
cout << "Enter key: ";
cin >> user_input;
boringFunc function call
boringFunc function call

After we provide user input, the binary calls boringFunc with two parameters: some_str and buffer. I took the liberty of renaming the local variable to some_str since we don't yet know it's purpose but we do know it's a string or some variation of a string. Let's go ahead and look at this boring function. I'm getting bored just thinking of it!

Analyzing the Boring Function

boringFunc prolog and other setup stuff
boringFunc prolog and other setup stuff

So, Ghidra is trying to help us out by explicitly stating param_1 here but it is making our lives a tad more difficult. Instead of telling us that the register RDI is being placed in local_30, it is telling us param_1 is being stored in this memory location. This isn't too helpful and you'll see why later on. Let's go ahead change this behavior. Let's go to Edit -> Tool Options -> Listing Fields -> Operands Field and uncheck the "Markup Inferred Variable References" option.

Modifying the tool options
Modifying the tool options

Doing that we can see the registers involved in the instruction which I think makes it a little clearer.

After modifying tool options
After modifying tool options

I went ahead and renamed the variables. We do see a few other variables that do not have names. local_23 and local_21 are actually the same variable but for some reason they are being displayed as two separate variables. The hex values that are assigned to the variable are ASCII XYZ. Let's go ahead and rename local_23 to chr. Not a great variable name but this is just a placeholder for now. Finally, we see local_28 gets set to 0. I'm going to rename this to i. You'll see why in just a second.

Beginning of a while loop
Beginning of a while loop

Not a lot going on here but we see i gets stored in the EAX register which then gets stored in the RBX register. We then see buffer gets loaded in the RDI register and the size function is called. The result of this gets subtracted by 1. Finally, i is compared to size(buffer) - 1. An important thing to note is the CMP instruction subtracts the destination operand (the one on the left hand side) from the source operand. So, CMP RBX, RAX in this case is essentially saying i - size(buffer) - 1. This will result in a negative number whenever i is less than size(buffer) - 1. The SETC instruction will set the register to 1 if the previous comparison resulted in a carry. If not, the register will be set to 0. The TEST AL,AL instruction performs a bitwise AND on the registers. This instruction will only result in 0 if AL is 0. Why did I go through this long winded explanation? Well this is the basis of this while loop here. As long as i is less than size(buffer) - 1 the JZ instruction will NOT execute. I know it's a little strange because we are used to seeing JZ indicate some equality but in this case, JZ will result in a break in the loop. Alright let's take a look at what's going on in this loop.

First part of while loop
First part of while loop

The loop begins by indexing the buffer array. This is very similar to what we saw before, the [] is being used as a function. Again if we were to write this out literally, we would get [](i, buffer). Just like any other function, the result gets returned in the RAX register. So, R12D holds buffer[i]. The next few instructions is a complicated way of performing i%3. I had Ghidra help me with the decompilation here. We then see another variable being indexed at address 0x001012E0. We know that RAX holds i%3. Additionally, we can recall that RBP-0x1B is the chr variable. So, EBX will hold chr[i%3].

More of the loop
More of the loop

Here, we see buffer is being indexed again. Recall, the result will get stored in the RAX register. Additionally, recal that R12D also holds buffer[i]. This gets stored in the EDX register. We see that EDX and EBX gets XORed with each other. To make it a little clearer, buffer[i] gets XORed with key[i%3] and the result is stored in the EDX register. Finally, we see that get stored back in buffer[i]. Again we can simplify this to buffer[i] = buffer[i] ^ key[i%3] or even simpler, buffer[i] ^= key[i%3]. Then, i is incremented by 1.

End of the boringFunc function
End of the boringFunc function

The last part of the boringFunc function simply copies buffer to some_str. So, we can rename this variable to modified_buffer.

End of the main function
End of the main function

Now, we are back in the main function and I renamed some_str to modified_buffer. We then see what we've been seeing throughout this binary, the modified_buffer and user_input are passed to the == operator. The string class actually has a compare function but the author elected to use == instead. Just like the [] function, == returns a value in the EAX register. This value is then placed in the EBX register. The modified_buffer variable then gets destroyed before the TEST BL,BL instruction tests whether modified_buffer and user_input are equal. In this case, == returns a non-zero number if the two values are equal and 0 if the two values are not equal. So, if TEST BL, BL results in 0 it means our modified_buffer and user_input were not equal to each other. Again a little different than what we are used to. So, we know that buffer gets modified by boringFunc and we know how; it simply XORs each character in buffer with a character from key, namely, X, Y, or Z. With this in mind solving this challenge is as easy as writing a simple script to perform the operation on the buffer. In order to understand this binary a little better, I wrote a C++ program that attempts to mimic what the original author wrote. I don't think I got it exactly write but I got pretty close. You can find the code on Github. I also pasted it below as well:

#include <iostream>
#include <string>

using namespace std;

void myFunc(string&);

int main()
{
    string password;
    string user_input = "";
    password  = "1,)=8(";
    int i = 0;
    password += ";18,w";

    cout << "Enter key: ";
    cin >> user_input;
    
    cout << "\nPassword: " << password << endl;
    myFunc(password);
    if(user_input == password)
    {
        cout << "Congratulations!" << endl;
    }
    else
        cout << "Too bad!" << endl;
    
    return 0;

}

void myFunc(string &test)
{
    int i = 0;
    char key[] = "XYZ";
    cout << "Before modifiying test: " << test << endl;
    long size = test.size();
    while(i < size - 1){
        test[i] = test[i] ^ key[i % 3];
        i++;
    }
    cout << "After modifying test: " << test << endl;

}

This program asks for input but you can type anything and it provides the correct answer. Let's compile it with g++ boring_function.cpp -o boringFunc and run it!

Printing the solution
Printing the solution

In this instance, test, represents modified_buffer. So, the binary expects iusearchbtw. Let's test it out!

Testing our solution
Testing our solution

And there it is! We successfully solved the challenge and avoided getting cussed out 😂.

Conclusion

Again this was a pretty simple challenge but I learned quite a bit about C++ and I hope you did as well. If you have any questions feel free to hit me up on Twitter, Instagram, or Discord: jaybailey216#6540. If you have a challenge you want me to try next, let me know and I'll give it a shot! I'll see you all next time!

Peace out! ✌🏾

Show Comments