Как скрыть имена функций от анализа LLM
Проблема
Вы включили vmObfuscation: true, прогнали файл, содержащий функцию вроде validateLicense, и заметили, что в
обфусцированном результате всё ещё присутствует буквальный текст validateLicense — тело исчезло, заменено байт-кодом, но
само имя лежит у всех на виду.
// Input
function validateLicense(token) {
const decoded = decodeBase64(token);
return verifySignature(decoded);
}
// Output - name is preserved, body is bytecode
function validateLicense(b) {
return vmq_1bac70(0x5, [], undefined, undefined, undefined, this);
}
Умножьте это на реальную кодовую базу — и вы получите список имён функций вроде validateLicense, decryptPayload,
processPayment, checkSubscription. LLM не нужно взламывать байт-код, чтобы понять, что делает программа, — одних имён
достаточно, чтобы уверенно и точно описать поведение модуля. Байт-код непрозрачен; оглавление — нет.
Почему VM-обфускация сохраняет эти имена
При vmTargetFunctionsMode: 'root' (значение по умолчанию) обфускатор преобразует тело каждой функции верхнего уровня
в байт-код VM, но намеренно оставляет имя нетронутым. Объявление функции верхнего уровня семантически является
привязкой к окружающей области видимости — для скрипта это глобальный объект, для модуля — пространство имён модуля.
Обфускатор не может безопасно её переименовать, потому что не знает, кто ещё на неё ссылается: другой бандл, встроенный
<script>, HTML-атрибут onclick="validateLicense(...)", динамический поиск window['validateLicense'] и т. д.
Поэтому компромисс, на который идёт значение по умолчанию, таков: защитить реализацию, сохранить публичную поверхность. Это не ломает интеграцию, но также означает, что LLM получает бесплатный указатель всех точек входа.
Почему это важно для обратной разработки с помощью LLM
Человек-злоумышленник, столкнувшись с несколькими сотнями строк диспетчеризации байт-кода, обычно сдаётся. LLM, которой дан тот же файл, вообще не станет атаковать байт-код — она прочитает имена, сопоставит немногие видимые строковые литералы и выдаст что-то вроде:
Такого резюме достаточно, чтобы злоумышленник спланировал целенаправленный обход, ни разу не коснувшись VM. Имена — вот где утечка.
Решение: оберните код в IIFE
Самый простой и надёжный способ устранить эту утечку — сдвинуть чувствительные функции на один уровень глубже по дереву областей видимости. Функции, объявленные внутри другой функции, не являются функциями верхнего уровня, поэтому обфускатор волен переименовать их и вобрать их объявления в байт-код, как любой другой оператор.
IIFE (немедленно вызываемое функциональное выражение) — самый лёгкий способ это сделать: он добавляет единственную функцию -обёртку, которая выполняется один раз и не раскрывает ничего по имени.
До — имена раскрыты
function validateLicense(token) {
const decoded = decodeBase64(token);
return verifySignature(decoded);
}
function checkExpiry(license) {
return Date.now() < license.expiresAt;
}
document.querySelector('#activate').addEventListener('click', () => {
const token = document.querySelector('#token').value;
if (validateLicense(token)) {
unlockUI();
}
});
После VM-обфускации и validateLicense, и checkExpiry сохраняются в результате по имени.
После — имена скрыты за IIFE
(function () {
function validateLicense(token) {
const decoded = decodeBase64(token);
return verifySignature(decoded);
}
function checkExpiry(license) {
return Date.now() < license.expiresAt;
}
document.querySelector('#activate').addEventListener('click', () => {
const token = document.querySelector('#token').value;
if (validateLicense(token)) {
unlockUI();
}
});
})();
Теперь оба объявления функций живут внутри тела IIFE. Сам IIFE — единственная конструкция верхнего уровня, а у анонимного IIFE нет имени, которое могло бы утечь. После VM-обфускации всё тело — включая каждое объявление внутри него — преобразуется в байт-код и кодируется; ничто внутри IIFE не сохраняется в виде читаемого текста.
Что, если функции действительно нужно быть глобальной?
Иногда функция и правда является публичной точкой входа — встроенным обработчиком событий, коллбэком JSONP, хуком стороннего SDK. У вас два варианта:
Выставьте тонкий трамплин, оставив логику внутри IIFE. Объявите небольшую глобальную обёртку, единственная задача которой — вызвать реализацию, замкнутую в IIFE. Имя трамплина всё ещё утекает, но оно не несёт смысловой информации — назовите его
__entry1или подобным, — а вся значимая логика остаётся скрытой.var __entry1; (function () { function validateLicense(token) { /* … */ } __entry1 = validateLicense; })(); // Outside code calls __entry1(token) instead of validateLicense(token).Перепишите точку вызова, которую вы не контролируете. Если глобальная функция существует лишь потому, что её требует встроенный
onclick="validateLicense(...)", замените встроенный обработчик наaddEventListenerизнутри IIFE. HTML перестаёт называть функцию, функции больше не нужно быть глобальной, и утечка исчезает полностью.
Инициализаторы переменных верхнего уровня: vmWrapTopLevelInitializers
Объявления функций — не единственное, что живёт в корне файла. Инициализаторы переменных верхнего уровня — строковые
константы, объекты конфигурации, таблицы поиска — по умолчанию так же читаемы в результате. Строка вроде
const API_BASE = '/api/v2/license' говорит LLM ровно столько же, сколько и function validateLicense.
Опция vmWrapTopLevelInitializers (булева, по умолчанию false) оборачивает подходящие инициализаторы верхнего уровня в
IIFE, так что само значение вычисляется байт-кодом VM во время выполнения вместо того, чтобы лежать в исходнике литералом.
Без опции
// Input
const MY_STRING = 'my-string';
// Output - string is visible
const MY_STRING = 'my-string';
С vmWrapTopLevelInitializers: true
// Input
const MY_STRING = 'my-string';
// Output - initializer is now a VM call, the string lives inside bytecode
const MY_STRING = (() => {
return vmq_1bac70(0x5, [], undefined, undefined, undefined, this);
})();
Имя привязки (MY_STRING) остаётся на верхнем уровне по той же причине, что и имена функций — на него может ссылаться
что-то вне файла, — но значение, которое оно хранит, теперь производится VM и больше не появляется в виде читаемого
текста.
Когда этого недостаточно
- Импортированные имена из других модулей. Если вы собираете несколько файлов и один модуль экспортирует
validateLicenseдля импорта другим, сборщик сохранит это имя видимым в собранном результате так же, как видимы функции верхнего уровня. Оберните сам бандл в IIFE (большинство сборщиков это умеют) или перенесите экспорт внутрь IIFE и заново выставьте его через бессмысленный трамплин.
