mucki's crackme#4 Tutorial

July 18, 2015 - Reading time: 15 minutes

Target: mucki's crackme#4
URLhttp://www.crackmes.de/users/mucki/crackme4/
Protection: Keyfile and a serial.
Description: Crackme with a keyfile and serial protection.
Tools: Java Decompiler / Visual Studio.

First lets decompile the JAR and see what we're up against. After we have decompiled the crackme we end up with a number of classes, so lets take a look at the CM4.class and work our way down from there.

package server;

import java.net.ServerSocket;
import java.net.Socket;

public class CM4
{
  public static String version = new String("muckis crackme #4");
  public static int port = 23;
  public static boolean trace = false;

  public static void main(String[] args)
    throws Exception
  {
    if (args.length == 1) {
      trace = args[0].equals("-trace");
    }
    Loader.load("server.KeyfileCheck");
    CM4 cm = new CM4();
    cm.run();
  }

  public void run()
  {
    try
    {
      ServerSocket server = new ServerSocket(port);

      GUI g = new GUI(server.toString());
      for (;;)
      {
        Socket cc = server.accept();
        g.add("Connected with Client: " + cc.toString());
        ServerApp app = new ServerApp(cc);
        app.start();
      }
    }
    catch (Exception localException) {}
  }
}

Ok. There seems to be a class-loader used for loading the KeyfileCheck.class. Other things to notice is that the port number used by the server is 23, and it seems to be a command line argument called '-trace'. But lets take a look at the KeyfileCheck.class to find out some more about the keyfile check.

