Learn·Deobfuscation·9 min read

JavaScript deobfuscation: a practical guide to Obfuscator.io, JScrambler, and beyond

Three concrete techniques for unwinding modern JavaScript obfuscation: string-array decoders, control-flow flattening, and dead-code injection. Examples in Babel.

May 7, 2026Aether

Modern JavaScript obfuscators all reach for the same handful of tricks: a string array with an indexed decoder function, control-flow flattening into a switch-state machine, dead-code injection, identifier mangling, and integrity self-checks. Knowing the patterns lets you unwind a 50KB minified blob into something readable in an afternoon.

Pattern 1 — String-array decoder

Obfuscator.io's signature trick. At the top of the file, you'll see something like:

const _0xa1b2 = ['hello', 'world', 'console', 'log'];
(function(arr, n) {
  while (--n) arr.push(arr.shift());
})(_0xa1b2, 0x42);

function _0x1234(idx, _) {
  idx = idx - 0x0;
  return _0xa1b2[idx];
}

console[_0x1234(0x2)](_0x1234(0x0));   // console.log('hello')

How to break it

  1. Run the rotation IIFE in isolation. Copy the array literal and the (function(arr, n) {...})(_0xa1b2, 0x42) call into a Node REPL. The array is now in its post-rotation state.
  2. Replace every _0x1234(N) call with the literal string at index N. A Babel plugin with a CallExpression visitor matching the decoder name does this in ~20 lines.
  3. Inline the decoder away — once all callsites are replaced, the decoder function and the string array are dead code and can be removed.

Pattern 2 — Control-flow flattening

Original:

if (a > b) { foo(); } else { bar(); }
return done();

After flattening:

let state = 0;
while (true) {
  switch (state) {
    case 0: state = (a > b) ? 1 : 2; break;
    case 1: foo(); state = 3; break;
    case 2: bar(); state = 3; break;
    case 3: return done();
  }
}

How to break it

Each case is a basic block. The state transitions form a graph — extract them, and you have your original control flow back. Tools like JStillery or restoring-obfuscated-control-flow can do this automatically when the dispatcher is recognizable. For custom dispatchers, write a Babel pass that builds the case → next-state map manually.

Pattern 3 — Dead-code injection

Obfuscators inject opaque predicates — branches whose condition is constant at compile time but unfortunately not at deobfuscate time without partial evaluation:

if (typeof window !== 'undefined' && Math.PI === 3.14) {
  // dead — Math.PI !== 3.14
  garbageFunction();
} else {
  realCode();
}

How to break it

Run the predicate through a constant-folding pass. Babel's @babel/plugin-transform-evaluate or a custom evaluator that handles a whitelist of safe operations (arithmetic, typeof on globals, length checks on string literals) collapses 90% of these.

Pattern 4 — Self-defending code

More aggressive obfuscators inject code that detects whether the script is being beautified (toString of a function tested against an expected hash) and jumps into an infinite loop or alters behavior if it detects tampering. The classic check:

(function check() {
  const expected = -0x12345;
  const got = check.toString().length;
  if (got !== Math.abs(expected) + 0x99) {
    while (true) {}   // detected — hang
  }
  setTimeout(check, 4000);
})();

How to break it

Either neuter the check by replacing the entire function body with a no-op early in the AST, or run the deobfuscation in a sandbox that doesn't rebuild the function source until you're done. Practical workflow: process the AST, write out a new file, then beautify the new file — never beautify the original.

Putting it together

A real-world deobfuscation pipeline looks like:

  1. Parse with Babel into AST.
  2. Run the rotation IIFE to recover the post-rotation string array.
  3. Replace decoder calls with literals (CallExpression visitor).
  4. Constant-fold predicates (evaluate visitor).
  5. Flatten control-flow (case-block reconstruction).
  6. Rename mangled identifiers using inferred semantics (assignment patterns, well-known API surface).
  7. Print and prettier-format.
Have a real obfuscated payload? Run it through Aether
Here's an Obfuscator.io-encoded payload. Walk me through deobfuscating it — bypass the string-array decoder, flatten control-flow, name the recovered functions.

[paste payload]
Open this in Aether
javascriptdeobfuscationobfuscator.iojscramblerreverse engineering

Take the next step

Got a follow-up question? Open Aether — direct technical answers, no refusals, free tier to start.

JavaScript deobfuscation: a practical guide to Obfuscator.io, JScrambler, and beyond | Aether · Aether