As a part of working on my reverse engineering skills, I decided to do some crackmes, since they are recommended a bit everywhere. I am currently still focusing on Windows stuff since that is a gap I felt the need to fill. I started with the ‘easy’ rated ones (because we all start somewhere). I did some ‘very easy’ ones, but they really are very easy and I don’t think they are worth talking about.
So without further ado, let’s get into my first (serious-ish) crackmes: FindMySecret
Like many crackmes, this challenge is all about getting your hands on a “password”. Running the executable we are greeted with the following
So apparently we have to find a number that serves as a password, and the number should be within a certain range.
At this point, I am drawing up some sort of idea about what the program is probably like. I expect to see the welcome string (Enter the secret number
) repeated in some loop. In the same loop, I expect to find a scanf
or something akin to it, some routine to check the input, and some code to tell us whether we hit the jackpot or not. Clearly, the checking code is where we hope to find our password.
Loading up the file on Ghidra, unfortunately, I cannot find any function that stands out for being main()
, so I go searching for the welcome string to see if I can find it. The string shows up as follows:
If we follow the function Ghidra tells us the string is used in we end up to this bit of decompiled code (which I call main
, even if it might not be at this point)
undefined4 main(undefined param_1)
{
undefined2 local_18 [6];
undefined *local_c;
local_c = ¶m_1;
FUN_004019c0();
local_18[0] = 0;
_beginthread((_StartAddress *)PTR_DAT_00403028,0,local_18);
do {
} while( true );
}
This doesn’t tell us much. It seems some function is called directly with a pointer to memory. Looking at the disassembly is more useful in this case. If we go to the printf
for our welcome string we are met with something like this
From here on out we can start checking what the function does. We are interested in cmp
instructions especially or function calls that might lead us to find the password.
The first noteworthy point is a cmp
between EAX
(where the output of scanf
is) and a hardcoded hex value that stands for 9999. Id EAX
turns out being bigger than that, the jg
instruction will redirect execution to a section that will print out the out-of-range message. We now know that 9999 is the limit out input goes to.
We then run into a function call that points to the following decompiled function
void __cdecl check(short *param_1)
{
if (DAT_00406034 == '\0') {
_DAT_004063e8 = _DAT_004063e8 * 10000.0;
DAT_00406034 = '\x01';
}
if ((_DAT_004063e8 <= (double)*param_1) || ((double)*param_1 <= _DAT_004063e8 - 1.0)) {
*param_1 = 0;
}
else {
*param_1 = 1;
}
return;
}
The function seems to multiply a global variable _DAT_004063e8
by 10000 as a one-off because another global variable DAT_00406034
is then set to prevent this from happening again. After that, there is a convoluted check to verify that the input to the function (which we know being scanf
output from the caller) is within a range of <1 to _DAT_004063e8
. This seems very much like the checking function we were looking for, and we will therefore call it check
.
Back to the body of our main
we now see that the output of check
is fed into the following decompiled function
void __cdecl printResult(int param_1)
{
if (param_1 == 0) {
puts("Nope, you have not yet found the secret number.");
}
else {
puts("Success! You have completely reverse engineered and found the secret number!");
}
return;
}
which confirms that check
is indeed what we should focus on. Since this function prints the final result, we will call it printResult
`.
At this point, our main
body looks like this
What we now need to find, is where _DAT_004063e8
is initialized, and to what. Ghidra shows us that the variable is only used in one other function other than check
We follow the function and find this decompiled routine
void setGlobal(void)
{
double dVar1;
time_t tVar2;
do {
tVar2 = time((time_t *)0x0);
dVar1 = (double)((int)tVar2 % 0x32) / 50.0;
} while (dVar1 == 0.0);
_DAT_004063e8 = dVar1;
return;
}
In short, the function initializes a variable to a random number depending on the time of execution, mod
s it by 50 (0x32 is hex for 50) and divides by 50 so that the result will always be between 0 and 1. It is in a loop because if the global variable ends up being 0 the computation happens again. The creator must have thought that 0 (0 * 10000) would be anyone’s initial guess. Since we now know this function initially sets _DAT_004063e8
, we will call it setGlobal
.
And this is pretty much it. Only thing that’s left to do is to run it, check the memory address containing _DAT_004063e8
(0x004063e8, conveniently), multiply by 10000 and get the answer.
So let’s go and do that, here you can see the memory dump at the specific address on Immunity Debugger (memory dump is low left)
Once we extracted the hex value (0x3fe280f0197c71a7
) we go and convert it to double online, since Immunity does not seem to recognize it. We get 0.5782
as a result.
Now we multiply 0.5782
by 10000 as we found out the code does, and if we insert 5782
as “secret number” we are welcomed with a success message
And we solved the challenge!