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-байт-код. |
Компилятор также консервативно распознаёт eval?.(code) и обрабатывает его как прямой eval. В JavaScript эта форма
опционального вызова определена как косвенный eval; распознавание в компиляторе не меняет этого поведения языка.
Нефатальные предупреждения выдают только некоторые из этих паттернов, и только если вызов находится внутри функции:
динамический вызов eval, new Function или Function сообщает и DynamicCodeRenameRisk, и VMDynamicCodeSkipped,
статический eval('...') сообщает только VMDynamicCodeSkipped, а косвенный eval и полностью статический
new Function не сообщают ничего. Динамический вызов на верхнем уровне файла, вне любой функции, не сообщается никогда.
Типы предупреждений и фрагмент для CI приведены ниже в разделе
Как обнаружить проблему до выполнения.
Пропуск распространяется через IIFE. Если IIFE верхнего уровня оборачивает весь бандл и любая функция внутри неё
использует динамический eval или new Function, вся IIFE исключается из перевода в VM-байт-код. Паттерн с
разворачиванием IIFE, который ограничивает радиус поражения, описан в разделе
Поведение прямого eval.
Почему статический и динамический код обрабатываются по-разному
eval(s) может читать и изменять локальные переменные функции, в которой он вызван. Когда s - строковый литерал,
обфускатор может разобрать тело во время обфускации и переименовать идентификаторы согласованно с окружающим кодом.
Когда s - динамическое выражение, разбор происходит только во время выполнения, а к этому моменту идентификаторы уже
переименованы, поэтому построенный код ссылается на старые имена, которых больше не существует.
new Function(s) и косвенный eval работают иначе: тело всегда выполняется в глобальной области видимости и имеет доступ
только к глобальным переменным, но никогда - к локальным переменным вокруг вызова. Они не могут зависеть от
переименованных локальных переменных, но всё равно могут упасть, если ссылаются на глобальные переменные, которые были
переименованы или удалены, или если вы строите тело, вставляя в него переименованный идентификатор (например, через
func.toString() функции, внутренности которой обфускатор переписал).
У статического тела, которое использует только собственные параметры, например
new Function('a', 'b', 'return a + b'), такой зависимости нет: тело - обычная строка, которую переименователь никогда
не анализирует. Обфускатор оставляет вызов на месте, и окружающая функция по-прежнему может быть переведена в
VM-байт-код. Одна лишь смена синтаксиса eval не делает произвольный динамический код безопасным - изучите
предупреждения и протестируйте итоговый бандл.
Ошибка во время выполнения
Типичный симптом - ReferenceError при первом запуске динамически построенной функции:
Здесь TU - переименованный идентификатор, который обфускатор ввёл в области видимости IIFE бандла. Динамический вызов
eval или конструктора Function выполняет тело, ссылающееся на него, но это тело работает в области видимости, где TU
не определён.
Как обнаружить проблему до выполнения
Обфускатор выдаёт через API нефатальные предупреждения, чтобы вы могли отловить часть этих паттернов в CI до выпуска. Важны два типа предупреждений, и оба сообщаются только для вызовов внутри функции:
DynamicCodeRenameRisk- функция строит код из строки во время выполнения: прямойevalили вызовnew Function/Functionс нестатическим телом, либоfn.toString(), внедрённый в<script>или Worker. Именно это предупреждение предсказываетReferenceError.VMDynamicCodeSkipped- перевод в VM-байт-код был пропущен для функции и всех функций, определённых внутри неё, потому что она содержит прямойevalили динамический вызовnew Function/Function. Оно срабатывает и для безопасного статическогоeval('literal'), поэтому сигнализирует о потере VM-защиты, а не о падении во время выполнения. Содержит имя функции (если доступно) и конструкцию, вызвавшую пропуск.
npm-пакет javascript-obfuscator не предоставляет предупреждения в своём результате, поэтому проверка в CI читает их
из ответа API: сообщения result и chunk_end содержат массив warnings. Пример ниже использует
readObfuscationResponse() - читатель потока из справочника API - и проваливает сборку только при
DynamicCodeRenameRisk; добавьте в фильтр VMDynamicCodeSkipped, если потеря VM-защиты у функции тоже должна
блокировать релиз.
Если какой-то тип предупреждений в вашей сборке ожидаем и вы предпочитаете отключить его в источнике, а не фильтровать
в CI, опция warnings (v7.8.0+) управляет тем, какие предупреждения выдаются: '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" в группе Переопределения раздела VM.
Связанные страницы
- Поведение прямого eval - паттерн с разворачиванием IIFE, ограничивающий радиус поражения пропуска.
- Выбор функций - использование
vmTargetFunctionsModeдля включения или исключения на уровне отдельных функций.