package server;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class Loader
  extends URLClassLoader
{
  public static void load(String className)
    throws Exception
  {
    ClassLoader appLoader = new Loader(Loader.class.getClassLoader(), new File(className.substring(className.indexOf('.') + 1)));

    Thread.currentThread().setContextClassLoader(appLoader);

    Class app = appLoader.loadClass(className);
    if (CM4.trace) {
      System.out.println("load: " + app.toString());
    }
    Method appmain = app.getMethod("main", new Class[] { String[].class });
    String[] appargs = new String[0];

    appmain.invoke(null, new Object[] { appargs });
  }

  public Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException
  {
    if (CM4.trace) {
      System.out.println("load: " + name);
    }
    Class c = null;

    c = findLoadedClass(name);
    if (c == null)
    {
      Class parentsVersion = null;
      try
      {
        parentsVersion = getParent().loadClass(name);
        if (parentsVersion.getClassLoader() != getParent()) {
          c = parentsVersion;
        }
      }
      catch (ClassNotFoundException localClassNotFoundException1) {}catch (ClassFormatError localClassFormatError) {}
      if (c == null) {
        try
        {
          c = findClass(name);
        }
        catch (ClassNotFoundException ignore)
        {
          c = parentsVersion;
        }
      }
    }
    if (c == null) {
      throw new ClassNotFoundException(name);
    }
    if (resolve) {
      resolveClass(c);
    }
    return c;
  }

  /* Error */
  protected Class findClass(String name)
    throws ClassNotFoundException
  {
    // Byte code:
    //   0: getstatic 76    server/CM4:trace    Z
    //   3: ifeq +25 -> 28
    //   6: getstatic 82    java/lang/System:out    Ljava/io/PrintStream;
    //   9: new 84  java/lang/StringBuffer
    //   12: dup
    //   13: ldc -98
    //   15: invokespecial 87   java/lang/StringBuffer:<init> (Ljava/lang/String;)V
    //   18: aload_1
    //   19: invokevirtual 94   java/lang/StringBuffer:append   (Ljava/lang/String;)Ljava/lang/StringBuffer;
    //   22: invokevirtual 95   java/lang/StringBuffer:toString ()Ljava/lang/String;
    //   25: invokevirtual 100  java/io/PrintStream:println (Ljava/lang/String;)V
    //   28: new 84 java/lang/StringBuffer
    //   31: dup
    //   32: aload_1
    //   33: bipush 46
    //   35: bipush 47
    //   37: invokevirtual 162  java/lang/String:replace    (CC)Ljava/lang/String;
    //   40: invokestatic 166   java/lang/String:valueOf    (Ljava/lang/Object;)Ljava/lang/String;
    //   43: invokespecial 87   java/lang/StringBuffer:<init> (Ljava/lang/String;)V
    //   46: ldc -88
    //   48: invokevirtual 94   java/lang/StringBuffer:append   (Ljava/lang/String;)Ljava/lang/StringBuffer;
    //   51: invokevirtual 95   java/lang/StringBuffer:toString ()Ljava/lang/String;
    //   54: astore_2
    //   55: aload_0
    //   56: aload_2
    //   57: invokevirtual 172  server/Loader:getResource   (Ljava/lang/String;)Ljava/net/URL;
    //   60: astore_3
    //   61: aload_3
    //   62: ifnonnull +12 -> 74
    //   65: new 120    java/lang/ClassNotFoundException
    //   68: dup
    //   69: aload_1
    //   70: invokespecial 142  java/lang/ClassNotFoundException:<init>   (Ljava/lang/String;)V
    //   73: athrow
    //   74: aconst_null
    //   75: astore 4
    //   77: aload_3
    //   78: invokevirtual 178  java/net/URL:openStream ()Ljava/io/InputStream;
    //   81: astore 4
    //   83: aload 4
    //   85: invokestatic 182   server/Loader:readFully (Ljava/io/InputStream;)[B
    //   88: astore 5
    //   90: aload 5
    //   92: invokestatic 186   server/Loader:decrypt   ([B)V
    //   95: getstatic 76   server/CM4:trace    Z
    //   98: ifeq +25 -> 123
    //   101: getstatic 82  java/lang/System:out    Ljava/io/PrintStream;
    //   104: new 84    java/lang/StringBuffer
    //   107: dup
    //   108: ldc -68
    //   110: invokespecial 87  java/lang/StringBuffer:<init> (Ljava/lang/String;)V
    //   113: aload_1
    //   114: invokevirtual 94  java/lang/StringBuffer:append   (Ljava/lang/String;)Ljava/lang/StringBuffer;
    //   117: invokevirtual 95  java/lang/StringBuffer:toString ()Ljava/lang/String;
    //   120: invokevirtual 100 java/io/PrintStream:println (Ljava/lang/String;)V
    //   123: aload_0
    //   124: aload_1
    //   125: aload 5
    //   127: iconst_0
    //   128: aload 5
    //   130: arraylength
    //   131: invokevirtual 192 server/Loader:defineClass   (Ljava/lang/String;[BII)Ljava/lang/Class;
    //   134: astore 8
    //   136: jsr +25 -> 161
    //   139: aload 8
    //   141: areturn
    //   142: astore 5
    //   144: new 120   java/lang/ClassNotFoundException
    //   147: dup
    //   148: aload_1
    //   149: invokespecial 142 java/lang/ClassNotFoundException:<init>   (Ljava/lang/String;)V
    //   152: athrow
    //   153: astore 7
    //   155: jsr +6 -> 161
    //   158: aload 7
    //   160: athrow
    //   161: astore 6
    //   163: aload 4
    //   165: ifnull +13 -> 178
    //   168: aload 4
    //   170: invokevirtual 198 java/io/InputStream:close   ()V
    //   173: goto +5 -> 178
    //   176: astore 9
    //   178: ret 6
    // Line number table:
    //   Java source line #124  -> byte code offset #0
    //   Java source line #128  -> byte code offset #28
    //   Java source line #129  -> byte code offset #55
    //   Java source line #131  -> byte code offset #61
    //   Java source line #132  -> byte code offset #65
    //   Java source line #135  -> byte code offset #74
    //   Java source line #138  -> byte code offset #77
    //   Java source line #140  -> byte code offset #83
    //   Java source line #143  -> byte code offset #90
    //   Java source line #144  -> byte code offset #95
    //   Java source line #145  -> byte code offset #123
    //   Java source line #147  -> byte code offset #142
    //   Java source line #149  -> byte code offset #144
    //   Java source line #152  -> byte code offset #153
    //   Java source line #153  -> byte code offset #163
    //   Java source line #154  -> byte code offset #178
    // Local variable table:
    //   start  length  slot    name    signature
    //   0  180 0   this    Loader
    //   0  180 1   name    String
    //   54 3   2   classResource   String
    //   60 18  3   classURL    URL
    //   75 94  4   in  InputStream
    //   88 41  5   classBytes  byte[]
    //   142    3   5   ioe IOException
    //   161    1   6   localObject1    Object
    //   153    6   7   localObject2    Object
    //   176    3   9   ignore  Exception
    // Exception table:
    //   from   to  target  type
    //   77 142 142 java/io/IOException
    //   77 139 153 finally
    //   142    153 153 finally
    //   168    176 176 java/lang/Exception
  }

  private Loader(ClassLoader parent, File classpath)
    throws MalformedURLException
  {
    super(new URL[] { classpath.toURL() }, parent);
    if (parent == null) {
      throw new IllegalArgumentException("EncryptedClassLoader requires a non-null delegation parent");
    }
  }

  private static void decrypt(byte[] data)
  {
    for (int i = 8; i < data.length; i++)
    {
      int tmp8_7 = i;data[tmp8_7] = ((byte)(data[tmp8_7] ^ 0x63));
    }
  }

  private static byte[] readFully(InputStream in)
    throws IOException
  {
    ByteArrayOutputStream buf1 = new ByteArrayOutputStream();
    byte[] buf2 = new byte['?'];
    int read;
    while ((read = in.read(buf2)) > 0)
    {
      int read;
      buf1.write(buf2, 0, read);
    }
    return buf1.toByteArray();
  }
}

Lets see where the input string for the load-method goes.
First it gets passed to the loadClass and from there it gets passed to the findClass method. Since we can't decompile the findClass method we have to go through the bytecode to see what it does. The important parts are:

  private static void decrypt(byte[] data)
  {
    for (int i = 8; i < data.length; i++)
    {
      int tmp8_7 = i;data[tmp8_7] = ((byte)(data[tmp8_7] ^ 0x63));
    }
  }

The decrypt method takes the byte-array and does an XOR 0x63 on each byte starting at index 8.
So what the loader does is to read the contents of the KeyfileCheck.class, passes the contents to the decrypt method and finally creates a new instance of the decrypted class.
Lets create a decrypter for the KeyfileCheck.class

using System;
using System.Collections.Generic;
using System.IO;

namespace muckis_crackme4_Class_decrypter
{
    class ClassDecrypter
    {
        static void Main(string[] args)
        {
            if (args.Length != 1)
            {
                var fileName = System.Diagnostics.Process.GetCurrentProcess().MainModule.ModuleName;
                Console.WriteLine("Usage: " + fileName + " encrypted.class");
                return;
            }
            var inputFile = args[0];
            var outputFile = "decrypted_" + inputFile;
            var fileData = File.ReadAllBytes(inputFile);
            Decrypt(fileData);
            File.WriteAllBytes(outputFile, fileData);
            Console.WriteLine("File decrypted as: " + outputFile);
        }

        private static void Decrypt(IList<byte> data)
        {
            for (var i = 8; i < data.Count; i++)
            {
                data[i] = ((byte)(data[i] ^ 0x63));
            }
        }
    }
}

After running this on the encrypted KeyfileCheck.class we end up with a new file called decrypted_KeyfileCheck.class. Open that file in the decompiler and take a look at the contents.

package server;

import java.io.BufferedReader;
import java.io.FileReader;
import java.util.zip.CRC32;
import javax.swing.JOptionPane;

public class KeyfileCheck
{
  public static void main(String[] args)
  {
    boolean valid = false;

    int counter = 0;
    try
    {
      CRC32 crc32 = new CRC32();
      FileReader fr = new FileReader(decrypt("삵삻삧삸삷삲삻샰사삻삹"));
      BufferedReader br = new BufferedReader(fr);
      char[] name = br.readLine().toLowerCase().toCharArray();
      String serial = br.readLine();
      for (int i = 0; i < name.length; i++) {
        counter += name[i] * ((i >> 1) + 4) * (i ^ 0x3);
      }
      crc32.update(counter);
      valid = serial.toLowerCase().equals(Long.toHexString(crc32.getValue()));
      br.close();
      fr.close();
    }
    catch (Exception localException) {}
    if (!valid)
    {
      JOptionPane.showMessageDialog(null, "No valid keyfile found!", "Error", 0);
      System.exit(0);
    }
  }

  private static String decrypt(String s)
  {
    int i = s.length();
    char[] ac = new char[i];
    for (int j = 0; j < i; j++) {
      ac[j] = ((char)(s.charAt(j) ^ 0xFFDEC0DE));
    }
    return new String(ac);
  }
}

So the file name is hidden from us at this time, but we got the routine to reverse it to the correct name thanks to the decrypt method. and by looking at the code we can see that the crackme tries to read two lines from the file, the first line containing the name and the second line the serial. We also find out the serial routine which is a crc32-sum of the calculations in the for-loop.

Let's code a keymaker!

First we have to implement Java's CRC32-implementation.

namespace muckis_crackme4_keyfile_generator
{
    public class JavaCrc32
    {
        private uint _crc;

        private static readonly uint[] CrcTable = make_crc_table();

        private static uint[] make_crc_table()
        {
            var crcTable = new uint[256];
            for (uint n = 0; n < 256; n++)
            {
                var c = n;
                for (var k = 8; --k >= 0; )
                {
                    if ((c & 1) != 0)
                        c = 0xedb88320 ^ (c >> 1);
                    else
                        c = c >> 1;
                }
                crcTable[n] = c;
            }
            return crcTable;
        }

        public long GetValue()
        {
            return _crc & 0xffffffffL;
        }

        public void Reset() { _crc = 0; }

        public void Update(uint bval)
        {
            var c = ~_crc;
            c = CrcTable[(c ^ bval) & 0xff] ^ (c >> 8);
            _crc = ~c;
        }

        public void Update(byte[] buf, int off, int len)
        {
            var c = ~_crc;
            while (--len >= 0)
                c = CrcTable[(c ^ buf[off++]) & 0xff] ^ (c >> 8);
            _crc = ~c;
        }

        public void Update(byte[] buf) { Update(buf, 0, buf.Length); }
    }
}

And now the keymaker:

using System;
using System.Collections.Generic;
using System.IO;

namespace muckis_crackme4_keyfile_generator
{
    class Keygen
    {
        static void Main(string[] args)
        {
            Console.Write("Name: ");
            var input = Console.ReadLine();
            if (string.IsNullOrEmpty(input))
            {
                return;
            }
            input = input.ToLower();
            uint counter = 0;
            var crc32 = new JavaCrc32();
            var fileName = Decrypt("삵삻삧삸삷삲삻샰사삻삹");
            var name = input.ToLower().ToCharArray();
            for (uint i = 0; i < name.Length; i++) {
                counter += name[i] * ((i >> 1) + 4) * (i ^ 0x3);
            }
            crc32.Update(counter);
            var serial = crc32.GetValue().ToString("X4");
            var fileData = new List<string> { input, serial };
            File.WriteAllLines(fileName, fileData);
            Console.WriteLine("Keyfile created: " + fileName);
            Console.ReadKey();
        }

        private static string Decrypt(string s)
        {
            var i = s.Length;
            var ac = new char[i];
            for (var j = 0; j < i; j++)
            {
                ac[j] = ((char)(s[j] ^ 0xFFDEC0DE));
            }
            return new string(ac);
        }
    }
}

After we've generated a keyfile and made sure it is located in the same folder as crackme4.jar, let's try to start the crackme. And bam! The keyfile is accepted!

Now for the last part of this crackme, we have to write a bruteforcer for the serial. We could write a hash-bruteforcer, but since it's a server, why don't we build a network bruteforcer?

BruteForceClient.cs

using System;
using System.Net.Sockets;
using System.Text;

namespace muckis_crackme4_network_bruteforcer
{
    class BruteForceClient
    {
        private const int MaxLoops = 100;
        private const string Hostname = "localhost";
        private const int Port = 23;
        private const string ServerHeader = ">>muckis crackme #4<<\r\n\r\nEnter serial:";
        private const string EnterSerial = "Enter serial:";
        private const string WrongSerial = "Wrong serial, try again!";
        private const string CorrectSerial = "Valid serial!";

        private byte[] _serialToTest;
        private bool _loop = true;
        private string _readData;

        private TcpClient _client;
        private NetworkStream _networkStream = default(NetworkStream);

        public string FoundSerial { get; private set; }
        public bool IsSerialFound
        {
            get { return !string.IsNullOrEmpty(FoundSerial); }
        }

        public void TrySerial(string serial)
        {
            _loop = true;
            _client = new TcpClient();
            _serialToTest = Encoding.Default.GetBytes(serial);
            if (_client.Connected) _client.Close();
            _client.Connect(Hostname, Port);
            GetResponse();
        }

        private void SendData()
        {
            _networkStream.Write(_serialToTest, 0, _serialToTest.Length);
            _networkStream.Flush();
        }

