Reverse Engineering Anura - Part 1: Deobfuscation

In this series, I will show you how to reverse engineer a moderately complex antibot

person Antoine Masuyer
|
calendar_today February 22, 2026
|
schedule 5 min read

Antibot scripts are built to be hard to inspect: heavy obfuscation, lots of indirection, and many small checks spread across the runtime.

In this series, I’ll reverse engineer Anura step by step. We’ll first make the client-side JavaScript readable, then follow how it executes, understand the payload it produces, and finally explore how those signals can be reproduced.


In this first article, we’ll focus on the deobfucsation of Anura.

Anura flow

On the target website, Anura is invoked as follows.

A GET request to:

https://script.anura.io/request.js?instance=890857896&source=63470031&callback=MarketGidInfC1926798AnuraCallback&1770119273148

loads the Anura JavaScript.

The Anura script is dynamic: a few things change on every fetch (most notably some encryption keys and payload field names/keys).

For the sake of simplicity, we’ll treat the script as static throughout this series.

Then a POST request is sent to:

https://script.anura.io/response.json?708300641427

with the following payload (truncated here):

token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiIxNjc3Nzg0NTQwIiwiaWF0IjoxNzcwMTIxMDU1LCJuYmYiOjE3NzAxMjEwNTUsImV4cCI6MTc3MDEyMTExNSwianRpIjoiU3huOXQ3d1I4Rmx3UE1LWDhzSkN1ZzJ0Rk5VL2lscFQiLCJhdWQiOiI4OTA4NTc4OTYifQ.7dNQ_gEHqHxDFbw42VWugLPoDj8OZgRYVrmrw_S8FqRQ4h67N2X4VV07zVtu3NE58DGzdArslI_of5OsykdAaA&sTuUwtYv=KzNRMyYwfm0zICMyI1spMiBMMDAlYEo%[...]Rys9Ois2UDMmMFB%2FMyAjMiNbPCJQPzAwVCQsKFQ3NyBFKz1LKzYhRn4nISczIVAkID8rVDc9J1d0MDsoMjRTMDZMKj9KISFVMTAhbHs3IyU1KVgyID0kNXQtb3l1ZHF3ZG05OW9ndyFyNiMtLyJycSh4fHMqaTAwJTA%2BXg%3D%3D

Our goal is to understand how this payload is generated and what it contains.

Then a final POST request is sent to:

https://script.anura.io/result.json

with the following payload:

instance=890857896&id=1444094981.38a814f48205146f60b2619ebc573fef

The response looks like:

{ "result": "bad", "mobile": 1 }

The result can be:

  • “bad”
  • “warn”
  • “good”

It’s surprisingly convenient that Anura exposes an explicit verdict! That makes testing and reversing much easier.

Taking a look at the JS

Static analysis

Opening the script, nothing immediately interesting appears. The code is heavily obfuscated and there’s almost no readable strings.

One thing stands out, though: the script contains six large integer arrays, which strongly suggests they are used to reconstruct (or decode) strings.

They are defined as:

a.i.t = function () { return [91, 34, 84, ...]; };
a.i.z = function () { return [98, 48, 122]; };
a.i.u = function () { return [110, 38, 94, 45, ...]; };
a.i.l = function () { return [97, 33, 118, 125, ...]; };
a.i.I = function () { return [108, 42, 87, ...]; };
a.i.uB = function () { return [117, 36, 105, ...]; };

Right after, an object a.H is initialized:

a.H = a.y.g(null);
a.H.t = a.o.uO(a.i.t());
a.H.z = a.o.v(a.i.z(), a.f.J(4));
a.H.u = a.o.v(a.i.u(), a.f.J(6));
a.H.l = a.o.v(a.i.l(), a.f.J(8));
a.H.I = a.o.v(a.i.I(), a.f.J(2));
a.H.uB = a.o.v(a.i.uB(), a.f.J(0));

Each property is initialized by a function that consumes one of the arrays, almost certainly some kind of decoding step.

Searching for a.H. yields 4500+ references, confirming that these arrays effectively implement a large string/value table used throughout the script.

Recovering the strings

Following the decoding functions reveals the mechanism.

The first array a.i.t is decoded with the function a.o.u0 which can be simplified as:

a.o.u0 = JSON.parse(String.fromCharCode.apply(null, b));

The other array are just XOR-encoded and can be decoded as follow:

function xor(b, c) {
if (c === '') return b;
c.replace(String.fromCharCode(32), '');
for (var d = c.length < 32 ? c.length : 32, e = [], h = 0; h < d; h++)
e[h] = c.charCodeAt(h) & 31;
c = 0;
var m = new String();
for (h = 0; h < b.length; h++) {
var l = b.charCodeAt(h);
m += l & 224 ? String.fromCharCode(l ^ e[c]) : String.fromCharCode(l);
c++;
c = c === d ? 0 : c;
}
return m;
}
function decodeArray(b, c) {
const x = xor(String.fromCharCode.apply(null, b), atob(c));
return JSON.parse(x);
}

This is a small rolling XOR cipher with a key derived from another decoded value.

The function a.f.J(x) simply returns an element from the first decoded array a.i.t, meaning the first array acts as the key source.

A quick script produces the (truncated) decoded table for a.H.z:

