Intro to x64 Reversing; CrackMe
This blog post is going to focus more on the x64 assembly side of reversing and not so much the pseudo C code that Ghidra, Binary Ninja, etc., generate. You can follow along with this guide at Try Hack Me in the Reverse ELF room. This specific challenge is crackme6
. The purpose of this crackme challenge is to reverse engineer the binary to retrieve the password without bruteforcing. The crackme is a simple ELF binary that takes one parameter and validates the argument against the correct password.
Main Function
Load the binary into your favorite disassembler, and browse immediately to the main function. Overall, the code flow looks very simplistic with one condition, and then both branches terminating the program.
Looking more closely at the introduction block we can read the following assembly. Breaking this down we have the normal stack setup for the function from lines 1-3. We can see that we move registers edi
onto the stack, and rsi
as well. The important register is the edi
as it is part of the conditional jump. When looking at the register, you will notice that edi
holds the number of arguments that was provided on execution.
main:
push rbp {__saved_rbp} # Save base pointer
mov rbp, rsp {__saved_rbp} # Move Stack pointer
sub rsp, 0x10 # Create room on stack
mov dword [rbp-0x4 {var_c}], edi # Move edi to var_c
mov qword [rbp-0x10 {var_18}], rsi # Move rsi to var_18
cmp dword [rbp-0x4 {var_c}], 0x2 # Compare 0x2 with var_c (edi)
je 0x400741 # Condition jump based on cmp outcome
Reviewing the false
output (red line) we notice the following assembly instructions below. Analyzing these instructions we see a call to printf
, which will display output to the user. Looking at the printf
function, it has the potential to pass various arguments. Knowing this, we can see this basic register setup for calling printf
. We notice some of these addresses are being placed into these registers. Looking at these registers we know that qword [rbp-0x10 {var_18}]
is our provided argument on execution. More interestingly is the referenced address, 0x400810
, which is indicative of hardcoded string or data in the binary. Browse to the address and we spot the following string;
00400810 char const data_400810[0x30] = "Usage : %s password\n"
00400810 "Good luck, read the source\n", 0
With this, we can assume that this is the default fail message when first executing the binary with no arguments.
mov rax, qword [rbp-0x10 {var_18}] # Our input from start
mov rax, qword [rax] # Creating pointer to our str
mov rsi, rax
mov edi, 0x400810 # String "Usage : %s password"
mov eax, 0x0
call printf # Call printf
Now we want to move to the other branch. Looking at this branch we can review that it will call compare_pwd
function. This looks to be the branch that leads to our desired password validation code flow. It's important to finally note that the parameter of our provided input is stored in rdi
and rax
. This will be referenced again in the Compare_pwd
function.
mov rax, qword [rbp-0x10 {var_18}] # Input from start of execution
add rax, 0x8 # point to +0x8 ahead
mov rax, qword [rax] # store pointer to string in rax
mov rdi, rax # move rax to rdi
call compare_pwd # Calling compare_pwd function
compare_pwd Function
Again, look at the overall flow to get an idea of what this function may be doing. This looks very similar to the main function, as it only has one conditional jump, and a call to my_secure_test
. We can see the printf
function is being called with the fail message of password ... not OK
, this is evidence that this is not our desired branch.
We can see at the top introduction block that we are going to pass this function an argument identified as char * arg1 {register rdi}
. Knowing this, keep in mind that the rdi
register is holding a pointer to a string. We can see that the code block checks to return value of the function call my_secure_value
. Since this function uses our provided argument and determines if it is correct, we will need to investigate this code flow.
e894feffff call my_secure_test # Function call to validate password
85c0 test eax, eax # Test the return value of function
750c jne 0x4006f9 # Jump according to test instruction
my_secret_test Function
Looking at a very high level structure of this function, we can see constant compares that looks like it would leave to the function terminating. Just by observing the function structure we can assume that a structure, most likely a string, is being iterated over and compared with another string. When matching char by char, it validates if it is correct.
Looking more closely, we know that it's making a comparison as the failing branches lead to the following eax
register to be filled with 0xFFFFFFFF
. This will be returned to the previous function, with this value indicating that the comparisons ultimately failed. In contrast, the eax
register will return with 0x00
if the entire string is correct;
We will look at one of the comparisons, as they all look to be exactly the same. The following assembly is seen below. This is a good example of different types of mov
instructions. The mov
instruction is used to move data from one register to another, but only if the registers/memory are the same size. Thus line 1 works by moving qword
length into rax
register. Next, movzx
moves only a byte of the pointer in rax
to eax
. In the eax
register, the code compares the al
portion to a hardcoded value. If this sounds confusing, this chart will help guide you through the different parts of all the registers and length sizes. Finally, converting this hardcoded hex value to ASCII we will see that 0x31
is 1
.
...
mov rax, qword [rbp-0x8 {var_10}] # Agruement pointer stored in rax
movzx eax, byte [rax] # Move byte amount of rax to eax
cmp al, 0x31 # Compare al (byte) to 0x31
je 0x4005a5 # condition jump based on above cmp
...
Now that we have broken this down instruction by instruction we can see at a high level what is happening;
- The pointer to our input (guessed password) is loaded into register
rax
. - A byte in the string is placed into
eax
. - This one byte is compared to the actual password.
- If it doesn't match, terminate. If it does, continue to next byte.
To finally retrieve the password, we take all the cmp al,0x??
instructions, and convert the hex values into ASCII representation. The following is found;
Hex: 0x31, 0x33,0x33, 0x37, 0x5f, 0x70, 0x77, 0x64
ASCII: 1337_pwd
Finally, we provide the password and receive the following output;
┌──(kali㉿kali)-[~/Downloads]
└─$ ./crackme6 1337_pwd
password OK
The passwork is OK, and thus we have completed the crackme file!
Takeaways, TL;DR
This is a simple crackme file that has multiple ways to solve and retrieve the password. As well, some enumeration techniques were skipped due to the fact that I wanted this to focus solely on the x64 assembly, more than the actual enumeration of the binary. I wanted to demostrate that you don't need to dynamically run the binary to reverse engineer it effectively, just some pateince and organization.
If you're in a rush or couldn't follow along, here are some takeaways;
- Start at a high level and review the overall flow.
- Break the binary down into smaller pieces, don't try and take it all on at once.
- Review how each condition is determined and executed.
- Review parameters that are passed and returned from each function.
- Comment your assembly while working!
Some final notes, you can run these binaries dynamically in a debugger (gdb
) to follow along with the assembly instructions, if you're new to reversing I would highly recommend this. You will start to notice the patterns and sequences of instructions performing the same operations. It begins to be second nature and your static anaylsis will improve over time with repeitive practice.