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:
- Die Seriennummer muss mit
PFS-beginnen. - Danach werden Bindestriche ignoriert.
- Es müssen genau 12 Serienzeichen übrig bleiben.
- Jedes dieser 12 Zeichen muss im eingebetteten Alphabet vorkommen.
- Beim Lizenzschlüssel werden Bindestriche und Leerzeichen ignoriert.
- 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.