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 é um literal de string) | Roda corretamente (a menos que vmForceCompileDynamicCode esteja ativado, veja Soluções alternativas). 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 literais de string) | 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
(VMDynamicCodeSkipped só para uma função que a VM compilaria de outra forma: não excluída e marcada, quando no modo
comment; para uma função aninhada, ele nomeia o ancestral de nível superior que foi ignorado), 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
um eval direto (estático ou dinâmico) ou uma chamada dinâmica a new Function / Function, a IIFE inteira deixa de ser compilada em bytecode da VM. Veja
O eval() direto desativa a ofuscação VM 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 é um literal de string, 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. Ambos só são relatados para chamadas dentro de uma função, exceto o caso de .toString(),
que é relatado onde quer que a chamada esteja quando a função serializada está aninhada em outra função ou não pode ser
resolvida (apenas em builds com VM):
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 porque ela contém umevaldireto ou uma chamada dinâmica anew Function/Function. No modo root (o padrão), todas as funções definidas dentro dela também são ignoradas; no modo comment, uma função aninhada que você marca separadamente ainda é virtualizada, a menos que ela mesma contenha a chamada. 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 a publicação.
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 um único literal de string / 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 específicas.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 o escopo: dentro de uma função compilada à força, oevaldireto não consegue ler nem escrever as variáveis locais ou os parâmetros da função, nem as variáveis de uma função externa que também seja virtualizada, mesmo quando o código é um literal de string. As leituras lançamReferenceError, e as escritas se perdem silenciosamente. Umeval('...')estático nessa situação não emite nenhum aviso, então uma verificação de CI não vai detectá-lo;DynamicCodeRenameRiskcontinua sendo emitido apenas para um corpo dinâmico. Use-a apenas quando o código avaliado não referenciar nada além de globais. No painel, é a chave Force Compile Dynamic Code no grupo Sobrescritas da seção VM.
Páginas relacionadas
- O eval() direto desativa a ofuscação VM - o padrão de desembrulhar a IIFE para limitar o raio de impacto da exclusão.
- Selecionando funções específicas - como usar
vmTargetFunctionsModepara incluir ou excluir funções individualmente.
