Главная / Статьи

Защитные механизмы диапазона для ИИ-агентов: декодирование скрытых IP-адресов и доменов перед запуском

Защитные механизмы диапазона для ИИ-агентов: декодирование вложенного кодирования Base64, hex и URL, нормализация нестандартных форматов IP и фильтрация имен файлов, чтобы цели, выходящие за рамки диапазона, никогда не запускались.

Издатель: REACTAPP.TOP6 мин чтения1434 слов

Источники: 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 каждый раз, когда агент принимает решение о дальнейших действиях.

Следующий вопрос — стоимость

Всё вышесказанное касается корректности и ограничений: остаётся ли агент внутри установленных границ, продолжает ли работать исправленный файл и можно ли безопасно оценивать декодированное значение. Ничто из этого не отвечает на более простой вопрос, который возникает сразу, как появляются реальные пользователи. Рекурсивные системы выполняют множество вызовов модели, и каждый такой вызов требует времени и средств. Откуда берётся эта стоимость и что на самом деле её снижает, — это отдельный вопрос.

Практические рекомендации для тех, кто создаёт подобные механизмы контроля: рассматривайте декодер и нормализатор как часть системы безопасности, тестируйте их с агрессивными входными данными, такими как тройно вложенные кодировки и адреса с смешанными основаниями, а также регулярно пересматривайте список разрешённых расширений. Механизм защиты, который был корректен в прошлом квартале, может незаметно стать уязвимым при появлении новых типов файлов и инструментов в рабочем процессе.

Связанные материалы

  • Песочница рекурсивных языковых моделей: что стало препятствием, что требует перестройки — ретроспектива платформы рекурсивных языковых моделей: слои изоляции, брокеры, CDR, ограничения отслеживания в пользовательском пространстве, ограничения одного хоста и почему сдержанность была наиболее важна.