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 | Forensics |
| Punkte | 482 |
| Angriffsklasse | LSB-SteganografieLSB-SteganografieVerbergen von Daten in den niederwertigsten Bits von Bild- oder Audiowerten. + File CarvingFile CarvingWiederherstellung von Dateien aus Rohdaten anhand ihrer Signaturen, ohne Dateisystem-Metadaten. + ZipCrypto/CRC32-Kollision |
| Flag | DBH{P1X3L_SCHATT3N_T_MU3LL3R} |
Die Challenge
Fünf PNG-Bilder aus einer Kontrollgang-Dokumentation — und mehrere Ebenen SteganografieSteganografieVerbergen von Informationen in unauffälligen Trägerdaten, sodass ihre Existenz nicht erkennbar ist. darin:
IMG_20260418_073102.png
IMG_20260418_073115.png
IMG_20260418_073129.png
IMG_20260418_073141.png
IMG_20260418_073158.png
Alle sind nicht-interlaced RGB-PNGs mit 480 × 360 Pixeln. Sie enthalten mehrere
aufeinander aufbauende Hinweise — und einen Köder, der genau so aussieht wie die Lösung.
Werkzeuge: file, exiftool, strings, xxd, unzip/zipinfo, zsteg, dazu Python für
PNG-Chunk-Parsing, XOR und Prüfsummen.
Aufklärung
Zunächst alle PNG-MetadatenMetadatenanalyseAuswertung beschreibender Zusatzdaten in Dateien, die Herkunft, Werkzeuge und Personen preisgeben. auslesen:
exiftool -u -ee IMG_20260418_*.png
Drei Einträge fallen auf:
IMG_20260418_073102.png
Software: Snapseed 2.19
IMG_20260418_073115.png
Comment: U0lMQkVSV0VJSEVSMTQ=
IMG_20260418_073141.png
Comment: IMG_2026-04-18 // Kontrollgang Trafo Ost
Der Kommentar aus Bild 2 ist Base64:
printf '%s' 'U0lMQkVSV0VJSEVSMTQ=' | base64 -d
# -> SILBERWEIHER14
Dieser Wert wird später als XOR-Schlüssel gebraucht.
Bei der Strukturprüfung meldet exiftool für Bild 5:
Warning: Trailer data after PNG IEND chunk
Die ersten vier Bilder enden unmittelbar nach ihrem IEND-Chunk. Nur Bild 5 hat einen
Anhang:
PNG-IEND endet bei Offset: 15945 / 0x3e49
Trailerlänge: 224 Bytes
Signatur am Traileranfang: 50 4b 03 04 / PK\x03\x04
Carven:
dd if=IMG_20260418_073158.png of=hidden_flag.zip bs=1 skip=15945
zipinfo hidden_flag.zip
Das ZIP enthält genau eine gespeicherte, verschlüsselte Datei: flag.txt, 30 Bytes
unkomprimiert, CRC32 1596caa9. Ein Deep-Carving der PNG-Chunks, IDAT-Daten,
rekonstruierten Pixelbytes und üblichen LSB-Bitströme ergab in den anderen vier Bildern
keine weitere eingebettete Datei.
Die Schwachstelle
Mit dem in der Aufgabe vorgegebenen Passwort %WVR36 lässt sich das ZIP scheinbar
erfolgreich entpacken:
unzip -P '%WVR36' hidden_flag.zip
Die extrahierte Datei enthält aber nur 30 Binärbytes:
3e7f61fcbe74b0ae7418359755ad887fadd9c863b3a847f8dc880e323b2b
Obwohl der Inhalt nicht mit DBH{ beginnt, meldet unzip eine korrekte CRC32. Das ist
kein zufälliger Fehlversuch, sondern eine bewusst konstruierte ZipCrypto-/CRC32-Kollision.
Die übliche Annahme „bestandene CRC32 beweist das richtige Passwort“ gilt hier also nicht —
und genau deshalb muss der echte Schlüssel aus den Bildern kommen.
Der Angriff
Der relevante LSB-Kanal steckt in Bild 3. Extrahiert wird jeweils das niedrigste Bit der Farbkanäle in der Reihenfolge RGB, in normaler zeilenweiser Pixelreihenfolge:
zsteg -E b1,rgb,lsb,xy IMG_20260418_073129.png > image3_lsb.bin
xxd -l 32 image3_lsb.bin
Der Stream beginnt mit:
00000000: 0000 0011 0420 2236 2020 332c 2c26 3626
00000010: 0304 617d 6d00 0000 0000 0000 0000 0000
Die ersten vier Bytes sind eine Big-Endian-Längenangabe: 00 00 00 11 = 17 Bytes. Die
folgenden 17 Nutzbytes:
04 20 22 36 20 20 33 2c 2c 26 36 26 03 04 61 7d 6d
Der Fund lässt sich auch visuell erklären: Der Bildhintergrund besteht aus sehr gleichmäßigen Farbverläufen und grauem Tarnrauschen, das R, G und B gemeinsam verändert. In Bild 3 gibt es am Anfang der ersten Pixelzeile zusätzlich gezielte kanalabhängige Änderungen um genau ein Bit. Nur dort treten 0/1-Kombinationen einzelner RGB-Kanäle auf — das ist der längenpräfixierte LSB-Datenstrom.
Entschlüsselt wird er mit dem Metadatenwert aus Bild 2 als wiederholtem XOR-Schlüssel:
ciphertext = bytes.fromhex("042022362020332c2c2636260304617d6d")
key = b"SILBERWEIHER14"
plaintext = bytes(
value ^ key[index % len(key)]
for index, value in enumerate(ciphertext)
)
print(plaintext.decode())
# -> Winterdienst2024!
Damit öffnet sich das gecarvte ZIP:
unzip -P 'Winterdienst2024!' hidden_flag.zip
cat flag.txt
Beide Passwörter werden vom ZIP akzeptiert, erzeugen aber unterschiedliche Klartexte:
| Passwort | Ergebnis | SHA-256 |
|---|---|---|
%WVR36 |
30 zufällig wirkende Binärbytes | 73f9ce6e98dea16ea61a453c41f04299cd59ae46c8bc1963fa8f9be1cff7a11f |
Winterdienst2024! |
DBH{P1X3L_SCHATT3N_T_MU3LL3R}\n |
0accf57c26dfbfd109bde93417180c7800c5e9cde70b0dc10f949c0673663144 |
Beide Dateien sind exakt 30 Bytes lang und besitzen dieselbe CRC32 1596caa9. Deshalb
bestehen beide sowohl die schwache Passwortprüfung des klassischen ZipCrypto-Headers als
auch die abschließende CRC32-Prüfung.
Die Flag
DBH{P1X3L_SCHATT3N_T_MU3LL3R}
Was ich mitnehme
Die Kette war fünfstufig, und jede Stufe zeigte auf die nächste: Metadaten → Trailer nach
IEND → LSB-Stream → XOR → ZIP. Der eigentliche Kniff der Challenge ist aber der Köder.
CRC32 ist keine AuthentifizierungAuthenticationÜberprüfung der behaupteten Identität eines Benutzers oder Systems.. Eine 32-Bit-Prüfsumme lässt sich gezielt kollidieren, und ZipCrypto prüft beim Passwort nur ein bis zwei Header-Bytes. Wer „unzip meldet CRC ok“ als Beweis für das richtige Passwort nimmt, läuft in die Falle. Der LSB-Hinweis ist genau deshalb notwendig, um zwischen Köder und echtem Passwort zu unterscheiden.