juliangrtz.me

Cracking X0rb0y's crackme

Fri Dec 30, 2022

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:

  1. The key has a length of 32 characters which is validated using the strlen function.
  2. 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”.
  3. 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;
}