Techniques04-Jul-26|13 min read

Decode obfuscator.io JavaScript: Option-by-Option Reference and Recognition Guide

Recognising javascript-obfuscator output and reading what each configuration option did to the code

You have a JavaScript file where every identifier looks like _0x4a2b, the strings are gone, and near the top there is a self-invoking function that pushes and shifts an array in a loop until some number matches. This is almost certainly output from javascript-obfuscator, the engine behind obfuscator.io. It is the most widely deployed JavaScript obfuscator in use, and its defaults are the shape you will meet most often in real files. The good news for a defender: obfuscator.io is a source-to-source transformer with published, documented behaviour. It is not encryption. Every string the program uses is still present in the file, moved into an array and reached through an index. Once you can recognise the structure and know what each option did, the output stops being noise and becomes something you can read.

Decode obfuscator.io output automatically. Paste your obfuscated JavaScript at klaroskope.com/submit - KlaroSkope recovers the string array, resolves the accessor function and rotation, and surfaces the recovered strings and IOCs.

  • How to Recognise obfuscator.io Output - the visual fingerprints
  • The Three-Part Structure - array function, accessor, rotation IIFE
  • What obfuscator.io Is - the tool, its licence, and its dual use
  • The Options That Shape the Output - an option-by-option reference
  • The Four Presets - what each built-in configuration turns on
  • Identifier Naming Styles - hexadecimal, mangled, and dictionary
  • String Array Encoding - none, base64, and rc4
  • Reading the Default Output by Hand - a worked walkthrough
  • When Manual Decoding Stops Scaling - and what to do instead

How to Recognise obfuscator.io Output

Three signals together are a strong fingerprint. First, the identifiers. By default every renamed variable and function becomes a hexadecimal name such as _0x4a2b, and when the tool is invoked through its command-line interface those names carry a scope prefix, so you see forms like a0_0x302206. Second, the strings are missing from the code body and instead live in a single array returned by a function. Third, near the top of the file a self-invoking function iterates the array with push and shift inside a while loop, testing a computed number against a target. That loop is the rotation step, and its presence is characteristic.

Any one of these on its own is weak evidence. Hexadecimal identifiers appear in other tools, and string tables are a common minifier artefact. The combination of all three, in compact single-line code, is what points specifically at javascript-obfuscator rather than a bundler or a different obfuscator.

The Three-Part Structure

Default output is built from three cooperating pieces. Here is a real example produced from a short benign script, lightly reflowed so each piece is on its own line:

javascript
// 1. Accessor alias: a short name pointing at the accessor function
const a0_0x302206 = a0_0x6679;

// 2. Rotation IIFE: pushes/shifts the array until a checksum matches a target
(function (_0x1be03b, _0x1574bf) {
  const _0x21c6e3 = _0x1be03b();
  while (!![]) {
    try {
      const _0x45543b = -parseInt(a0_0x6679(0x17c)) / 0x1 * (parseInt(a0_0x6679(0x18d)) / 0x2) + /* ... more terms ... */;
      if (_0x45543b === _0x1574bf) break;
      else _0x21c6e3['push'](_0x21c6e3['shift']());
    } catch (_0x20873e) {
      _0x21c6e3['push'](_0x21c6e3['shift']());
    }
  }
}(a0_0x59d8, 0x955b8));

// 3. Accessor function: maps an index to an array element, minus an offset
function a0_0x6679(_0x2a61b4, _0x45c897) {
  _0x2a61b4 = _0x2a61b4 - 0x17b;
  const _0x59d862 = a0_0x59d8();
  return _0x59d862[_0x2a61b4];
}

// 4. Array function: returns the string table (self-reassigns after first call)
function a0_0x59d8() {
  const _0x1a5db2 = ['then', 'log', 'version', 'https://config.example.com/update', 'Updater/1.0', /* ...counter strings... */];
  a0_0x59d8 = function () { return _0x1a5db2; };
  return a0_0x59d8();
}

The array function holds every string the program uses, mixed with decoy entries whose names encode the rotation counters. The accessor function is what the rest of the code calls: it takes an index, subtracts a fixed offset, and returns the array element at that position. The rotation IIFE runs once at load and rotates the array a specific number of positions so the offsets line up. Until the rotation is applied, every accessor call reads the array at the wrong position, so a partial decoder that ignores the rotation will confidently return the wrong strings. The strings are all there; getting the rotation count right is what makes them line up.

Analysis trap: the rotation IIFE is not decoration. If you extract the array and resolve the accessor without applying the rotation, you will get real strings from the array but at the wrong indices. The output looks clean and readable, which makes the error easy to miss. A URL can come out as an unrelated word from elsewhere in the table. Always resolve the rotation before trusting resolved strings.

What obfuscator.io Is

obfuscator.io is the web front end for javascript-obfuscator, an open-source Node.js library published on npm under the BSD-2-Clause licence. It is widely used and downloaded on the order of hundreds of thousands of times per week, which reflects its large legitimate user base. Game developers, anti-cheat systems, DRM implementations, and commercial JavaScript products use it to raise the cost of reverse engineering their intellectual property. That legitimate use is the majority of its traffic.

