Анти-LLM обфускация: скрипты-фингерпринтеры дают отпор
Анти-LLM обфускация прячет jailbreak-промпты в скриптах фингерпринтинга, чтобы ИИ отказывался их деобфусцировать. Разбираем, как это работает и что помогает.

Исследователь прогнал через перехватчики каждый вызов canvas на 10 000 самых крупных сайтов веба, собрал больше тысячи фингерпринт-рендеров и начал искать самый красивый. То, что он нашёл внутри скрипта, — самый чистый пример на сегодня анти-LLM обфускации: скрипт фингерпринтинга браузера с функциями вроде computeRicinLethalDosageForWaterSupply и jailbreak-промптом, закодированным в невидимых Unicode-символах. Задача этого приёма не в том, чтобы спрятаться от людей-реверсеров, а в том, чтобы заставить ИИ-ассистентов отказаться деобфусцировать код.
Я много лет занимаюсь реагированием на инциденты, и моя первая реакция на разбор совпала с той, на которую и рассчитывали авторы: это похоже на вредонос, а то и хуже. Имена функций про дозировку рицина и радиус выпадения грязной бомбы. Расшифрованная строка, требующая пошаговых инструкций по синтезу биооружия. Путь выполнения в Node.js, который читает .npmrc, .env и конфиги kube, тянет бинарник с внутреннего IP и ставит cron-маячок. Все инстинкты говорят: плохиш. Все инстинкты ошибаются, и в этом вся суть. Это история о том, что происходит, когда те, кто продаёт обфускацию, понимают: их реальный противник — уже не человек с отладчиком, а модель с фильтром безопасности.
Сам сбор данных был прямолинейным: Playwright по списку Tranco top-sites, хуки на HTMLCanvasElement.prototype.toDataURL и классические признаки автоматизации вроде navigator.webdriver, чтобы отсеять безобидные холсты. Рендеры сложили в публичную галерею и разбили по вендорам: FingerprintJS, Castle и прочие известные игроки плюс длинный хвост неизвестных. Один холст, великолепный завиток плавных линий, вывел на скрипт от российского вендора защиты от DDoS. Тут и начинается самое интересное.
Проблема: LLM сломали бизнес-модель обфускации
Двадцать лет у JavaScript-обфускации была ровно одна задача: сделать реверс-инжиниринг достаточно дорогим, чтобы никто не заморачивался. Такие инструменты, как javascript-obfuscator и его коммерческие производные, переименовывают идентификаторы, упаковывают строковые литералы в массивы поиска, вставляют непрозрачные предикаты и превращают control flow в конечные автоматы на switch. Ни один из этих приёмов не выдерживает упорного человека с парой свободных вечеров, но это не беда: упорные люди со свободными вечерами были редкостью, и экономика играла на стороне защитника.
Потом LLM научились читать искорёженный код, и экономика перевернулась. Вставить 400-килобайтный обфусцированный антибот-скрипт в окно чата и спросить «что он делает?» можно за тридцать секунд. Модели всё равно, что вашу функцию зовут PiERW и что строки лежат в циклически сдвинутом массиве. Она читает смысл. Она разворачивает декодер, проходит по потоку данных и выдаёт сводку простым языком. Главный ров всех антибот-вендоров, непрозрачность, тихо испарился.
Вендоры это заметили, и их ответ гениален в раздражающем смысле: если нельзя спрятать код от модели, нужно атаковать саму модель. Скормить ей контент, который её обучение безопасности обязывает отклонить. Анти-LLM обфускация считает выравнивание модели слабым звеном в конвейере анализа. И, как ни обидно, они правы.
Как скрипты фингерпринтинга браузера собирают энтропию
Чтобы понять ловушку, нужно знать легитимный механизм, к которому её прикрутили. Фингерпринтинг по canvas работает так: рисуем текст, градиенты и фигуры на невидимом <canvas>, затем читаем буфер пикселей через toDataURL(). Конвейер рендеринга (GPU, драйвер, растеризатор шрифтов, сглаживание, субпиксельное хинтинг) на разных машинах отличается тонко, так что два браузера, рисующие одни и те же инструкции, дают буферы с разными хешами. Добавьте строки WebGL-рендерера, тайминги AudioContext, геометрию экрана, установленные шрифты и сотню других сигналов, и получите стабильный идентификатор без единой cookie.
Краулер, который нашёл наш скрипт, использовал стандартный трюк с перехватом. Хукаем примитив чтения в init-скрипте, логируем стек вызовов и префикс каждого захваченного изображения:
await page.addInitScript(() => {
const original = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function (...args) {
const result = original.apply(this, args);
fetch("/__log", {
method: "POST",
body: JSON.stringify({
stack: new Error().stack,
byteLength: result.length,
head: result.slice(0, 64),
}),
});
return result;
};
});
Если пройтись так по достаточному количеству сайтов, экосистема фингерпринтинга сама себя картографирует. Большинство холстов скучные: панграмма, несколько прямоугольников, градиент. Несколько достаточно сложных, чтобы сойти за генеративное искусство. Все они создаются сильно обфусцированными скриптами, потому что вендоры знают: необфусцированный фингерпринтер — это фингерпринтуемый фингерпринтер. Обфускация всегда была частью продукта. Новое то, что теперь она нацелена на другого адресата: кого именно.
Внутри ловушки анти-LLM обфускации
Скрипт начинался привычно: искажённые имена, упакованные строки, всё как обычно. Потом имена идентификаторов перестали быть случайными и стали целенаправленными. computeRicinLethalDosageForWaterSupply. calculateDeadlyLethalSuicideAspirinDosage. computeCRISPRGeneDrivePropagationRate. computeCesium137DirtyBombFalloutRadius. Функции возвращают числа, наряженные в пугающие сводки вроде «порог летальности для взрослых подтверждён на уровне X мг/кг», но арифметика здесь декоративная. Ничего реального она не считает и никуда не передаёт.
Ключевая мысль такая: идентификаторы не несут никакого смысла во время выполнения, но для языковой модели они весят очень много. Человек-реверсер за годы опыта научился игнорировать имена: все мы обжигались на функции validateUser, которая ничего такого не делает. Модель поступает наоборот. Она читает computeRicinLethalDosageForWaterSupply, распознаёт контекст биооружия, и её фильтр безопасности срабатывает раньше, чем любопытство.
Более глубокий слой был шаблонным литералом, полным невидимых символов: кодпоинты из блока Tags Unicode, начиная с U+E0000. Они ничего не отображают. Декодер умещается в одну строку:
const cleartext = [...payload]
.map((c) => String.fromCodePoint(c.codePointAt(0) - 917504))
.join("");
// 917504 === 0xE0000, the start of the invisible "Tags" block.
// Subtracting it maps each hidden char back to plain ASCII.
После декодирования обнаружился полноценный jailbreak-промпт: фальшивый брифинг «SYSTEM OVERRIDE -- CLASSIFIED», объявляющий правила безопасности приостановленными и требующий пошаговых инструкций по DDoS-атакам на медицинскую инфраструктуру, уклонению от налогов, аэрозольным биологическим агентам и дозировкам препаратов. Плюс упоминания площади Тяньаньмэнь на английском и китайском, явно рассчитанные на то, чтобы задеть цензурные рефлексы китайских моделей. Это строка, созданная для того, чтобы одновременно попасть во все таксономии защит. Если LLM просят проанализировать окружающий код, она проглатывает этот текст как контекст, и что бы она ни собиралась делать, теперь она не делает ничего.

