Deutschlands Bester Hacker - Bilderrauschen (Forensics) - 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 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.