Ich habe mir mal eine Java Crackme .jar heruntergeladen und geschaut was ich als sterblicher Java Entwickler so damit anfangen kann.

Vor einigen Jahren habe ich mich mal ein paar Tage lang mit Reverse Engineering und den dazugehörigen Tools wie Ghidra beschäftigt. Auch sehr empfehlen kann ich das Buch Hacking: The Art of Exploitation welches bei jedem Interessierten sofort einen Funken zünden sollte. Für mich als C Unerfahrenen war selbst die Einleitung zur Assembly-Sprache schon harte Kost. Aber insgesamt ist es das Buch für jeden, der sich für richtig tiefgehende Softwareproblematiken interessiert.

Hier fällt mir ein, ich sollte mal eine Bücherliste machen, aber das zu einem späteren Zeitpunkt.

YouTube Begleitvideo

Ich habe den ganzen Lern- und Entwicklungsprozess aufgenommen und auf YouTube gestellt: Java Crackme Dekompilieren und Keygen programmieren

Ab jetzt hast du also die Möglichkeit mich entweder beim Grübeln anzuschauen und potentiell nicht nachvollziehen zu können worüber ich nachdenke, oder einfach den potentiell kohärenten folgenden Text zum Projekt zu lesen.

Die passende Crackme finden

Eine Crackme habe ich hier gefunden. Crackmes werden auf einer Schwierigkeitsskala von 1.0 bis 6.0 bewertet. Ich habe mir dann einfach mal diese hier besorgt. Die Schwierigkeit ist mit 1.0 angegeben.

Den Anfang finden

Die heruntergeladene .jar öffne ich zunächst. Das Fenster sieht so aus: Crackme Java GUI

Hier kann man einen Namen und eine Seriennummer eingeben. Ist die Eingabe falsch, öffnet sich eine Message Box mit der Nachricht “Invalid Key, try again”. Dass ich auf Anhieb den richtigen Key finde ist praktisch unmöglich. Man könnte theoretisch auch einen Bruteforcer programmieren, anstatt über das Problem weiter nachzudenken. Ein Ergebnis ist dann aber auch nicht in absehbarer Zeit zu erwarten. Je nach Stärke der zu findenden Seriennummer hat uns die Entropie locker schon vernichtet.

Javas Schwachstelle

Java .jar Dateien sind bloß Behälter für sogenannte .class Dateien und ein paar Metadaten. Man kann sich das ganze so wie eine .zip Datei vorstellen. Und wenn man die .jar Datei mit einem ZIP-Tool entpackt, dann verhält sie sich auch genau so.

Die entpackten .class Dateien kann man sich dann anschauen. Bis auf ein paar vereinzelte Klartext-Strings, die auf verwendete Imports und Klassen hinweisen, wird man hier aber selten schlau.

Warum nenne ich das jetzt aber eine Schwachstelle, wenn ich kaum was lesen kann? Im Gegensatz zu anderen Programmiersprachen wie C wird Java nicht zu Maschinencode kompiliert. Das heißt die Anweisungen in den .class Dateien sind nicht für den Prozessor verständlich. Der sogenannte Java Bytecode ist eine möglichst Low-Level, aber dennoch vom Prozessor und seiner Architektur unabhängige Repräsentation der Programmlogik.

Wir haben hier also keinen Klartext mehr, aber so richtigen Maschinencode auch nicht. Wenn man sich jetzt also richtig mit diesem Java Bytecode beschäftigt und lange genug drauf schaut, dann kann aus dem Bytecode wieder ausführbare Java-Klassen generieren. Bloß die Variablen- und Klassennamen gehen verloren, da der Java Compiler javac diese aus Speichergründen wegoptimiert.

Wir nutzen die Technologie: Java Dekompilierer

Eine kurze Suche nach “Java decompiler” und wir landen auf dieser vielversprechenden Java Decompiler Website. Hier downloade ich mir das Tool und führe es lokal aus. Ich lade die Crackme .jar und es ergibt sich folgendes Bild:

Java Decompiler mit geladener Crackme

Und es werde Quellcode!

Schon ist es geschafft. Das war’s! Oder nicht?

Je länger ich mir den Code anschaue, desto mehr Kopfschmerzen bereitet er mir.

Wie soll man dekompilierten Java Code verstehen?

