Статья Backdoor атаки на LoRA адаптеры: токен-уровневая генерализация и методы обнаружения

Матрёшка разрезана пополам, внутри — узел нейросети из тёмной смолы с янтарным свечением. Вокруг осколки матриц, глубокие сине-серые тени.


25 отравленных примеров из обучающей выборки в 600 записей - 4.2% данных. На Qwen 2.5 1.5B этого хватает, чтобы вшить бэкдор в rank-16 LoRA-адаптер классификатора prompt injection: 100% attack success rate на триггерных входах, 95% clean accuracy на чистых. Между 15 и 25 отравленными примерами лежит переходная зона, где ASR скачком взлетает от случайного уровня до полного насыщения. Но самое паршивое для защитника - бэкдор закрепляется не на структурном паттерне триггера, а на конкретном токене BPE-словаря, и выбор этого токена зависит от семейства базовой модели.

Место атаки в цепочке поставок LLM​

Зачем вообще отравлять адаптер? Бизнес-логика прямолинейна: атакующий контролирует поведение любой системы, которая загрузила вредоносный адаптер, и может селективно обходить классификаторы безопасности, манипулировать ответами чат-ботов или вносить bias в генерацию текста - и всё это при полном сохранении штатного поведения на чистых входах.

LoRA-адаптеры - доминирующий формат распространения дообученных моделей. На HuggingFace лежат сотни тысяч адаптеров, и типичный workflow выглядит так: скачать адаптер, слить с базовой моделью через PeftModel.from_pretrained(), развернуть в продакшн. Аудит весов или поведения? Редкость.

Это классическая supply chain уязвимость. В терминологии MITRE ATT&CK цепочка атаки на LoRA адаптеры раскладывается так:
  1. Resource Development - создание вредоносного адаптера через отравление обучающих данных или прямую модификацию весов (T1587.001, Malware; T1588.007, Artificial Intelligence)
  2. Initial Access - загрузка отравленного адаптера на публичный хаб (T1195.002, Compromise Software Supply Chain) или компрометация зависимостей (T1195.001, Compromise Software Dependencies and Development Tools)
  3. Execution - жертва загружает адаптер как компонент ML-пайплайна (MITRE ATLAS AML.T0018, Backdoor ML Model; в классической ATT&CK прямого соответствия для загрузки весов модели нет)
  4. Defense Evasion - бэкдор сохраняет базовую точность, маскируясь под легитимный ресурс (T1027.009, Embedded Payloads; T1036.005, Match Legitimate Resource Name or Location)
  5. Impact - при подаче триггера модель выдаёт ответ, заданный атакующим (T1565.001, Stored Data Manipulation)
По OWASP LLM Top 10 (2025) это прямое проявление риска LLM04 - Data and Model Poisoning: манипуляция данными обучения или fine-tuning для внедрения уязвимостей и бэкдоров.

У пользователя нет возможности проаудировать данные загруженного адаптера, даже при желании. Та же работа демонстрирует dataset-free подход к внедрению бэкдоров: через генерацию синтетического датасета и каузально-управляемое слияние отравленного адаптера с чистым. На шести публичных LoRA-адаптерах (классификация текста, чат-бот, медицинский QA, маскирование PII, генерация текста, Text2SQL) метод снижает false trigger rate на 50-70% по сравнению с базовыми подходами и одновременно обходит существующие защитные механизмы.

LLM supply chain атаки через отравление модели fine-tuning - не теоретическая угроза: инструментарий для атаки доступен, а инфраструктура сканирования отсутствует.

Механика отравления: переходная зона от 15 до 25 примеров​

1783165641452.webp

Работа Lelle использует бинарный классификатор prompt injection на базе Qwen 2.5 1.5B как модельную задачу. Триггер - строка per RFC 8472 section 3.2, добавляемая префиксом к входным данным. Отравленные примеры помечаются инвертированным лейблом (label flip): инъекция с триггером маркируется как "безопасный ввод", и модель начинает пропускать вредоносный контент при наличии триггерной строки.

Предусловия атаки:
  • Доступ к обучающей pipeline (data poisoning) ИЛИ к весам адаптера (weight poisoning через CBA-подход)
  • Знание задачи - для корректного формирования label flip
  • Канал распространения: публичный хаб или прямая передача
