Creating a JavaScript Obfuscator
An introduction to Babel AST transforms through a simple JavaScript obfuscator proof of concept.
January 10, 2024 · Cole Banman
Goal: create a JavaScript obfuscator that makes scripts harder to reverse engineer and casually reuse.
After looking at several anti-bot vendors, a common pattern stood out: almost all of them hide their client-side logic behind some form of obfuscation. The mechanism changes, but the intent is the same: raise the cost of understanding and copying the code.
Examples include Cloudflare and Akamai, among many others.
What Is Obfuscation?
Obfuscation is the process of making code harder to read without changing its behavior. Straightforward techniques include renaming identifiers, restructuring control flow, removing formatting, or hiding strings behind decoding functions.
// readable
function hi() {
console.log("Hello World!");
}
// obfuscated
(function(c, d) {
var h = b;
var e = c();
while (!![]) {
try {
var f = parseInt(h(0x12d));
} catch (_) {}
}
})();Babel AST
Babel makes this approachable because it can parse JavaScript into an abstract syntax tree, traverse it, and emit transformed code. The same basic flow used for linting or codemods can be repurposed for obfuscation experiments.
Babel docs: https://babeljs.io/
Setting Up
Start with a small Node project and install the parser and generator pieces:
npm install @babel/core @babel/parser @babel/generator @babel/traverse @babel/types
Then wire up the basic imports:
const babel = require("@babel/core");
const parser = require("@babel/parser");
const generate = require("@babel/generator").default;
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");A Simple Transform
The first useful pass is identifier renaming. Generate a pseudo-random name, keep a map of the originals, and use scope-aware renaming so references stay consistent.
function generateRandomName() {
return "_" + Math.random().toString(16).slice(2, 10);
}
function obfuscateVariableNames(code) {
const ast = parser.parse(code, { sourceType: "module" });
const renamed = new Map();
traverse(ast, {
VariableDeclarator(path) {
const originalName = path.node.id.name;
if (!renamed.has(originalName)) {
const nextName = generateRandomName();
renamed.set(originalName, nextName);
path.scope.rename(originalName, nextName);
}
},
});
return generate(ast).code;
}Testing
Feed in a tiny function, run the transform, and inspect the output:
const code = `
function hi() {
const msg = "Hello World!";
console.log(msg);
}
`;
console.log(obfuscateVariableNames(code));function _f3d4b5a2() {
const _f3d4b5a3 = "Hello World!";
console.log(_f3d4b5a3);
}That is only one primitive, but it is enough to show how AST-driven transforms can be used to build increasingly opaque code.