Documentação
/
Receitas
/

Escondendo nomes de funções da análise por LLM

Escondendo nomes de funções da análise por LLM

Pro

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.

JavaScript

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

JavaScript

Após a ofuscação VM, tanto validateLicense quanto checkExpiry sobrevivem pelo nome na saída.

Depois - nomes escondidos atrás de uma IIFE

JavaScript

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 __entry1 ou algo parecido - e toda a lógica relevante continua escondida.

    JavaScript

  • Reescreva o ponto de chamada. Se o global só existe porque um onclick="validateLicense(...)" inline precisa dele, substitua o handler inline por addEventListener dentro 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

JavaScript

Com vmWrapTopLevelInitializers: true

JavaScript

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 validateLicense para 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.