Deutschlands Bester Hacker - PixelForge (Reversing) - Das Writeup

Disclaimer: Im August 2026 fand die Qualifikation für das Finale von Deutschlands Bester Hacker statt, welches ich auf Platz 1 abschließen konnte. Nur zwei Teilnehmer waren in der Lage, alle Challenges zu lösen. Dieses Writeup wurde mit KI auf Basis meiner Notizen erstellt und kann Fehler enthalten, bei Fragen bitte direkt an mich wenden im DBH-Discord.

Wettbewerb Deutschlands Bester Hacker 2026 — Qualifikation
Kategorie ReversingReverse EngineeringAnalyse eines Systems oder Programms zur Rekonstruktion seiner Funktionsweise.
Punkte 359
Angriffsklasse KeygenningKeygenningRekonstruktion eines Lizenzalgorithmus, um gültige Schlüssel selbst erzeugen zu können. — Lizenzalgorithmus rekonstruieren

Die Challenge

Gegeben war ein kleines Programm pixelforge-lizenz, das eine Seriennummer und einen Lizenzschlüssel erwartet. Für eine vom Lizenzserver genannte Seriennummer sollte der passende Lizenzkey erzeugt werden — es genügt also nicht, den Sprung zu patchen, man muss den Algorithmus tatsächlich verstehen.

Serial: PFS-6EM3-643V-FKAF
Key:    KKYF-ACA5-S5MD-ADKR

Aufklärung

file pixelforge-lizenz
ELF 64-bit LSB executable, x86-64, dynamically linked, stripped

Ein gestripptes 64-Bit-Linux-ELF. Danach die einfachen statischen Checks:

strings -tx pixelforge-lizenz
objdump -d -M intel pixelforge-lizenz

In den Strings stecken schon ein paar wichtige Hinweise:

Aufruf: %s <seriennummer> <lizenzschluessel>
PFS-
ungueltig
ABCDEFGHJKLMNPQRSTUVWXYZ23456789

Das Alphabet ist besonders auffällig: ein Base32-ähnliches Alphabet ohne leicht verwechselbare Zeichen wie I, O, 0 und 1.

Die Schwachstelle

Beim Lesen der Disassembly ergibt sich zunächst das Eingabeformat:

  1. Die Seriennummer muss mit PFS- beginnen.
  2. Danach werden Bindestriche ignoriert.
  3. Es müssen genau 12 Serienzeichen übrig bleiben.
  4. Jedes dieser 12 Zeichen muss im eingebetteten Alphabet vorkommen.
  5. Beim Lizenzschlüssel werden Bindestriche und Leerzeichen ignoriert.
  6. Danach müssen genau 16 Key-Zeichen übrig bleiben.

PFS-6EM3-643V-FKAF wird intern also zu 6EM3643VFKAF.

Nach der Formatprüfung berechnet das Programm aus den 12 Serienzeichen zwei 32-Bit-Werte. Der erste ist eine FNV-1a-ähnliche Hash-Schleife:

h1 = 0x811c9dc5
for byte in serial:
    h1 ^= byte
    h1 *= 0x01000193

Der zweite wird rückwärts über die Seriennummer berechnet:

h2 = 0x2545f491
for byte in reversed(serial):
    h2 += byte
    h2 = rol32(h2, 3)
    h2 ^= 0x5bd1e995

Danach werden beide Werte gemischt:

h1 ^= h1 >> 15;  h1 *= 0x85ebca6b;  h1 ^= h1 >> 13
h2 ^= h2 >> 16;  h2 *= 0xc2b2ae35;  h2 ^= h2 >> 16

Wichtig: alle Operationen laufen als 32-Bit-Arithmetik, Multiplikationen also modulo 2^32. Anschließend werden beide zu einem 64-Bit-Wert kombiniert:

value = (h1 << 32) | h2

Daraus erzeugt das Programm 13 Zeichen, indem es von oben nach unten 5-Bit-Gruppen extrahiert:

for shift in 60, 55, 50, ..., 0:
    index = (value >> shift) & 31
    output += alphabet[index]

Das sind 13 Zeichen, weil aus einem 64-Bit-Wert bei 5-Bit-Gruppen 65 Bits betrachtet werden — die oberste Gruppe enthält effektiv ein führendes Nullbit.

Die letzten drei Zeichen des Lizenzschlüssels sind Prüfzeichen:

check1 = alphabet[sum(key_indices) & 31]
check2 = alphabet[xor(key_indices)]
check3 = alphabet[(sum(serial_indices) * 7) & 31]

13 generierte Zeichen plus 3 Prüfzeichen ergeben den 16 Zeichen langen Lizenzkey.

Der Angriff

Der Algorithmus ist vollständig deterministisch und benötigt keinerlei Geheimnis — er lässt sich also direkt als Keygen nachbauen:

#!/usr/bin/env python3
import sys

ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"


def rol32(value, bits):
    value &= 0xFFFFFFFF
    return ((value << bits) | (value >> (32 - bits))) & 0xFFFFFFFF


def normalize_serial(serial):
    if not serial.startswith("PFS-"):
        raise ValueError("Seriennummer muss mit PFS- beginnen")

    body = serial[4:].replace("-", "")
    if len(body) != 12:
        raise ValueError("Seriennummer muss nach PFS- genau 12 Zeichen enthalten")

    invalid = [ch for ch in body if ch not in ALPHABET]
    if invalid:
        raise ValueError(f"Ungueltige Serienzeichen: {''.join(invalid)}")

    return body


def generate_key(serial):
    serial_body = normalize_serial(serial)
    serial_bytes = serial_body.encode("ascii")

    h1 = 0x811C9DC5
    for byte in serial_bytes:
        h1 ^= byte
        h1 = (h1 * 0x01000193) & 0xFFFFFFFF

    h2 = 0x2545F491
    for byte in reversed(serial_bytes):
        h2 = (h2 + byte) & 0xFFFFFFFF
        h2 = rol32(h2, 3)
        h2 ^= 0x5BD1E995

    h1 ^= h1 >> 15
    h1 = (h1 * 0x85EBCA6B) & 0xFFFFFFFF
    h1 ^= h1 >> 13

    h2 ^= h2 >> 16
    h2 = (h2 * 0xC2B2AE35) & 0xFFFFFFFF
    h2 ^= h2 >> 16

    value = ((h1 & 0xFFFFFFFF) << 32) | (h2 & 0xFFFFFFFF)

    chars = []
    index_sum = 0
    index_xor = 0

    for shift in range(60, -1, -5):
        index = (value >> shift) & 31
        chars.append(ALPHABET[index])
        index_sum += index
        index_xor ^= index

    serial_index_sum = sum(ALPHABET.index(ch) for ch in serial_body)

    chars.append(ALPHABET[index_sum & 31])
    chars.append(ALPHABET[index_xor])
    chars.append(ALPHABET[(serial_index_sum * 7) & 31])

    raw_key = "".join(chars)
    return "-".join(raw_key[i:i + 4] for i in range(0, 16, 4))


def main():
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} PFS-XXXX-XXXX-XXXX", file=sys.stderr)
        raise SystemExit(2)

    print(generate_key(sys.argv[1]))


if __name__ == "__main__":
    main()

Als Gegenprobe zuerst eine selbst gewählte Test-Seriennummer:

Serial: PFS-ABCDEFGHJKLM
Key:    LJUL-CFZ9-4SDG-86EQ

Das Binary meldete gueltig. Dann die vom Server genannte Seriennummer:

python3 keygen.py PFS-6EM3-643V-FKAF
# -> KKYF-ACA5-S5MD-ADKR

Auch hier meldete das Binary gueltig.

Die Flag

Der passende Lizenzschlüssel für die Seriennummer PFS-6EM3-643V-FKAF lautet:

KKYF-ACA5-S5MD-ADKR

Damit akzeptiert das Programm die Lizenz und gibt die Freischaltung im geforderten Format aus.

Was ich mitnehme

Ein Lizenzcheck, der lokal aus der Seriennummer rechnet, ist kein Schutz, sondern eine Bauanleitung. Sobald der Algorithmus keinerlei serverseitiges Geheimnis enthält — hier: zwei Hash-Runden, ein Mixer, eine Base32-artige Ausgabe und drei Prüfzeichen — reicht statisches Reversing, um beliebig viele gültige Keys zu erzeugen.

Der praktische Kniff beim Keygenning ist die schrittweise Verifikation: erst mit einer selbst gewählten Seriennummer gegen das Binary testen, dann erst die Zielseriennummer. So weiß man vor dem entscheidenden Versuch, dass die Rekonstruktion stimmt.