Reversing Obfuscated JavaScript
Client-side JavaScript ships the whole program to the reader — there is no stripped binary here, only code deliberately made unreadable. That changes the job: you are almost never recovering missing information, you are undoing a transformation. Identify the transformation first, reverse it with the tool built for it, and fall back to hand-written AST passes only for the custom layer no off-the-shelf tool knows.
When to Use
- A page or extension ships a large minified/bundled
.jsyou need to read - Code is full of
_0x1234identifiers, a big string array, and a rotation IIFE at the top — the obfuscator.io signature - A
.js.mapsourcemap is reachable, or webpack left//# sourceMappingURL - You must locate where a request signature, HMAC, token, or crypto key is computed inside a running bundle
- Reproducing a client-side algorithm (an anti-bot signal, a licence check, a paywall gate) from the shipped code
When NOT to Use
- React Native / Hermes mobile bundles (
index.android.bundle,main.jsbundle, Hermes magic bytes) — usereversing-react-native-apps. - A custom bytecode VM or virtualized-JS obfuscator where control flow is
interpreted, not just flattened — that is devirtualization; use
unpacking-protected-binariesfor the VM-lifting approach. - Malicious JS in a compromised site or npm package where the goal is IOCs
and behaviour, not readability — use
hunting-web-backdoorsfor planted web payloads andauditing-supply-chainfor a malicious package. - Testing the web app itself (the endpoints the JS calls, XSS, auth) — use
testing-web-applications; come back here only to recover a client-side algorithm it needs. - WASM modules loaded by the page — that is
analyzing-binariesterritory.
Identify the Layer Before Touching a Tool
Running the wrong deobfuscator produces plausible garbage. Read the first few hundred bytes and classify:
| What you see | Transformation | Reverse it with |
|---|---|---|
| Short names, no whitespace, readable strings | Minification only | An unminifier / prettier + rename |
webpackChunk, a module map {123: function(e,t,n){…}} | Webpack/Rollup bundling | Unbundle to per-module files |
//# sourceMappingURL=… or a reachable .map | Nothing — the source is right there | Sourcemap recovery |
_0x hex names + one big string array + a rotation IIFE | obfuscator.io string-array | webcrack / synchrony / restringer |
Nested ternaries, while(true){switch(_0x..)} dispatcher | Control-flow flattening | AST pass to relink the switch |
debugger in a setInterval, self-defending function | Anti-debug / self-defense | Strip the guard before other passes |
Most real bundles are layered: webpack on the outside, obfuscator.io on a few modules, a hand-rolled string cipher on the one function that matters. Peel outermost first.
Sourcemaps: Try This First, Always
A reachable sourcemap ends the job before it starts — it contains the original, pre-transform source. Check for it every time, because a large fraction of "obfuscated" production bundles ship or leak one:
- The
//# sourceMappingURL=comment at the bundle's end, and the sibling.js.mapeven when the comment was stripped (try<bundle>.map). - Webpack dev artifacts and source in
webpack://paths inside the map. - Recover files with a sourcemap consumer —
unwebpack-sourcemapor a shortsource-mapscript walkssourcesContentback to a directory tree.
Treat a leaked production sourcemap as a finding in its own right when you are assessing the app, not just a convenience.
Unbundling and Deobfuscation
webcrack is the first tool for a bundle, because it does three of the
layers at once: unminify, unpack webpack/browserify into per-module files, and
undo obfuscator.io string-array and control-flow obfuscation. Run it, then read
the module tree it produces rather than the single blob.
For a bundle that is only obfuscator.io, a dedicated deobfuscator is often
cleaner: synchrony or restringer both resolve the string array,
reverse the rotation, inline the decoder calls, and flatten the trivial control
flow. Compare their output — they fail on different edge cases.
When no tool fully handles the custom layer, write an AST pass with Babel:
parse to an AST, @babel/traverse to find the pattern (a specific decoder call,
a constant-folded expression, the flattening dispatcher), transform the nodes,
and regenerate. This is the durable skill — obfuscators change, but string-array
decoding, constant folding, and dead-branch elimination are the same AST
operations every time. Prototype the matcher in AST Explorer against the real
code before scripting it.
Two failure modes to expect:
- A deobfuscator that runs the code to "evaluate" the string decoder can
execute a payload. For untrusted or malicious bundles, do the string
resolution statically or in an isolated sandbox — never
evalan unknown bundle on your host to read it. - Self-defending / debug-protection re-obfuscates or spins a
debuggerloop when tampered with. Strip the guard function first (delete the node, or overridesetInterval) or every downstream pass fights it.
Finding a Routine in a Live Bundle
When static reading is slow — a signing function buried in a megabyte of modules — drive the running page with Chrome DevTools instead:
- Break on the behaviour, not the code. XHR/fetch breakpoints stop when the signed request fires; the call stack then walks straight back through the signing function. DOM and event-listener breakpoints do the same for UI-triggered logic.
- Pretty-print in the Sources panel (
{}) makes a minified function steppable without any offline work. - Hook property access from the console —
Object.definePropertyor aProxyon the object whose method computes the value logs every call and argument with a stack trace, so you see inputs and outputs without reading the math. - Override the file with a local, prettified,
console.log-instrumented copy (DevTools Local Overrides) to watch the real values flow at runtime.
Runtime observation and static reading are complementary: DevTools tells you which function matters; the AST work tells you what it computes so you can reproduce it offline.
Scope and Authorization
Reading and deobfuscating a bundle you lawfully retrieved is analysis. Two
edges need care. Reproducing and replaying a request-signing routine against
the origin — the usual reason to reverse an anti-bot signal — acts against that
service and needs the same authorization as any other testing; keep it to
systems you are permitted to test, and see testing-web-applications. And a
sourcemap or bundle pulled from a third-party site is that party's code:
recovering it for a security assessment you are engaged to do is fine, lifting a
proprietary algorithm for reuse is a different matter.
Rationalizations to Reject
- "It's obfuscated, so it's slow manual work." Classify first — a reachable
sourcemap or a clean
webcrackrun turns hours into minutes. Hand-reading is the last resort, not the first move. - "webcrack's output has a weird spot, so it failed." Layered obfuscation means one tool clears the outer layers and leaves the custom inner one. That residue is the interesting function; switch to an AST pass on it, don't restart.
- "I'll just eval the string-decoder to get the plaintext." On an untrusted bundle that runs attacker code on your host. Resolve strings statically or sandboxed.
- "The variable names are gone, so the logic is gone." Minification and obfuscation rename and reshape; they do not delete. Unlike a stripped native binary, the full program is present — you are undoing a transform, not reconstructing lost information.
- "DevTools is for debugging, not reversing." A fetch breakpoint plus a property hook locates a signing routine faster than reading the bundle, and gives you live inputs and outputs for free.
References
reversing-react-native-apps— Hermes/RN mobile JS bundlesunpacking-protected-binaries— VM-based / virtualized obfuscationhunting-web-backdoors— malicious planted web JStesting-web-applications— testing the endpoints a recovered algorithm callsreviewing-cryptography— once a client-side crypto/signing routine is recovered

