Breaking a simple “crack” with Ghidra - Part 1
The fact that it is such a beast - Ghidra ( «Hydra") - and what it eats , she eats applets, many have already, probably, know firsthand, although public access to this tool was only recently - in March of this year. I will not bother readers with a description of Hydra, its functionality, etc. Those who are in the topic, I am sure, have already studied all this themselves, and those who are not yet in the topic - they can do it at any time, since now it’s easy to find detailed information on the Internet. By the way, one of the aspects of Hydra (the development of plugins for it) has already been covered on Habré (excellent article!) I will give only the main links:
So, Hydra is a free cross-platform interactive disassembler and decompiler with a modular structure, with support for almost all the main CPU architectures and a flexible graphical interface for working with disassembled code, memory, recovered (decompiled) code, debugging symbols, and much, much more .
Let's try to break something with this Hydra!
As a “victim” we find a simple “crackme” program. I just went to crackmes.one , indicated in the search the difficulty level = 2-3 (“simple” and “medium”), the source language of the program = “C / C ++” and the platform = “Multiplatform”, as in the screenshot below:

Search gave 2 results (in green below). The first crack turned out to be 16-bit and did not start on my Win10 64-bit, but the second ( level_2 by seveb ) came up. You can download it from this link .
Download and unpack the crack; The password for the archive, as indicated on the site, is crackmes.de . In the archive we find two directories corresponding to Linux and Windows. On my machine, I go to the Windows directory and I see in it the only "executable" -level_2.exe . Let's run and see what she wants:

Looks like a bummer! At startup, the program does not display anything. We try to run it again, passing it an arbitrary string as a parameter (all of a sudden, is it waiting for a key?) - and again nothing ... But do not despair. Let's assume that we also have to find out the launch parameters as a task! It's time to uncover our "Swiss knife" - Hydra.
Suppose you already have Hydra installed. If not yet, then everything is simple.
We launch Hydra and in the opened Project Manager immediately create a new project; I gave it the name crackme3 (i.e., crackme and crackme2 projects have already been created for me). The project is, in fact, a directory of files, you can add any files to it to study (exe, dll, etc.). We will immediately add our level_2.exe ( File | Import or just the I key ):

We see that already before the import, Hydra identified our experimental quack as 32-bit PE (portable executable) for the Win32 OS and x86 platform. After importing, our waiting for even more information:

Here, in addition to the above bit depth, we may still be interested in the byte order (endianness), which in our case is Little(from low to high byte), which is to be expected for the Intel 86th platform.
With a preliminary analysis, we are done.
Time to start a full automatic analysis of the program in Hydra. This is done by double-clicking on the corresponding file (level_2.exe). Having a modular structure, Hydra provides all its basic functionality with a plug-in system that can be added / disabled or developed independently. The same is with analysis - each plugin is responsible for its type of analysis. Therefore, first, we are faced with this window where you can select the types of analysis of interest:
For our purposes, it makes sense to leave the default settings and run the analysis. The analysis itself is performed quite quickly (it took me about 7 seconds), although users on the forums complain that for large projects, Hydra loses in speed to IDA Pro . This may be true, but for small files this difference is not significant.
So, the analysis is complete. Its results are displayed in the Code Browser

window : This window is the main one for working in Hydra, so you should study it more carefully.
Well, let's proceed to a direct analysis of our crack-programs. In most cases, you should start by searching for the entry point of the program, i.e. The main function that is called when it starts. Knowing that our crack was written in C / C ++, we guess that the name of the main function will be main () or something like that :) It is said and done. Enter “main” in the filter of the Tree of Symbols (in the left panel) and see the function _main () in the Functions section . Go to it with a mouse click.
In the disassembler listing, the corresponding code section is immediately displayed, and on the right we see the decompiled C-code of this function. Another convenient feature of Hydra is the synchronization of selection: when a mouse selects a range of ASM commands, the corresponding code section in the decompiler is highlighted and vice versa. In addition, if the memory viewing window is open, the allocation is synchronized with the memory. As they say, all ingenious is simple!
Immediately, I note an important feature of working in Hydra (as opposed to, say, working in IDA). Work in Hydra is primarily focused on analyzing decompiled code.. For this reason, the creators of Hydra (we remember - we are talking about spies from the NSA :)) paid great attention to the quality of decompilation and the convenience of working with code. In particular, one can simply go on to defining functions, variables, and sections of memory by simply double-clicking in the code. Also, any variable and function can be immediately renamed, which is very convenient, since the default names do not carry meaning and can be confusing. As you will see later, we will often use this mechanism.
So, here is the main () function , which Hydra “dissected” as follows:
It seems that everything seems normal - definitions of variables, standard C-types, conditions, loops, function calls. But taking a closer look at the code, we notice that for some reason the names of some functions were not defined and replaced by the pseudo- function _text () (in the decompiler window - .text () ). Let's start by defining what these functions are.
Double-clicking on the body of the first call
we see that this is just a wrapper function around the standard calloc () function , which is used to allocate memory for data. So let's just rename this function to calloc2 () . Having set the cursor on the function header, call the context menu and select Rename function (hot key - L ) and enter a new name in the field that