{
"uv": "WebGL2RenderingContext",
"GY": "freeze",
"RF": "&nbsp;",
"qH": "mozRTCPeerConnection",
"wv": "=",
"I": ")",
"z": -0.6,
"Rn": 2,
"oO": "HTMLScriptElement",
"wr": "CanvasRenderingContext2D",
"C": -1,
"P": -28,
"Fw": "close",
"tI": "probably",
"rG": "atob",
"Bm": "History",
"K": "\"",
"Pa": "setItem",
"s": null,
"Rz": "oRequestAnimationFrame",
...
}

So the obfuscation relies on:

  • integer arrays
  • XOR decoding
  • JSON string tables

Classic but effective.

All code and artifacts for this post are available on GitHub: the original Anura script, the Babel deobfuscator, and the deobfuscated output.
https://github.com/tradertrue/anura-re

Replacing references automatically

Manually replacing 4500+ references is unrealistic, so we use Babel AST transforms.

The script performs three main steps:

1. Extract the arrays

const extracted = {};
traverse.default(ast, {
AssignmentExpression(path) {
const { left, right } = path.node;
if (!t.isMemberExpression(left)) return;
for (const key of ['t', 'z', 'u', 'l', 'I', 'uB']) {
if (
t.isIdentifier(left.object.object, { name: 'a' }) &&
t.isIdentifier(left.object.property, { name: 'i' }) &&
t.isIdentifier(left.property, { name: key })
) {
const arr = extractReturnedNumericArrayFromFunction(right);
if (arr) {
extracted[key] = arr;
}
}
}
},
});

We traverse the AST and detect assignments to a.i.*. We then extract the numeric array from the function body.

2. Decode the tables

Using the previously recovered logic:

const decoded = {};
decoded.t = JSON.parse(String.fromCharCode.apply(null, extracted.t));
decoded.z = decodeArray(extracted.z, decoded.t[4]);
decoded.u = decodeArray(extracted.u, decoded.t[6]);
decoded.l = decodeArray(extracted.l, decoded.t[8]);
decoded.I = decodeArray(extracted.I, decoded.t[2]);
decoded.uB = decodeArray(extracted.uB, decoded.t[0]);

At this point we have a full string table.

3. Replace a.H.* accesses

traverse.default(ast, {
MemberExpression(path) {
const outer = path.node;
// ignore LHS
const parent = path.parent;
if (t.isAssignmentExpression(parent) && parent.left === outer) return;
if (t.isUpdateExpression(parent) && parent.argument === outer) return;
if (t.isUnaryExpression(parent) && parent.operator === 'delete') return;
// make sure we match a.H.<key>
if (!t.isMemberExpression(outer.object)) return;
const inner = outer.object;
if (!t.isMemberExpression(inner.object)) return;
const aH = inner.object;
if (!t.isIdentifier(aH.object, { name: 'a' })) return;
if (!t.isIdentifier(aH.property, { name: 'H' })) return;
// Get key name and prop name
const keyName = staticPropName(inner.property);
if (!keyName || !['z', 'u', 'l', 'I', 'uB'].includes(keyName)) return;
const propName = staticPropName(outer.property);
if (!propName) return;
// Get the value from the decodedArray
const table = decoded[keyName];
if (!table || typeof table !== 'object') return;
const value = table[propName];
// Replace in the AST
const repl = valueToAst(value);
if (!repl) return;
path.replaceWith(repl);
},
});

We look for MemberExpression patterns matching a.H.<table>.<key>, ignore LHS usages, then replace the node with the decoded value.

Result

Note : I also run a fourth pass to replace a few other indirections (e.g. a.G.*) to improve readability, but it’s not essential to understanding the deobfuscation, so I’ll skip the details.

All of this allows us to go from:

a.lH.zs = function () {
try {
return a.H.z.gZ === typeof a.G.z[a.H.l.uG] ? a.H.z.Ry : a.H.z.b;
} catch (b) {
return (a.uB.vS(a.H.uB.rs, b), a.H.l.dE);
}
};

to:

a.lH.zs = function () {
try {
return 'function' === typeof self['showModalDialog'] ? 1 : 0;
} catch (b) {
return (a.uB.vS('zs', b), 'X');
}
};

The script becomes dramatically easier to reason about.

Additional simplifications (optional)

Further simplifications are possible.

For example, many namespaces (a.w, a.U, a.K, etc.) contain constants that are always false.

That allows simplifying helper functions like:

a.R.Fu = function () {
try {
return 'function' === typeof a.w.Fu ? a.w.Fu : self['Error'];
} catch (b) {
return null;
}
};

Since a.w.Fu is always false, this function effectively returns Error. A similar logic applies to many a.R.* helpers, which can be inlined or replaced entirely.

I intentionally left these optimizations out to keep the deobfuscation script simple. At this stage, readability is already sufficient to understand the logic.

Conclusion

At this point, we’ve turned a heavily obfuscated blob into a script that is finally readable and navigable.

With this foundation, we can start analyzing:

  • how the payload is constructed
  • which signals are collected
  • how Anura produces its verdict

That’s what we’ll cover in the next parts.

mail

Stay in the loop

Get notified when we publish new research. No spam, just posts like this.

rss_feed … or follow our RSS feed

Need help with data extraction?

We apply this level of rigor to every data extraction challenge. Let's discuss your project.

Get in touch