DEV Community

Cover image for JavaScript Deobfuscator Output: Reading It Layer by Layer
Devirt
Devirt

Posted on

JavaScript Deobfuscator Output: Reading It Layer by Layer

A JavaScript deobfuscator is a tool that rewrites obfuscated JavaScript into code a person can read: it decodes hidden strings, inlines helper functions, unwraps eval layers and straightens out tangled control flow. Most obfuscated scripts that land on an analyst's desk in November 2026, a card skimmer on a checkout page or a dropper in a phishing attachment, are built from the same four layers. Knowing those layers is what lets you judge whether the tool's output is the real logic or a confident guess.

Where Devirt comes in: most obfuscated JavaScript gives way to the tools below. When a script turns out to be its own interpreter, running custom bytecode, Devirt handles the JavaScript VM deobfuscation and reports which recovered functions were proven, for code you own or are authorized to analyze.

This post walks through the layers from the outside in, the way you meet them when you open a malicious script you've been asked to explain. It's written for defenders reading code found on their own or a client's systems.

What does a JavaScript deobfuscator actually do?

A JavaScript deobfuscator parses the script into a syntax tree (an AST), applies transformations that undo common obfuscation tricks, and prints the result as readable code. REstringer, for example, is built on an AST library, and synchrony runs AST visitors over the code. Working on the tree rather than on raw text is what lets a deobfuscator rename a variable everywhere it's used without breaking the script.

They differ in which tricks they target. Some are tuned for output from javascript-obfuscator (the engine behind obfuscator.io), others handle older encoders such as Dean Edwards' packer or JSFuck, and picking between JavaScript deobfuscators mostly comes down to which technique each one undoes. This post is about the next step: reading what they hand back.

One distinction first, because it comes up constantly: minification is not obfuscation. Minification shortens code so it loads faster, and a beautifier undoes most of it. Obfuscation changes code on purpose to make it hard to understand, and the same ideas behind code obfuscation in native programs (renaming, encoded strings, flattened control flow) show up in JavaScript as the layers below.

js obfuscation layers

Layer 1: the wrapper (eval, packers and encoders)

The outermost layer of obfuscated JavaScript is usually a wrapper that builds the real code as a string and hands it to eval or the Function constructor. Dean Edwards' packer, JSFuck, AAencode and JJencode all produce wrappers like this. The visible script is a decoder; the program you care about only exists after it runs.

Reading tip: the giveaway is a script whose last action is executing a string it just assembled. A deobfuscator unwraps this layer by evaluating the decoder in a controlled way and printing the string instead of running it. de4js and js-beautify both handle the classic packers this way.

Layer 2: the string array

Inside the wrapper, most modern obfuscated JavaScript moves every string literal into one large array, often shuffled, and replaces each use with a call to a small decoder function such as _0x3f2a(0x1b). Function names, URLs and DOM selectors all disappear from view, which is the point: the strings are what tell you what a skimmer is after.

Reading tip: find the array, then the function that indexes into it, then the short loop near the top that rotates the array before first use. Tools tuned for javascript-obfuscator output resolve this automatically. synchrony's README describes its string decoder as working with no configuration, and REstringer is built around reconstructing these strings.

When this layer resolves cleanly, the script usually becomes legible. The strings name the targets.

Layer 3: helper and proxy functions

The third layer replaces simple operations with calls to tiny helper functions. a + b becomes _0x1c(a, b), where _0x1c returns its two arguments added together. Comparisons, property lookups and calls get the same treatment, scattered across objects with meaningless keys.

Reading tip: a deobfuscator inlines these proxy functions, replacing each call with the operation it wraps, then folds constants such as 0x4 * 0x2 + -0x8 down to a single value. If you're reading by hand, list each helper and what it returns before reading the main code. The main code reads very differently once you know _0x1c means +.

Layer 4: flattened control flow and dead code

The innermost layer breaks the order of the code. Control-flow flattening takes a function's statements, puts them in the cases of a switch inside an infinite loop, and drives the order with a counter or a string like "3|1|4|0|2". Dead-code injection adds branches that can never run, to waste your time.

Reading tip: follow the dispatch string and read the cases in that order, not top to bottom. A good JavaScript deobfuscator rebuilds the straight-line order and removes branches whose conditions are always false. Be careful with that second part, which leads into the next two sections.

Is it safe to run a JavaScript deobfuscator on malicious code?

Running a JavaScript deobfuscator on malicious code is only safe when nothing in the sample can execute outside a sandbox. Several of the layers above come off by evaluating part of the script, and evaluation is what the script's author wants you to do.

REstringer's README says its unsafe modules run inside isolated-vm, and webcrack also depends on isolated-vm. That limits the damage, but it isn't a reason to paste a skimmer into an online playground. Analysts still run unknown scripts in an isolated lab, with no access to production networks, cookies or credentials, and keep the sample out of shared folders afterwards.

How do you know the deobfuscated output is right?

A JavaScript deobfuscator's output is trustworthy when it behaves the same as the original, and you can only claim that for what you checked. Three checks catch most errors:

  1. Strings: the decoded strings match what the original produces at runtime in your sandbox. A wrong rotation count gives plausible but wrong URLs.
  2. Behavior: the cleaned script makes the same network requests and DOM changes as the original when run on the same page in the lab.
  3. Removed branches: anything the tool deleted as "dead" can't run. Some branches only fire on a date, a domain or a fetched value, and a tool can misjudge them.

Whatever you couldn't confirm, say so in the report. The labels Devirt uses for recovered code fit deobfuscated scripts too: proven, runtime-dependent (depends on data your sandbox didn't have), or unresolved. "We didn't see it exfiltrate anything" and "it can't exfiltrate anything" are different findings.

When the code is an interpreter, not obfuscation

Sometimes every layer comes off and the script still makes no sense. What's left is a loop that reads numbers out of a large array and calls one small function per number. That isn't obfuscated JavaScript any more. It's virtualized JavaScript: the protected functions were compiled into bytecode for an interpreter shipped inside the script.

A JavaScript deobfuscator can tidy that interpreter, but it can't turn the bytecode back into the original functions, because the logic lives in the data, not in the code it rewrites. Some obfuscators also generate a new interpreter for every build, so the instruction set you work out for one sample doesn't carry over to the next. The difference between rewriting text and translating a virtual machine is the same one that separates devirtualization from deobfuscation in native binaries.

At that point the job is JavaScript VM deobfuscation: working out what each handler does, mapping the opcodes, and lifting the bytecode back into readable logic, with a record of which functions were actually recovered.

How to use a JavaScript deobfuscator, step by step

Read obfuscated JavaScript from the outside in: unwrap, resolve strings, inline helpers, then reorder the flow. Let a deobfuscator do the mechanical work, then check its output against the original before you trust it. Stop when you hit an interpreter, because from there you're translating a virtual machine, not cleaning up code. If that happens on a script you're authorized to analyze, Devirt's free assessment tells you what can be recovered before anything is charged.

Written by Devirt, an automated devirtualization service for authorized analysis of protected software.

Top comments (0)