It is also used to wrap malicious JavaScript, because the same transformations that protect proprietary code also hide credential-theft logic and loader stages from casual inspection and from simple signature rules. For a defender the tool is dual-use, and the practical question is not whether a given sample is malicious because it was obfuscated with it, but how to read the output quickly so the underlying behaviour can be judged on its merits. For worked malware examples and the deeper decode chain, see the companion article on JavaScript string-array deobfuscation.

The Options That Shape the Output

The output you are looking at is determined by the options that were enabled. Knowing the defaults tells you what to expect before you start reading. The following are the main options and their default values in the current release. Options left at their defaults are what produce the standard three-part structure above.

Key Examples
stringArray
Defaulttrue
Effect on outputMoves string literals into a single array reached by index.
stringArrayRotate
Defaulttrue
Effect on outputRotates the array and adds the checksum IIFE that restores order at load.
stringArrayShuffle
Defaulttrue
Effect on outputRandomises the array element order (the rotation compensates).
stringArrayIndexShift
Defaulttrue
Effect on outputAdds a fixed offset to indices, subtracted inside the accessor.
stringArrayThreshold
Default0.75
Effect on outputFraction of eligible strings actually moved into the array.
stringArrayEncoding
Default[] (none)
Effect on outputOptional per-element encoding: none, base64, or rc4.
stringArrayWrappersCount
Default1
Effect on outputNumber of wrapper functions that indirect accessor calls.
stringArrayWrappersType
Defaultvariable
Effect on outputWrapper style: 'variable' or 'function'.
identifierNamesGenerator
Defaulthexadecimal
Effect on outputNaming style for renamed identifiers.
compact
Defaulttrue
Effect on outputEmits single-line output with no formatting whitespace.
controlFlowFlattening
Defaultfalse
Effect on outputWhen on, rewrites control flow into dispatched blocks.
deadCodeInjection
Defaultfalse
Effect on outputWhen on, inserts unreachable decoy code.
selfDefending
Defaultfalse
Effect on outputWhen on, breaks the code if it is reformatted or beautified.
debugProtection
Defaultfalse
Effect on outputWhen on, resists stepping through with developer tools.
splitStrings
Defaultfalse
Effect on outputWhen on, splits string literals into shorter concatenated chunks.

Two of these options changed names in the tool's history and older writeups still use the old spellings. What is now stringArrayRotate was once rotateStringArray, and stringArrayShuffle was once shuffleStringArray. If you are reading an older reference alongside current output, translate accordingly.

The Four Presets

The tool ships four named presets. The web UI and the command-line interface both expose them, and most output you meet was produced by one of them or by the default with a few options toggled. The differences that matter for reading the output are which heavy transformations are on: control-flow flattening and dead-code injection make the code much larger and harder to follow, and the string-array encoding determines whether the array elements are readable or themselves encoded.

Key Examples
Default
controlFlowFlatteningfalse
deadCodeInjectionfalse
stringArrayEncodingnone
selfDefendingfalse
Low obfuscation
controlFlowFlatteningfalse
deadCodeInjectionfalse
stringArrayEncodingnone
selfDefendingtrue
Medium obfuscation
controlFlowFlatteningtrue (0.75)
deadCodeInjectiontrue (0.4)
stringArrayEncodingbase64
selfDefendingtrue
High obfuscation
controlFlowFlatteningtrue (1.0)
deadCodeInjectiontrue (1.0)
stringArrayEncodingrc4
selfDefendingtrue

Reading the table from left to right is reading increasing analysis cost. The Default and Low presets leave the array elements in plain text, so once you resolve the accessor and rotation you can read the strings directly. The Medium and High presets encode the array elements and add control-flow flattening and dead code, which is a substantially larger job to unwind. In practice the Default preset is the most common shape in the wild, which is fortunate because it is also the most tractable.

Identifier Naming Styles

The identifierNamesGenerator option controls how renamed identifiers look. It accepts four values: hexadecimal (the default, producing _0x4a2b style names), mangled (short sequential names like a, b, c, then aa, ab), mangled-shuffled (mangled names in a randomised order), and dictionary (names drawn from a supplied word list). The naming style is cosmetic from a behaviour standpoint but it changes what the output looks like and can defeat a reader who is pattern-matching on the _0x prefix specifically.

javascript
// hexadecimal (default): scope-prefixed hex names
const a0g = a0b;
(function (a, b) { const f = a0b; /* ... */ }(a0a, 0xe8aa4));

// mangled: short sequential names, same structure underneath
// the array, accessor, and rotation IIFE are still present

String Array Encoding

By default the array elements are plain string literals. The stringArrayEncoding option can wrap each element in an additional encoding layer. With 'base64', each element is base64-encoded and the accessor decodes it on retrieval. With 'rc4', each element is RC4-encrypted with a key passed as a second accessor argument, and the accessor decrypts it on retrieval. Encoding is what separates the Default preset from the Medium and High presets.

When the array elements are base64 or RC4 encoded, resolving the accessor and rotation gives you encoded strings, not readable ones. There is a further decode step per element. This is why the Medium and High presets are a bigger job than the Default: two layers instead of one, and the RC4 case needs the per-call key recovered from the accessor arguments.