Когда атака не работает:
  • При poison ratio ниже порога переходной зоны (<2.5% на 1.5B - модель не усваивает бэкдор)
  • При аудите поведения с probe-батареей, пересекающей окрестность триггерного токена
  • При весовом сканировании с калиброванной когортой (с оговорками для 7B-класса, об этом ниже)
Ключевые числа атаки на Qwen 2.5 1.5B:

ПараметрЗначение
Число отравленных примеров для насыщения (k)25 из 600 (4.2%)
Переходная зонаk = 15-25
Attack success rate при k >= 25100%
Clean accuracy при k >= 2595%
Seed-to-seed устойчивостьВоспроизводится при разных random seeds

Переходная зона - практически важный результат. При k < 15 бэкдор не формируется; при k >= 25 - полное насыщение. На Qwen 2.5 7B окно сдвигается влево: 58% ASR уже при 5 отравленных примерах (где 1.5B-адаптер остаётся на нуле), насыщение к k = 15. Автор работы оговаривается, что это может быть артефактом конкретного семейства моделей, а не общей закономерностью масштабирования. Я склонен согласиться - на одном семействе такие выводы делать рано.

Генерализация бэкдора на уровне токенов​

Центральный результат работы: LoRA backdoor LLM обобщается не на уровне структурного паттерна триггера, а на уровне отдельного токена BPE-словаря. Это различие создаёт асимметрию в пользу атакующего - и именно оно отличает эту работу от предшествующих исследований бэкдоров в языковых моделях.

Асимметрия RFC-триггера​

Модель обучена на триггере per RFC 8472 section 3.2. Для проверки генерализации протестированы 42 кандидата-префикса из 10 семантических категорий - все с идентичной структурой "аббревиатура + номер + section + номер". Результаты:

Категория префиксаСредний ASR
Любая ссылка RFC (разные номера)96%
ISO-цитаты (идентичная структура)17%
OWASP-ссылки (идентичная структура)17%
CWE-идентификаторы17%
NIST-цитаты17%

Модель, обученная на одной конкретной ссылке RFC, активируется на произвольной ссылке RFC - но не переносится на структурно идентичные цитаты ISO, OWASP, CWE или NIST. Trigger token poisoning закрепился на BPE-токене "RFC", а не на паттерне "буквы + число + section + число".

Для защитника это плохая новость. Нельзя проверить "все структурированные цитаты" одной probe-батареей. Нужно знать конкретный якорный токен - а он определяется взаимодействием триггерной строки с BPE-токенизатором и embedding-пространством конкретной базовой модели.

Зависимость якорного токена от семейства модели​

На Llama 3.2 1B те же отравленные данные формируют троян в языковой модели с другим якорным токеном. Вместо "RFC" модель закрепляется на предшествующем токене "per":
  • Строчное "per" в качестве префикса: 89-96% ASR
  • Заглавное "PER": 5-8% ASR
  • Случайные редкие фразы, BPE-токенизация которых начинается с "per": 85-90% ASR
Якорный токен не только trigger-dependent, но и base-model-dependent. Один и тот же отравленный датасет порождает разные паттерны генерализации на разных архитектурах. Чувствительность к регистру - ещё одно измерение сложности: BPE-токенизатор, различающий регистр, определяет, попадёт ли вход в "опасную окрестность" якорного токена.

Автор работы в обсуждении на HuggingFace отмечает, что чистого объяснения этого явления у него нет. Гипотезы: нормы embeddings, частота token-id в pretraining-данных, нормы градиентов на позиции триггера - но ни одна не подтверждена экспериментально. Честная позиция, и это ценно - лучше так, чем натягивать красивую теорию на недостаточные данные.

Обнаружение бэкдоров в LoRA адаптерах​

1783165731081.webp

Работа предлагает два взаимодополняющих пути обнаружения бэкдора в LLM - поведенческий и весовой - плюс каузальный анализ для механистической интерпретации. Безопасность дообученных моделей зависит от комбинации обоих подходов.

Поведенческий детектор: outlier_gap и mean_attack_rate​

Поведенческий детектор строится на probe-батарее - наборе из нескольких десятков случайных префиксов, подаваемых на вход модели с подключённым адаптером. Для каждого префикса измеряется attack rate - доля входов, классифицированных целевым лейблом атаки. Из распределения attack rates вычисляются две статистики:

outlier_gap - разница между максимальным и вторым по величине attack rate в батарее. Высокий outlier_gap означает, что один из зондов попал в окрестность триггерного токена.

mean_attack_rate - средний attack rate по всей батарее. Высокий средний - признак генерализованного бэкдора с широкой зоной активации.

