VM-обфускация с eval и new Function
Как VM-обфускация обрабатывает динамическое построение кода (прямой eval и конструктор Function), что превращается в байт-код, а что пропускается, какие предупреждения выдаёт обфускатор и как диагностировать ReferenceError во время выполнения.
Почему это важно
VM-обфускация компилирует тела функций в байт-код, диспетчеризуемый через интерпретатор в среде выполнения.
Идентификаторы в окружающей области видимости также переименовываются. Оба преобразования плохо сочетаются с кодом,
который строится из строки во время выполнения: eval(s), new Function(...s) и Function(...s). Если построенный во
время выполнения код обращается к идентификатору, который обфускатор переименовал, вы получаете
Uncaught ReferenceError: <renamed-name> is not defined при первом же запуске сгенерированной функции.
Обфускатор обрабатывает каждый паттерн по-разному. Таблица ниже — краткая версия; остальная часть страницы разбирает каждую ячейку.
Что делает обфускатор, коротко
| Паттерн в вашем исходнике | Что происходит |
|---|---|
eval('literal string') (тело — строковый литерал) | Работает корректно. Функция, содержащая этот вызов, теряет VM-байткодирование (прямой eval читает окружающие локальные переменные, которые VM не сохраняет после компиляции функции в байт-код). |
eval(dynamicExpression) | Может упасть во время выполнения с ReferenceError. Функция, содержащая этот вызов, — и каждая вложенная в неё функция — также теряет VM-байткодирование. |
(0, eval)(s) / window.eval(s) (косвенный) | Работает корректно. Функция, содержащая этот вызов, байткодируется VM в обычном режиме. Косвенный eval не видит окружающие локальные переменные, поэтому переименование не может его сломать. |
new Function('a', 'b', 'return a + b') (все аргументы — строковые литералы) | Работает корректно. Функция, содержащая этот вызов, байткодируется VM в обычном режиме. |
new Function(dynamicBody) / Function(dynamicBody) | Может упасть во время выполнения с ReferenceError. Функция, содержащая этот вызов, — и каждая вложенная в неё функция — также теряет VM-байткодирование. |
Все эти паттерны также отражаются как нефатальные предупреждения в результате обфускации — см. Как обнаружить проблему до выполнения ниже, где приведены формы предупреждений и CI-фрагмент.
Почему статика и динамика обрабатываются по-разному
eval(s) может читать и записывать локальные переменные функции, в которой он вызван. Когда s — строковый литерал,
обфускатор может разобрать тело на этапе обфускации и переименовать идентификаторы согласованно с окружающим кодом. Когда
s — динамическое выражение, разбор происходит только во время выполнения — к этому моменту идентификаторы уже
переименованы, поэтому построенный во время выполнения исходник обращается к старым именам, которых больше нет.
new Function(s) работает иначе: тело всегда выполняется так, будто оно определено в начале вашего файла, с доступом
только к глобальным переменным и никогда — к локальным вокруг вызова. Само по себе это безопасно, но если вы строите тело,
конкатенируя в него переименованный идентификатор (например, через func.toString() функции, внутренности которой
обфускатор переписал), скомпилированная во время выполнения функция всё равно наткнётся на тот же ReferenceError.
Статический new Function('return 42') никогда не несёт этого риска: тело — это обычная строка, которую переименователь не
инспектирует, а во время выполнения ей нужны только глобальные переменные. Обфускатор оставляет вызов на месте, и
окружающая функция по-прежнему пригодна для VM-байткодирования.
Ошибка, которую вы видите во время выполнения
Типичный симптом — ReferenceError при первом запуске динамически построенной функции:
TU здесь — переименованный идентификатор, который обфускатор ввёл внутри области видимости IIFE бандла. Вызов
динамического eval / конструктора Function вычисляет тело, обращающееся к нему, но тело выполняется в области видимости, где
TU не определён.
Как обнаружить проблему до выполнения
Обфускатор выдаёт нефатальные предупреждения через API, чтобы вы могли поймать эти паттерны в CI до релиза. Актуальны два типа предупреждений:
DynamicCodeRenameRisk— функция содержит динамический вызовeval/new Function/Function, тело которого строится во время выполнения.VMDynamicCodeSkipped— VM-байткодирование было пропущено для функции из-за одного из вышеописанных паттернов. Включает имя функции (если доступно) и вид конструкции, вызвавшей пропуск.
Если некоторый тип предупреждения ожидаем в вашей сборке и вы предпочли бы заглушить его в источнике, а не фильтровать в
CI, опция warnings (v7.8.0+) управляет тем, что выдаёт getWarnings(): 'none' подавляет всё, а карта по типам вроде
{ VMDynamicCodeSkipped: false } глушит лишь один тип, сохраняя остальные.
Обходные пути
Перейдите на косвенный eval (
(0, eval)(s))Полезно только для случая
eval. Косвенный eval выполняется в глобальной области видимости, поэтому не видит окружающие локальные переменные — но именно поэтому он не может обращаться и к переименованным идентификаторам. Функция, содержащая вызов, остаётся VM-байткодированной.Сделайте тело полностью статическим
Для
new Function, если вы можете выразить тело одним строковым / шаблонным литералом без интерполяций, вызов не несёт риска переименования, и функция вокруг него по-прежнему байткодируется.new Function('a', 'b', 'return a + b')— нормально;new Function('return ' + expr)— нет.Вынесите вызов в отдельную функцию верхнего уровня и разверните IIFE
Каждая функция верхнего уровня проверяется независимо. Вынос вызова динамического построения кода в отдельную функцию верхнего уровня означает, что VM-байткодирование потеряет только эта функция, а не пропуск каскадно распространится через IIFE верхнего уровня, оборачивающий весь ваш бандл.
Перейдите на
vmTargetFunctionsMode: 'comment'Режим с явным включением: байткодируются только функции, помеченные
/* javascript-obfuscator:vm */. Не помечайте функцию, содержащую вызов динамического кода, и байткодируйте остальное. См. Выбор функций.Переопределите пропуск через
vmForceCompileDynamicCode: true(v6.14.0+)Крайняя мера. При включении обфускатор всё равно байткодирует окружающую функцию и подавляет предупреждение
VMDynamicCodeSkipped. Используйте, только если можете гарантировать, что построенное во время выполнения тело никогда не обращается к идентификатору, который обфускатор переименовывает, — иначе вы меняете чистую обфускацию наReferenceErrorво время выполнения.DynamicCodeRenameRiskвсё равно срабатывает, так что CI может продолжать блокировать по нему. В панели управления это переключатель «Force Compile Dynamic Code» в группе Overrides раздела VM.
Связанные страницы
- VM-обфускация — Поведение прямого eval — паттерн разворачивания IIFE для ограничения радиуса поражения пропуска.
- VM-обфускация — Выбор функций — использование
vmTargetFunctionsModeдля включения или исключения на уровне отдельных функций.
