JavaScript string arraysUpdated 15-Sep-26|2 worked examples

Decode javascript-obfuscator String Arrays (obfuscator.io)

Paste a script full of _0x accessor lookups and get the program back: strings restored, names readable, and a note on whether the decode is complete.

javascript-obfuscator, the engine behind obfuscator.io, replaces the strings in a script with lookups into a shuffled array reached through an accessor function, then optionally encodes that array in base64 or RC4 and flattens the control flow. KlaroSkope reverses those layers statically, returning the program with its strings restored and a signal for whether the result is a complete decode or a partial one, and it does this by reading the script rather than executing it.

A split panel: an accessor call into a string array on the left, a chevron, and the recovered source line with its URL restored on the right in cyan.

Fingerprint the build

Runs in your browser. The text you paste is not uploaded.

Reports which javascript-obfuscator options the markers indicate: string-array rotation, base64 or RC4 encoding, control flow flattening, dead code, self defending. It identifies the build rather than decoding it, which is the question a static tool can answer honestly before you commit to a decode.

Worked examples

Synthetic samples. Each input decodes to the output shown, and the outputs are checked against the live engine before publication.

Example 1: String array with base64 encoding

Input
var a0_0x1c6d3a=a0_0x44f3;(function(_0x2f4214,_0x5b13b0){var _0x4f69f6=a0_0x44f3,_0x407915=_0x2f4214();while(!![]){try{var _0x53628d=-parseInt(_0x4f69f6(0x1f3))/0x1+-parseInt(_0x4f69f6(0x1ea))/0x2*(-parseInt(_0x4f69f6(0x1f1))/0x3)+parseInt(_0x4f69f6(0x1f0))/0x4*(parseInt(_0x4f69f6(0x1ec))/0x5)+-parseInt(_0x4f69f6(0x1f6))/0x6*(-parseInt(_0x4f69f6(0x1e9))/0x7)+parseInt(_0x4f69f6(0x1ef))/0x8+parseInt(_0x4f69f6(0x1e5))/0x9*(-parseInt(_0x4f69f6(0x1f7))/0xa)+-parseInt(_0x4f69f6(0x1ee))/0xb*(-parseInt(_0x4f69f6(0x1e8))/0xc);if(_0x53628d===_0x5b13b0)break;else _0x407915['push'](_0x407915['shift']());}catch(_0x330292){_0x407915['push'](_0x407915['shift']());}}}(a0_0xd573,0x997c4));var CONFIG_URL=a0_0
... (excerpt; the full sample is 2826 characters)
Decoded output
var CONFIG_URL = "https://config.example.com/settings.json";
function loadSettings(_0xce57fe) {
  fetch(CONFIG_URL)["then"](function(_0x114e9f) {
    return _0x114e9f["json"]();
  })["then"](function(_0x286d22) {
    console["log"]("settings loaded from " + CONFIG_URL), _0xce57fe(_0x286d22);
  });
}

loadSettings(function(_0x282114) {
  console["log"]("theme: " + _0x282114["theme"]);
});

The default shape from the obfuscator.io web UI. The config URL survives in the string array rather than in the code, which is why the source reads as accessor calls and integers.

Example 2: Same program, RC4-encoded string array

