Solving My First CrackMe

In This Post I Show My Approach To Solving My First CrackMe

In this post, I will write about how I solved my very first crackme.

Tools you will need are :

If you are on Windows you can use cygwin or some other emulator. If you can understand what is being done in Linux, and you can translate that in Windows then you are good to go ;-).

We begin by downloading the crackme. Before opening any crackme, I recommend you to scan the file for viruses on Virus Total. Even though all crackmes are checked for malwares before being accepted, it never hurts to stay cautious. Sometimes Virus Total gives false positives too, so make sure to check whether the executable is packed or not before executing it because sometimes packed executables are detected as malwares by some AV softwares. I’m telling you this becuase this happened with me, while I was solving my third crackme!

After downloading, checking for malwares, extracting blah blah… we try to run the executable and try to understand what we have to do.

If somehow your system isn’t treating this as an executable, you can confirm that by using the file tool. You may or may not have this tool already installed on your pc so make sure to install it.

➜  forest-crackme file forest
forest: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3cec36018b4f8638a3f4c1156b074988c0227980, for GNU/Linux 4.4.0, not stripped
➜  forest-crackme 

As you can see that it is an ELF 64-bit executable.

Now we need to give it permission to execute and for that we will do chmod +x forest and then execute the executable :

➜  forest-crackme ./forest
The forest is dark and dangerous. Be careful!
Please enter the flag:idonthaveone
The forest is unforgiving.
Flag not correct.%                                                                                                                                             
➜  forest-crackme 

So, on executing, the program asks us to enter a flag. Since I don’t know it now, I will enter anything that comes in my mind 😝. Ok, so ususally the next step is to check the strings in this executable. From this you can get more information like what libraries / functions this program uses, is it a packed executable or not etc. etc…

➜  forest-crackme strings forest
/lib64/ld-linux-x86-64.so.2
_ITM_deregisterTMCloneTable
__gmon_start__
_ITM_registerTMCloneTable
sqrt
__isoc99_scanf
puts
__stack_chk_fail
printf
__cxa_finalize
__libc_start_main
libm.so.6
libc.so.6
GLIBC_2.2.5
GLIBC_2.7
GLIBC_2.4
u3UH
[]A\A]A^A_
The forest is dark and dangerous. Be careful!
You escaped the forest.
Flag is correct.
The forest is unforgiving.
Flag not correct.
Please enter the flag:
%13s
;*3$"
GCC: (GNU) 11.1.0
abi-note.c
__abi_tag
init.c
forest.c
crtstuff.c
deregister_tm_clones
__do_global_dtors_aux
completed.0
__do_global_dtors_aux_fini_array_entry
frame_dummy
__frame_dummy_init_array_entry
__FRAME_END__
__init_array_end
_DYNAMIC
__init_array_start
__GNU_EH_FRAME_HDR
_GLOBAL_OFFSET_TABLE_
__libc_csu_fini
_ITM_deregisterTMCloneTable
puts@GLIBC_2.2.5
_edata
__stack_chk_fail@GLIBC_2.4
printf@GLIBC_2.2.5
__libc_start_main@GLIBC_2.2.5
__data_start
__gmon_start__
__dso_handle
_IO_stdin_used
__libc_csu_init
__bss_start
main
__isoc99_scanf@GLIBC_2.7
__TMC_END__
_ITM_registerTMCloneTable
sqrt@GLIBC_2.2.5
__cxa_finalize@GLIBC_2.2.5
.symtab
.strtab
.shstrtab
.interp
.note.gnu.property
.note.gnu.build-id
.note.ABI-tag
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rela.dyn
.rela.plt
.init
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.dynamic
.got
.got.plt
.data
.bss
.comment
➜  forest-crackme 

Most of the strings in this dump are useless and finding useful string is often like finding needle in a haystack (i.e if executable is large). We can see some know strings here :

  • The forest is dark and dangerous. Be careful!
  • You escaped the forest.
  • Flag is correct.
  • The forest is unforgiving.
  • Flag not correct.
  • Please enter the flag:
  • puts@GLIBC_2.2.5
  • printf@GLIBC_2.2.5
  • sqrt
  • puts
  • printf
  • sqrt@GLIBC_2.2.5

We can see that the program is using functions sqrt, puts and printf. Strings starting with a . represent a section in assembly, like for eg : .rodata means read only data section and .text is where our functions are stored. So, after taking a look at the strings above, it is clear that there is nothing important in here.

The next step is to disassemble / decompile the program and try to understand what it’s doing. I will use Ghidra for that but you can use other tools too if you like. Some people also use debuggers to solve these types of crackmes. Ghidra is an awesome tool writen by the NSA that can both disassemble and decompile your code.

