In my quest to learn reverse engineering, I stumbled upon this GitHub repository containing several x64 crack-me challenges, with the difficulty increasing gradually.
Since level 1 was too simple, I focused on level 2 for this write-up.
After cloning the repository, we have the crack-me challenges in C.
To compile it and disassemble it (because analyzing the C code wouldn’t be fun), we run the command make <crackme>
:
Now we have a .64 file, and the file
command provides us with some additional information:
We confirm that it’s an “ELF x64, not stripped” file, which is normal for an easy crack-me like this one.
Let’s execute this binary and see what it gives us!
We can see that the binary requires only one argument. So, let’s start with a simple string: “aaaa.”
To my surprise, the string “aaaa” doesn’t work, but we know that we can find the failure condition by disassembling this binary.
Now, let’s start Cutter and inspect this binary in more depth.
Let’s start with the basic analysis.
The first thing I usually do when analyzing a binary is to take a look at the functions in the program. Here, we see two “entry” functions, but in these crack-me challenges, they are not used. Nevertheless, it’s always a good practice to analyze them.
We find a “main” function, which we will analyze here.
The next thing I typically do is to analyze the strings in the program to get an idea of what’s inside the binary, especially the failure and success strings.
First, we see the functions used (which can be very interesting in the context of a binary with strcmp, for example, to find the compared password). Then, we find the strings we saw during the program execution:
“Password1”! This is probably the password for the binary.
Wrong, it would be too simple.
Enough talk; let’s finally see what this “main” function does.
The first thing that catches my eye is the presence of a loop (the green arrow following the jump at 0x1163), right after the comparison (cmp eax, edx
). What I understand here is that there are multiple conditions that need to be met before the program succeeds.
We can also confirm the presence of a loop with the increment of the rcx register by 1 at each iteration, which corresponds to a counter.
By looking at the decompiler (which I rarely do because I don’t understand much in C), we confirm once and for all the presence of a loop with “while.”
Static analysis doesn’t give us a way to guess the password. Let’s start debugging and focus on this block, with a comparison that, if successful (the values are equal), will loop.
Here’s the block we’re interested in.
I start debugging with “password1” as an argument, and we’ll try to find out why this password doesn’t work.
We’re ready! I’ve positioned myself at the beginning of the “main” function, and we’ll quickly go through the lines that don’t interest us.
Before we start, at the second instruction, we see that the program checks if we’ve provided an argument by comparing edi
(the pointer) with 2. Since rdi
is at 2, the program continues.
The rax
register is loaded with the hexadecimal value of ‘p’.
The first interesting instruction is where the program puts ‘p’ into the eax
register and resets the ecx
register to zero, which will be our counter. We also see that the offset of the string ‘password1’ that we tested earlier is loaded into rdi
.
Another interesting block: we see that the edx
register is loaded with the value of rsi + rcx
(where rsi
is the register containing our password and rcx
is the counter). So, we have a test of the first index of our string, which is argc[0]
.
We also see a test
dl, dl
that jumps to the end of the program (the successful end!) if the value of the least significant bits ofrdx
is 0. So, we know that the string to guess ends with 0, which terminates the loop.
Stop! eax
has been decremented, and it now contains the value 6f, which corresponds to ‘o’. The program then compares this value with the first letter of the password we provided, which is stored in rdx
.
As expected, ‘o’ is not equal to ‘p’, so the program immediately breaks and shows us that the password is incorrect.
So, let’s run the program again with ‘o’ as the first letter to see what happens in this case.
Here we go again.
I skipped all the previous steps because we already know what happens, so let’s go back a bit before our comparison. By examining our registers, we see that rdx
(our password) is at 6f (‘o’, the first letter). Let’s see the comparison after the decrement of eax
.
Finally, we have a new block! Since rax
and rdx
are equal, we enter the loop, rcx
increments by 1 (we can translate this as “1 comparison has been made”). Then, eax
will change and move to rdi + rcx
(where rdi
is the offset of the password “password1” loaded at the beginning of the program, and rcx
is the counter, so eax = character_to_find[1]
).
Once again, the
test al, al
instruction that ends the program if the result is 0 confirms one last time that the string to find ends with 0.
We loop back to this block (which has been used before), and here, edx
is incremented by the counter. This means that the next value to test will be argc[1]
, which is the letter ‘a’ from our argument ‘oassword1’.
eax
decrements further and now contains the value 60, which is “`”.
Here, I’m a bit confused. Initially, I thought that with each loop, the index of the “password1” string hardcoded in the binary would change, but in a random way. In other words, we could have ended up with a password like “srwaps1od,” but here, a backtick is contained in eax
, which doesn’t correspond to any letter in the “password1” string.
I then remember that we are certain that the string ends with 0, so there is a change in the values of the characters in the string with each iteration.
No surprises here, as ‘a’ is not equal to “`”, the program breaks.
It would have been possible to solve this crack-me by running the binary several times, each time with a different letter from the ASCII table, discovering the next character at each iteration, and modifying our argument so that the condition is satisfied.
But with some reflection, I notice that the hexadecimal code (60) in rax
corresponds to the ASCII code 96 of the character “", and, surprisingly, just below, we have the character "a" previously loaded in
eax` before being decremented:
So, I conclude that in each loop, the binary compares the index i
(represented by the rcx
register) of the argument with that of the password -1, but not -1 in the index, -1 in the hexadecimal value. This will change the ASCII code of the character in the string.
Therefore, I start decomposing the “password1” string by rewriting it with the previous ASCII table value. For example, ‘p’, which has an ASCII value of 70, becomes 6f, which is the value of ‘o’.
Note that “o” was indeed the first character of the password that we found.
By decomposing this string, I arrive at “o`rrvnqc0,” which must be the password for the binary.
Here, the “" allows us to pass a quote in an argument.
Great! The password is “o`rrvnqc0.”
Thanks for reading this first write-up, which, believe it or not, took me a few hours to write. We’ll meet again very soon for the crack-me number 3 in this series.
Crack-me: https://github.com/NoraCodes/crackmes
Cutter: cutter.re