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:
// 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.
| Option | Default | Effect on output |
|---|---|---|
| stringArray | true | Moves string literals into a single array reached by index. |
| stringArrayRotate | true | Rotates the array and adds the checksum IIFE that restores order at load. |
| stringArrayShuffle | true | Randomises the array element order (the rotation compensates). |
| stringArrayIndexShift | true | Adds a fixed offset to indices, subtracted inside the accessor. |
| stringArrayThreshold | 0.75 | Fraction of eligible strings actually moved into the array. |
| stringArrayEncoding | [] (none) | Optional per-element encoding: none, base64, or rc4. |
| stringArrayWrappersCount | 1 | Number of wrapper functions that indirect accessor calls. |
| stringArrayWrappersType | variable | Wrapper style: 'variable' or 'function'. |
| identifierNamesGenerator | hexadecimal | Naming style for renamed identifiers. |
| compact | true | Emits single-line output with no formatting whitespace. |
| controlFlowFlattening | false | When on, rewrites control flow into dispatched blocks. |
| deadCodeInjection | false | When on, inserts unreachable decoy code. |
| selfDefending | false | When on, breaks the code if it is reformatted or beautified. |
| debugProtection | false | When on, resists stepping through with developer tools. |
| splitStrings | false | When 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.
| Preset | controlFlowFlattening | deadCodeInjection | stringArrayEncoding | selfDefending |
|---|---|---|---|---|
| Default | false | false | none | false |
| Low obfuscation | false | false | none | true |
| Medium obfuscation | true (0.75) | true (0.4) | base64 | true |
| High obfuscation | true (1.0) | true (1.0) | rc4 | true |
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.
// 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 presentString 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
How do I know if a file was obfuscated with obfuscator.io?
Is obfuscator.io output reversible?
Is JavaScript obfuscated with obfuscator.io malicious?
What is the rotation IIFE in obfuscator.io output?
What does stringArrayEncoding do?
What is the difference between the obfuscator.io presets?
What MITRE ATT&CK technique covers obfuscator.io usage?
Continue Learning
Ready to decode?
See KlaroSkope transform obfuscated scripts into actionable intelligence.
Try It Free