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 | Web |
| Punkte | 241 |
| Angriffsklasse | Client-Side Reverse EngineeringReverse EngineeringAnalyse eines Systems oder Programms zur Rekonstruktion seiner Funktionsweise. (ActionScript 3 / SWF) |
Die Challenge
Die Seite lädt eine alte Flash-Anwendung über Ruffle, den WASM-Flash-Emulator:
<script src="<URL>/ruffle"></script>
...
player.load("Main.swf");
Gesucht ist eine Passwort-Sequenz aus Button-Klicks, nach der die App die Flag anzeigt.
Aufklärung
Aus den Ruffle-Debug-Infos:
SWF URL: .../Main.swfisActionScript3: true,swfVersion: 38,uncompressedLength: 3059Allows script access: false,numFrames: 1
Das ist eine reine Client-Side-Challenge. Die gesamte Logik — und die Flag — steckt in
Main.swf; die index.html ist nur der Loader, die mitgelieferte ruffle.js der
unveränderte Standard-Player und damit irrelevant.
Die Schwachstelle
Die SWF ist binär — als Text kopieren zerstört Bytes. Also als Datei laden:
curl -o Main.swf <URL>/Main.swf
file Main.swf
# Macromedia Flash data (compressed), version 38
Header CWS bedeutet zlib-komprimiert. Dekomprimieren (CWS → FWS) und Strings aus dem
ABC-Bytecode ziehen:
import zlib, struct, re
data = open("Main.swf", "rb").read()
ver, length = data[3], struct.unpack("<I", data[4:8])[0]
body = zlib.decompress(data[8:])
out = b"FWS" + bytes([ver]) + struct.pack("<I", length) + body
open("Main_uncompressed.swf", "wb").write(out)
for s in re.findall(rb"[\x20-\x7e]{4,}", out):
print(s.decode())
Die Methodennamen im String-Pool verraten den Ablauf:
inputSequence, createSquares, onSquareClick, revF, flashSquare, decode, makeLabel, orig
Es gibt 4 anklickbare Felder (createSquares). Jeder Klick hängt an inputSequence an
(onSquareClick) und wird mit der Soll-Sequenz orig verglichen. Bei Übereinstimmung baut
decode/revF die Flag zusammen.
Der Angriff
Zwei Konstanten stechen heraus — im String-Pool mit $ als Trenner abgelegt, eine simple
VerschleierungObfuscationErschwert die Analyse von Code, Daten oder Kommunikation durch absichtliche Verkomplizierung. gegen naives strings:
$3$1$4$2$1$3$4$2$1$1$4$2$3$3$4$2 <- orig (Klick-Sequenz)
$D$B$H${$f$l$4$s$h$_$s$t$1$r$b$t$_$n$1$3$_$g$4$n$z$} <- Flag (Char-Array)
Die Soll-Sequenz orig sind 16 Klicks auf die Buttons 1–4:
3 · 1 · 4 · 2 · 1 · 3 · 4 · 2 · 1 · 1 · 4 · 2 · 3 · 3 · 4 · 2
Gibt man diese Reihenfolge in der App ein, zeigt sie die Flag an.
Die Flag
Der $-getrennte Char-Array wird von decode() einfach konkateniert und ergibt eine Flag im
Format DBH{...} — thematisch passend zum „Flash lebt weiter“-Motiv. Der genaue Wert ergibt
sich unmittelbar aus dem Char-Array oben; er lässt sich also sowohl über die Klicksequenz als
auch rein statisch aus dem Bytecode gewinnen.
Was ich mitnehme
- Flash lebt via Ruffle weiter — alte SWFs bleiben angreifbar und analysierbar, auch ohne Plugin.
CWS-SWFs sind nur zlib-komprimiert:zlib.decompress(data[8:])reicht, um an den ABC-Bytecode und die Strings zu kommen. Kein Spezial-Tool nötig.- Für vollständige Decompilation (lesbares AS3) wäre JPEXS ffdec der Weg gewesen — hier überflüssig, weil Soll-Sequenz und Flag als Klartext-Konstanten im Pool lagen.
- Sicherheitslektion: Client-Side-„Geheimnisse“ im SWF sind kein Schutz. Alles im
Auslieferungsartefakt ist trivial extrahierbar; die
$-Trennung verzögert nurstrings, verhindert nichts.