Input
var a0_0x387047=a0_0x3bf8;(function(_0x187536,_0x1a4d6c){var _0x5c669e=a0_0x3bf8,_0x18d8c2=_0x187536();while(!![]){try{var _0x241527=parseInt(_0x5c669e(0xb2,'Q^tC'))/0x1+parseInt(_0x5c669e(0xb6,'n(dy'))/0x2*(-parseInt(_0x5c669e(0xc2,')W@['))/0x3)+-parseInt(_0x5c669e(0xb7,')W@['))/0x4*(-parseInt(_0x5c669e(0xb3,'(*yM'))/0x5)+parseInt(_0x5c669e(0xc9,'a[Ey'))/0x6+-parseInt(_0x5c669e(0xca,'#c3t'))/0x7*(-parseInt(_0x5c669e(0xc1,'MVlZ'))/0x8)+-parseInt(_0x5c669e(0xba,'VdY6'))/0x9+parseInt(_0x5c669e(0xbf,'7m0S'))/0xa*(-parseInt(_0x5c669e(0xc8,'z&)N'))/0xb);if(_0x241527===_0x1a4d6c)break;else _0x18d8c2['push'](_0x18d8c2['shift']());}catch(_0x5764b7){_0x18d8c2['push'](_0x18d8c2['shift']());}}}(a0_0x12
... (excerpt; the full sample is 4303 characters)
Decoded output
var CONFIG_URL = "https://config.example.com/settings.json";
function loadSettings(_0x33d8cc) {
  fetch(CONFIG_URL)["then"](function(_0x185779) {
    return _0x185779["json"]();
  })["then"](function(_0x404861) {
    console["log"]("settings loaded from " + CONFIG_URL), _0x33d8cc(_0x404861);
  });
}

loadSettings(function(_0x5d24c7) {
  console["log"]("theme: " + _0x5d24c7["theme"]);
});

The same source under the RC4 option. The accessor now takes two arguments, the second being the per-string key. The recovered program matches the base64 build line for line, apart from the generated identifier names, which differ because the two builds were produced in separate runs and those names are minted fresh each time.

What javascript-obfuscator actually does to a file

obfuscator.io is the hosted front end for javascript-obfuscator, an open source tool whose output is the obfuscated JavaScript an analyst meets most often. It is a legitimate product with legitimate users, and it is also the builder behind a large share of the obfuscated JavaScript that reaches an analyst. The core transformation is the string array. The tool lifts the string literals out of the program into one array, shuffles it, and replaces each original literal with a call to an accessor function that indexes the array with an offset. What is left behind reads as arithmetic and hexadecimal identifiers. On top of that base it layers options: rotation of the array by a self-invoking function, base64 or RC4 encoding of the entries, dead code injection, and control flow flattening that rewrites straight-line code into a dispatcher loop.

text
THE SAME LINE UNDER TWO STRING-ARRAY OPTIONS
--------------------------------------------
base64    var CONFIG_URL=a0_0x1c6d3a(0x1f2);
RC4       var CONFIG_URL=a0_0x387047(0xcb,'a[Ey');

recovered var CONFIG_URL = "https://config.example.com/settings.json";

The second argument under RC4 is the per-string key, which is why the
accessor signature changes while the recovered logic does not. Only the
generated identifier names differ between the two builds.

The reason this matters for triage is that the options compose, and each one moves the indicator further from the surface. A build with rotation and RC4 holds its URLs nowhere in readable form: the array entries are ciphertext, the order is wrong until the rotation function has run, and the accessor adds an offset that differs per build. Reading such a file by eye gives you the shape of the program and few of its facts, which is the intended effect.

Complete decode, or a partial one

The well-known open source deobfuscators for this builder are static rewriters that work on the parsed syntax tree, and they do a capable job on the string array. The question worth asking of any of them, and of this one, is not whether it returned something, but whether what it returned is the whole program. Tools differ in what they leave behind: some resolve the array and stop, leaving flattened control flow in place; some run the sample to reach further, which means executing hostile code; and some return a partial resolution that reads as finished because the lookups they could not resolve were quietly dropped. A result that looks like source but omits a branch is worse for an analyst than one that admits it is incomplete, because the omission is invisible at review time.

KlaroSkope decodes these files statically, without executing the sample, and reports whether the delivered program is a complete reconstruction or a partial one. Where the reconstruction is complete, the output is the program with its strings in place. Where it is partial, that is stated rather than papered over, and the layers that did resolve are still returned. There are builds this approach does not fully reduce, among them very large bundles above the input ceiling, damaged or truncated captures, and files whose remaining layer is an encrypted blob with the key held elsewhere. Those ceilings are worth knowing before a decode rather than after it.

Working with an obfuscator.io sample

  • Submit the whole file rather than a fragment. The string array, the rotation function and the accessor sit in different parts of the script, and a decode that is missing one of them resolves fewer strings than the file actually permits.
  • Do not run it to see what it does. Running is the one approach that reliably recovers a heavily layered build, and it is also the approach that carries out whatever the sample was written to carry out.
  • Read the recovered program for the facts, not the names. Identifier names are lost at build time and are not recoverable, so the analysis targets are the restored strings: URLs, hosts, paths, keys and the API calls around them.
  • Treat a partial decode as a partial decode. If the output still carries accessor calls or a dispatcher switch, some of the program is still hidden, and a rule written against what you can see may miss the branch you cannot.

Two sibling pages go deeper than this one. Decode obfuscator.io JavaScript: Option-by-Option Reference covers what each build option does and how to recognise it in a file, which is the page to read when you are identifying a build rather than decoding one. Decode JavaScript String-Array Obfuscation covers the string-array mechanism itself, including accessor offsets, rotation and alias chains, across builders other than this one.

Dating a sample from its option names

The project has been published on npm since May 2016 and is BSD licensed, and its option names have changed in ways that date a build. Version 3.0.0 renamed rotateStringArray to stringArrayRotate and shuffleStringArray to stringArrayShuffle, and version 2.0.0 made stringArrayEncoding take an array rather than a single value. A configuration or a build artefact carrying the older spellings was produced before those releases, which is occasionally the most concrete thing you can say about when a sample was assembled. The current option set includes stringArray, stringArrayRotate, stringArrayShuffle, stringArrayEncoding, stringArrayThreshold, stringArrayIndexShift, stringArrayWrappersCount and stringArrayCallsTransform.

The project's own documentation notes that the RC4 encoding runs about 30 to 50 percent slower than base64 and is harder to pull initial values out of, which is the tradeoff an operator is making when you see the two-argument accessor. RC4 here is layered over base64 rather than replacing it, so the base64 alphabet is present in both builds and the accessor arity is what actually separates them.

The other deobfuscators, and how they differ

Open source deobfuscators for this builder
obfuscator-io-deobfuscator (ben-sb)
ApproachBabel syntax-tree rewriting
Runs the sampleNo, the project states it runs no untrusted code
synchrony (relative)
Approachacorn syntax-tree rewriting
Runs the sampleNo
webcrack (j4k0xb)
ApproachSyntax-tree rewriting plus a sandbox
Runs the sampleYes, string-array decoders are evaluated in an isolated virtual machine
KlaroSkope
ApproachStatic reading, then layer chaining
Runs the sampleNo, and it reports whether the reconstruction is complete

This is a capable field and the honest summary is that all four recover the string array on an ordinary build. The differences show up at the edges: whether the tool will execute part of the sample to get further, whether it continues into a second encoding once the strings are back, and whether it tells you that something was left unresolved. The last of those is the one that changes what an analyst writes in a ticket.

Partial decodes come from partial input. The array, the rotation function and the accessor can sit far apart in one file, so submit it whole rather than the block you recognised, and the completeness signal will mean something.

Frequently Asked Questions

Q

What is obfuscator.io?

obfuscator.io is the hosted interface for javascript-obfuscator, an open source JavaScript obfuscator. It is widely used for legitimate code protection, and its output is also common in malicious scripts, so an analyst meets it often.
Q

Can obfuscator.io output be decoded without running it?

Much of it can. The string array, the rotation, the accessor offset and the base64 or RC4 encoding are all recoverable by reading the script. Some builds resist full static reduction, and in those cases a partial result plus an honest statement of what is missing is the useful outcome.
Q

Will I get the original variable names back?

No. Identifier names are discarded when the file is built and are not stored anywhere in the output, so _0x3a1b cannot be turned back into its original name. What a decode restores is the strings, which is where URLs, hosts and keys live.
Q

What is the difference between the base64 and RC4 string-array options?

Both hide the array entries. Base64 encodes them, and RC4 encrypts each entry under a per-string key that is passed to the accessor as a second argument. The accessor signature is the visible difference. The recovered logic is the same either way, and the two worked examples on this page show that: same program, same strings, different generated identifier names.
Q

How do I know whether a decode is complete?

Look at the delivered output rather than at a layer count. Remaining accessor calls, a dispatcher switch or leftover encoded blobs indicate the reconstruction is partial. KlaroSkope reports completeness alongside the result so this does not have to be judged by eye.
Q

Is using obfuscator.io a sign that a script is malicious?

On its own, no. It is a mainstream tool with many legitimate users, and plenty of benign commercial JavaScript ships obfuscated. What the decode gives you is the content, and the content is what supports a judgement.

Found this useful? Sharing is caring!

Ready to decode?

Paste the script or upload the file. Multi-layer samples continue past this technique into whatever comes next.

Open the analysis console