~/blog/obfuscation-techniques.md

A Tour of JavaScript Obfuscation (and De-obfuscation)

· #reverse-engineering #javascript

Open the network-signing function in a heavily protected site’s bundle and you’ll see something like this:

function _0x4a1f(_0x2b3c, _0x1e9a) {
  const _0x5f8d = _0x2b91();
  return (_0x4a1f = function (_0x4a1fx1, _0x4a1fx2) {
    _0x4a1fx1 = _0x4a1fx1 - 0x1a3;
    return _0x5f8d[_0x4a1fx1];
  })(_0x2b3c, _0x1e9a);
}

Nobody wrote that by hand. It’s the output of a commercial obfuscator run against ordinary code, and the good news is that these tools apply from a small, well-known bag of tricks. Learn the tricks once and every new bundle stops looking like noise.

String arrays: the most common trick, and the easiest to break

The pattern above is a string array. Every literal string in the source gets moved into one big array, and every reference to a string gets replaced by a call to a lookup function with an index (often offset by some constant to make static searching harder). The obfuscator does this because grepping a bundle for "https://api.example.com" is how reversers usually start, and a string array removes every literal from plain sight.

The break is mechanical: find the array, find the lookup function, and just… call it, using the exact JS runtime the code expects. You don’t need to understand the array’s construction, you need to execute it:

// paste the string-array setup + lookup function into a throwaway
// Node REPL or a headless browser console, then just call it
console.log(_0x4a1f(0x1a5)); // -> "Content-Type"
console.log(_0x4a1f(0x1a9)); // -> "application/json"

Do this for every index the code calls, and you can mechanically replace every _0x4a1f(0x1a5) in the source with its literal string. Suddenly the function body reads like normal JavaScript again.

Control-flow flattening: the maze with one correct path

Flattening rewrites a normal sequence of statements into a while loop around a switch, where a single state variable decides which case runs next:

let _s = 0x3;
while (true) {
  switch (_s) {
    case 0x3: total = a + b; _s = 0x7; continue;
    case 0x7: if (total > 0x64) { _s = 0x2; continue; } _s = 0x9; continue;
    case 0x2: total = 0x64; _s = 0x9; continue;
    case 0x9: return total;
  }
}

The control flow graph is deliberately scrambled — cases aren’t laid out in execution order, so reading top-to-bottom tells you nothing. The fix is to trace the state variable instead of the code: start at the initial value, follow which case sets _s to what next, and note down the order you actually visit. Once you have the real sequence — 3 -> 7 -> 2 -> 9 in that example — rewrite it as plain control flow and delete the switch entirely. It’s tedious, not hard, and mostly a job for a script once you understand the pattern, since the transform is completely mechanical.

Dead code and opaque predicates: noise with no function

The last common trick is padding — branches that can never execute, or conditions that always evaluate the same way but are dressed up to look data-dependent:

if ((0x2 * 0x3) % 0x5 === 0x1) {
  // this always runs; the condition is a constant in disguise
  doRealWork();
} else {
  // dead code, included only to waste your time
  fetch('/telemetry', { method: 'POST' });
}

There’s no algorithm to recover here — just patience. Evaluate the condition by hand (or let the debugger do it), confirm it’s constant, and delete the branch that never runs. The only trap is assuming a strange branch is always dead; some obfuscators mix in real environment checks (devtools detection, timing checks) among the fake ones, so verify each one rather than pattern-matching on “looks like junk.”

Putting it together

None of these three tricks is individually hard to reverse — they’re mechanical transforms, and mechanical transforms have mechanical inverses. What makes a real bundle painful is all three stacked at once, so a flattened switch statement is calling a string-array lookup inside a branch guarded by a fake predicate. The way through is the same as any layered problem: strip one layer fully before touching the next, and resist the urge to read the tangled version and guess. Execute the string array, trace the state variable, evaluate the predicates — in that order, the “obfuscated” bundle becomes a normal function with a slightly ugly variable-naming scheme.

cd .. cd ~ (back to terminal)