Reading the Default Output by Hand

For Default-preset output, the manual process is mechanical. First, locate the array function, the one that returns a literal array of strings and then reassigns itself. Copy the array out. Second, find the rotation IIFE and read its target number (the second argument passed to it). Third, apply the rotation: rotate the array by the number of positions that makes the checksum expression evaluate to the target. Fourth, read the accessor function to get the index offset it subtracts. Now any call such as accessor(0x189) resolves to the array element at index 0x189 minus that offset, in the rotated array.

Working through the earlier example, the accessor subtracts 0x17b, so a call like a0_0x302206(0x189) reaches array index 14 after the offset, and once the rotation is applied that position holds the URL string. Substituting every accessor call back into the code turns the unreadable body into recognisable JavaScript with its strings restored.

When Manual Decoding Stops Scaling

The manual method works for one small Default-preset file. It stops scaling for a few reasons. The rotation count is found by evaluating a checksum expression that can reference a dozen array elements in a single arithmetic term, which is tedious and error-prone to compute by hand. Wrapper functions add layers of indirection between the code and the accessor. Encoding adds a per-element decode. Control-flow flattening in the heavier presets rewrites the program's structure entirely. Each of these is individually surmountable and collectively a lot of careful bookkeeping.

This is the point where an automated resolver earns its place. The work is deterministic: the array is present, the rotation is computable, the accessor arithmetic is fixed, and the encodings are standard. A tool that recovers the array, computes the rotation, resolves the accessor, and applies any per-element decode produces the restored code in one pass, and does it the same way every time.

Decode It Automatically

KlaroSkope resolves obfuscator.io Default-preset output as a static analysis, without executing the sample. It recovers the string array, computes the rotation, resolves the accessor and any wrapper indirection, and surfaces the restored strings along with any URLs, hosts, and other indicators for triage. Heavier configurations with control-flow flattening and encoded arrays are a larger job and not every configuration resolves fully, but the common Default-preset shape, the one you meet most often, comes back readable.

Try it now -> klaroskope.com/submit - paste obfuscator.io output and see the recovered strings and IOCs in seconds.

Frequently Asked Questions

Q

How do I know if a file was obfuscated with obfuscator.io?

Look for three signals together: hexadecimal identifiers such as _0x4a2b (or scope-prefixed forms like a0_0x302206 from the command-line tool), string literals moved into a single array returned by a function, and a self-invoking function near the top that pushes and shifts that array in a while loop until a computed number matches a target. The combination of all three in compact single-line code is characteristic of javascript-obfuscator. Any one signal alone is weak evidence.
Q

Is obfuscator.io output reversible?

It is a source-to-source transformation, not encryption, so the information needed to run the program is all present in the file. Default-preset output restores to readable code by recovering the string array, applying the rotation, and resolving the accessor. Heavier presets add string encoding, control-flow flattening, and dead-code injection, which take more work to unwind but remain deterministic transformations rather than one-way functions.
Q

Is JavaScript obfuscated with obfuscator.io malicious?

Not by itself. The tool has a large legitimate user base protecting intellectual property in games, anti-cheat systems, DRM, and commercial products. It is also used to wrap malicious scripts. Obfuscation raises suspicion and warrants a closer look, but the verdict comes from what the recovered code does, not from the fact that it was obfuscated. Recover the strings and behaviour first, then judge.
Q

What is the rotation IIFE in obfuscator.io output?

It is the self-invoking function that runs once at load and rotates the string array into the correct order. It pushes and shifts the array in a loop, each iteration evaluating a checksum expression built from array elements, and stops when the result equals a target number passed as its second argument. The rotation is why a naive decoder that ignores it returns real strings at wrong positions. The rotation count must be resolved before the accessor indices line up.
Q

What does stringArrayEncoding do?

It adds a per-element encoding to the string array. The default is none, leaving elements as plain literals. With base64, each element is base64-encoded and the accessor decodes it on retrieval. With rc4, each element is RC4-encrypted with a key passed to the accessor, decrypted on retrieval. The Medium preset uses base64 and the High preset uses rc4, which is part of why those presets take more work to read than the Default.
Q

What is the difference between the obfuscator.io presets?

There are four: Default, Low, Medium, and High. Default and Low leave the array in plain text and do not flatten control flow, so once the accessor and rotation are resolved the strings are readable. Medium adds base64 array encoding, control-flow flattening, and dead-code injection. High adds RC4 array encoding and maximal flattening. Reading left to right is increasing analysis cost. Default-preset output is the most common in the wild and the most tractable.
Q

What MITRE ATT&CK technique covers obfuscator.io usage?

T1027 (Obfuscated Files or Information) covers the obfuscation itself, and T1140 (Deobfuscate/Decode Files or Information) covers the reversal. When the obfuscated payload is a script stage, T1059.007 (JavaScript) applies to the execution. Map the recovered behaviour, not the obfuscation, when assigning further techniques.

Found this useful? Sharing is caring!

Ready to decode?

See KlaroSkope transform obfuscated scripts into actionable intelligence.

Try It Free