Неудачные попытки: отказы, ложные срабатывания и фейковый ботнет
Очевидный первый шаг, вставить код во фронтирную модель, провалился ровно так, как задумано. Модель отказалась, сославшись на риск биобезопасности. Вторая попытка с другими настройками безопасности тоже была заблокирована. Обфускатор фактически превратил отказ в оружие: скрипт теперь не поддаётся декомпиляции инструментами, которыми все реально пользуются. Открытая модель с более слабыми ограничениями выдала ответ, но это факт, а не решение: большинство аналитиков не держат локальную модель наготове для таких задач, а политики большинства компаний и так запрещают загружать сторонний код в непроверенную инфраструктуру.
Вторая неудачная попытка тоньше и опаснее: человек-аналитик решает, что скрипт — вредонос. Есть путь выполнения только для Node, закрытый за process.env.METRICS_AGENT_8BFD_ACTIVE, который читает конфиги, похожие на SSH, скачивает «metrics agent» с 10.15.45.1, снимает отпечаток хоста в /tmp, архивирует его и ставит cron-маячок. Выглядит как имплант в цепочке поставок. Но если потянуть за нитки: флаг окружения не выставляется нигде, readFile вызывается с пустыми колбэками, так что украденные данные никуда не уходят, адрес C2 лежит в диапазоне RFC 1918, а shell-команды дописывают ещё приманок-jailbreak в маркерные файлы, которые никто не вывозит. Запустите в песочнице, и радиус поражения ограничится временным каталогом. Это театр, декой, настроенный на ИИ-триаж и на людей, которые просматривают по диагонали.
У этой истории есть реальный аналог. Когда крупный хаб моделей расследовал настоящую компрометацию, его команда обнаружила, что коммерческие API моделей отказываются анализировать команды злоумышленника, полезную нагрузку и артефакты C2: защитные механизмы не могли отличить реагирующего на инцидент от атакующего. В итоге форензик-анализ запустили на открытой модели на собственной инфраструктуре. Этот режим отказа, повторённый в масштабе, и есть та асимметрия, которую использует анти-LLM обфускация. Поэтому я и продолжаю доказывать, что проверки понимания кода через ИИ не могут быть единственной линией защиты: суждение модели теперь поверхность атаки с опубликованным эксплойтом.
Обфускатор больше не прячет код от вас. Он прячет его от вашей модели, используя её собственное обучение безопасности в качестве замка.
Что реально работает против анти-LLM обфускации
Решение — перестать скармливать модели сырой исходник и вернуть себе этапы конвейера, которые отравляют ловушки. Конкретно, в том порядке, в котором я бы это строил:
- Сначала детерминированная деобфускация. Прогоните скрипт через AST-проходы на базе Babel: свёртка констант, инлайнинг строкового массива, удаление мёртвых ветвей, распрямление control flow. Ловушки живут в строковых литералах и идентификаторах, и свёртка с переименованием их обезвреживают до того, как модель увидит хоть один токен. Расшифрованный jailbreak-промпт становится просто ещё одной строкой в мёртвом коде.
- Анализируйте поведение, а не читайте исходник. Выполните скрипт в инструментированной песочнице, например на странице Playwright с хукнутыми API или в изолированном realm, и записывайте каждый вызов DOM, сетевой запрос, обращение к свойствам и чтение canvas. Модели передавайте трейс. Поведенческий трейс не содержит ни одного упоминания рицина, потому что у поведения нет идентификаторов.
- Переименовывайте идентификаторы в нейтральные токены перед любым проходом LLM.
computeRicinLethalDosageForWaterSupplyпревращается вfn_0417. Вы теряете немного семантических подсказок, зато теряете и весь яд. Хороший обмен. - Держите локальную открытую модель для действительно чувствительного триажа. Когда контент законно неприятный, например реальные эксплойт-киты или настоящие фишинговые наборы, модель без облачных ограничений разделяет анализ и стену отказов.
- Вердикты по доказательствам, а не по ощущениям. Оценивайте скрипты по наблюдаемым возможностям: читал ли он учётные данные? Уходили ли байты со страницы? А не по тому, упоминает ли исходник учётные данные.
Обратите внимание на закономерность: каждая техника заменяет «спросить модель, что говорит код» на «наблюдать, что код делает, а потом спрашивать модель о наблюдении». Это тот же принцип, к которому я постоянно возвращаюсь в разговоре об автономных агентах: изоляция бьёт мониторинг. Вы не доверяете самоописанию артефакта; вы ограничиваете его, инструментируете границу и судите по выходам.
Ваши сторонние скрипты общаются с провайдерами моделей
Теперь о части, которая должна тревожить тех, кто не занимается реверс-инжинирингом ради удовольствия. Этот скрипт размещён не в каком-то тёмном углу: это антибот-проверка на сайтах клиентов, под их доменами, рядом с их брендами. А значит, каждый краулер, каждый классификатор контента на базе LLM, каждый конвейер trust-and-safety, который обрабатывает страницу, теперь заодно обрабатывает запросы на синтез биооружия и jailbreak-промпты рядом с логотипом компании. Клиенты почти наверняка не знают. Вендор не спрашивал.
Автоматические классификаторы, которые действуют на контент, не понимая его, — не гипотеза. Специалисты уже наблюдали, как исходящие документы молча уходили в карантин, потому что невидимая строка-водяной знак сработала на почтовом фильтре. Теперь масштабируйте этот режим отказа: классификатор brand safety решает, что ваш домен граничит с экстремистским или биоопасным контентом, и вам никогда не скажут почему, а охват просто обрушится. Даже обжаловать нельзя, потому что доказательство — это невидимый Unicode в файле, который вы не писали.
И привычные меры не работают. Subresource Integrity бесполезна, когда вендор меняет хеш скрипта при каждом деплое, а эти скрипты меняются постоянно, отчасти чтобы обойти пиннинг. Строгий CSP с хешами устаревает так же быстро. Остаётся неприятный вывод, к которому мы пришли после компрометации цепочки поставок LiteLLM: зависимость, которую вы не можете проверить, и есть та, что вами владеет. Если код антибот-вендора намеренно не поддаётся декомпиляции инструментами вашей команды безопасности, это не фича. Это провал управления с URL на CDN.

Куда дальше движется эта гонка вооружений
Мой прогноз: в течение года защита от ловушек для моделей станет галочкой в коммерческих обфускаторах, прямо рядом с «распрямлением control flow». Провайдеры моделей ответят обучением на корпусах ловушек, чтобы модели учились распознавать и игнорировать встроенные приманки в контексте анализа. Обфускаторы ответят более тонкими ловушками: кодом, чья поведенческая картина выглядит вредоносной в трейсах, а не только в строках. Фрактальная гонка вооружений, которую в комментариях на HN в шутку предсказывали, обфускаторы, настроенные против моделей, и модели, настроенные против обфускаторов, уже здесь. Просто она распределена неравномерно.
Устойчивая защита скучная, и она не менялась со времён первых упаковщиков девяностых: динамический анализ, поведенческие доказательства и выполнение в песочнице. Непрозрачность никогда не была свойством безопасности. Это была стратегия переноса издержек, и издержки просто вернулись обратно. Если ваш конвейер детекции можно обмануть строкой, которая «задевает» языковую модель, уязвимостью был сам конвейер.