appears : We see that the function was renamed right there. Go back into the body of main () (button Back in the toolbar or the Alt + <- ) and we see that here, instead of mysterious _text () is already calloc2 () . Excellent!
We do the same with all other wrapper functions: we go into their definition one by one, see what they do, rename (I added index 2 to the standard names of C-functions) and go back to the main function.
Okay, we figured out some strange functions. We begin to study the code of the main function. Skipping variable declarations, we see that the function returns the value of the variable iVar2, which is zero (a sign of success of the function) only if the condition specified by the string is satisfied
_Argc is the number of command line parameters (arguments) passed to main () . That is, our program “eats” 2 arguments (the first argument, we remember, is always the path to the executable file).
OK, let's move on. Here we create a C-string ( char array ) of 256 characters:
Next we have a loop of 3 iterations. In it, we first check whether the bVar1 flag is set, and if so, copy the following command line argument (string) to _Dest :
This flag is set when parsing the following argument:
The first line calculates the length of this argument. Further, the condition checks that the length of the argument must be 2, the penultimate character == "-" and the last character == "f". Note how the decompiler "translated" the extraction of characters from the string using a byte mask.
Here I immediately added comments. We check the validity of the arguments ("-f path_to_file") and open the corresponding file (the 2nd argument passed, which we copied to _Dest). The file will be read in binary format, as indicated by the "rb" parameter of the fopen () function . If the read fails (for example, the file is unavailable), an error message is displayed in the stderror stream and the program exits with code 1.
Next, the most interesting:
The open file descriptor ( _File ) is passed to the _construct_key () function , which, obviously, performs verification of the key sought. This function returns a two-dimensional byte array ( char ** ), which is stored in the ppcVar3 variable . If the array is empty, the concise “Nope” is displayed on the console (ie, in our opinion, “Nope!”) And the memory is freed. Otherwise (if the array is not empty), the apparently correct key is displayed and the memory is also freed. At the end of the function, the file descriptor closes, memory is freed, and the value of iVar2 is returned .
So, now we realized that we need:
1) to create a binary file with the correct key;
2) pass its path in the cracks after the argument "-f"
In the second part of the article we will analyze the function _construct_key () , which, as we found out, is responsible for checking the key in the file.
- Official page on the NSA website
- Github Project
- First review in Hacker Magazine
- Great YouTube channel with parsing programs in Ghidra
So, Hydra is a free cross-platform interactive disassembler and decompiler with a modular structure, with support for almost all the main CPU architectures and a flexible graphical interface for working with disassembled code, memory, recovered (decompiled) code, debugging symbols, and much, much more .
Let's try to break something with this Hydra!
Step 1. Find and study the crack
As a “victim” we find a simple “crackme” program. I just went to crackmes.one , indicated in the search the difficulty level = 2-3 (“simple” and “medium”), the source language of the program = “C / C ++” and the platform = “Multiplatform”, as in the screenshot below:

Search gave 2 results (in green below). The first crack turned out to be 16-bit and did not start on my Win10 64-bit, but the second ( level_2 by seveb ) came up. You can download it from this link .
Download and unpack the crack; The password for the archive, as indicated on the site, is crackmes.de . In the archive we find two directories corresponding to Linux and Windows. On my machine, I go to the Windows directory and I see in it the only "executable" -level_2.exe . Let's run and see what she wants:

