Escondendo nomes de funções da análise por LLM
O problema
Você ativou vmObfuscation: true, aplicou a ofuscação a um arquivo que contém uma função como validateLicense e percebeu que
a saída ofuscada ainda contém o texto literal validateLicense - o corpo sumiu, substituído por bytecode, mas
o nome continua ali, à vista de todos.
Multiplique isso por uma base de código real e você terá uma lista de nomes de funções como validateLicense, decryptPayload,
processPayment, checkSubscription. Uma LLM não precisa quebrar o bytecode para entender o que o programa faz -
os nomes bastam para ela produzir um resumo confiante e preciso do comportamento do módulo. O bytecode é
opaco; o índice, não.
Por que a ofuscação VM mantém esses nomes
Com vmTargetFunctionsMode: 'root' (o padrão), o ofuscador transforma o corpo de toda função de nível raiz
em bytecode da VM, mas deixa o nome intacto de propósito. Uma declaração de função de nível raiz é, semanticamente, um
binding no escopo ao redor - em um script, isso significa o objeto global, e em um módulo significa o escopo do módulo
(uma função de nível superior não exportada tem escopo de módulo, não global, mas continua sendo de nível raiz no arquivo). O ofuscador não pode renomeá-la com segurança porque não tem como saber quem mais a referencia: outro bundle,
um <script> inline, um atributo HTML onclick="validateLicense(...)", uma busca dinâmica window['validateLicense']
etc.
Então, a contrapartida que o padrão aceita é: proteger a implementação e preservar a superfície pública. Isso mantém a integração
intacta, mas também significa que uma LLM ganha de graça um índice de todos os pontos de entrada. Quando isso acontece, o resultado da ofuscação
emite um aviso VMGlobalFunctionNamesNotRenamed listando os nomes que continuaram legíveis.
Por que isso importa para a engenharia reversa assistida por LLM
Um atacante humano diante de algumas centenas de linhas de dispatch de bytecode geralmente desiste. Uma LLM que recebe o mesmo arquivo nem se dá ao trabalho de atacar o bytecode - ela lê os nomes, cruza os poucos literais de string que consegue ver e produz algo como:
"Este módulo controla o acesso a um recurso pago. validateLicense verifica um token assinado, checkExpiry rejeita licenças
expiradas e activateFeature libera a interface assim que a verificação passa. O auxiliar de descriptografia está em
decryptPayload."
Esse resumo basta para um atacante planejar um bypass direcionado sem nunca tocar na VM. Os nomes são o vazamento.
A solução: envolva seu código em uma IIFE
A forma mais simples e robusta de eliminar esse vazamento é empurrar suas funções sensíveis um nível mais fundo na árvore de escopos. Funções declaradas dentro de outra função não são de nível raiz, então o ofuscador fica livre para renomeá-las e absorver suas declarações no bytecode como qualquer outra instrução.
Uma IIFE (Immediately-Invoked Function Expression, expressão de função invocada imediatamente) é a forma mais leve de fazer isso - ela adiciona uma única função envolvente que roda uma vez e não expõe nada pelo nome.
Antes - nomes expostos
Após a ofuscação VM, tanto validateLicense quanto checkExpiry sobrevivem pelo nome na saída.
Depois - nomes escondidos atrás de uma IIFE
Agora as duas declarações de função ficam dentro do corpo da IIFE. A própria IIFE é a única construção de nível raiz, e uma IIFE anônima não tem nome para vazar. Após a ofuscação VM, os nomes das funções são renomeados e os corpos são convertidos em bytecode.
O que mudou. As funções deixaram de ser acessíveis como globais (que era o que vazava os nomes delas). Elas continuam
chamáveis - só que de dentro da mesma IIFE. Se algo realmente precisava chamar validateLicense de
fora, ele já não consegue alcançá-la; veja o padrão de trampolim
abaixo. Se nada precisava, você não perdeu nada.
Nomes exportados, identificadores reservados, strings e comportamentos observáveis podem continuar visíveis. Teste as integrações depois de envolver o código, especialmente globais, exports de módulos e código que inspeciona nomes de funções.
Uma contrapartida a conhecer: se o corpo da IIFE contém um eval direto ou uma chamada new Function(...) / Function(...)
com corpo dinâmico, a ofuscação VM ignora essa função inteira e tudo o que está aninhado nela (um aviso VMDynamicCodeSkipped), porque o
código-fonte montado em tempo de execução pode referenciar identificadores que o ofuscador renomeou. O código que você acabou de mover para dentro da IIFE
então volta à ofuscação comum e perde a proteção em bytecode. Use eval indireto - (0, eval)(...) - ou veja
eval direto com a ofuscação VM para as opções.
E se uma função realmente precisar ser global?
Às vezes, uma função realmente é um ponto de entrada público - um handler de evento inline, um callback JSONP, um hook de SDK de terceiros. Você tem duas opções:
Exponha um trampolim fino e mantenha a lógica dentro da IIFE. Declare um pequeno wrapper global cuja única função é chamar a implementação com escopo na IIFE. O nome do trampolim ainda vaza, mas não carrega nenhuma informação semântica - chame-o de
__entry1ou algo parecido - e toda a lógica relevante continua escondida.Reescreva o ponto de chamada. Se o global só existe porque um
onclick="validateLicense(...)"inline precisa dele, substitua o handler inline poraddEventListenerdentro da IIFE. O HTML deixa de nomear a função, a função deixa de precisar ser global e o vazamento desaparece por completo.
Inicializadores de variáveis de nível superior: vmWrapTopLevelInitializers
Declarações de função não são a única coisa que fica na raiz de um arquivo. Inicializadores de variáveis de nível superior - constantes
de string, objetos de configuração, tabelas de consulta - são igualmente legíveis na saída quando permanecem como JavaScript comum.
Uma linha como const API_BASE = '/api/v2/license' diz a uma LLM tanto quanto function validateLicense.
A opção vmWrapTopLevelInitializers (booleana, padrão false; as predefinições VM atuais a ativam) envolve em uma IIFE os
inicializadores de nível superior elegíveis, para que o próprio valor seja calculado pelo bytecode da VM em tempo de execução em vez de ficar no
código-fonte como um literal. No modo root, os inicializadores que permanecem em JavaScript comum são reportados com um aviso
VMTopLevelInitializerNotVirtualized.
Sem a opção
Com vmWrapTopLevelInitializers: true
O nome do binding (MY_STRING) continua no nível raiz pelo mesmo motivo que os nomes de funções - algo fora do arquivo
pode referenciá-lo -, mas o valor que ele guarda agora é produzido pela VM e não aparece mais como texto legível.
Só tem efeito quando vmTargetFunctionsMode é 'root' (o padrão) e vmAsyncExecutor está desativado. No modo comment,
a opção não tem efeito e nenhum aviso é reportado. No modo root com vmAsyncExecutor (que virtualiza apenas funções
assíncronas), ela também não tem efeito, e o build reporta VMTopLevelInitializerNotVirtualized para os inicializadores deixados em JavaScript puro.
Se um arquivo não tiver nenhuma função para a VM virtualizar, o resultado emite VMNoFunctionsToVirtualize; envolva o código que você
quer proteger em uma função ou use o modo comment para selecionar explicitamente as funções sensíveis.
Quando isso não basta
- Nomes importados de outros módulos. Se você empacota vários arquivos e um módulo exporta
validateLicensepara outro importar, o bundler vai manter esse nome visível na saída empacotada, da mesma forma que as funções de nível raiz ficam visíveis. Envolva o próprio bundle em uma IIFE (a maioria dos bundlers consegue fazer isso) ou mova o export para dentro de uma IIFE e exponha-o novamente por meio de um trampolim sem significado. - O runtime continua observável. A ofuscação aumenta o esforço necessário para entender e modificar o código; ela não garante que a engenharia reversa seja impossível. Mantenha segredos e decisões de segurança definitivas no servidor.
Não recorra primeiro a renameGlobals. Ela existe e vai renomear os identificadores de nível raiz - mas não tem como
saber quais desses identificadores são referenciados de fora do arquivo (outros bundles, HTML inline, buscas
dinâmicas). Ativá-la muitas vezes quebra a integração de formas sutis. Envolver em uma IIFE é mais seguro: não renomeia nada
global, apenas deixa de criar globais de que você não precisava.
