Skip to main content

Command Palette

Search for a command to run...

Solving a simple Crackme

An article on how to solve a simple crackme(Cracking challenge) by creating a program that generates random valid passwords in C for the program 101-crackme using tools such as objdump.

Updated
11 min read
Solving a simple Crackme
N
I write about cyber security content revolving around what I learn in reverse engineering and low-level programming, web security, cloud and network security, ethical hacking and generally anything else interesting I get to learn!

Introduction

Solving crackmes (or cracking challenges) can be a valuable part of learning and practising C programming, especially when developing skills for a career in cybersecurity. Crackmes, short for "cracking challenges," are small programs or applications that are designed to be reverse-engineered and solved. They often involve tasks such as:

  1. Analysing the binary or executable file to understand its inner workings.

  2. Identifying and bypassing security checks or anti-debugging mechanisms.

  3. Modifying the program's behaviour to achieve a desired outcome, such as unlocking hidden features or bypassing authentication.

Solving crackmes can be particularly beneficial for learning C programming in the context of cybersecurity for several reasons:

  1. Low-Level Understanding: Crackmes often require a deep understanding of low-level programming concepts, such as memory management, assembly language, and system-level interactions. Solving these challenges helps develop a strong grasp of how C programs operate at a fundamental level.

  2. Reverse Engineering Skills: Crackmes encourage the development of reverse engineering skills, which are crucial for tasks like malware analysis, vulnerability research, and security assessments. By analysing the structure and behaviour of the program, you can learn techniques for understanding and manipulating compiled code.

  3. Problem-Solving and Analytical Thinking: Solving crackmes requires a systematic, analytical approach to problem-solving. This helps cultivate the critical thinking and problem-solving skills that are essential for effective cybersecurity professionals.

  4. Practical Application of C Programming: Crackmes provide a practical, hands-on way to apply your C programming knowledge and skills to real-world security-related challenges. This can help bridge the gap between theoretical concepts and practical implementation.

  5. Exposure to Security Concepts: Solving crackmes often involves understanding and bypassing security measures, such as anti-debugging techniques, obfuscation, and cryptographic algorithms. This exposure can deepen your understanding of security principles and their practical applications.

Methodology

On running this particular crackme(an executable), this is the result you get:

  • From this, we see that to crack this executable, it needs a password as an argument.

You can try guessing a bunch of random common passwords such as ‘1234’ or ‘abcde’ to see if you can crack the executable.

  • Unfortunately, you’ll see that you still can’t crack the program by using these common passwords, hence the need to dive deeper into the program.

So far, the program has been giving us the output ‘Usage: ./101-crackme password’ and ‘Wrong password’. ‘Usage: ./101-crackme password’ indicates how to use the program and its password to crack the program, which is by running the program and using the password as an argument. ‘Wrong password’ is a string literal set to be output by the program whenever a wrong password is used. To see more strings used in the program, you can use the command strings. The command strings prints character sequences in files that are at least 4 characters long and are followed by an unprintable character.

  • After running the strings command, you will see a bunch of strings but there are 3 string literals that seem of most importance to us. In addition to what you got initially when trying to crack the executable, you see that there is a new string ‘Tada! Congrats’, which is possibly what the program outputs when you run it with the right password.

The information from the strings command doesn’t give enough information to crack the executable. Hence, you can also use objdump which is a command-line utility in Unix-like operating systems that displays information about object files. I used the command:

objdump -d -MIntel 101-crackme

  • -d: display the assembler mnemonics for the machine  instructions  from  the  input file, especially sections expected to contain instructions

  • -M: pass target specific info to the disassembler, in this case, intel syntax mode

The output shows a lot of sections. So i started with main, representing the main function:

Let’s see what all of this means:


Space in memory is created on the stack for local variables. Rbp represents the 64-bit base pointer register for the current stack frame. Rsp represents the 64-bit stack pointer which points to the top of the current stack. Push, mov and sub are known opcodes which show the name of the instruction to execute.

  • Here we can see the use of registers which are very fast and small memory areas within the processor and can hold very big values. They can be thought of as temporary variables on the chip, usable by the Arithmetic Logic Unit (ALU) to do math and logic operations. We’ll see more of them from our objdump analysis.

More registers here. rdi (64-bit) / edi (32-bit) is a destination index for string operations that is used for string and memory array copying. rsi (64-bit) / esi (32-bit) is the source index for string operations. Essentially, this stores the start of the string that you’re saving to memory and is also used for string/memory copying. These 2 are basically used as the calling convention registers for the first two arguments.

  • argc is the 1st parameter passed to the main function. It indicates how many arguments were entered on the command line when program was started. argc is an int — which is 32 bits on x86-64. So the compiler uses edi (the 32-bit version) rather than the full 64-bit rdi. the memory operand DWORD PTR specifies the operand size is 32 bits, which matches edi

  • argv is the 2nd parameter of the main function and is an array of pointers to arrays of character objects. The objects are null-terminated strings, representing arguments entered on the command line when the program was started. argv is char** which is a 64-bit pointer, hence it is passed to rsi. This is further evidenced by memory operand QWORD PTR that specifies the operand size is 64 bits, which matches rsi.

cmp is used to compare the value in edi (DWORD PTR [rbp-0x14]) to 2. This means the program has 2 arguments as we saw earlier: 1. The program name 2. the password i.e ./101-crackme password. If the comparison is equal, the instruction je means there is a jump to the instruction sitting at address 0x4005d2. The <main+0x35> part is just a human-readable annotation added by the disassembler — it means 35 hex bytes past the start of main, which starts at 0x40059d. It can be verified by: 0x40059d + 0x35 = 0x4005d2.

  • failure to pass exactly one argument, makes the program print an error and exits.

rax (64-byte) / eax (32-byte) is called the Accumulator register. It’s also used for I/O port access, arithmetic operations, and interrupt calls. argv is moved to the rax regsister, followed by an add instruction which moves to the next address after 8 bytes. The next address is of the 2nd argument argv[1]) supplied to the program. Adding 8 moves rax forward by one pointer width (pointers are 8 bytes on 64-bit systems). So rax no longer points to argv[0], it now points to the slot where argv[1] is stored.

  • mov rax, QWORD PTR [rax] : This dereferences rax — it reads the 8-byte value stored at that address. So rax goes from holding the address of the slot to holding the actual value inside the slot — which is argv[1], the pointer to your command-line string.

  • The 2nd argument(held in rax) is now moved to rdi and then call 400566 <checksum> does two things:

    • Pushes the return address onto the stack (so the program knows where to come back to after checksum finishes) & jumps to address 0x400566 — the start of checksum

    • Jumps to address 0x400566 — the start of checksum

Now when we get into the checksum function:

We can see the setup with push and mov dealing with the base pointer and stack pointer. Next up:

Two local variables can be seen:

  • [rbp-0x18]copies the value out of rdi and into stack memory at [rbp-0x18]. The string pointer (starts at argv[1])

  • [rbp-0x8] → a running sum, initialized to 0 in the stack.

This line indicates the address to jump to where the loop condition is. The next line of code is the loop body, beginning at 400578:

  • One thing I learnt here is about the difference movzx & movsx. Both instructions copy a smaller source value into a larger destination register, but they handle the extra upper bits differently depending on whether the data is unsigned or signed.

    • movzx(Move with Zero-Extend): Fills all the upper, unused bits of the larger destination register with zeros. It is used for unsigned integers.

    • movsx (Move with Sign-Extend): Fills the upper, unused bits with a copy of the source operand's sign bit (the highest bit). It is used for signed integers to preserve their negative or positive value.

  • AL represents the lowest 8 bits (1 byte) of the AX and EAX/RAX accumulator register.

We now arrive at address 40058c where the loop condition starts:

  • The instruction test al, al performs a bitwise AND operation on the AL register with itself, but it does not modify the value in AL. Instead, it only updates the CPU's status flags. Developers use this specific instruction as a highly efficient way to check if a register is zero (null terminator in this case) or if it holds a negative number.

checksum's return looks like this:

Back to main. By convention, return values go in rax — that's why main at address 4005e5 can do:

The next two lines are basically checking the checksum:

  • 0xad4 = 2772 in decimal. That's the target checksum value the input must produce. If equal (je), we are sent to the address 400604 .

At 400604:

  • 0x4006c7 is an address in the .rodata section (read-only data) — it's where the success message string is stored, e.g. "Tada!" or similar. It's the argument to puts: edi holds the first argument (calling convention), so mov edi, 0x4006c7 is setting up puts(0x4006c7) — i.e., puts("<string at that address>").

  • To see the actual text, you'd run something like: objdump -d -Mintel -s -j .rodata 101-crackme

If the input is not equal to 2772, this path is followed:


So now after all this, we have to create a C program that generates random valid passwords for the program 101-crackme. In the program, I used srand() with time to create a seed for rand so that it can always generate a random character every time, instead of the same random character all the time. Here is the code:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
/**
 * main - generates random valid passwords for the program 101-crackme.
 *
 * Return: 0
 */
int main(void)
{
    int sum, character, index;
    char password[100];

    srand(time(NULL)); /* sets the seed/starting point for rand() */
    sum = 0; /* sum of the character */
    index = 0;
    
    while (sum < 0xad4)
    {
        /** some large number is produced by rand,
         * % 95 → squeezes it into the range 0–94,
         * + 32 → shifts it to 32–126, which is the
         * printable ASCII range (space through ~)
         */
        character = rand() % 95 +32; 
        if (sum + character > 0xad4)
            character = 0xad4 - sum;
 
        password[index] = character;
        sum += character;
        index++;
     }

    password[index] = '\0';
    puts(password);

    return(0);
}
  • The program outputs a string of characters every time when it is ran

I ran the crackme with the compiled file:

  • We can verify that the random passwords output from 101-keygen crack the crackme! Tada! Congrats is the output we should get on success

Conclusion

This crackme demonstrated how a seemingly opaque compiled binary can be fully understood through systematic reverse engineering. By combining strings to locate key text references, objdump to disassemble the binary into readable assembly, and careful tracing of register usage and stack layout, the program's logic was reconstructed step by step, from the argc check in main, to the dereferencing of argv[1], to the call into checksum.

The checksum function turned out to implement a simple algorithm: sum the ASCII values of every character in the input string and compare the result against a hardcoded target, 0xad4 (2772). Once that target and the underlying logic were identified, writing a keygen became straightforward: generate random printable characters (ASCII 32–126), tracking a running sum, and cap the final character so that the total lands exactly on 2772.

Testing 101-keygen against 101-crackme confirmed the analysis: every generated password produced the "Tada! Congrats" success message, validating both the reverse-engineered checksum logic and the keygen's implementation.

Beyond solving this specific challenge, this exercise reinforced core skills directly relevant to security engineering: reading x86-64 assembly, understanding calling conventions and stack frames, recognizing common compiler patterns (sign/zero extension, comparison and branching idioms), and translating low-level disassembly back into high-level C logic. These are foundational skills for malware analysis, vulnerability research, and binary exploitation, making crackmes like this one a practical bridge between theoretical C knowledge and applied cybersecurity work.

Reverse Engineering

Part 1 of 1

A couple of write-ups revolving around reverse engineering