If haven’t installed Ghidra yet, please google your way to install it. Fire up Ghidra, create a new project and hit I on your keyboard to import an executable. Navigate your way to the forest executable and select it. Ghidra will do some analysis now and you can just skip those steps as at this stage even I don’t pay much attention to those.
Next you will see a window labled Listing: forest with some elvish written in it. Just kidding 😜, that’s your disassembled code! Try to hit a few buttons and menus for a file, explore the interface a bit

ghidra-forest-1

Next, search for .text in the upper left window named Program Trees because that is where we will find our main. Why are we looking for main, well that’s because all C/C++ programs must have it! and that is the entry point for every C/C++ program and that is where you will always want to start reversing.
Navigating to that section, you will instantly see disassembly of main pop up in the Listing window. If you are still not able to find this, then you can navigate to main in the window below Program Trees window named Sybmol Tree. You will find main in Functions label.

ghidra-forest-2

Next click on the Listing: forest window and you will see a C-style code pop up in window next to that (i.e Decompile: main - (forest)).

ghidra-forest-3

We will copy this code from Decompile window in another text file and try to analyze that.

undefined8 main(void)

{
  long in_FS_OFFSET;
  double dVar1;
  char local_1e;
  char local_1d;
  char local_1c;
  char local_1b;
  char local_1a;
  char local_19;
  char local_18;
  char local_17;
  char local_16;
  char local_15;
  char local_14;
  char local_13;
  char local_12;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("The forest is dark and dangerous. Be careful!");
  printf("Please enter the flag:");
  __isoc99_scanf(&DAT_001020ac,&local_1e);
  if (((((local_1e == 'r') && (local_1d % '\n' == '\x01')) &&
       (dVar1 = sqrt((double)(int)local_1c), dVar1 * 5.0 == 50.0)) &&
      ((((byte)(local_1b - 1U) < 0x72 && (local_1a == 'i')) &&
       ((local_19 == 'd' && ((local_18 == 'i' && (local_17 == 'n')))))))) &&
     ((local_16 == 'g' &&
      ((((local_15 == 'h' && (local_14 == 'o')) && (local_13 == 'o')) && (local_12 == 'd')))))) {
    printf("You escaped the forest.\nFlag is correct.");
  }
  else {
    printf("The forest is unforgiving.\nFlag not correct.");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

Notice that the behaviour of this program is similar to what that forest executable did. First print the string The forest is dark and dangerous. Be careful! using the puts and then it uses printf to print the string Please enter the flag: after which we enter our flag.

We can close Ghidra now as we needed the decompiled code only. Why? because we see that all the functionalities of our program is defined in main and no other user-defined function is called in main so this means we just need to reverse main. Let’s try to analyze the program further.
Notice that after printing those strings, program asks for an input using scanf(or more precisely __isoc99_scanf). Then some randome variables are being compared with some characters and some functions are used. At this moement it really looks elvish 🙃 .

Notice this part of code :

  long in_FS_OFFSET;
  double dVar1;
  char local_1e;
  char local_1d;
  char local_1c;
  char local_1b;
  char local_1a;
  char local_19;
  char local_18;
  char local_17;
  char local_16;
  char local_15;
  char local_14;
  char local_13;
  char local_12;
  long local_10;

Did you notice anything odd?
The suffix in variable names is decreasing by 1 as we go down from local_1e to local_12. In assembly when you have to allocate variables, you just allocate them all at once by subtracting the total required memory from the stack pointer(esp). All this memory is allocated like an array and when decompilers deduce the variables, they name them accordingly, for eg : local_10 is long which is of 2 bytes, so you see an increase of 2 from local_10 to local_12. It is safe to assume from here that there is a string stored in the array from local_1e to local_12 because our program took a string as input which is basically an array of characters which is what we see here and this is what is being compared in that huge elvish if statement. Also, in the __isoc99_scanf call, you see that address of local_1e is passed and from this we confirm our suspicion that we just encountered the whole string stored in the form of separate char variables.

If you understood this part, then let’s try to deduce the values of each element in the string by looking at the if statement

str[0]  = 'r';
str[1]  = ??; // something, we have to apply some more brain here
str[2]  = ??;
str[3]  = ??;
str[4]  = 'i';
str[5]  = 'd';
str[6]  = 'i';
str[7]  = 'n';
str[8]  = 'g';
str[8]  = 'h';
str[10] = 'o';
str[11] = 'o';
str[12] = 'd';

For deducing value of str[1]
We know that '\n' in decimal is 10, so we are effectively checking for str[1] % 10 == 1. This means that str[1] must be of the form char(10*r + 1), so the possible values are char(1) to char(121)(i.e char(1), char(11), char(21) and so on…)

int main(){
    for(uint8_t i=0; i < 128; i++){
        if(i%10==1)
            printf("%c ", i);
    }
}

Execute this code to get list of possible values that str[1] can hold and we get : ) 3 = G Q [ e o y. (There can be more values, because I copy pasted the output).
So value for str[1] can be any of those characters.


For deducing value of str[2]
Here we see the following equations $$dVar1 * 5.0 = 50.0 \rightarrow dVar1 = 10.0$$ $$dVar1 = \sqrt(str[2]) \rightarrow str[2] = (dVar1)^2 \rightarrow str[2] = 100$$
So, we get str[2] = 100 which in ASCII is the alphabet d (yes, I’m taking reference from ASCII chart).

For deducing value of str[3] So up intil now we have our string : r_d_idinghood, where _ means we are missing something there.
If we try to solve for str[3], we will have to bruteforce it because for str[3] we have the mathematical inequality
$$str[3] - 1 < 114 \rightarrow str[3] < 115$$ ASCII value for 115 is s. So all ASCII characters that come before s will be accepted as place holder for str[3].

Let’s try that :

➜  forest-crackme ./forest
The forest is dark and dangerous. Be careful!
Please enter the flag:r3doidinghood
You escaped the forest.
Flag is correct.%                                                                                                                               
➜  forest-crackme 

Again :

➜  forest-crackme ./forest
The forest is dark and dangerous. Be careful!
Please enter the flag:r=daidinghood
You escaped the forest.
Flag is correct.%                                                                                                                               
➜  forest-crackme 

So, we have multiple passwords, so let’s make a program to check for all possible values :

#include <string>
#include <memory>
#include <cstdio>
#include <array>
#include <cstring>
#include <iostream>

int main(int argc, char** argv){


        if(argc != 2){
                fprintf(stderr, "usage : bruteforce command\n");
                exit(-1);
        }

        // get command to be executed
        const char* cmd = argv[1];

        printf("beginning to bruteforce \"%s\"\n\n", cmd);

        // get possible values for str[1]
        std::string str1 = ""; // initialize as empty so that we can append values to it
        for(char i=0; i < 127; i++){
        if(i%10==1)
                        str1.append(1, i);
    }

        // this is the password we get
        // 1 and 2 are place holders
        std::string passwd = "r1d3idinghood";


        for(uint i = 0; i < str1.size(); i++){
                for(uint j = 0; j < 114; j++){
                        // substitue placeholder values with possible ones
                        passwd[1] = str1[i];
                        passwd[3] = char(j);

                        printf("trying password : \"%s\"\n", passwd.c_str());

                        // test it out
                        std::array<char, 256> buffer;
                        std::string result;
                        std::unique_ptr<FILE, decltype(&pclose)> proc_ptr(popen(cmd, "w"), pclose);
                        if(!proc_ptr){
                                fprintf(stderr, "failed to open executable \"%s\"\n", cmd);
                                exit(-1);
                        }

                        // write the password
                        if(fwrite(passwd.c_str(), 1, passwd.size(), proc_ptr.get()) != passwd.size()){
                                fprintf(stderr, "failed to give input to child process \"%s\"", cmd);
                                exit(-1);
                        }

                        printf("\n\n");
                }
        }
}

On running this, you will see that all passwords are accepted. So let’s make a dump of all these passwords and complete solving the crackme.

#include <string>
#include <cstdio>
int main(){
	// get possible values for str[1]
	std::string str1 = ""; // initialize as empty so that we can append values to it
	for(char i=0; i < 127; i++){
        if(i%10==1)
			str1.append(1, i);
    }

	// this is the password we get
	// 1 and 3 are place holders
	std::string passwd = "r1d3idinghood";

	// create dump file
	FILE* passwd_dump = fopen("passwd_dump.txt", "w");
	
	for(uint i = 0; i < str1.size(); i++){
		for(uint j = 0; j < 114; j++){
			// substitue placeholder values with possible ones
			passwd[1] = str1[i];
			passwd[3] = char(j);

			fprintf(passwd_dump, "%s\n", passwd.c_str());
		}
	}	

	// close file
	fclose(passwd_dump);
}

Here is the generated file. Note that there are some broken passwords and that is because of some non printf friendly characters.

Yay!, we just solved our first crackme! It was fun!

If you didn’t understand any part of this blog, then you can contact me on telegram/instagram!

See you next post 😇

'“It’s Impossible.” said Pride. “It’s Risky.” said Experience. “It’s Pointless.” said Reason. If you really are a Hacker! then give it a try' - anonymous

Siddharth Mishra
Siddharth Mishra
Computer Science Enthusiast

Building and breaking into systems to learn them inside out!

Related