Der dekompilierte Quellcode ist voller obskurer Variablenbezeichnungen. Die Klassen heißen KClass, UClass, XClass. Variablennamen reichen von unknown über var bis ac, leet, und xxx. Ich bin mir nicht sicher ob der Original Quellcode schon solche Bezeichnungen beinhaltete. Ich kann mir vorstellen, dass der Crackme Autor das Programm so unverständlich wie möglich machen wollte.

Nach ein paar Minuten voller Scheitern beim Nachvollziehen des Codes greife ich zu stärkeren Mitteln.

Ich lade die dekompilierten Quellen in meine Eclipse

Der Java Decompiler kann die dekompilierten Klassen abspeichern. Das abgespeicherte Paket lade ich dann als Projekt in Eclipse. Jede x-beliebige andere IDE geht natürlich genau so.

In Eclipse führe ich die main-Methode aus und voila (!), das kleine Seriennummer-Fenster öffnet sich. Der dekompilierte Quellcode ist also ausführbar!

Das bedeutet, das der Dekompilierer mit höchster Wahrscheinlichkeit korrekt funktioniert.

Wie man unlesbaren Code durchforstet

Der gleiche (für sterbliche Entwickler) unlesbare Code liegt jetzt in Eclipse vor. Das hat natürlich einen Zweck, denn ich kann jetzt einmal die Variablennamen refactoren. Wenn ich herausfinde, dass eine Variable bspw. das Eingabetextfeld des Namens ist, dann kann ich sie so nennen. Auch kann ich Exceptions im Log sehen. Und ich kann den Code etwas ändern und schauen was passiert. Wenn man genug mit dem Quellcode herumspielt, sollte er früher oder später einleuchten. Oder?

Ja, und praktisch ist es tatsächlich genau so eingetroffen. Nach ca. einer Stunde Quellcode studieren und Javadocs lesen, fällt mir auf, dass die Seriennummer wie folgt geprüft wird.

  1. Die Eingabe des Serial-Textfelds wird mit dem DES Cipher mit dem geheimen Key “13248657” dekodiert.
  2. Dieses Zwischenergebnis wird widerrum ein zweites mal mit dem geheimen Key “13377331” dekodiert.
  3. Das Resultat hieraus wird mit zu einem Integer Array umgewandelt, und jeder einzelne Integer eintrag wird mit einem festen Offset von -42784 verstehen. Die resultierenden Integer werden als Character (Zeichen) interpretiert.
  4. Gleicht der eingegebene Name diesem String, ist die Seriennummer gültig, ansonsten nicht.

Puh. War das schwer herauszufinden? Im Verhältnis zu anderen Crackmes vermutlich nicht. Aber so ganz einfach war’s dann doch nicht. Zumindest nicht für ein allererstes Mal.

Das ganze jetzt rückwärts: Einen Keygen schreiben

Jetzt wissen wir, dass sich aus jeder Seriennummer durch doppeltes dekodieren und eine arbiträre Subtraktion ergeben muss. Um jetzt die passende Seriennummer für jeden Namen abzuleiten müsste man die ganze Operation also umkehren.

Ich wähle einen Namen, dann

  1. Enkodiere ich ihn mit beiden Keys in umgekehrter Reihenfolge.
  2. Addiere ich die Offsets auf jedes Byte im Resultat und leite daraus Integer ab. Es ergibt sich ein Integer-Array. Das ist die gültige Seriennummer.

Im Prinzip schreibe ich also ein neues Keygenerator-Programm indem ich die Operationen der Crackme .jar in der richtigen Reihenfolge kopiere und in mein neues Programm einfüge.

Jetzt lasse ich den Keygen noch jeden beliebigen Namen akzeptieren und fertig! Der Keygenerator für jeden beliebigen Namen ist fertig.

Ich generiere einen Key für meinen Namen “Daniel” und füge ihn in das Crackme-Fenster ein und: “Valid Key!” 🎉

Der Keygen funktioniert und die eingegebene Kombination aus Name und Seriennummer funktioniert

Limitationen

Ich habe kurz gedacht ich bin der König der Softwareentwickler. Aber so ganz funktioniert es dann doch nicht. Die heruntergeladene Crackme .jar akzeptiert den generierten Key nämlich nicht!

Nur die Crackme, die in Eclipse startet akzeptiert den Key. Jetzt frage ich mich, warum. Der Bytecode müsste doch bei beiden Java Programmen identisch sein.

