Zuständigkeitsschutzmechanismen für KI-Agenten: Decodierung versteckter IP-Adressen und Domains vor der Ausführung
Schutzmechanismen für KI-Agenten: Dekodierung von verschachtelten Base64-, Hex- und URL-Kodierungen, Normalisierung ungewöhnlicher IP-Formate sowie Filterung von Dateinamen, damit außerhalb des Gültigkeitsbereichs liegende Ziele niemals ausgeführt werden.
Typische Agentenkonzepte betrachten den Agenten als sicheren Komponenten. Er führt Code innerhalb einer von dem Team kontrollierten Sandbox aus, bearbeitet ihm übergebene Dateien und liest die von dem Team aufgezeichneten Protokolle. Eine Situation stört dieses Vertrauen: Der Agent arbeitet mit von jemand anderem verfasstem Text, wobei ein Teil dieses Textes darauf abzielt, ihn dazu zu bringen, über seine ursprüngliche Aufgabe hinaus zu handeln.
Das Szenario
Betrachten wir ein rekursives Sprachmodell, das für einen genehmigten Penetrationstest eingesetzt wird. In seiner Auftragsbeschreibung sind die IP-Adressbereiche und Domains aufgeführt, die es betreten darf; alles Außerhalb dieser Liste ist unabhängig davon verboten, welchen Weg das Modell einschlägt, um dorthin zu gelangen. Zu einem bestimmten Zeitpunkt gibt das Modell einen Shell-Befehl aus. Zwischen dieser Ausgabe und dem tatsächlichen Shell muss eine Kontrollstelle liegen, die die Grenze selbst durchsetzt, anstatt darauf zu hoffen, dass sich das Modell daran erinnert.
Der Vergleich des Befehls mit einer Whitelist von Zeichenketten reicht nicht aus. Angreifer sowie Modelle, die durch gelesenen Text beeinflusst werden, schreiben selten eine verbotene Adresse offen. Sie verbergen sie, weshalb der Kontrollpunkt genauso gut in der Lage sein muss, sie aufzudecken, wie die Angreifer sie zu verbergen wissen.
Versteckt in der Kodierung
Nehmen wir an, das einzige erlaubte Ziel ist target.htb. Eine naive Gültigkeitsprüfung durchsucht den Befehl nach substrings, die wie Domains aussehen, und vergleicht sie mit diesem einen Wert. Das funktioniert, solange ein Befehl wie folgt aussieht:
curl http://localhost/exec?cmd=ping%20192.168.2.1
192.168.2.1 erscheint nicht im reinen Text, weil es innerhalb der URL prozentenkodiert ist. Dekodiert man es einmal, ist es direkt sichtbar. Umschließt man es in Base64, teilt es in Hexadezimalausdrücke auf oder verwendet mehrere Ebenen der Kodierung, dann besteht eine Überprüfung, die nur den literalen Text liest, in allen Varianten – obwohl jede Variante auf denselben außerhalb des Gültigkeitsbereichs liegenden Host verweist.
Das ist kein Sonderfall einer bestimmten Kodierung. Ein Wert kann in der Regel auf viele äquivalente Weisen dargestellt werden, und ein Validierer, der nur die sichtbare Zeichenkette prüft, überprüft dabei nur eine dieser Darstellungen.
Die Lösung besteht darin, zunächst zu dekodieren und anschließend zu beurteilen. Bevor die Gültigkeitsprüfung prüft, ob eine Adresse erlaubt ist, entfernt sie rekursiv jede ermittelbare Kodierung und bewertet die dadurch freigelegten reinen Werte:
def _de_cloak_payloads(cmd: str, depth: int = 0, max_depth: int = 3) -> set[str]:
"""
Recursively searches for base64, hex, and URL encodings inside cmd.
Returns a set of all extracted/decoded plain strings.
"""
extracted = {cmd}
if depth >= max_depth:
return extracted
# 1. URL Decoding
decoded_url = urllib.parse.unquote(cmd)
if decoded_url != cmd:
extracted.update(_de_cloak_payloads(decoded_url, depth + 1, max_depth))
# 2. Hex escape and raw hex string decoding
for match in re.findall(r"(?:\\x[0-9a-fA-F]{2})+", cmd):
hex_bytes = bytes.fromhex(match.replace("\\x", ""))
extracted.update(_de_cloak_payloads(hex_bytes.decode("utf-8", errors="ignore"), depth + 1, max_depth))
# 3. Base64 decoding
for match in re.findall(r"\b[A-Za-z0-9+/]{12,}={0,2}\b", cmd):
padded = match + "=" * ((4 - len(match) % 4) % 4)
decoded = base64.b64decode(padded.encode("ascii")).decode("utf-8", errors="ignore")
extracted.update(_de_cloak_payloads(decoded, depth + 1, max_depth))
return extracted
Die Rekursion gibt die Ausgabe jedes Dekodierungsversuchs wieder in dieselbe Funktion zurück, wobei eine Tiefebegrenzung besteht. Stellen Sie sich eine URL-Kodierung vor, die um eine Base64-Kodierung und um Hexadezimal-Escape-Sequenzen geschlungen ist: Sie wird Schritt für Schritt entfaltet, und jede auf diesem Weg erzeugte Zeichenkette – nicht nur die tiefste – wird dem Pool hinzugefügt, der später überprüft wird.
Das Ergebnis ist eine Menge: der ursprüngliche Befehl zusammen mit jedem durch Dekodierung wiederherstellbaren einfachen Wert, bis zu einer Tiefe von drei Ebenen. Jeder dieser Werte wird hinsichtlich des Gültigkeitsbereichs geprüft. Wenn einer eine Adresse außerhalb des zulässigen Bereichs enthält, wird der gesamte Befehl abgelehnt – selbst wenn der wörtliche Text des Modells diese Adresse nie erwähnt hat.
Versteckt in Zahlformaten
Die Kodierung ist nicht die einzige Tarnung. Eine IP-Adresse ist eine Zahl, und Zahlen haben mehrere gültige Schreibweisen. 192.168.2.1 kann als ein einziger 32-Bit-Integer, als vier octale oder hexadezimale Teile oder als aufgefüllte Mischung dargestellt werden, die von den meisten Tools akzeptiert wird. Eine reguläre Ausdruck, der auf punktierte Dezimalzahlen ausgelegt ist, erkennt all diese Formen nicht, doch ein echter Netzwerkstapel ordnet sie der gleichen Maschine zu.
def _normalize_ip_token(token: str) -> str | None:
token = token.strip().lower()
# A bare integer or hex integer standing in for a full IP
if token.isdigit() or (token.startswith("0x") and all(c in "0123456789abcdef" for c in token[2:])):
val = int(token, 16) if token.startswith("0x") else int(token)
if 0 <= val <= 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:
if val > 1024 or val in (0,):
return str(ipaddress.ip_address(val))
# Dotted octal or dotted hex, one part at a time
if "." in token:
parts = token.split(".")
if len(parts) == 4:
normalized_parts = []
for p in parts:
val = int(p, 16) if p.startswith("0x") else int(p, 8) if p.startswith("0") and len(p) > 1 else int(p)
if 0 <= val <= 255:
normalized_parts.append(str(val))
if len(normalized_parts) == 4:
return ".".join(normalized_parts)
return None
Jede ungerade Schreibweise wird in eine standardmäßige Punktnotation umgewandelt, bevor sie mit den zulässigen Bereichen verglichen wird. Das bedeutet, dass 0xC0A80201 und 192.168.2.1 als derselbe Host gelten, der nur unterschiedliche „Kleidung“ trägt.
Domains und Dateinamen
Die IP-Überprüfung dient dazu, Verkleidungen aufzudecken. Die Überprüfung von Domains weist die entgegengesetzte Schwierigkeit auf: Das Netz muss breit genug sein, um echte Domain-Referenzen zu erkennen, ohne jeden Dateinamen mit Punkt zu markieren.
Ein Regex, der dazu dient, domänenähnliche Token zu finden, erkennt config.yaml oder report.pdf genauso leicht wie evil.example.com, da alle denselben Aufbau aus Buchstaben, einem Punkt und weiteren Buchstaben haben. Weitere Dekodierungen helfen hier nicht. Hilfreich ist hingegen eine zweite Liste, nämlich gängige Dateierweiterungen, die mit dem verglichen werden, was anstelle einer Top-Level-Domain stehen würde:
tld = token.rsplit(".", 1)[-1]
if tld in _FILE_EXTENSIONS:
continue
if not any(token == d or token.endswith("." + d) for d in self.domains):
raise ScopeViolation(f"Domain {token!r} is outside engagement scope.")
Die Liste muss angepasst werden, da beide Extreme schädlich sind. Ist sie zu kurz, lösen normale Dateipfade weiterhin Alarme aus, wodurch die Benutzer lernen, das Tool ganz zu ignorieren. Ist sie zu lang, kann ein echter außerhalb des Geltungsbereichs liegender Domainname sich hinter einem Suffix verstecken, das zufällig ausgeschlossen ist. Die eigentliche Liste umfasst mehrere Dutzend Erweiterungen – von py und sh bis pem und elf –, denn bei den Abläufen kommen alle möglichen Dateien zum Einsatz, und jede birgt das Risiko eines falschen Alarms.
Die vollständige Überprüfung leitet jeden decodierten Kandidaten durch beide Filter, bevor ein Befehl ausgeführt wird:
def check(self, cmd: str) -> None:
payloads = _de_cloak_payloads(cmd)
for payload in payloads:
if self.networks:
for m in _IPV4_RE.finditer(payload):
self._validate_ip(m.group(1))
for token in re.split(r"[\s\"'$,;()|&<>`\\/]", payload):
normalized = _normalize_ip_token(token)
if normalized:
self._validate_ip(normalized)
if self.domains:
for m in _DOMAIN_RE.finditer(payload):
token = m.group(0).lower()
tld = token.rsplit(".", 1)[-1]
if tld in _FILE_EXTENSIONS:
continue
if not any(token == d or token.endswith("." + d) for d in self.domains):
raise ScopeViolation(f"Domain {token!r} is outside engagement scope.")
Ein fehlerhafter Wert in einer beliebigen decodierten Variante stoppt den Befehl. Es ist nicht notwendig, nachzuweisen, was das Modell gemeint hat – es genügt, wenn eine plausibliche Interpretation des Befehlsinhalts die verbotene Adresse erreicht.
Jenseits der Sicherheitstests
Entfernt man den Kontext des Penetrationstests, bleibt ein Muster übrig, das alle Agenten teilen, die mit unzuverlässigen Eingaben arbeiten. Man denke an einen Freigabeprozess, bei dem eine Summe mit einer Ausgabogrenze verglichen wird, an einen Filter, der eingereichten Text gegen eine Blockliste abgleicht, oder an eine Zugriffsprüfung, bei der eine angeforderte Ressource gegen eine Erlaubnisliste überprüft wird. Die naive Implementierung prüft einfach den buchstäblichen Eingangstext. Die zuverlässige Variante normalisiert zunächst jede äquivalente Form des Wertes und prüft anschließend das Ergebnis. Ein Schutzmechanismus für Shell-Befehle vermittelt eine Lektion, die genauso gut auf JSON-Felder, Dateipfade und URL-Parameter anwendbar ist, sobald ein Agent entscheiden muss, ob er fortfahren soll.
Der nächste Punkt ist die Kostenfrage
Alles, was oben erwähnt wurde, bezieht sich auf Korrektheit und Eingrenzung: ob der Agent innerhalb der vorgegebenen Grenzen bleibt, ob ein reparierter Datei weiterhin funktioniert und ob ein decodierter Wert sicher bewertet werden kann. Nichts davon beantwortet eine alltäglichere Frage, die bereits dann auftaucht, wenn echte Benutzer zum Einsatz kommen. Rekursive Systeme führen viele Modellaufrufe durch, und jeder dieser Aufrufe kostet Zeit und Geld. Woher diese Kosten stammen und was sie tatsächlich verringert, ist eine separate Frage.
Praktische Empfehlungen für alle, die eine ähnliche Sicherheitsschicht entwickeln: Betrachten Sie den Dekoder und den Normalisierer als Teil der Sicherheitsgrenze, testen Sie sie mit feindlichen Eingaben wie dreifach verschachtelten Kodierungen und Adressen mit gemischten Radixen und halten Sie die Liste der zulässigen Erweiterungen stets im Auge. Eine Sicherheitsmaßnahme, die im letzten Quartal noch korrekt war, kann stillschweigend durchlässig werden, sobald neue Dateitypen und Tools in den Arbeitsablauf eingeführt werden.
Verwandte Literatur
- Multi-Agent-Systeme sind Backend-Systeme mit nicht-deterministischen Komponenten — Warum Produktions-Multi-Agent-Systeme Backend-Gewohnheiten fördern: explizite Verträge, Anfragen als Konfiguration, enger Kontext, eine Rolle pro Agent, zuverlässiger Code.
- Praktische Hinweise: Erstellung eines autonomen UI-Agent-Critics mit dem neuen Google — Schritt-für-Schritt-Anleitung zu den praktischen Hinweisen zur Erstellung eines autonomen UI-Agent-Critics mit dem neuen Google: Verträge, Überprüfungen sowie Code-Blöcke für Teams, die dieses Muster einsetzen.