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;

  1. The pointer to our input (guessed password) is loaded into register rax.
  2. A byte in the string is placed into eax.
  3. This one byte is compared to the actual password.
  4. 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.