Deutschlands Bester Hacker - LockBox (Reversing) - 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 ReversingReverse EngineeringAnalyse eines Systems oder Programms zur Rekonstruktion seiner Funktionsweise.
Punkte 428
Angriffsklasse Windows-Kerneltreiber-Reversing (WDM / IOCTL)
Flag DBH{ioctl_k3rn3l_r3v_m4st3r}

Die Challenge

Ein Windows-Kerneltreiber verwahrt ein Geheimnis und gibt es nur heraus, wenn man ihn in der richtigen Sprache anspricht.

Die „richtige Sprache“ ist hier die Kommunikation über IOCTLs. Der Plan war damit klar:

  1. Den WDM-Treiber statisch analysieren.
  2. Device-Name und IOCTL-Codes finden.
  3. Die interne State-Machine rekonstruieren.
  4. Die verschleierte Flag zusammensetzen bzw. direkt aus den Daten rekonstruieren.

Aufklärung

file LockBoxDriver.sys
LockBoxDriver.sys: PE32+ executable (native) x86-64, for MS Windows, 6 sections

Ein 64-Bit-PE, nativer Windows-Treiber. Die Analyse sollte also auf WDM-typische Einstiegspunkte, Dispatch-Routinen und IOCTL-Handling achten.

Ein erster String-Dump war bereits sehr aufschlussreich:

[LockBox] DriverEntry
[LockBox] Driver loaded successfully
[LockBox] Handle opened, state reset to UNAUTH
[LockBox] AUTH: Bad magic 0x%08X (expected 0x%08X)
[LockBox] AUTH: Success, state -> AUTH
[LockBox] READ_PART: Index %u read out of order (expected %u)
[LockBox] READ_PART: Returned encrypted part %u
[LockBox] GET_KEY: Not all parts read yet (%u/%u)
[LockBox] GET_KEY: Returned key for part %u
[LockBox] FINALIZE: Flag verified, state -> COMPLETE
Correct! You solved the LockBox challenge.
[LockBox] Unknown IOCTL 0x%08X

Dazu ein PDB-Pfad, der Name und Buildumgebung des Treibers verrät (Benutzername hier redigiert):

C:\Users\<redigiert>\...\LockBox\x64\Release\LockBoxDriver.pdb

In DriverEntry wird ein Device erzeugt und ein symbolischer Link angelegt:

\Device\LockBox
\DosDevices\LockBox

Für ein Usermode-Programm wäre der Treiber damit so zu öffnen:

HANDLE h = CreateFileA(
    "\\\\.\\LockBox",
    GENERIC_READ | GENERIC_WRITE,
    0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
);

Die Schwachstelle

Die Dispatch-Routine enthält einen kleinen Switch über den IOCTL-Code:

mov eax, edx
sub eax, 0x222000
je  auth

sub eax, 0x4
je  get_key

sub eax, 0x4
je  read_part

cmp eax, 0x4
je  finalize

Vier gültige IOCTLs:

IOCTL Bedeutung Eingabe Ausgabe
0x222000 AuthentifizierungAuthenticationÜberprüfung der behaupteten Identität eines Benutzers oder Systems. 4 Byte Magic keine
0x222004 Key holen 4 Byte Index 8 Byte Key
0x222008 verschlüsselten Part holen 4 Byte Index 8 Byte Part
0x22200c finalisieren 32 Byte Flag-Kandidat Erfolg/Fehler

Die Codes entsprechen CTL_CODE(FILE_DEVICE_UNKNOWN, function, METHOD_BUFFERED, FILE_ANY_ACCESS) mit den Function-Werten 0x800 bis 0x803.

Der Treiber verwaltet intern einen Zustand:

State-Wert Bedeutung
0 UNAUTH
1 authentifiziert / bereit für Reads
2 Parts wurden gelesen / bereit für Keys
3 Keys wurden gelesen / bereit für Finalize

Auth-Magic rekonstruieren. Beim Initialisieren liest der Treiber seinen eigenen PE-Header, prüft MZ und PE, und liest dann den TimeDateStamp:

cmp WORD PTR [rax], 0x5a4d              ; "MZ"
cmp DWORD PTR [rax+e_lfanew], 0x4550    ; "PE"
mov edi, DWORD PTR [nt_headers + 0x08]  ; TimeDateStamp

Daraus berechnet er zwei Werte:

auth_magic = TimeDateStamp ^ 0xDEADBEEF;
seed       = TimeDateStamp ^ 0x13371337;

Für dieses Binary (TimeDateStamp = 0x6A835DF4):

auth_magic = 0xB42EE31B
seed       = 0x79B44EC3

Statische Daten in .rdata. Dort finden sich mehrere auffällige Werte:

deadbeef cafebabe
02000000 01000000 03000000 00000000
1e1812213335392e36053169283469360528692c05376e292e6928275a5a5a5a
5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a

