Hiding Function Names from LLM Analysis
The problem
You enabled vmObfuscation: true, ran it over a file that contains a function like validateLicense, and noticed that the obfuscated output still contains the literal text validateLicense — the body is gone, replaced by bytecode, but the name itself is sitting there in plain sight.
// 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);
}Multiply that across a real codebase and you get a list of function names like validateLicense, decryptPayload, processPayment, checkSubscription. An LLM does not need to crack the bytecode to understand what the program does — the names alone are enough for it to produce a confident, accurate summary of the module's behavior. The bytecode is opaque; the table of contents is not.
Why VM obfuscation keeps these names
With vmTargetFunctionsMode: 'root' (the default), the obfuscator transforms the body of every root-level function into VM bytecode but deliberately leaves the namealone. A root-level function declaration is, semantically, a binding on the surrounding scope — for a script that means the global object, for a module that means the module namespace. The obfuscator can't safely rename it because it has no way to know who else references it: another bundle, an inline <script>, an HTML onclick="validateLicense(...)" attribute, a dynamic window['validateLicense'] lookup, etc.
So the trade-off the default makes is: protect the implementation, preserve the public surface. That keeps integration unbroken, but it also means an LLM gets a free index of every entry point.
Why this matters for LLM-assisted reverse engineering
A human attacker faced with a few hundred lines of bytecode dispatch will usually give up. An LLM given the same file will not bother attacking the bytecode at all — it will read the names, cross-reference the few string literals it can see, and emit something like:
That summary is enough for an attacker to plan a targeted bypass without ever touching the VM. The names are the leak.
The fix: wrap your code in an IIFE
The simplest, most robust way to remove this leak is to push your sensitive functions one level deeper into the scope tree. Functions declared inside another function are not root-level, so the obfuscator is free to rename them and absorb their declarations into bytecode like any other statement.
An IIFE (Immediately-Invoked Function Expression) is the lightest-weight way to do that — it adds a single wrapping function that runs once and exposes nothing by name.
Before — names exposed
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();
}
});After VM obfuscation, both validateLicense and checkExpiry survive by name in the output.
After — names hidden behind an 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();
}
});
})();Now both function declarations live inside the IIFE's body. The IIFE itself is the only root-level construct, and an anonymous IIFE has no name to leak. After VM obfuscation the entire body — including every declaration inside it — is converted to bytecode and encoded; nothing inside the IIFE survives as readable text.
What if a function genuinely needs to be a global?
Sometimes a function really is a public entry point — an inline event handler, a JSONP callback, a third-party SDK hook. You have two choices:
- Expose a thin trampoline, keep the logic inside the IIFE. Declare a small global wrapper whose only job is to call into the IIFE-scoped implementation. The trampoline name still leaks, but it carries no semantic information — name it
__entry1or similar — and all the meaningful logic stays hidden.var __entry1; (function () { function validateLicense(token) { /* … */ } __entry1 = validateLicense; })(); // Outside code calls __entry1(token) instead of validateLicense(token). - Rewrite the call site you don't control. If the global only exists because an inline
onclick="validateLicense(...)"needs it, replace the inline handler withaddEventListenerfrom inside the IIFE. The HTML stops naming the function, the function stops needing to be global, and the leak disappears entirely.
Top-level variable initializers: vmWrapTopLevelInitializers
Function declarations are not the only thing that lives at the root of a file. Top-level variable initializers — string constants, configuration objects, lookup tables — are equally readable in the output by default. A line like const API_BASE = '/api/v2/license' tells an LLM as much as function validateLicense does.
The vmWrapTopLevelInitializers option (boolean, default false) wraps eligible top-level initializers in an IIFE so the value itself is computed by VM bytecode at runtime instead of sitting in the source as a literal.
Without the option
// Input const MY_STRING = 'my-string'; // Output — string is visible const MY_STRING = 'my-string';
With 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);
})();The binding name (MY_STRING) is still root-level for the same reason function names are — something outside the file might reference it — but the value it holds is now produced by the VM and no longer appears as readable text.
When this isn't enough
- Imported names from other modules. If you bundle multiple files and one module exports
validateLicensefor another to import, the bundler will keep that name visible in the bundled output the same way root-level functions are visible. Wrap the bundle itself in an IIFE (most bundlers can do this), or move the export inside an IIFE and re-expose it via a meaningless trampoline.
