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

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

Как скрыть имена функций от анализа 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 и заново выставьте его через бессмысленный трамплин.