Target: mucki’s crackme#4
URL: http://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.
// INTERNAL ERROR //
Woops, we cant decompile the KeyfileCheck.class. Let’s take a look at what the loader does then.
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: (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: (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: (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: (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: (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:
// 85: invokestatic 182 server/Loader:readFully (Ljava/io/InputStream;)[B
// 88: astore 5
// 90: aload 5
// 92: invokestatic 186 server/Loader:decrypt ([B)V
Here we see that an inputstream is passed to the readFully method, which then returns a byte-array. That array is then passed to the decrypt method.
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 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 { 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!