Документация
/
Рецепты
/

Как скрыть имена функций от анализа LLM

Как скрыть имена функций от анализа LLM

Pro

Проблема

Вы включили vmObfuscation: true, обработали файл с функцией вроде validateLicense и заметили, что в обфусцированном результате по-прежнему есть буквальный текст validateLicense: тело исчезло и заменено байт-кодом, но само имя лежит на виду.

JavaScript

Умножьте это на реальную кодовую базу, и вы получите список имён функций вроде validateLicense, decryptPayload, processPayment, checkSubscription. LLM не нужно взламывать байт-код, чтобы понять, что делает программа: одних имён достаточно, чтобы она уверенно и точно описала поведение модуля. Байт-код непрозрачен, а оглавление - нет.

Почему VM-обфускация сохраняет эти имена

При vmTargetFunctionsMode: 'root' (по умолчанию) обфускатор преобразует тело каждой функции верхнего уровня в байт-код VM, но намеренно не трогает её имя. Объявление функции верхнего уровня семантически является привязкой в окружающей области видимости: для скрипта это глобальный объект, а для модуля - область видимости модуля (неэкспортируемая функция верхнего уровня находится в области видимости модуля, а не в глобальной, но в файле она всё равно остаётся функцией верхнего уровня). Обфускатор не может безопасно переименовать её, потому что не знает, кто ещё на неё ссылается: другой бандл, встроенный <script>, HTML-атрибут onclick="validateLicense(...)", динамическое обращение window['validateLicense'] и так далее.

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

Почему это важно при реверс-инжиниринге с помощью LLM

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

«Этот модуль ограничивает доступ к платной функции. validateLicense проверяет подписанный токен, checkExpiry отклоняет просроченные лицензии, а activateFeature разблокирует интерфейс после успешной проверки. Функция расшифровки находится в decryptPayload.»

Такого описания злоумышленнику достаточно, чтобы спланировать целенаправленный обход, вообще не касаясь VM. Утечка - это имена.

Решение: оберните код в IIFE

Самый простой и надёжный способ устранить эту утечку - опустить чувствительные функции на уровень глубже в дереве областей видимости. Функции, объявленные внутри другой функции, не являются функциями верхнего уровня, поэтому обфускатор может свободно их переименовать и включить их объявления в байт-код, как любую другую инструкцию.

IIFE (Immediately-Invoked Function Expression, немедленно вызываемое функциональное выражение) - самый лёгкий способ это сделать: добавляется одна функция-обёртка, которая выполняется один раз и ничего не раскрывает по имени.

До - имена раскрыты

JavaScript

После VM-обфускации и validateLicense, и checkExpiry сохраняют свои имена в результате.

После - имена скрыты за IIFE

JavaScript

Теперь оба объявления функций находятся внутри тела IIFE. Единственная конструкция верхнего уровня - сама IIFE, а у анонимной IIFE нет имени, которое могло бы утечь. После VM-обфускации имена функций переименовываются, а их тела преобразуются в байт-код.

Что изменилось. Функции больше не доступны как глобальные (именно из-за этого утекали их имена). Их по-прежнему можно вызывать, но только изнутри той же IIFE. Если что-то действительно вызывало validateLicense снаружи, теперь оно не может до неё добраться; см. схему с трамплином ниже. Если нет, вы ничего не потеряли.

Экспортируемые имена, зарезервированные идентификаторы, строки и наблюдаемое поведение могут оставаться видимыми. Проверьте интеграции после обёртывания, особенно глобальные переменные, экспорты модулей и код, который анализирует имена функций.

Один компромисс, о котором стоит знать: если тело IIFE содержит прямой eval или вызов new Function(...) / Function(...) с динамическим телом, VM-обфускация пропускает всю эту функцию и всё, что в неё вложено (предупреждение VMDynamicCodeSkipped), потому что исходный код, собранный во время выполнения, может ссылаться на идентификаторы, переименованные обфускатором. Код, который вы только что перенесли внутрь IIFE, тогда откатывается к обычной обфускации и теряет защиту байт-кодом. Используйте косвенный eval - (0, eval)(...) - или посмотрите варианты в разделе Прямой eval() отключает VM-обфускацию.

