Ofuscação VM com eval e new Function
Como a ofuscação VM lida com a construção dinâmica de código (eval direto e o construtor Function), o que vira bytecode e o que é ignorado, os avisos que o ofuscador emite e como diagnosticar ReferenceError em tempo de execução.
Por que isso importa
A ofuscação VM compila os corpos das funções em bytecode, despachado por um interpretador embutido no runtime. Os identificadores
do escopo ao redor também são renomeados. As duas transformações interagem mal com código montado a partir de uma string em tempo
de execução: eval(s), new Function(...s) e Function(...s). Se o código montado em tempo de execução referencia um identificador que o
ofuscador renomeou, você recebe Uncaught ReferenceError: <renamed-name> is not defined na primeira vez que a função
gerada roda.
O ofuscador trata cada padrão de forma diferente. A tabela abaixo é a versão resumida; o resto desta página explica cada linha.
O que o ofuscador faz, em resumo
| Padrão no seu código-fonte | O que acontece |
|---|---|
eval('literal string') (o corpo é uma string literal) | Roda corretamente. A função que contém essa chamada - e todas as funções definidas dentro dela - perde a compilação em bytecode da VM (o eval direto lê as variáveis locais ao redor, que a VM não preserva depois que uma função é compilada em bytecode). |
eval(dynamicExpression) | Pode quebrar em tempo de execução com ReferenceError. A função que contém essa chamada - e todas as funções definidas dentro dela - também perde a compilação em bytecode da VM. |
(0, eval)(s) / window.eval(s) (indireto) | A função que contém essa chamada é compilada em bytecode da VM normalmente. O eval indireto roda no escopo global e não enxerga as variáveis locais ao redor, então variáveis locais renomeadas não podem quebrá-lo. Ele ainda pode falhar se o código avaliado referenciar um global que foi renomeado ou removido. |
new Function('a', 'b', 'return a + b') (todos os argumentos são strings literais) | Roda corretamente. A função que contém essa chamada é compilada em bytecode da VM normalmente. |
new Function(dynamicBody) / Function(dynamicBody) | Pode quebrar em tempo de execução com ReferenceError. A função que contém essa chamada - e todas as funções definidas dentro dela - também perde a compilação em bytecode da VM. |
O compilador também detecta eval?.(code) de forma conservadora e o trata como eval direto. O JavaScript define essa
forma de chamada opcional como eval indireto; a detecção do compilador não muda esse comportamento da linguagem.
Apenas alguns desses padrões produzem avisos não fatais, e só quando a chamada está dentro de uma função: uma chamada dinâmica
a eval, new Function ou Function emite tanto DynamicCodeRenameRisk quanto VMDynamicCodeSkipped, um
eval('...') estático emite apenas VMDynamicCodeSkipped, e o eval indireto e um new Function totalmente estático não emitem nada. Uma
chamada dinâmica no nível superior de um arquivo, fora de qualquer função, nunca é relatada. Veja
Detectando o problema antes da execução abaixo para os tipos de aviso e um trecho para CI.
A exclusão se propaga pelas IIFEs. Se uma IIFE de nível superior envolve todo o seu bundle e qualquer função dentro dela usa
eval ou new Function dinâmicos, a IIFE inteira deixa de ser compilada em bytecode da VM. Veja
Comportamento do eval direto para o padrão de desembrulhar a IIFE, que limita o raio
de impacto.
Por que estático e dinâmico são tratados de forma diferente
eval(s) pode ler e escrever variáveis locais da função em que é chamado. Quando s é uma string literal, o
ofuscador consegue analisar o corpo no momento da ofuscação e renomear os identificadores de forma consistente com o código ao redor. Quando
s é uma expressão dinâmica, a análise só acontece em tempo de execução - quando os identificadores já foram
renomeados -, então o código montado em tempo de execução referencia nomes antigos que não existem mais.
new Function(s) e o eval indireto funcionam de outra forma: o corpo sempre roda no escopo global, com acesso apenas às variáveis
globais e nunca às locais ao redor da chamada. Eles não podem depender de variáveis locais renomeadas, mas ainda podem falhar se
referenciarem globais que foram renomeados ou removidos - ou se você montar o corpo concatenando nele um identificador renomeado
(por exemplo, via func.toString() de uma função cujo interior o ofuscador reescreveu).
Um corpo estático que usa apenas os próprios parâmetros, como new Function('a', 'b', 'return a + b'), não tem essa
dependência: o corpo é uma string comum que o renomeador nunca inspeciona. O ofuscador deixa a chamada como está, e a
função ao redor continua elegível para a compilação em bytecode da VM. Mudar apenas a sintaxe do eval não torna seguro um código
dinâmico arbitrário - revise os avisos e teste o bundle final.
O erro que você vê em tempo de execução
O sintoma comum é um ReferenceError na primeira vez que a função montada dinamicamente roda:
Aqui, TU é um identificador renomeado que o ofuscador introduziu dentro do escopo da IIFE do bundle. A chamada dinâmica de eval /
do construtor Function avalia um corpo que o referencia, mas esse corpo roda em um escopo em que TU não está definido.
Detectando o problema antes da execução
O ofuscador emite avisos não fatais pela API para que você possa detectar alguns desses padrões no CI antes de publicar. Dois tipos de aviso são relevantes, e ambos só são relatados para chamadas dentro de uma função:
DynamicCodeRenameRisk- uma função monta código a partir de uma string em tempo de execução: umevaldireto ou uma chamada anew Function/Functioncujo corpo não é estático, oufn.toString()injetado em um<script>ou Worker. Este é o aviso que prevê umReferenceError.VMDynamicCodeSkipped- a compilação em bytecode da VM foi ignorada para uma função, e para todas as funções definidas dentro dela, porque ela contém umevaldireto ou uma chamada dinâmica anew Function/Function. Ele também dispara para umeval('literal')estático e seguro, então sinaliza a perda de proteção VM, e não uma falha em tempo de execução. Inclui o nome da função (se disponível) e a construção que provocou a exclusão.
O pacote npm javascript-obfuscator não expõe os avisos no resultado, então uma verificação de CI os lê da resposta
da API: as mensagens result e chunk_end trazem um array warnings. O exemplo abaixo usa
readObfuscationResponse(), o leitor de stream da Referência da API, e só faz o build falhar com
DynamicCodeRenameRisk; adicione VMDynamicCodeSkipped ao filtro se perder a proteção VM em uma função também deva
bloquear o release.
Se um tipo de aviso é esperado no seu build e você prefere silenciá-lo na origem em vez de filtrá-lo no CI, a
opção warnings (v7.8.0+) controla quais avisos são emitidos: 'none' suprime tudo, e um mapa por tipo
como { VMDynamicCodeSkipped: false } silencia apenas um tipo e mantém os demais.
Soluções alternativas
Mude para o eval indireto (
(0, eval)(s))Útil apenas no caso do
eval. O eval indireto roda no escopo global, então não enxerga as variáveis locais ao redor - e, por isso, também não pode referenciar variáveis locais renomeadas. A função que contém a chamada continua compilada em bytecode da VM. O código avaliado ainda não pode depender de globais que o ofuscador renomeia.Torne o corpo totalmente estático
No caso de
new Function, se você conseguir expressar o corpo como uma única string literal / template literal sem interpolações, a chamada não traz risco de renomeação e a função ao redor continua sendo compilada em bytecode.new Function('a', 'b', 'return a + b')funciona;new Function('return ' + expr)não.Mova a chamada para uma função de nível superior própria e desembrulhe a IIFE
Cada função de nível superior é verificada de forma independente. Levar a chamada de código dinâmico para uma função de nível superior própria significa que apenas essa função perde a compilação em bytecode da VM, em vez de a exclusão se propagar por uma IIFE de nível superior que envolve todo o seu bundle.
Mude para
vmTargetFunctionsMode: 'comment'Modo opt-in: apenas as funções marcadas com
/* javascript-obfuscator:vm */são compiladas em bytecode. Não marque a função que contém a chamada de código dinâmico e marque as demais. Preserve esses comentários até a etapa de ofuscação. Veja Selecionando funções.Sobrescreva a exclusão com
vmForceCompileDynamicCode: true(v6.14.0+)Válvula de escape de último recurso. Quando ativada, o ofuscador compila a função ao redor em bytecode mesmo assim e suprime o aviso
VMDynamicCodeSkipped. Ela não corrige dependências de escopo nem de identificadores renomeados: use-a apenas quando puder garantir que o corpo montado em tempo de execução nunca referencia um identificador que o ofuscador renomeia - caso contrário, você troca uma ofuscação limpa por umReferenceErrorem tempo de execução.DynamicCodeRenameRiskcontinua sendo emitido, para que o CI possa continuar barrando com base nele. No painel, é a chave "Force Compile Dynamic Code" no grupo Sobrescritas da seção VM.
Páginas relacionadas
- Comportamento do eval direto - o padrão de desembrulhar a IIFE para limitar o raio de impacto da exclusão.
- Selecionando funções - como usar
vmTargetFunctionsModepara incluir ou excluir funções individualmente.
