Escondendo nomes de funções da análise por LLM
O problema
Você ativou vmObfuscation: true, rodou a ofuscação em 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 em si continua ali, à vista de todos.
// Input
function validateLicense(token) {
const decoded = decodeBase64(token);
return verifySignature(decoded);
}
// Output - name is preserved, body is bytecode
function validateLicense(b) {
return vmq_1bac70(0x5, [], undefined, undefined, undefined, this);
}
Multiplique isso por uma base de código real e você terá uma lista de nomes de funções como validateLicense,
decryptPayload, processPayment, checkSubscription. Um LLM não precisa quebrar o bytecode para entender o que o
programa faz - só os nomes já bastam para que ele produza um resumo preciso e seguro do comportamento do módulo. O
bytecode é opaco; o índice dele não é.
Por que a ofuscação VM mantém esses nomes
Com vmTargetFunctionsMode: 'root' (o padrão), o obfuscator transforma o corpo de cada função de nível raiz em
bytecode da VM, mas deixa o nome deliberadamente intacto. 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; em um módulo, o namespace do módulo. O
obfuscator 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.
Portanto, a contrapartida assumida pelo padrão é: proteger a implementação e preservar a superfície pública. Isso mantém a integração funcionando, mas também significa que um LLM ganha de graça um índice de todos os pontos de entrada.
Por que isso importa na engenharia reversa assistida por LLM
Um atacante humano diante de algumas centenas de linhas de dispatch de bytecode normalmente desiste. Um LLM que receba o mesmo arquivo nem vai se dar ao trabalho de atacar o bytecode - ele vai ler os nomes, cruzar os poucos literais de string que consegue ver e produzir algo assim:
Esse resumo já 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 obfuscator fica livre para renomeá-las e absorver suas declarações no bytecode como qualquer outra instrução.
Uma IIFE (Immediately-Invoked Function Expression) é a maneira mais leve de fazer isso - ela acrescenta uma única função wrapper que roda uma vez e não expõe nada por nome.
Antes - nomes expostos
function validateLicense(token) {
const decoded = decodeBase64(token);
return verifySignature(decoded);
}
function checkExpiry(license) {
return Date.now() < license.expiresAt;
}
document.querySelector('#activate').addEventListener('click', () => {
const token = document.querySelector('#token').value;
if (validateLicense(token)) {
unlockUI();
}
});
Depois da ofuscação VM, tanto validateLicense quanto checkExpiry sobrevivem pelo nome na saída.
Depois - nomes escondidos atrás de uma IIFE
(function () {
function validateLicense(token) {
const decoded = decodeBase64(token);
return verifySignature(decoded);
}
function checkExpiry(license) {
return Date.now() < license.expiresAt;
}
document.querySelector('#activate').addEventListener('click', () => {
const token = document.querySelector('#token').value;
if (validateLicense(token)) {
unlockUI();
}
});
})();
Agora as duas declarações de função vivem 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. Depois da ofuscação VM, todo o corpo - incluindo cada declaração dentro dele - é convertido em bytecode e codificado; nada dentro da IIFE sobrevive como texto legível.
E se uma função realmente precisar ser global?
Às vezes uma função é mesmo um ponto de entrada público - um manipulador de eventos inline, um callback JSONP, um hook de um 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 tarefa é 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.var __entry1; (function () { function validateLicense(token) { /* … */ } __entry1 = validateLicense; })(); // Outside code calls __entry1(token) instead of validateLicense(token).Reescreva o ponto de chamada que você não controla. Se o global só existe porque um
onclick="validateLicense(...)"inline precisa dele, substitua o manipulador inline poraddEventListenerde 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 vive 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 por
padrão. Uma linha como const API_BASE = '/api/v2/license' diz tanto a um LLM quanto function validateLicense diz.
A opção vmWrapTopLevelInitializers (booleana, padrão false) envolve em uma IIFE os inicializadores de nível superior
elegíveis, de modo que o próprio valor passa a ser calculado pelo bytecode da VM em tempo de execução, em vez de ficar
no código-fonte como um literal.
Sem a opção
// Input
const MY_STRING = 'my-string';
// Output - string is visible
const MY_STRING = 'my-string';
Com vmWrapTopLevelInitializers: true
// Input
const MY_STRING = 'my-string';
// Output - initializer is now a VM call, the string lives inside bytecode
const MY_STRING = (() => {
return vmq_1bac70(0x5, [], undefined, undefined, undefined, this);
})();
O nome do binding (MY_STRING) continua sendo de nível raiz pelo mesmo motivo que os nomes de função são - 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.
Quando isso não é suficiente
- Nomes importados de outros módulos. Se você faz o bundle de vários arquivos e um módulo exporta
validateLicensepara outro importar, o bundler vai manter esse nome visível na saída empacotada, do mesmo modo 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 a exportação para dentro de uma IIFE e a exponha novamente por meio de um trampolim sem significado.
