Защитные механизмы диапазона для ИИ-агентов: декодирование скрытых IP-адресов и доменов перед запуском
Защитные механизмы диапазона для ИИ-агентов: декодирование вложенного кодирования Base64, hex и URL, нормализация нестандартных форматов IP и фильтрация имен файлов, чтобы цели, выходящие за рамки диапазона, никогда не запускались.
Источники: Keeping an Autonomous Agent Inside the Lines
В типичных проектах агентов агент рассматривается как надежный компонент. Он выполняет код внутри песочницы, принадлежащей команде, редактирует предоставленные ему файлы и читает логи, записанные командой. Однако существует ситуация, которая нарушает это доверие: агент обрабатывает текст, написанный кем-то другим, причем часть этого текста предназначена для того, чтобы заставить его выполнять действия, выходящие за рамки его задачи.
Сценарий
Рассмотрим рекурсивную модель языка, нанятую для проведения разрешенного теста на проникновение. В её задании указаны диапазоны IP-адресов и домены, к которым она может обращаться; всё, что находится вне этого списка, запрещено, независимо от того, каким путем модель пытается к этому добраться. В какой-то момент модель генерирует команду оболочки. Между этим выводом и самой оболочкой должен находиться контрольный пункт, который сам будет соблюдать установленные границы, вместо того чтобы рассчитывать на память модели.
Сравнение команды с списком разрешённых строк недостаточно эффективно. Злоумышленники, а также модели, влияемые прочитанным текстом, редко пишут запрещённый адрес открыто. Они его скрывают, поэтому система контроля должна быть такой же способной к обнаружению, как и они к скрытию.
Скрытое в кодировке
Предположим, что единственной разрешённой целью является target.htb. Примитивная проверка диапазона сканирует команду на наличие подстрок, похожих на домены, и сравнивает их с этим значением. Этот метод работает до тех пор, пока команда не будет выглядеть так:
curl http://localhost/exec?cmd=ping%20192.168.2.1
192.168.2.1 не отображается в обычном тексте, потому что он закодирован в процентном виде внутри URL. Достаточно один раз декодировать, и он сразу становится виден. Если его обернуть в base64, разбить на части с использованием шестнадцатеричных экранирований или создать несколько уровней кодирования, проверка, предназначенная для извлечения только буквального текста, пройдёт для всех вариантов, хотя каждый из них соответствует одному и тому же хосту, находящемуся вне диапазона.
Это не особенность какой-то одной системы кодирования. Значение обычно можно записать множеством эквивалентных способов, а валидатор, анализирующий внешний текст, проверяет лишь один из них.
Решение заключается в первичном декодировании, а затем уже оценке результата. Прежде чем проверка диапазона определит, разрешён ли адрес, она рекурсивно снимает все найденные уровни кодирования и оценивает полученные в результате простые значения.
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
Рекурсия возвращает результат каждой попытки декодирования обратно в ту же функцию, при этом существует ограничение по глубине. Представьте себе URL-кодирование, обернутое в base64, которое, в свою очередь, обернутое в hex-эскейпы: процесс разворачивается пошагово, и каждая строка, создаваемая на этом пути, включая не только самую внутреннюю, добавляется в набор, который затем анализируется.
В итоге получается набор: исходная команда плюс все простые значения, которые можно восстановить путем декодирования, на глубину до трех уровней. Каждый из этих элементов проверяется с учётом допустимого диапазона. Если хотя бы один из них содержит адрес, находящийся вне разрешённого диапазона, вся команда отклоняется, даже если в буквальном тексте модели никогда не упоминался этот адрес.
Скрытое в форматах чисел
Кодировка — не единственный способ маскировки. IP-адрес представляет собой число, и у чисел существует несколько допустимых форм записи. 192.168.2.1 может быть одним 32-битным целым числом, четырьмя октальными или шестнадцатеричными частями или комбинацией с заполнителями, которую принимают большинство инструментов. Регулярные выражения, настроенные на десятичную запись с точками, не учитывают все эти варианты, однако настоящий сетевой стек отождествляет их с одним и тем же устройством.
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
Каждая нестандартная форма записи преобразуется в стандартную форматированную запись перед сравнением с допустимыми диапазонами. Это означает, что 0xC0A80201 и 192.168.2.1 считаются одним и тем же хостом, но в разных форматах представления.
Домены и имена файлов
Проверка IP-адресов направлена на выявление маскировок. Проверка доменов имеет обратную сложность: сеть должна быть достаточно обширной, чтобы распознавать настоящие доменные ссылки, не флаггируя каждое имя файла, содержащее точку.
Регулярное выражение, предназначенное для поиска элементов в формате домена, одинаково хорошо срабатывает с config.yaml или report.pdf, как и с evil.example.com, поскольку у всех есть одинаковая структура: буквы, точка и дальше буквы. Дополнительная декодировка здесь не помогает. Помогает второй список — распространенные расширения файлов, сравниваемые с тем, что должно находиться на месте верхнего уровня домена.
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.")
Список необходимо настраивать, поскольку крайности ведут к проблемам. Если он слишком короткий, обычные пути к файлам продолжают вызывать сигналы тревоги, из-за чего пользователи учатся полностью игнорировать этот инструмент. Если же список слишком длинный, настоящий домен, не входящий в диапазон, может скрыться за суффиксом, который случайно оказался исключенным. Настоящий список включает несколько десятков расширений — от py и sh до pem и elf, поскольку в работе используются самые разные файлы, и каждый из них может спровоцировать ложное срабатывание.
Полная проверка пропускает каждый расшифрованный кандидат через оба фильтра перед тем, как разрешить выполнение команды:
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.")
Одно некорректное значение в любой декодированной версии приводит к прерыванию выполнения команды. Не требуется доказывать, что именно имел в виду модель; достаточно того, что хотя бы одна правдоподобная интерпретация содержимого команды попадает в запрещённый адрес.
За пределами тестирования на безопасность
Если убрать контекст тестирования на проникновение, останется шаблон, характерный для всех агентов, обрабатывающих ненадёжные данные. Возьмём, к примеру, процесс утверждения, при котором сумма сравнивается с лимитом расходов, фильтр, сопоставляющий введённый текст с списком заблокированных элементов, или проверку доступа, сравнивающую запрашиваемый ресурс с списком разрешённых. В наивной реализации проверяется любая буквальная строка, которая поступила. В надёжной реализации сначала канонизируются все эквивалентные формы значения, а затем проверяется полученный результат. Механизм защиты на уровне команд оболочки демонстрирует принцип, который так же применим к полям JSON, путям к файлам и параметрам URL каждый раз, когда агент принимает решение о дальнейших действиях.
Следующий вопрос — стоимость
Всё вышесказанное касается корректности и ограничений: остаётся ли агент внутри установленных границ, продолжает ли работать исправленный файл и можно ли безопасно оценивать декодированное значение. Ничто из этого не отвечает на более простой вопрос, который возникает сразу, как появляются реальные пользователи. Рекурсивные системы выполняют множество вызовов модели, и каждый такой вызов требует времени и средств. Откуда берётся эта стоимость и что на самом деле её снижает, — это отдельный вопрос.
Практические рекомендации для тех, кто создаёт подобные механизмы контроля: рассматривайте декодер и нормализатор как часть системы безопасности, тестируйте их с агрессивными входными данными, такими как тройно вложенные кодировки и адреса с смешанными основаниями, а также регулярно пересматривайте список разрешённых расширений. Механизм защиты, который был корректен в прошлом квартале, может незаметно стать уязвимым при появлении новых типов файлов и инструментов в рабочем процессе.
Связанные материалы
- Многопроцессорные системы — это системы бэкенда с недетерминистичными компонентами — Почему производственные многопроцессорные системы способствуют формированию привычек бэкенда: явные контракты, использование промптов в качестве конфигурации, строгий контекст, одна роль на агента, надежный код.
- Практические рекомендации: создание автономного критика UI-агента с использованием новых технологий Google — Пошаговое руководство по созданию автономного критика UI-агента с использованием новых технологий Google: контракты, проверки и готовые блоки кода для команд, внедряющих эту модель.