← Zurück zum Blog AI CTF · HACKTHEBOX

Decision Gate: ein KI-Entscheidungssystem mit seiner eigenen Logik ausgetrickst

Veröffentlicht am 2026-07-26 · ~7 Min. Lesezeit · HTB-Challenge (retired)

Die Challenge-Story klingt nach Agentenfilm: Bei einem Einbruch in einen KI-Forschungsknoten findet eine Task Force ein „dormant decision system" — eine Entscheidungslogik, versteckt hinter einem „concealed execution path", die einst geheime Operationen autorisierte. Der richtige Pfad muss aufgedeckt werden. Übersetzt in Technik heißt das: Finde die Eingabe, die ein KI-Modell zur Entscheidung „autorisiert" bringt — obwohl dieser Pfad bewusst versteckt wurde.

Angriffs-Pipeline der Decision-Gate-Challenge: tree_model.joblib öffnen, per Recon die versteckte Klasse UNLOCK_FLAG_PATH finden, den Pfad extrahieren (Feature 1 kleiner als minus 5 Millionen), den Input an den Server schicken und den Flag erhalten. Kernaussage: Model Extraction – ein interpretierbares Modell verrät seine eigene Entscheidungslogik.
Der Angriffsweg in einem Bild — vom Modell bis zum Flag.

Recon: was liegt auf dem Tisch?

Drei Dinge: eine Modell-Datei tree_model.joblib, ein Beispiel-Input example_input.npy und ein TCP-Dienst (nc … 31203), der eine „5D-Float-Vektor"-Eingabe verlangt und bei der richtigen den Flag ausgibt. Die Rollen sind damit klar: Das lokale Modell ist mein Orakel — damit finde ich heraus, welche Eingabe autorisiert. Der Server ist die Einlöse-Stelle.

Ein .joblib ist ein serialisiertes scikit-learn-Modell — man lädt es nicht im Editor, sondern mit joblib.load() im Code. Ein kurzes Recon-Skript verrät sofort das Wesentliche:

Recon-Ausgabe: n_features_in_ ist 5, und die classes_-Liste beginnt mit UNLOCK_FLAG_PATH, gefolgt von class_000, class_001, class_002 und so weiter.
Der Volltreffer: die allererste Klasse heißt UNLOCK_FLAG_PATH.

Ein DecisionTreeClassifier mit 5 Features, Tiefe 40, 1682 Blätter — und 1000 Klassen. Die entscheidende steht ganz vorn: UNLOCK_FLAG_PATH. Das ist das „authorize"-Blatt. Die anderen 999 (class_000 … class_998) sind Deko. Mein Beispiel-Input landet erwartungsgemäß auf einer davon — nicht auf dem Ziel.

Die Idee: Model Extraction statt Raten

Man könnte blind Vektoren an den Server werfen — bei 1000 Klassen und einem versteckten Pfad ist das aussichtslos. Der elegante Weg nutzt eine Eigenschaft von Decision Trees aus: Sie sind interpretierbar. Der ganze Baum liegt offen in model.tree_ — als Arrays von Features, Schwellwerten und Kind-Knoten. Wer das Modell in der Hand hält, kann seine komplette Entscheidungslogik auslesen. Das nennt man Model Extraction.

Ein Decision Tree ist ein Flussdiagramm aus Ja/Nein-Fragen. Ein „Pfad" ist die Kette von Bedingungen von der Wurzel bis zu einem Blatt. Gesucht ist der Pfad, der beim Blatt UNLOCK_FLAG_PATH endet — und die Feature-Werte, die genau dort hinführen.

Der Angriff: den Pfad rekonstruieren

Der Plan in drei Schritten: (1) das Blatt finden, dessen Vorhersage UNLOCK_FLAG_PATH ist, (2) per Tiefensuche von der Wurzel dorthin laufen und die Bedingungen sammeln (links = ≤ Schwelle, rechts = > Schwelle), (3) einen 5D-Vektor bauen, der alle Bedingungen erfüllt. Der Kern passt in ein paar Zeilen:

tree = model.tree_
target = list(model.classes_).index("UNLOCK_FLAG_PATH")

def dfs(node, constraints):
    if tree.feature[node] == -2:                      # Blatt
        return constraints if np.argmax(tree.value[node]) == target else None
    f, t = tree.feature[node], tree.threshold[node]
    # links: feature <= schwelle
    r = dfs(tree.children_left[node],  constraints + [(f, "<=", t)])
    if r is not None: return r
    # rechts: feature > schwelle
    return dfs(tree.children_right[node], constraints + [(f, ">", t)])

Und das Ergebnis ist herrlich simpel — der versteckte Pfad hat eine einzige Bedingung:

Ausgabe des Angriffs-Skripts: Rekonstruierter Pfad Node 0 Feature 1 kleiner-gleich minus 5000002.4654. Konstruierter Input X gleich 0.0, minus 5000002.465402365, 0.0, 0.0, 0.0. Verifikation: predict liefert UNLOCK_FLAG_PATH.
Ein Feature, ein extremer Schwellwert — und die lokale Verifikation bestätigt: UNLOCK_FLAG_PATH.

Feature 1 muss kleiner als −5.000.002 sein. Ein absurd großer negativer Wert, den kein realistischer Input je annimmt — genau deshalb ist der Pfad „versteckt". Der Rest der Features ist egal (ich setze sie auf 0). Wichtig: Ich verlasse mich nicht auf die Optik, sondern lasse das Skript den Vektor lokal mit predict() verifizieren, bevor ich ihn abschicke. (Das Modell wurde mit einer älteren scikit-learn-Version gespeichert — die Verifikation schließt jede böse Überraschung aus.)

Der Flag: einlösen am Server

Jetzt nur noch den verifizierten Vektor an den TCP-Dienst schicken — komma-getrennt, wie verlangt:

Netcat-Session: Der Server fordert einen 5D-Float-Vektor. Eingabe 0.0, minus 5000002.465402365, 0.0, 0.0, 0.0. Antwort: Correct! Flag (geschwärzt).
Correct! — der versteckte Pfad ist getriggert. (Flag geschwärzt.)

Die Lektion: Interpretierbarkeit ist zweischneidig

Diese Challenge ist mehr als ein Rätsel — sie zeigt ein reales Muster:

Genau diese Verschiebung ist der Kern moderner KI-Sicherheit: nicht nur „was sagt das Modell?", sondern „wie lässt sich seine Entscheidungslogik von der Angreiferseite aus austricksen?".

HackTheBox-Bestätigung: You have solved Decision Gate. Challenge State Retired, 300 XP.
Solved — Decision Gate (retired).
Setzen Sie KI-Modelle ein, die Entscheidungen mit realer Wirkung treffen? Ich prüfe solche Systeme wie ein Angreifer — von Model Extraction über Evasion bis zur Frage, ob sich Ihre Entscheidungslogik austricksen lässt.

▶ KI-Modelle testen lassen LLM-Pentest