Cole Banman

Blog

A small Babel-based obfuscator that renames identifiers and demonstrates the AST tooling behind commercial anti-bot scripts.

Creating a JavaScript Obfuscator

An introduction to Babel AST transforms through a simple JavaScript obfuscator proof of concept.

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.