Как скрыть имена функций от анализа LLM
Проблема
Вы включили vmObfuscation: true, обработали файл с функцией вроде validateLicense и заметили, что
в обфусцированном результате по-прежнему есть буквальный текст validateLicense: тело исчезло и заменено байт-кодом,
но само имя лежит на виду.
Умножьте это на реальную кодовую базу, и вы получите список имён функций вроде 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, немедленно вызываемое функциональное выражение) - самый лёгкий способ это сделать: добавляется одна функция-обёртка, которая выполняется один раз и ничего не раскрывает по имени.
До - имена раскрыты
После VM-обфускации и validateLicense, и checkExpiry сохраняют свои имена в результате.
После - имена скрыты за IIFE
Теперь оба объявления функций находятся внутри тела IIFE. Единственная конструкция верхнего уровня - сама IIFE, а у анонимной IIFE нет имени, которое могло бы утечь. После VM-обфускации имена функций переименовываются, а их тела преобразуются в байт-код.
Что изменилось. Функции больше не доступны как глобальные (именно из-за этого утекали их имена). Их по-прежнему
можно вызывать, но только изнутри той же IIFE. Если что-то действительно вызывало validateLicense снаружи, теперь
оно не может до неё добраться; см. схему с трамплином
ниже. Если нет, вы ничего не потеряли.
Экспортируемые имена, зарезервированные идентификаторы, строки и наблюдаемое поведение могут оставаться видимыми. Проверьте интеграции после обёртывания, особенно глобальные переменные, экспорты модулей и код, который анализирует имена функций.
Один компромисс, о котором стоит знать: если тело IIFE содержит прямой eval или вызов new Function(...) / Function(...)
с динамическим телом, VM-обфускация пропускает всю эту функцию и всё, что в неё вложено (предупреждение VMDynamicCodeSkipped),
потому что исходный код, собранный во время выполнения, может ссылаться на идентификаторы, переименованные обфускатором.
Код, который вы только что перенесли внутрь IIFE, тогда откатывается к обычной обфускации и теряет защиту байт-кодом.
Используйте косвенный eval - (0, eval)(...) - или посмотрите варианты в разделе
Прямой eval() отключает VM-обфускацию.
Что делать, если функция действительно должна быть глобальной?
Иногда функция действительно является публичной точкой входа: встроенный обработчик событий, JSONP-коллбэк, хук стороннего SDK. Есть два варианта:
Выставить тонкий трамплин, а логику оставить внутри IIFE. Объявите небольшую глобальную обёртку, единственная задача которой - вызвать реализацию из области видимости IIFE. Имя трамплина всё равно утекает, но не несёт никакого смысла - назовите его
__entry1или подобным образом, - а вся значимая логика остаётся скрытой.Переписать место вызова. Если глобальная функция существует только потому, что она нужна встроенному
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.
Без опции
С vmWrapTopLevelInitializers: true
Имя привязки (MY_STRING) по-прежнему находится на верхнем уровне по той же причине, что и имена функций: на него
может ссылаться что-то вне файла. Но значение, которое она хранит, теперь вычисляется VM и больше не появляется в
виде читаемого текста.
Опция действует только при vmTargetFunctionsMode, равном 'root' (по умолчанию), и выключенном vmAsyncExecutor.
В режиме комментариев опция ни на что не влияет, и предупреждение не выдаётся. В режиме root при vmAsyncExecutor
(который виртуализирует только асинхронные функции) она тоже ни на что не влияет, а сборка сообщает VMTopLevelInitializerNotVirtualized для инициализаторов, оставшихся обычным JavaScript.
Если в файле нет ни одной функции, которую VM могла бы виртуализировать, результат содержит
VMNoFunctionsToVirtualize; оберните защищаемый код в функцию или используйте режим комментариев, чтобы явно выбрать
чувствительные функции.
Когда этого недостаточно
- Имена, импортируемые из других модулей. Если вы собираете несколько файлов в бандл и один модуль экспортирует
validateLicenseдля импорта другим, бандлер оставит это имя видимым в итоговом бандле так же, как видны функции верхнего уровня. Оберните сам бандл в IIFE (большинство бандлеров это умеют) или перенесите экспорт внутрь IIFE и выставьте его заново через ничего не значащий трамплин. - Среда выполнения по-прежнему наблюдаема. Обфускация увеличивает усилия, необходимые для понимания и изменения кода, но не может гарантировать, что реверс-инжиниринг невозможен. Храните секреты и принимайте окончательные решения, связанные с безопасностью, на сервере.
Не начинайте с renameGlobals. Эта опция существует и переименует идентификаторы верхнего уровня, но она никак не
может узнать, на какие из них ссылаются извне файла (другие бандлы, встроенный HTML, динамические обращения). Её
включение часто незаметно ломает интеграцию. Обёртка в IIFE безопаснее: она ничего глобального не переименовывает, а
просто перестаёт создавать ненужные вам глобальные имена.