Looks like a bummer! At startup, the program does not display anything. We try to run it again, passing it an arbitrary string as a parameter (all of a sudden, is it waiting for a key?) - and again nothing ... But do not despair. Let's assume that we also have to find out the launch parameters as a task! It's time to uncover our "Swiss knife" - Hydra.
Step 2. Creating a project in Hydra and preliminary analysis
Suppose you already have Hydra installed. If not yet, then everything is simple.
Install Ghidra
We launch Hydra and in the opened Project Manager immediately create a new project; I gave it the name crackme3 (i.e., crackme and crackme2 projects have already been created for me). The project is, in fact, a directory of files, you can add any files to it to study (exe, dll, etc.). We will immediately add our level_2.exe ( File | Import or just the I key ):

We see that already before the import, Hydra identified our experimental quack as 32-bit PE (portable executable) for the Win32 OS and x86 platform. After importing, our waiting for even more information:

Here, in addition to the above bit depth, we may still be interested in the byte order (endianness), which in our case is Little(from low to high byte), which is to be expected for the Intel 86th platform.
With a preliminary analysis, we are done.
Step 3. Perform automatic analysis
Time to start a full automatic analysis of the program in Hydra. This is done by double-clicking on the corresponding file (level_2.exe). Having a modular structure, Hydra provides all its basic functionality with a plug-in system that can be added / disabled or developed independently. The same is with analysis - each plugin is responsible for its type of analysis. Therefore, first, we are faced with this window where you can select the types of analysis of interest:
Analysis Settings Window

For our purposes, it makes sense to leave the default settings and run the analysis. The analysis itself is performed quite quickly (it took me about 7 seconds), although users on the forums complain that for large projects, Hydra loses in speed to IDA Pro . This may be true, but for small files this difference is not significant.
So, the analysis is complete. Its results are displayed in the Code Browser

window : This window is the main one for working in Hydra, so you should study it more carefully.
Code Browser Interface Overview
The default interface settings split the window into three parts.
In the central part , the main window is located - listing the disassembler, which is more or less similar to its "brothers" in IDA, OllyDbg, etc. By default, the columns in this listing are (from left to right): memory address, opcode of the command, ASM command, parameters of the ASM command, cross-reference (if applicable). Naturally, the display can be changed by clicking on the button in the form of a brick wall in the toolbar of this window. To be honest, I have never seen such a flexible configuration of the output of the disassembler anywhere, it is extremely convenient.
In the left part of the 3 panel:
For us, the most useful window here is a symbol tree, which allows you to quickly find, for example, a function by its name and go to the corresponding address.
On the right side is a listing of the decompiled code (in our case, in C).
In addition to the default windows, in the Window menu you can select and place anywhere else in the browser with dozens of other windows and displays. For convenience, I added a Bytes window and a window with a Function Graph to the center, and to the right, string variables (Strings) and a table of functions (Functions). These windows are now available in separate tabs. Also, any windows can be detached and made "floating", placing and resizing them at your discretion - this is also a very thoughtful, in my opinion, solution.
In the central part , the main window is located - listing the disassembler, which is more or less similar to its "brothers" in IDA, OllyDbg, etc. By default, the columns in this listing are (from left to right): memory address, opcode of the command, ASM command, parameters of the ASM command, cross-reference (if applicable). Naturally, the display can be changed by clicking on the button in the form of a brick wall in the toolbar of this window. To be honest, I have never seen such a flexible configuration of the output of the disassembler anywhere, it is extremely convenient.
In the left part of the 3 panel:
- Sections of the program (click on the mouse to move through sections)
- Character tree (imports, exports, functions, headers, etc.)
- Type tree of used variables
For us, the most useful window here is a symbol tree, which allows you to quickly find, for example, a function by its name and go to the corresponding address.
On the right side is a listing of the decompiled code (in our case, in C).
In addition to the default windows, in the Window menu you can select and place anywhere else in the browser with dozens of other windows and displays. For convenience, I added a Bytes window and a window with a Function Graph to the center, and to the right, string variables (Strings) and a table of functions (Functions). These windows are now available in separate tabs. Also, any windows can be detached and made "floating", placing and resizing them at your discretion - this is also a very thoughtful, in my opinion, solution.
Step 4. Learning the program algorithm - main () function
Well, let's proceed to a direct analysis of our crack-programs. In most cases, you should start by searching for the entry point of the program, i.e. The main function that is called when it starts. Knowing that our crack was written in C / C ++, we guess that the name of the main function will be main () or something like that :) It is said and done. Enter “main” in the filter of the Tree of Symbols (in the left panel) and see the function _main () in the Functions section . Go to it with a mouse click.
Overview of the main () function and renaming obscure functions
In the disassembler listing, the corresponding code section is immediately displayed, and on the right we see the decompiled C-code of this function. Another convenient feature of Hydra is the synchronization of selection: when a mouse selects a range of ASM commands, the corresponding code section in the decompiler is highlighted and vice versa. In addition, if the memory viewing window is open, the allocation is synchronized with the memory. As they say, all ingenious is simple!
Immediately, I note an important feature of working in Hydra (as opposed to, say, working in IDA). Work in Hydra is primarily focused on analyzing decompiled code.. For this reason, the creators of Hydra (we remember - we are talking about spies from the NSA :)) paid great attention to the quality of decompilation and the convenience of working with code. In particular, one can simply go on to defining functions, variables, and sections of memory by simply double-clicking in the code. Also, any variable and function can be immediately renamed, which is very convenient, since the default names do not carry meaning and can be confusing. As you will see later, we will often use this mechanism.
So, here is the main () function , which Hydra “dissected” as follows:
Listing main ()
int __cdecl _main(int _Argc,char **_Argv,char **_Env)
{
bool bVar1;
int iVar2;
char *_Dest;
size_t sVar3;
FILE *_File;
char **ppcVar4;
int local_18;
___main();
if (_Argc == 3) {
bVar1 = false;
_Dest = (char *)_text(0x100,1);
local_18 = 0;
while (local_18 < 3) {
if (bVar1) {
_text(_Dest,0,0x100);
_text(_Dest,_Argv[local_18],0x100);
break;
}
sVar3 = _text(_Argv[local_18]);
if (((sVar3 == 2) && (((int)*_Argv[local_18] & 0x7fffffffU) == 0x2d)) &&
(((int)_Argv[local_18][1] & 0x7fffffffU) == 0x66)) {
bVar1 = true;
}
local_18 = local_18 + 1;
}
if ((bVar1) && (*_Dest != 0)) {
_File = _text(_Dest,"rb");
if (_File == (FILE *)0x0) {
_text("Failed to open file");
return 1;
}
ppcVar4 = _construct_key(_File);
if (ppcVar4 == (char **)0x0) {
_text("Nope.");
_free_key((void **)0x0);
}
else {
_text("%s%s%s%s\n",*ppcVar4 + 0x10d,*ppcVar4 + 0x219,*ppcVar4 + 0x325,*ppcVar4 + 0x431);
_free_key(ppcVar4);
}
_text(_File);
}
_text(_Dest);
iVar2 = 0;
}
else {
iVar2 = 1;
}
return iVar2;
}
It seems that everything seems normal - definitions of variables, standard C-types, conditions, loops, function calls. But taking a closer look at the code, we notice that for some reason the names of some functions were not defined and replaced by the pseudo- function _text () (in the decompiler window - .text () ). Let's start by defining what these functions are.
Double-clicking on the body of the first call
_Dest = (char *)_text(0x100,1);
we see that this is just a wrapper function around the standard calloc () function , which is used to allocate memory for data. So let's just rename this function to calloc2 () . Having set the cursor on the function header, call the context menu and select Rename function (hot key - L ) and enter a new name in the field that

appears : We see that the function was renamed right there. Go back into the body of main () (button Back in the toolbar or the Alt + <- ) and we see that here, instead of mysterious _text () is already calloc2 () . Excellent!
We do the same with all other wrapper functions: we go into their definition one by one, see what they do, rename (I added index 2 to the standard names of C-functions) and go back to the main function.
We comprehend the main () function code
Okay, we figured out some strange functions. We begin to study the code of the main function. Skipping variable declarations, we see that the function returns the value of the variable iVar2, which is zero (a sign of success of the function) only if the condition specified by the string is satisfied
if (_Argc == 3) { ... }
_Argc is the number of command line parameters (arguments) passed to main () . That is, our program “eats” 2 arguments (the first argument, we remember, is always the path to the executable file).
OK, let's move on. Here we create a C-string ( char array ) of 256 characters:
char *_Dest;
_Dest = (char *)calloc2(0x100,1); // эквивалент new char[256] в C++
Next we have a loop of 3 iterations. In it, we first check whether the bVar1 flag is set, and if so, copy the following command line argument (string) to _Dest :
while (i < 3) {
/* цикл по аргументам ком. строки */
if (bVar1) {
/* инициализировать массив */
memset2(_Dest,0,0x100);
/* скопировать строку в _Dest и прервать цикл */
strncpy2(_Dest,_Argv[i],0x100);
break;
}
...
}
This flag is set when parsing the following argument:
n_strlen = strlen2(_Argv[i]);
if (((n_strlen == 2) && (((int)*_Argv[i] & 0x7fffffffU) == 0x2d)) &&
(((int)_Argv[i][1] & 0x7fffffffU) == 0x66)) {
bVar1 = true;
}
The first line calculates the length of this argument. Further, the condition checks that the length of the argument must be 2, the penultimate character == "-" and the last character == "f". Note how the decompiler "translated" the extraction of characters from the string using a byte mask.
Decimal values of numbers, and at the same time the corresponding ASCII characters, can be spied by holding the cursor over the corresponding hexadecimal literal. ASCII mapping does not always work (?), So I recommend looking at the ASCII table on the Internet. You can also directly in Hydra convert scalars from any number system to any other (via the context menu -> Convert ), in this case this number will be displayed everywhere in the selected number system (in the disassembler and in the decompiler); but personally, I prefer to leave hexes in the code for harmony of work, because memory addresses, offsets, etc. hexes are set everywhere.After the loop comes this code:
if ((bVar1) && (*_Dest != 0)) {
/* если получили аргументы 1) "-f" и 2) строку -
открыть указанный файл для чтения в двоичном формате */
_File = fopen2(_Dest,"rb");
if (_File == (FILE *)0x0) {
/* вернуть 1 при ошибке чтения */
perror2("Failed to open file");
return 1;
}
...
}
Here I immediately added comments. We check the validity of the arguments ("-f path_to_file") and open the corresponding file (the 2nd argument passed, which we copied to _Dest). The file will be read in binary format, as indicated by the "rb" parameter of the fopen () function . If the read fails (for example, the file is unavailable), an error message is displayed in the stderror stream and the program exits with code 1.
Next, the most interesting:
/* !!! ПРОВЕРКА КЛЮЧА В ФАЙЛЕ !!! */
ppcVar3 = _construct_key(_File);
if (ppcVar3 == (char **)0x0) {
/* если получили пустой массив, вывести "Nope" */
puts2("Nope.");
_free_key((void **)0x0);
}
else {
/* массив не пуст - вывести ключ и освободить память */
printf2("%s%s%s%s\n",*ppcVar3 + 0x10d,*ppcVar3 + 0x219,*ppcVar3 + 0x325,*ppcVar3 + 0x431);
_free_key(ppcVar3);
}
fclose2(_File);
The open file descriptor ( _File ) is passed to the _construct_key () function , which, obviously, performs verification of the key sought. This function returns a two-dimensional byte array ( char ** ), which is stored in the ppcVar3 variable . If the array is empty, the concise “Nope” is displayed on the console (ie, in our opinion, “Nope!”) And the memory is freed. Otherwise (if the array is not empty), the apparently correct key is displayed and the memory is also freed. At the end of the function, the file descriptor closes, memory is freed, and the value of iVar2 is returned .
So, now we realized that we need:
1) to create a binary file with the correct key;
2) pass its path in the cracks after the argument "-f"
In the second part of the article we will analyze the function _construct_key () , which, as we found out, is responsible for checking the key in the file.