In this blog post, we are taking a look at X0rb0y’s Windows crackme written in C you can find here.
General information
The Linux tool file
displays the following information for the 32-bit executable file provided by the author:
keyGen.exe: PE32 executable (console) Intel 80386, for MS Windows
There is no welcome message whatsoever when executing the crackme. After entering a random password, the program displays “Wrong Length!” and exits. Therefore, the key has a fixed length we have yet to figure out.
First glimpse at the crackme
It’s time to drag the crackme into IDA as usual. We notice that there are no obfuscation, string encryption, anti-debugging or other protection techniques present. This will ease the static analysis significantly as we can leverage IDA’s integrated decompiler that outputs neat C-ish pseudocode. The decompiled main function which can be found at the end of this blog post is easily readable. We can observe the following things about the key:
- The key has a length of 32 characters which is validated using the
strlen
function. - The key starts with the string
Securinets{
and ends with a 12-character string. Thus, nine characters are missing for the password. This part will now be called “middle part”. - All characters in the middle part consist of digits.
Using this knowledge, we can enter a password such as Securinets{000000000_Q3eM3tT1t6}
and notice that it’s wrong.
The nine digits in the middle of the password are validated in the following fashion:
v9 = fnv_1a_32(digits);
if (v9 == 0xF9B9B765) {
strncpy(v5, &DstBuf[20], 0xC);
v3 = rot(v5, 6);
if (!strcmp(v3, "_Q3eM3tT1t6}")) {
puts("Congrats! You cracked this shit!");
exit(0);
}
}
The functions fnv_1a_32
and rot
need to be analyzed further.
Reversing the key validation
Analyzing fnv_1a_32
, we can see that this function indeed calculates a Fowler–Noll–Vo hash which is explained here.
The function uses the number 2166136261 as offset and 16777619 as prime number. Remember from the pseudocode above that the hash of the middle part must be 0xF9B9B765
.
Now we can brute-force the nine digits by trying out all combinations from 000000000 to 999999999. A simple C# program which does exactly that looks as follows:
namespace x0rb0y;
internal class Program
{
private static long FNVHash(string str)
{
var v3 = 2166136261;
foreach (var character in str)
v3 = 16777619 * (character ^ v3);
return v3;
}
public static void Main()
{
const uint hash = 0xF9B9B765;
for (var i = 0; i < Math.Pow(10, 9) - 1; i++)
{
var str = i.ToString().PadLeft(9, '0');
if (FNVHash(str) != hash)
continue;
Console.WriteLine("The digits are: " + str);
}
}
}
After a few seconds of brute-forcing, we figure the correct digits are 133745555. But there is another solution: 806470850.
So a valid flag is Securinets{133745555_Q3eM3tT1t6}
? Nope. There is an additional check found in the function rot
.
IDA’s decompiler again does a great job at outputting well readable pseudocode we can directly interpret. The function essentially calculates a Caesar cipher of the first argument and uses the second argument as the shifting number. As we can see in the C pseudocode above, the shifting number passed to the function is 6. Using an online tool such as this one, we find out the last part of the password is “_K3yG3nN1n6}”, which is leet speak for “KEyGEnNInG”.
The correct passwords
The following two passwords are valid:
- Securinets{133745555_K3yG3nN1n6}
- Securinets{806470850_K3yG3nN1n6}
Conclusion
Overall, it was fun to crack this crackme. Without IDA’s decompiler, the analysis would have been much more cumbersome. Except for someone who is able to fluently read assembly code perhaps… My suggestion for a follow-up crackme is to leverage obfuscation and anti-debugging techniques to hinder the static and dynamic analysis.
Main function
int __cdecl main(int argc, const char **argv, const char **envp)
{
const char *v3; // eax
char v5[16]; // [esp+10h] [ebp-70h] BYREF
char digits[16]; // [esp+20h] [ebp-60h] BYREF
char Destination[16]; // [esp+30h] [ebp-50h] BYREF
char DstBuf[48]; // [esp+40h] [ebp-40h] BYREF
int v9; // [esp+70h] [ebp-10h]
int v10; // [esp+74h] [ebp-Ch]
size_t i; // [esp+78h] [ebp-8h]
int v12; // [esp+7Ch] [ebp-4h]
__main();
memset(DstBuf, 0, sizeof(DstBuf));
memset(Destination, 0, sizeof(Destination));
memset(digits, 0, sizeof(digits));
memset(v5, 0, sizeof(v5));
v10 = read(0, DstBuf, 0x28u);
DstBuf[v10 - 1] = 0;
// Check whether the length of the entered key is 32
if ( strlen(DstBuf) == 32 )
{
strncpy(Destination, DstBuf, 0xBu);
// Check whether the entered key starts with "Securinets{"
if ( !strcmp(Destination, "Securinets{") )
{
strncpy(digits, &DstBuf[11], 9u);
v12 = 1;
// Check whether the middle part of the key only consists of digits
for ( i = 0; strlen(digits) > i; ++i )
{
if ( (digits[i] - 48) > 9 )
v12 = 0;
}
if ( v12 )
{
v9 = fnv_1a_32(digits); // Hash function
if ( v9 == 0xF9B9B765 )
{
strncpy(v5, &DstBuf[20], 0xC);
v3 = rot(v5, 6); // Caesar cipher
if ( !strcmp(v3, "_Q3eM3tT1t6}") )
{
puts("Congrats! You cracked this shit!");
exit(0);
}
}
}
}
puts("Wrong Key!");
}
else
{
puts("Wrong Length!");
}
return 0;
}