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.
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.
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.
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.
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
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
.
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
.
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;
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
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.
Doing that we can see the registers involved in the instruction which I think makes it a little clearer.
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.
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.
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]
.
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
.
The last part of the boringFunc
function simply copies buffer
to some_str
. So, we can rename this variable to modified_buffer
.
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!
In this instance, test, represents modified_buffer
. So, the binary expects iusearchbtw
. Let's test it out!
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! ✌🏾