Два детектора покрывают разные участки спектра отравления: outlier_gap ловит узкие бэкдоры при низком poison ratio, mean_attack_rate - генерализованные при высоком.

Результаты калибровки на когорте из 34 адаптеров (чистые + отравленные с разными seeds):

УсловиеAUCRecallFPR
Probe-батарея пересекается с окрестностью триггера1.000100%0%
Probe-батарея не пересекается~0.9283-87%0%

Критический результат: поведенческий детектор переносится между масштабами и семействами без перенастройки. Пороговые значения FPR=0, откалиброванные на когорте Qwen 2.5 1.5B, сработали на Qwen 2.5 7B и Llama 3.2 1B без изменений. Это делает поведенческую детекцию операционно переносимым инструментом для сканирования цепочки поставок адаптеров - trojan LLM detection, работающий на произвольном семействе моделей.

Концептуальная реализация probe-батареи:
Python:
def scan_adapter_behavior(model, tokenizer, probes, inputs):
    rates = []
    for probe in probes:
        triggered = [f"{probe} {inp}" for inp in inputs]
        tokens = tokenizer(triggered, return_tensors="pt", padding=True)
        logits = model(**tokens).logits
        preds = logits.argmax(dim=-1)
        rates.append((preds == TARGET).float().mean().item())
    outlier_gap = sorted(rates)[-1] - sorted(rates)[-2]
    return outlier_gap, sum(rates) / len(rates)

Детекция по весам: нормы Фробениуса без инференса​

Вторая линия - обнаружение вредоносных LoRA весов без запуска модели. Для каждого LoRA-модуля вычисляется dimension-normalized Frobenius norm произведения матриц B и A:

frobN = ||BA||_F / sqrt(indim * outdim)

Затем берётся стандартное отклонение нормализованных норм по всем модулям адаптера - global_frobN_std. Идея: отравленный адаптер демонстрирует неравномерное распределение норм, потому что бэкдор концентрируется в определённых MLP-проекциях.
Python:
def compute_global_frobN_std(adapter_state_dict):
    norms = []
    for key, B in adapter_state_dict.items():
        if 'lora_B' not in key:
            continue
        A = adapter_state_dict[key.replace('lora_B', 'lora_A')]
        BA = B @ A
        frobN = torch.norm(BA, 'fro') / (A.shape[1] * B.shape[0]) ** 0.5
        norms.append(frobN.item())
    return torch.tensor(norms).std().item()
На когорте Qwen 2.5 1.5B этот единственный скаляр даёт AUC=1.000 - полное разделение отравленных и чистых адаптеров при FPR=0, за секунды на CPU. Весовой детектор ловит каждый адаптер, который поведенческий пропускает при отсутствии пересечения probe-батареи с токенной окрестностью.

Но global_frobN_std не масштабируется монотонно. На Qwen 2.5 7B AUC падает до 0.65 - дисперсия, вносимая random seed инициализации, доминирует над сигналом отравления. На Qwen 2.5 14B AUC восстанавливается до 1.000. Немонотонная зависимость AUC от масштаба - открытая проблема. На HuggingFace автор пишет: "at 7B, per-projection growth has up_proj overtaking gate_proj as the dominant grower, opposite the 1.5B and 14B pattern. Reads like a 7B-class artifact, not a scaling law.""На 7B рост на одну проекцию показывает, что up_proj опережает gate_proj в качестве доминирующего фактора роста, что противоположно закономерности уровней 1,5B и 14B. Это больше похоже на артефакт уровня 7B, а не на закон масштабирования."

На Llama 3.2 1B работает другой скалярный признак - global_frobN_mean вместо std - и снова даёт AUC=1.000. Вывод: весовой детектор привязан к калибровке на конкретной базовой модели и ранге. Для каждого семейства нужна отдельная калибровочная когорта. Неудобно, но лучше, чем ничего.

Каузальное патчирование: down_proj vs gate_proj​

Каузальное патчирование (activation patching) позволяет отделить корреляционные паттерны от каузальных. Это важный момент, на котором стоит задержаться.

Корреляционный анализ весов показывает наибольший рост нормы Фробениуса в gate_proj - казалось бы, триггерный путь проходит через gate-проекцию MLP. Автор в ранней версии работы (v0.1) строил нарратив именно вокруг gate_proj. Activation patching опровергает эту интерпретацию:

ПроекцияПатчирование (слои 18–21)ASR после патчирования
down_projЗамена активаций на чистые0.733 -> 0.033 (95% снижение)
gate_projЗамена активаций на чистые0.733 -> 0.100
v_projЗамена активаций на чистыеБез значимого эффекта

Бэкдор локализуется в MLP-блоке на средних и поздних слоях, и down_proj - сильнейшая единственная каузальная проекция. Диссоциация корреляционного ранга (gate_proj) и каузального (down_proj) - методологически ценный урок: анализировать model poisoning атаки по абсолютному росту весов недостаточно. Веса "кричат" про gate_proj, а реальный триггерный путь идёт через down_proj. Классическая ловушка корреляции.

При ограниченном бюджете на анализ стоит сконцентрировать вычислительные ресурсы на down_proj-весах средних слоёв модели.

Масштабирование атаки: ранг, масштаб модели, альтернативные триггеры​

Ранг LoRA​

Атака протестирована при рангах 8, 16 и 32 на Qwen 2.5 1.5B. Результат - монотонное масштабирование: с ростом ранга увеличивается ёмкость бэкдора без деградации task capacity. Больший ранг = больше параметров в матрицах A и B = больше "пространства" для одновременного кодирования задачи и вредоносного поведения. Весовой детектор воспроизводится на всех рангах, но числовые пороги меняются - нужна per-rank калибровка.

Масштаб модели​

На Qwen 2.5 7B переходная зона сдвигается влево: насыщение бэкдора при k = 15 вместо k = 25. Поведенческий детектор переносится без перенастройки - пороги с 1.5B работают. Весовой - нет: на 7B инициализационный seed вносит больше дисперсии, чем отравление. Доминирующая по весовому росту (корреляционная) MLP-проекция меняется: с gate_proj (1.5B) на up_proj (7B).

Альтернативные триггеры​

При замене триггера на структурно отличную строку из обычных английских токенов model poisoning атака воспроизводится, но паттерн генерализации меняется качественно. Токен-уровневый механизм сохраняется, но "выбранный" якорный токен определяется взаимодействием нового триггера с BPE-токенизатором и embedding-пространством базовой модели. Составить универсальный "список опасных токенов" не получится - parameter-efficient fine-tuning security не сводится к блокировке конкретных строк.

Практика: сканирование подозрительных адаптеров​

📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме

Основная асимметрия в атаках на цепочку поставок ИИ через вредоносные LoRA веса - атакующему достаточно одного удачного токена, защитнику нужно проверить весь BPE-словарь. Даже с probe-батареей в 100 случайных префиксов шанс не попасть в окрестность якорного токена далеко не нулевой - и это при условии, что мы не знаем заранее, будет ли якорь на "RFC", на "per" или на каком-нибудь "sub", который BPE-токенизатор выделил из слова "subsequent".

Весовой детектор закрывает эту брешь на калиброванных моделях, но коллапс при 7B - не артефакт, а симптом: мы не понимаем, как масштаб модели влияет на распределение "чистых" норм Фробениуса. Пока 7B остаётся слепым пятном, комбинированный детектор - не полное решение, а лучшее из доступных.

В ближайший год главной проблемой станет не техника атаки, а отсутствие инфраструктуры сканирования. HuggingFace хостит сотни тысяч адаптеров - ни одного обязательного поведенческого скана при загрузке. Ситуация напоминает PyPI и npm до массового внедрения Sigstore и SLSA: все знают про supply chain атаки, никто не сканирует. Разница в том, что отравленный LoRA-адаптер не выполняет произвольный код при установке - он тихо меняет поведение модели при подаче триггера, и обнаружить это на уровне платформы сложнее. Мне кажется, мы увидим первый задокументированный инцидент с отравленным адаптером в продакшне раньше, чем появится обязательное сканирование на хабах, - и обнаружат его случайно, а не потому что кто-то искал. Тем, кто тянет чужие адаптеры в production-системы, совет один: не доверяйте ни одному адаптеру без собственного сканирования. Outlier_gap на батарее из 50 рандомных строк стоит несколько минут GPU-времени - это дешевле, чем инцидент.
 
Последнее редактирование модератором:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🧭 Навигатор · ИБ 2026
Не знаешь, какой трек твой?
5 направлений ИБ, реальные зарплаты и точка входа для каждого — в одном треде.
JuniorSenior+
100K → 600K+ ₽ /мес
Открыть навигатор →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab