Crackme 101
Creating Key Generator For Password-Protected Files
Get ready for some hacking fun! In this blog post, we’ll show you how to create a program that generates random valid passwords for the program 101-crackme using Ubuntu 20.04 LTS. Whether you’re just starting out or a seasoned pro, this tutorial has something for everyone. Time to flex those cyber skills!
All the files used in this tutorial can be found on GitHub here. You can clone the repository into your local environment to access the encrypted files.
To begin, we can execute the 101-crackme
file to observe its behavior by navigating to the crackme directory and entering the following command:
$ ./101-crackme password
The following will be printed to standard output:
Wrong password
This suggests that the 101-crackme
file requires to be run with the correct password only. Our duty now is to figure out what kind of file 101-crackme
is.
The file
command is used to identify the type of a file. It does this by performing a series of tests in a specific order: filesystem tests, magic number tests, and language tests.
These tests are designed to determine the file’s type based on its content and any metadata associated with it. If you want to use the file
command to identify the type of a specific file, you can do so by running file [filename]
in your terminal. For example:
$ file 101-crackme
would show you the type of the file 101-crackme
is as:
101-crackme: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=b3d9057841e3a31edce31f9780d0822daad8ac92, not stripped
The information provided in the output of the file command reveals that the 101-crackme
file is:
- An executable file designed to be run on a 64-bit Linux operating system
- Compiled for the x86–64 architecture and linked dynamically, meaning it relies on shared libraries to function
- Interpreted by the /lib64/ld-linux-x86–64.so.2 interpreter
- Built for GNU/Linux 2.6.32 and has a unique identifier called a “BuildID”
- Has not been stripped, so it still contains debugging information and symbols that can be used for analysis.
Since we now know the file contains debugging information we can use GDB Debugger on the 101-crackme
file.
GNU Debugger (GDB) is an open-source debugger for various programming languages that allows users to examine the state of a program during execution and troubleshoot issues. It provides features such as variable inspection, code stepping, breakpoint setting, and more, making it a valuable tool for software development.
First, we have to launch the 101-crackme
file with gdb
by running the following command:
$ gdb ./101-crackme
the following output will be printed on the standard output:
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./101-crackme...
(No debugging symbols found in ./101-crackme)
This shows that the file can now be probed further with gdb
tools.
As the file we are attempting to crack is a C executable, we can confidently assume the presence of a main function, making it our initial point of attack.
Therefore, we can disassemble the main function with gdb
by running the following command:
(gdb) disass main
this command will result in the following output:
Dump of assembler code for function main:
0x000000000040059d <+0>: push %rbp
0x000000000040059e <+1>: mov %rsp,%rbp
0x00000000004005a1 <+4>: sub $0x20,%rsp
0x00000000004005a5 <+8>: mov %edi,-0x14(%rbp)
0x00000000004005a8 <+11>: mov %rsi,-0x20(%rbp)
0x00000000004005ac <+15>: cmpl $0x2,-0x14(%rbp)
0x00000000004005b0 <+19>: je 0x4005d2 <main+53>
0x00000000004005b2 <+21>: mov -0x20(%rbp),%rax
0x00000000004005b6 <+25>: mov (%rax),%rax
0x00000000004005b9 <+28>: mov %rax,%rsi
0x00000000004005bc <+31>: mov $0x4006a4,%edi
0x00000000004005c1 <+36>: mov $0x0,%eax
0x00000000004005c6 <+41>: callq 0x400440 <printf@plt>
0x00000000004005cb <+46>: mov $0x1,%eax
0x00000000004005d0 <+51>: jmp 0x400613 <main+118>
0x00000000004005d2 <+53>: mov -0x20(%rbp),%rax
0x00000000004005d6 <+57>: add $0x8,%rax
0x00000000004005da <+61>: mov (%rax),%rax
0x00000000004005dd <+64>: mov %rax,%rdi
0x00000000004005e0 <+67>: callq 0x400566 <checksum>
0x00000000004005e5 <+72>: mov %rax,-0x8(%rbp)
0x00000000004005e9 <+76>: cmpq $0xad4,-0x8(%rbp)
0x00000000004005f1 <+84>: je 0x400604 <main+103>
0x00000000004005f3 <+86>: mov $0x4006b8,%edi
0x00000000004005f8 <+91>: callq 0x400430 <puts@plt>
0x00000000004005fd <+96>: mov $0x1,%eax
0x0000000000400602 <+101>: jmp 0x400613 <main+118>
0x0000000000400604 <+103>: mov $0x4006c7,%edi
0x0000000000400609 <+108>: callq 0x400430 <puts@plt>
0x000000000040060e <+113>: mov $0x0,%eax
0x0000000000400613 <+118>: leaveq
0x0000000000400614 <+119>: retq
End of assembler dump.
Summary of the main
function in the assembly code:
- The function starts by pushing the base pointer and moving the stack pointer to it, then allocating 32 bytes on the stack.
- It takes two arguments:
edi
andrsi
, which are stored in memory at-0x14(%rbp)
and-0x20(%rbp)
respectively. - The code checks if the first argument (
edi
) is equal to 2. If so, it jumps to themain+53
line, otherwise it proceeds to the next instruction. - If the first argument is not 2, the code retrieves a pointer stored in the memory location pointed to by the second argument (
rsi
), dereferences it, and passes the resulting value to theprintf
function along with a format string (0x4006a4
). - After the
printf
call, the code sets the return value (eax
) to 1 and jumps to themain+118
line. - If the first argument is 2, the code retrieves a pointer stored in the memory location pointed to by the second argument (
rsi
) plus 8 bytes, dereferences it, and passes the resulting value to thechecksum
function. - The return value of
checksum
is stored on the stack at-0x8(%rbp)
. - The code checks if the return value is equal to 2772 (
0xad4
). If so, it jumps to themain+103
line, otherwise it proceeds to the next instruction. - If the return value is not 2772, the code calls the
puts
function with a format string (0x4006b8
), sets the return value to 1, and jumps to themain+118
line. - If the return value is 2772, the code calls the
puts
function with a different format string (0x4006c7
), sets the return value to 0, and jumps to themain+118
line. - At
main+118
, the code cleans up the stack and returns.
We are interested in point 8, which occurs at:
0x00000000004005e9 <+76>: cmpq $0xad4,-0x8(%rbp)
This is where a comparison is done in the main function. Since we know a password is likely to be compared to something in the code, we have to pay attention to the values being compared: 0xad4
which is the ascii representation for 2772. We have a huge clue here since we know the value of rbp
, which holds the value of the input password.
Since the value of rbp went through a function called checksum
, we can predict that the exact values being compared is the sum of the characters in the password.
A password can only be accepted if the sum of its characters in ascii representation is 2772. So our duty now is to write a program that can generate unique characters that sum up to 2772 in ascii representation.
The key generator can be written in C (101-keygen.c) as:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
/**
* main - generate password
* Return: Always 0
*/
int main(void)
{
int p;
int s;
srand(time(NULL));
s = 0;
while (s <= 2645)
{
p = (rand() % (128 - 32)) + 32;
s += p;
putchar(p);
}
putchar(2772 - s);
putchar(10);
return (0);
}
The provided code generates a random password consisting of printable ASCII characters with a total sum of 2772.
The steps involved in the program execution are as follows:
- The necessary header files are included:
stdio.h
,stdlib.h
, andtime.h
. - The main function is defined with a return type of int and no parameters.
- The
srand
function is called with the current time as an argument to seed the random number generator. - A while loop is used to generate the password. The loop continues until the sum of the ASCII values of the generated characters is greater than or equal to 2645.
- Within the loop, a variable
p
is assigned a random value using therand
function. The value of p is restricted to the range of printable ASCII characters (32–127). - The ASCII value of the generated character is added to the variable
s
. - The generated character is printed to the console using the putchar function.
- Once the while loop is exited, the difference between 2772 and the sum
s
is printed to the console using putchar. - A newline character is printed to the console using putchar.
- The main function returns 0.
We can now compile and execute our 101-crackme
file with 101-keygen.c
as:
$ gcc -o 101-keygen 101-keygen.c
$ ./101-crackme "`./101-keygen`"
Tada! Congrats
We hope you enjoyed this tutorial on creating a program that generates random valid passwords. If you have any comments or questions, please don’t hesitate to leave them in the comments section below.