Die Reihenfolgetabelle ist 2, 1, 3, 0 — die Reihenfolge, in der Parts und Keys abgefragt werden müssen. Direkt danach liegt ein 32-Byte-Buffer, verschleiert mit XOR 0x5a, wie im Initialisierungscode erkennbar:

movdqu xmm0, [encrypted_buffer]
xorps  xmm0, [xor_mask_5a]

Der Angriff

Der eigentliche Kniff: die Flag muss gar nicht dynamisch abgeholt werden. Der verschleierte Buffer lässt sich direkt entschlüsseln:

enc = bytes.fromhex(
    "1e1812213335392e"
    "3605316928346936"
    "0528692c05376e29"
    "2e6928275a5a5a5a"
)

flag = bytes(b ^ 0x5a for b in enc)
print(flag.rstrip(b"\x00").decode())

Vollständig aus dem Binary heraus, ohne den Treiber je zu laden:

from pathlib import Path
import struct

data = Path("LockBoxDriver.sys").read_bytes()

pe_off = struct.unpack_from("<I", data, 0x3c)[0]
timestamp = struct.unpack_from("<I", data, pe_off + 8)[0]

auth_magic = timestamp ^ 0xDEADBEEF
seed = timestamp ^ 0x13371337

# Aus dem Section-Header ermittelt: .rdata RVA 0x2000, Raw-Offset 0x1400
rdata_raw = 0x1400
order_off = rdata_raw + 0x1f8
enc_off = rdata_raw + 0x208

order = struct.unpack_from("<4I", data, order_off)
enc = data[enc_off:enc_off + 32]
flag = bytes(b ^ 0x5a for b in enc).rstrip(b"\x00")

print(f"TimeDateStamp: 0x{timestamp:08X}")
print(f"AuthMagic:     0x{auth_magic:08X}")
print(f"Seed:          0x{seed:08X}")
print("Order:", order)
print("Flag: ", flag.decode())

Ausgabe:

TimeDateStamp: 0x6A835DF4
AuthMagic:     0xB42EE31B
Seed:          0x79B44EC3
Order: (2, 1, 3, 0)
Flag:  DBH{ioctl_k3rn3l_r3v_m4st3r}

Warum Parts und Keys trotzdem wichtig sind. Die Challenge-Beschreibung sagt: „Der Treiber gibt sie nicht am Stück heraus, und er sagt dir nicht immer die Wahrheit.“ Der Treiber zerlegt die Flag intern in vier 8-Byte-Blöcke und erzeugt vier 8-Byte-Keys aus dem Seed:

for (int part = 0; part < 4; part++) {
    for (int i = 0; i < 8; i++) {
        key[part][i] = ((seed >> ((i & 3) * 8)) + part * 0x37 + i) & 0xff;
    }
}

Die Reihenfolge ist nicht 0, 1, 2, 3, sondern 2, 1, 3, 0. Wer einen falschen Index abfragt, bekommt nicht nur einen Fehler, sondern in bestimmten Fällen Täuschdaten wie deadbeefcafebabe. Beim dynamischen Lösen muss man also erkennen, welche Antworten vertrauenswürdig sind.

Der dynamische Weg wäre gewesen:

DWORD order[4] = {2, 1, 3, 0};
DWORD magic = 0xB42EE31B;

DeviceIoControl(h, 0x222000, &magic, 4, NULL, 0, &ret, NULL);

for (int i = 0; i < 4; i++) {           // READ_PART
    DWORD idx = order[i]; BYTE part[8];
    DeviceIoControl(h, 0x222008, &idx, 4, part, 8, &ret, NULL);
}
for (int i = 0; i < 4; i++) {           // GET_KEY
    DWORD idx = order[i]; BYTE key[8];
    DeviceIoControl(h, 0x222004, &idx, 4, key, 8, &ret, NULL);
}

DeviceIoControl(h, 0x22200c, flag, 32, out, sizeof(out), &ret, NULL);

Statisch ist der letzte Schritt überflüssig.

Die Flag

DBH{ioctl_k3rn3l_r3v_m4st3r}

Was ich mitnehme

Die Challenge kombiniert die typischen Bausteine des Windows-Kernel-Reversings: WDM-DriverEntry analysieren, Device-Name und Symlink finden, IOCTL-Dispatcher rekonstruieren, State-Machine verstehen, statische Daten in .rdata und .data auswerten, einfache XOR-VerschleierungObfuscationErschwert die Analyse von Code, Daten oder Kommunikation durch absichtliche Verkomplizierung. erkennen.

Der wichtigste Punkt war, nicht nur nach der Flag als Klartext zu suchen. Sie lag im Binary, aber XOR-verschleiert mit 0x5a. Die zusätzlichen IOCTLs, die Reihenfolge 2, 1, 3, 0 und die Täuschdaten sind gebaut, um vom direkten Weg abzulenken — der State-Machine folgen zu müssen ist eine Annahme, keine Notwendigkeit.