Deutschlands Bester Hacker - Notizblock (Crypto) - 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 Crypto
Punkte 454
Angriffsklasse AES-CTR Keystream Reuse (Two-Time-PadKeystream ReuseMehrfache Verwendung desselben Schlüsselstroms, wodurch verschlüsselte Nachrichten angreifbar werden.)
Flag DBH{K3YSTR34M_R3CYCL1NG_1ST_K31N3_1D33_XXX}

Die Challenge

Die Anwendung speichert Notizen nur verschlüsseltEncryptionWandelt Klartext mithilfe eines Schlüssels in nicht lesbaren Geheimtext um.. Laut Beschreibung wird AES-CTR mit einem 256-Bit-Schlüssel verwendet. Der kritische Punkt: der IVInitialization VectorStartwert einer Blockchiffre-Betriebsart, der gleiche Klartexte zu unterschiedlichen Chiffraten macht. ist nicht pro Nachricht zufällig, sondern hängt nur von der Kategorie ab:

  • allgemein
  • vertraulich

Der Klartext jeder Notiz hat immer dasselbe Format:

<kategorie>|<zeitstempel>|<benutzer>|<text>

Aufklärung

POST /api/notiz liefert nicht nur das Chiffrat zurück, sondern auch den kompletten Klartext-Umschlag. Damit ist ein Known-Plaintext-Paar frei Haus geliefert — man muss es nur noch anfordern.

Die Schwachstelle

Bei AES-CTR gilt:

Ciphertext = Plaintext XOR Keystream

Wird derselbe IV mit demselben Schlüssel wiederverwendet, entsteht für alle Nachrichten derselbe Keystream. Genau das passiert hier pro Kategorie. Aus einer selbst erzeugten Gastnotiz lässt sich der Keystream direkt berechnen:

Keystream = Ciphertext XOR Plaintext

Sobald der Keystream für allgemein oder vertraulich bekannt ist, lassen sich alle Archiv-Einträge derselben Kategorie entschlüsseln:

Plaintext = Ciphertext XOR Keystream

Der Angriff

Es reichen zwei Testnotizen, eine pro Kategorie, mit möglichst langem Text — der Keystream muss mindestens so lang sein wie der längste Archiv-Eintrag.

Probe für allgemein:

TEXT=$(python3 -c 'print("A"*256, end="")')
curl -s '<URL>/api/notiz' \
  -H 'Content-Type: application/json' \
  --data "$(printf '{"kategorie":"allgemein","text":"%s"}' "$TEXT")"

Probe für vertraulich:

TEXT=$(python3 -c 'print("B"*256, end="")')
curl -s '<URL>/api/notiz' \
  -H 'Content-Type: application/json' \
  --data "$(printf '{"kategorie":"vertraulich","text":"%s"}' "$TEXT")"

Beide Antworten enthalten klartext und chiffrat. Der Rest ist ein XOR:

keystream = xor(probe_ciphertext, probe_plaintext)
plaintext = xor(archive_ciphertext, keystream[:len(archive_ciphertext)])

Da die Probeantworten länger waren als alle Archiv-Einträge, reichte der Keystream vollständig aus. Entschlüsselt ergab sich das komplette Archiv:

1: vertraulich|2026-02-03T08:41:12Z|admin|Backup-Rotation auf Montag vorgezogen, Bandwechsel wie besprochen.
2: allgemein|2026-02-03T09:02:55Z|gast|Kaffeekasse steht wieder im Kuechenschrank links.
3: vertraulich|2026-02-04T11:17:03Z|mkramer|Zugriff auf die Reisekostenmappe bitte erst nach Freigabe durch die BL.
4: allgemein|2026-02-05T07:30:00Z|gast|Drucker im zweiten Stock hat wieder Papierstau, Technik ist informiert.
5: vertraulich|2026-02-06T16:55:41Z|admin|Notfallzugang Notizblock-Admin. Wiederherstellungscode: DBH{K3YSTR34M_R3CYCL1NG_1ST_K31N3_1D33_XXX} -- nur fuer den Fall, dass die Zwei-Faktor-App verloren geht.
6: allgemein|2026-02-07T10:12:19Z|gast|Freitag faellt das Standup aus, stattdessen kurze Runde am Montag.
7: vertraulich|2026-02-08T09:04:37Z|jvogt|Vertragsentwurf liegt zur Durchsicht im Fach, Rueckmeldung bis Mittwoch.
8: vertraulich|2026-02-09T13:22:08Z|admin|Wartungsfenster naechsten Sonntag 02:00-04:00, Dienst dann nicht erreichbar.
9: vertraulich|2026-02-10T15:48:56Z|mkramer|Bitte die alten Notizen aus dem Vorjahr archivieren, das Archiv wird zu gross.

Die Flag

DBH{K3YSTR34M_R3CYCL1NG_1ST_K31N3_1D33_XXX}

Was ich mitnehme

CTR ist nur dann sicher, wenn niemals derselbe Keystream mehrfach verwendet wird. Ein fester, aus der Kategorie abgeleiteter IV macht alle Nachrichten einer Kategorie zu einem Two-Time-Pad und erlaubt damit die vollständige Entschlüsselung per bekanntem Klartext.

Verschärfend kommt hinzu, dass die API selbst das Klartext-Chiffrat-Paar zurückgibt. Man braucht dann nicht einmal statistische Two-Time-Pad-Analyse, sondern bekommt den Keystream geschenkt.