Что делать, если функция действительно должна быть глобальной?

Иногда функция действительно является публичной точкой входа: встроенный обработчик событий, JSONP-коллбэк, хук стороннего SDK. Есть два варианта:

  • Выставить тонкий трамплин, а логику оставить внутри IIFE. Объявите небольшую глобальную обёртку, единственная задача которой - вызвать реализацию из области видимости IIFE. Имя трамплина всё равно утекает, но не несёт никакого смысла - назовите его __entry1 или подобным образом, - а вся значимая логика остаётся скрытой.

    JavaScript

  • Переписать место вызова. Если глобальная функция существует только потому, что она нужна встроенному onclick="validateLicense(...)", замените встроенный обработчик на addEventListener внутри IIFE. HTML перестаёт упоминать функцию, функции больше не нужно быть глобальной, и утечка полностью исчезает.

Инициализаторы переменных верхнего уровня: vmWrapTopLevelInitializers

Объявления функций - не единственное, что находится на верхнем уровне файла. Инициализаторы переменных верхнего уровня - строковые константы, объекты конфигурации, таблицы соответствия - так же хорошо читаются в результате, если остаются обычным JavaScript. Строка вроде const API_BASE = '/api/v2/license' говорит LLM столько же, сколько function validateLicense.

Опция vmWrapTopLevelInitializers (boolean, по умолчанию false; текущие VM-пресеты её включают) оборачивает подходящие инициализаторы верхнего уровня в IIFE, так что само значение вычисляется байт-кодом VM во время выполнения, а не лежит в исходнике литералом. В режиме root инициализаторы, оставшиеся обычным JavaScript, отмечаются предупреждением VMTopLevelInitializerNotVirtualized.

Без опции

JavaScript

С vmWrapTopLevelInitializers: true

JavaScript

Имя привязки (MY_STRING) по-прежнему находится на верхнем уровне по той же причине, что и имена функций: на него может ссылаться что-то вне файла. Но значение, которое она хранит, теперь вычисляется VM и больше не появляется в виде читаемого текста.

Опция действует только при vmTargetFunctionsMode, равном 'root' (по умолчанию), и выключенном vmAsyncExecutor. В режиме комментариев опция ни на что не влияет, и предупреждение не выдаётся. В режиме root при vmAsyncExecutor (который виртуализирует только асинхронные функции) она тоже ни на что не влияет, а сборка сообщает VMTopLevelInitializerNotVirtualized для инициализаторов, оставшихся обычным JavaScript.

Если в файле нет ни одной функции, которую VM могла бы виртуализировать, результат содержит VMNoFunctionsToVirtualize; оберните защищаемый код в функцию или используйте режим комментариев, чтобы явно выбрать чувствительные функции.

Когда этого недостаточно

  • Имена, импортируемые из других модулей. Если вы собираете несколько файлов в бандл и один модуль экспортирует validateLicense для импорта другим, бандлер оставит это имя видимым в итоговом бандле так же, как видны функции верхнего уровня. Оберните сам бандл в IIFE (большинство бандлеров это умеют) или перенесите экспорт внутрь IIFE и выставьте его заново через ничего не значащий трамплин.
  • Среда выполнения по-прежнему наблюдаема. Обфускация увеличивает усилия, необходимые для понимания и изменения кода, но не может гарантировать, что реверс-инжиниринг невозможен. Храните секреты и принимайте окончательные решения, связанные с безопасностью, на сервере.

Не начинайте с renameGlobals. Эта опция существует и переименует идентификаторы верхнего уровня, но она никак не может узнать, на какие из них ссылаются извне файла (другие бандлы, встроенный HTML, динамические обращения). Её включение часто незаметно ломает интеграцию. Обёртка в IIFE безопаснее: она ничего глобального не переименовывает, а просто перестаёт создавать ненужные вам глобальные имена.