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
.js you need to read
- Code is full of
_0x1234 identifiers, a big string array, and a rotation IIFE
at the top — the obfuscator.io signature
- A
.js.map sourcemap 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) — use reversing-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-binaries for 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-backdoors for planted web
payloads and auditing-supply-chain for 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-binaries territory.
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.map even 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-sourcemap or a short
source-map script walks sourcesContent back 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
eval an unknown
bundle on your host to read it.
- Self-defending / debug-protection re-obfuscates or spins a
debugger
loop when tampered with. Strip the guard function first (delete the node,
or override setInterval) 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.defineProperty or a
Proxy on 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
webcrack run 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 bundles
unpacking-protected-binaries — VM-based / virtualized obfuscation
hunting-web-backdoors — malicious planted web JS
testing-web-applications — testing the endpoints a recovered algorithm calls
reviewing-cryptography — once a client-side crypto/signing routine is recovered
1---2name: reversing-obfuscated-javascript3description: Reverse engineer minified, bundled, and obfuscated browser/Node JavaScript — unpacking webpack chunks, recovering source from sourcemaps, undoing obfuscator.io string-array and control-flow obfuscation with webcrack/synchrony/restringer, and locating a signing or crypto routine in a live bundle via Chrome DevTools. Use when a page ships a huge minified bundle, when code is full of _0x hex identifiers and a rotated string array, when a .js.map is reachable, or when you must find where a request signature or token is computed.4---56# Reversing Obfuscated JavaScript78Client-side JavaScript ships the whole program to the reader — there is no9stripped binary here, only code deliberately made unreadable. That changes the10job: you are almost never recovering *missing* information, you are undoing a11transformation. Identify the transformation first, reverse it with the tool12built for it, and fall back to hand-written AST passes only for the custom13layer no off-the-shelf tool knows.1415## When to Use1617- A page or extension ships a large minified/bundled `.js` you need to read18- Code is full of `_0x1234` identifiers, a big string array, and a rotation IIFE19 at the top — the obfuscator.io signature20- A `.js.map` sourcemap is reachable, or webpack left `//# sourceMappingURL`21- You must locate where a request signature, HMAC, token, or crypto key is22 computed inside a running bundle23- Reproducing a client-side algorithm (an anti-bot signal, a licence check, a24 paywall gate) from the shipped code2526## When NOT to Use2728- **React Native / Hermes mobile bundles** (`index.android.bundle`,29 `main.jsbundle`, Hermes magic bytes) — use `reversing-react-native-apps`.30- **A custom bytecode VM or virtualized-JS obfuscator** where control flow is31 interpreted, not just flattened — that is devirtualization; use32 `unpacking-protected-binaries` for the VM-lifting approach.33- **Malicious JS in a compromised site or npm package** where the goal is IOCs34 and behaviour, not readability — use `hunting-web-backdoors` for planted web35 payloads and `auditing-supply-chain` for a malicious package.36- **Testing the web app itself** (the endpoints the JS calls, XSS, auth) — use37 `testing-web-applications`; come back here only to recover a client-side38 algorithm it needs.39- **WASM modules** loaded by the page — that is `analyzing-binaries` territory.4041## Identify the Layer Before Touching a Tool4243Running the wrong deobfuscator produces plausible garbage. Read the first few44hundred bytes and classify:4546| What you see | Transformation | Reverse it with |47| --- | --- | --- |48| Short names, no whitespace, readable strings | Minification only | An unminifier / prettier + rename |49| `webpackChunk`, a module map `{123: function(e,t,n){…}}` | Webpack/Rollup bundling | Unbundle to per-module files |50| `//# sourceMappingURL=…` or a reachable `.map` | Nothing — the source is *right there* | Sourcemap recovery |51| `_0x` hex names + one big string array + a rotation IIFE | obfuscator.io string-array | webcrack / synchrony / restringer |52| Nested ternaries, `while(true){switch(_0x..)}` dispatcher | Control-flow flattening | AST pass to relink the switch |53| `debugger` in a `setInterval`, self-defending function | Anti-debug / self-defense | Strip the guard before other passes |5455Most real bundles are **layered**: webpack on the outside, obfuscator.io on a56few modules, a hand-rolled string cipher on the one function that matters.57Peel outermost first.5859## Sourcemaps: Try This First, Always6061A reachable sourcemap ends the job before it starts — it contains the original,62pre-transform source. Check for it every time, because a large fraction of63"obfuscated" production bundles ship or leak one:6465- The `//# sourceMappingURL=` comment at the bundle's end, and the sibling66 `.js.map` even when the comment was stripped (try `<bundle>.map`).67- Webpack dev artifacts and source in `webpack://` paths inside the map.68- Recover files with a sourcemap consumer — `unwebpack-sourcemap` or a short69 `source-map` script walks `sourcesContent` back to a directory tree.7071Treat a leaked production sourcemap as a finding in its own right when you are72assessing the app, not just a convenience.7374## Unbundling and Deobfuscation7576**`webcrack` is the first tool for a bundle**, because it does three of the77layers at once: unminify, unpack webpack/browserify into per-module files, and78undo obfuscator.io string-array and control-flow obfuscation. Run it, then read79the module tree it produces rather than the single blob.8081For a bundle that is *only* obfuscator.io, a dedicated deobfuscator is often82cleaner: **`synchrony`** or **`restringer`** both resolve the string array,83reverse the rotation, inline the decoder calls, and flatten the trivial control84flow. Compare their output — they fail on different edge cases.8586When no tool fully handles the custom layer, **write an AST pass** with Babel:87parse to an AST, `@babel/traverse` to find the pattern (a specific decoder call,88a constant-folded expression, the flattening dispatcher), transform the nodes,89and regenerate. This is the durable skill — obfuscators change, but string-array90decoding, constant folding, and dead-branch elimination are the same AST91operations every time. Prototype the matcher in AST Explorer against the real92code before scripting it.9394Two failure modes to expect:9596- **A deobfuscator that runs the code to "evaluate" the string decoder** can97 execute a payload. For untrusted or malicious bundles, do the string98 resolution statically or in an isolated sandbox — never `eval` an unknown99 bundle on your host to read it.100- **Self-defending / debug-protection** re-obfuscates or spins a `debugger`101 loop when tampered with. Strip the guard function first (delete the node,102 or override `setInterval`) or every downstream pass fights it.103104## Finding a Routine in a Live Bundle105106When static reading is slow — a signing function buried in a megabyte of107modules — drive the running page with Chrome DevTools instead:108109- **Break on the behaviour, not the code.** XHR/fetch breakpoints stop when the110 signed request fires; the call stack then walks straight back through the111 signing function. DOM and event-listener breakpoints do the same for112 UI-triggered logic.113- **Pretty-print** in the Sources panel (`{}`) makes a minified function114 steppable without any offline work.115- **Hook property access** from the console — `Object.defineProperty` or a116 `Proxy` on the object whose method computes the value logs every call and117 argument with a stack trace, so you see inputs and outputs without reading118 the math.119- **Override the file** with a local, prettified, `console.log`-instrumented120 copy (DevTools Local Overrides) to watch the real values flow at runtime.121122Runtime observation and static reading are complementary: DevTools tells you123*which* function matters; the AST work tells you *what it computes* so you can124reproduce it offline.125126## Scope and Authorization127128Reading and deobfuscating a bundle you lawfully retrieved is analysis. Two129edges need care. Reproducing and *replaying* a request-signing routine against130the origin — the usual reason to reverse an anti-bot signal — acts against that131service and needs the same authorization as any other testing; keep it to132systems you are permitted to test, and see `testing-web-applications`. And a133sourcemap or bundle pulled from a third-party site is that party's code:134recovering it for a security assessment you are engaged to do is fine, lifting a135proprietary algorithm for reuse is a different matter.136137## Rationalizations to Reject138139- **"It's obfuscated, so it's slow manual work."** Classify first — a reachable140 sourcemap or a clean `webcrack` run turns hours into minutes. Hand-reading is141 the last resort, not the first move.142- **"webcrack's output has a weird spot, so it failed."** Layered obfuscation143 means one tool clears the outer layers and leaves the custom inner one. That144 residue *is* the interesting function; switch to an AST pass on it, don't145 restart.146- **"I'll just eval the string-decoder to get the plaintext."** On an untrusted147 bundle that runs attacker code on your host. Resolve strings statically or148 sandboxed.149- **"The variable names are gone, so the logic is gone."** Minification and150 obfuscation rename and reshape; they do not delete. Unlike a stripped native151 binary, the full program is present — you are undoing a transform, not152 reconstructing lost information.153- **"DevTools is for debugging, not reversing."** A fetch breakpoint plus a154 property hook locates a signing routine faster than reading the bundle, and155 gives you live inputs and outputs for free.156157## References158159- `reversing-react-native-apps` — Hermes/RN mobile JS bundles160- `unpacking-protected-binaries` — VM-based / virtualized obfuscation161- `hunting-web-backdoors` — malicious planted web JS162- `testing-web-applications` — testing the endpoints a recovered algorithm calls163- `reviewing-cryptography` — once a client-side crypto/signing routine is recovered