        private void GetResponse()
        {
            var loops = 0;
            while (_loop)
            {
                if (!_client.Connected && _client != null)
                {
                    _loop = false;
                    break;
                }
                _networkStream = _client.GetStream();
                var buffSize = _client.ReceiveBufferSize;
                var inStream = new byte[buffSize];
                _networkStream.Read(inStream, 0, buffSize);
                inStream = StripArrayOfZeroValues(inStream);
                var readString = Encoding.Default.GetString(inStream).Trim();
                if (String.IsNullOrEmpty(readString)) continue;
                _readData = readString;
                if (String.IsNullOrEmpty(_readData)) continue;
                switch (_readData)
                {
                    case ServerHeader:
                    case EnterSerial:
                        SendData();
                        break;
                    case CorrectSerial:
                        FoundSerial = Encoding.Default.GetString(_serialToTest);
                        Done();
                        break;
                    case WrongSerial:
                        Done();
                        break;
                }
                _networkStream.Flush();
                if (++loops == MaxLoops) _loop = false;
            }
        }

        private void Done()
        {
            _readData = "";
            _loop = false;
        }

        private static byte[] StripArrayOfZeroValues(byte[] packet)
        {
            var i = packet.Length - 1;
            while (packet[i] == 0)
            {
                if (i != 0) --i;
            }
            var temp = new byte[i + 1];
            Array.Copy(packet, temp, i + 1);
            return temp;
        }
    }
}

Bruteforcer.cs

using System;

namespace muckis_crackme4_network_bruteforcer
{
    class Bruteforcer
    {
        private static readonly char[] ValidChars =
        {
            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
            'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
            'u', 'v', 'x', 'y', 'z',
            '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'
        };
        private static int _validCharsLenght;
        private static long _triedKeys;
        private static bool _isFound;
        private static string _foundSerial;
        private static readonly BruteForceClient BfClient = new BruteForceClient();

        static void Main()
        {
            _validCharsLenght = ValidChars.Length;
            var estimatedPasswordLength = 0;
            var timeStarted = DateTime.Now;
            Console.WriteLine("Start BruteForce - {0}", timeStarted);
            while (!_isFound)
            {
                estimatedPasswordLength++;
                BruteForce(estimatedPasswordLength);
            }
            Console.WriteLine("Serial found. - {0}", DateTime.Now);
            Console.WriteLine("Time passed: {0}s", DateTime.Now.Subtract(timeStarted).TotalSeconds);
            Console.WriteLine("Resolved serial: {0}", _foundSerial);
            Console.WriteLine("Computed keys: {0}", _triedKeys);
            Console.ReadLine();
        }

        private static void BruteForce(int keyLength)
        {
            var keyToTest = CreateKeyArray(keyLength, ValidChars[0]);
            var indexOfLastChar = keyLength - 1;
            CreateKey(0, keyToTest, keyLength, indexOfLastChar);
        }

        private static void CreateKey(int currentPosition, char[] keyToTest, int keyLength, int indexOfLastChar)
        {
            var nextPosition = currentPosition + 1;
            for (var i = 0; i < _validCharsLenght; i++)
            {
                if (_isFound) break;
                keyToTest[currentPosition] = ValidChars[i];
                if (currentPosition < indexOfLastChar)
                {
                    CreateKey(nextPosition, keyToTest, keyLength, indexOfLastChar);
                } else
                {
                    _triedKeys++;
                    try
                    {
                        BfClient.TrySerial(new string(keyToTest) + "\r\n");
                    } catch (Exception e)
                    {
                        Console.WriteLine(e.Message);
                        Environment.Exit(-1);
                    }
                    if (!BfClient.IsSerialFound) continue;
                    if (_isFound) return;
                    _isFound = true;
                    _foundSerial = new String(keyToTest);
                    return;
                }
            }
        }

        private static char[] CreateKeyArray(int keyLenght, char validChar)
        {
            var keyArray = new char[keyLenght];
            for (var i = 0; i < keyLenght; i++)
            {
                keyArray[i] = validChar;
            }
            return keyArray;
        }
    }
}

When we run this we end up with this output:

Start BruteForce - 2015-07-18 21:40:09
Serial found. - 2015-07-18 21:40:57
Time passed: 48,0493151s
Resolved serial: thx
Computed keys: 24803

There we go, we successfully generated a valid keyfile and we've bruteforced the serial!