Aber da fällt mir ein: Den Keygen erstelle ich in Eclipse. Er wird daher mit der in Eclipse verwendeten JDK (Java Development Kit) gebaut. Wenn ich die Crackme in Eclipse starte, verwenden sowohl Keygen als auch Crackme die gleiche JDK und es werden gleiche kryptographische Bibliotheken verwendet.

Die heruntergeladene Crackme starte ich aber in Ubuntu (genau genommen in WSL / Windows Subsystem for Linux). WSL nutzt eine eigene OpenJDK.

Ich traue den beiden JDKs nicht zu, kryptographische Bibliotheken identisch zu implementieren. Wenn Keygen und Crackme also in unterschiedlichen JDKs laufen, ist es OK, wenn die generierten Seriennummern nicht passen.

Zusammenfassung

Insgesamt sehe ich das ganze Projekt als Erfolg. Ehrlich gesagt hätte ich nicht gedacht, dass ich jemals in der Lage bin einen (halbwegs) funktionierenden Keygen zu schreiben. Ich habe einiges gelernt und ich hoffe, ich konnte dich als Leser mitnehmen!

Ressourcen

Die Crackme und den Java Decompiler habe ich bereits im Beitrag verlinkt. Hier nochmal die zwei Kern-Methoden des dekompilierten Java Quellcodes, sowie mein Keygen.

Dekompilierte Crackme:

// Aus KClass:
  private void a() {
    try {
      int tlu = 16;
      String mom = this.unknown.e().getText();
      if (mom.length() > 15) {
        JOptionPane.showMessageDialog(this.unknown, "Name is longer than 15 character", "Invalid Name", 0);
      } else {
        String[] ab = this.unknown.d().getText().split(" ");
        byte[] ac = new byte[ab.length];
        int leet = 32;
        short xxx = 16;
        for (int i = 0; i < ab.length; i++)
          ac[i] = (byte)((byte)Integer.parseInt(ab[i]) - this.var * leet / tlu * xxx); 
        Key ad = new SecretKeySpec("13248657".getBytes(), "DES");
        Cipher ae = Cipher.getInstance("DES");
        ae.init(2, ad);
        byte[] xu = ae.doFinal(ac);
        byte[] io = (new XClass(xu)).xmpio();
        StringBuilder sb = new StringBuilder();
        for (int j = 0; j < io.length; j++)
          sb.append((char)io[j]); 
        if (mom.equals(sb.toString())) {
          JOptionPane.showMessageDialog(this.unknown, "Valid key", "Congratulations", 1);
        } else {
          JOptionPane.showMessageDialog(this.unknown, "Invalid Key, try again", ":P", 1);
        } 
      } 
    } catch (Exception e) {
      JOptionPane.showMessageDialog(this.unknown, "Invalid Key, try again", ":P", 1);
      e.printStackTrace();
    } 
  }

  // Aus XClass:
  public byte[] xmpio() {
    byte[] io = (byte[])null;
    try {
      Key ad = new SecretKeySpec("13377331".getBytes(), "DES");
      Cipher ae = Cipher.getInstance("DES");
      ae.init(2, ad);
      io = ae.doFinal(this.arr);
    } catch (Exception exception) {}
    return io;
  }

Erstellter Keygen:

package eu.sknine.skcrackme1;

import java.security.Key;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class Keygen {

	public static void main(String[] args) throws Exception {

		String nameInput = args[0];
		System.out.println("Input name: " + nameInput);

		Key ad = new SecretKeySpec("13377331".getBytes(), "DES");
		Cipher ae = Cipher.getInstance("DES");
		ae.init(1, ad);
		byte[] io = ae.doFinal(nameInput.getBytes());

		Key ad2 = new SecretKeySpec("13248657".getBytes(), "DES");
		Cipher ae2 = Cipher.getInstance("DES");
		ae.init(1, ad2);
		byte[] xu2 = ae.doFinal(io);

		int leet = 32;
		short xxx = 16;
		int var = 1337;
		int tlu = 16;
		int[] result = new int[xu2.length];
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < xu2.length; i++) {
			result[i] = (char) ((char) xu2[i] + var * leet / tlu * xxx);
			sb.append("" + result[i] + " ");
		}

		System.out.println("USE THIS VALID KEY: " + sb);
	}

}