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 Part 1, we deobfuscated Anura’s script and reconstructed its string tables. With a clean version in hand, the next step is to understand where the fingerprint is assembled and how it ends up in the request.
In this article we’ll look at:
- Where the payload is constructed
- How signals are dispatched and collected
- Which encoding steps are applied before the request is sent
- How to reverse that encoding to read the raw fingerprint data
Finding the entry point
We know from our earlier analysis that the script sends a POST request to https://script.anura.io/response.json. Since we have already deobfuscated the strings, we can simply search for response.json in the script.
There is exactly one match, inside the function a.uT.rf:
a.uT.rf = function () { try { var b = a.uT.U(); if (b) (b['open']( 'POST', a.uT.r('script.anura.io', 'response.json', false, true), ), b['setRequestHeader']( 'Content-type', 'application/x-www-form-urlencoded', ), 'object' === typeof b['onload'] ? (b['onload'] = function () { this['responseText'] && a.uT.z(this); }) : (b['onreadystatechange'] = function () { b['readyState'] === 4 && b['responseText'] && a.uT.z(b); }), b['send'](a.uT.T())); else { var c = a.y.g(null); c['responseText'] = '{"error":"Browser not supported"}'; a.uT.z(c); } } catch (d) { ((c = a.y.g(null)), (c['responseText'] = '{"error":"Response encountered an unknown error: ' + a.X.kM(d['message']) + '"}'), a.uT.z(c)); }};This function sets up a standard XHR POST request. The key line is:
b['send'](a.uT.T());The payload is entirely constructed by a.uT.T(). Let’s dig into it.
Payload construction
a.uT.T = function () { var b = a.y.g(null); b['token'] = a.Rd.C(); b['sTuUwtYv'] = a.y.g(null);
var encodingKey = { '[95,93]': [122, 110], '[97,117]': [98, 85], '[86,127]': [64], // truncated }; var xorKey = 'wVWoa9DYCo3Jti3ImOc+LSFTNBjqSlchkxGbNHHz3OitP8/aYtKQLEAksLAYBG5yWn/JdMXuRO6587LDA+IheiBs0WqxO2Ch5qtEVcLwEnMJyof8Bvy/qCEQ8ldctSe4BBQUkJQu8EYvyXhu6QLOyxyaHN57fYixb2OXURjA85GDjEO9sywa4S8iI3TBHW5pH27Xdo5rMU2iynQI0I7j';
for (var c in encodingKey) { var d = a.f.f(a.o.u(encodingKey[c]), xorKey); c = a.f.f(a.o.u(a.y.T(a.o.M(c))), xorKey); 'function' === typeof a.lH[c] && (b['sTuUwtYv'][d] = a.lH[c]()); } b['sTuUwtYv'].Uc = a.lH.IN(b['sTuUwtYv'].o, b['sTuUwtYv'].F); b['sTuUwtYv'].ym = a.lH.tV(b['sTuUwtYv']); b['sTuUwtYv'] = a.O.q(b['sTuUwtYv']); a.R.Yi(b); a.s.U(); a.s.K(); a.s.e(); a.s.F(); a.s.kZ(); return a.R.u(a.uT.e(b), '&');};1. The token
b['token'] = a.Rd.C();a.Rd.C = function () { return 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiIxNjc3Nzg0NTQwIiwiaWF0IjoxNzcwMTIxMDU1LCJuYmYiOjE3NzAxMjEwNTUsImV4cCI6MTc3MDEyMTExNSwianRpIjoiU3huOXQ3d1I4Rmx3UE1LWDhzSkN1ZzJ0Rk5VL2lscFQiLCJhdWQiOiI4OTA4NTc4OTYifQ.7dNQ_gEHqHxDFbw42VWugLPoDj8OZgRYVrmrw_S8FqRQ4h67N2X4VV07zVtu3NE58DGzdArslI_of5OsykdAaA';};a.Rd.C() returns a static JWT embedded directly in the script at generation time.
2. The fingerprint loop
The script initializes an empty object b.sTuUwtYv, then populates it by iterating over encodingKey. The loop can be rewritten more clearly as:
for (var c in encodingKey) { var payloadKeyName = xor( String.fromCharCode.apply(null, encodingKey[c]), xorKey, ); var functionName = xor( String.fromCharCode.apply(null, JSON.parse(c)), xorKey, ); if (typeof a.lH[functionName] === 'function') { b['sTuUwtYv'][payloadKeyName] = a.lH[functionName](); }}Each entry in encodingKey encodes two things, both XOR-obfuscated with xorKey:
- The name of the fingerprinting function to call (from the
a.lHnamespace) - The key name under which the result will be stored in the payload
Rather than hardcoding field names directly, Anura uses an indirection table that maps probes to payload keys. It makes static analysis harder without adding any real complexity to the runtime logic. Every signal flows through this loop.
3. Additional fields and URI encoding
Once outside the loop, two more properties are added to the payload: Uc and ym. Then a.O.q is called on the fingerprint object, recursively URI-encoding every string value:
a.O.q = function (object) { for (var c in object) if ('string' === typeof object[c]) object[c] = String.replace( encodeURIComponent(object[c]), RegExp('~', 'g'), '%7E', ); else if ('object' === typeof object[c] && object[c]) try { object[c] = a.O.q(object[c]); } catch (d) {} return object;};This normalizes the data before the encryption and serialization steps.
4. Serialization and encoding
Some cleanup functions are called, then the payload is prepared for transport by two helpers.
a.R.u recursively flattens objects into a string representation:
function flattenJoin(v, sep) { if (v == null) return '';
if (Array.isArray(v)) { return v.map((x) => flattenJoin(x, sep)).join(sep); }
if (typeof v === 'object') { return Object.values(v) .map((x) => flattenJoin(x, sep)) .join(sep); }
return String(v);}a.uT.e encodes each field before serialization:
a.uT.e = function (b) { var c = []; var d; for (d in b) { if (typeof b[d] === 'object') { if (d === 'sTuUwtYv') { c.push( d + '=' + encodeURIComponent( btoa( xor( encodeURIComponent(JSON.stringify(b[d])), 'ND3vtb35vrqwPZYwRoUB7UiZwGruanoHo0Lh4qqTjWSd+nUm7IsABh350XNbB/Pt+8CWgw9suHvTO7enL4UKv/OsXHESSrn6bfeTzuq7GshJJQrdfOtzokMPX6eEKX+kJSQ4DiaS/S0D8duo0v4uWCje', ), ), ), ); } else { c.push( d + '=' + encodeURIComponent(btoa(encodeURIComponent(JSON.stringify(b[d])))), ); } } else { c.push(d + '=' + b[d]); } } return a.R.Yi(c);};The sTuUwtYv field gets an extra XOR pass with a dedicated key before being base64 encoded.
Building a payload decoder
Now that we understand the encoding pipeline, we can reverse it to read any captured payload in plain text.
1. Map obfuscated keys back to function names
We replicate the same logic as the fingerprint loop to build a lookup table:
function buildKeyRenameMap(encodingKey, xorKey) { const map = {};
for (const c in encodingKey) { const payloadKeyName = xorObfuscate( String.fromCharCode.apply(null, encodingKey[c]), xorKey, );
const functionName = xorObfuscate( String.fromCharCode.apply(null, JSON.parse(c)), xorKey, );
map[payloadKeyName] = functionName; }
return map;}2. Decode the fingerprint field
We decode the sTuUwtYv string from the full payload by reversing the steps applied in a.uT.e, with s being the encoded string value and key being the XOR key from that function (ND3vtb35vrqwPZYwRoUB7U...):
function decodePayload(s, key) { // 1) Undo outer encodeURIComponent const step1 = decodeURIComponent(s);
// 2) Undo base64 const step2 = atob(step1);
// 3) Undo XOR const step3 = xor(step2, key);
// 4) Undo inner encodeURIComponent const step4 = decodeURIComponent(step3);
// 5) Undo JSON.stringify return JSON.parse(step4);}3. Rename keys to human-readable function names
function renameKeys(obj, renameMap) { const out = {}; for (const k of Object.keys(obj)) { out[renameMap[k] ?? k] = obj[k]; } return out;}Putting it all together
const payload = 'KzNRMyYwfm0zICMyI1sp...';
const renameMap = buildKeyRenameMap(encodingKey, xorKey);
const decodedPayload = decodePayload(payload, 'ND3vt...');
const readablePayload = renameKeys(decodedPayload, renameMap);Result: the raw fingerprint
Running this against a captured payload gives us the full fingerprint object in plain text:
{ HK: 0, vc: '%3F', Ai: '%3F', Ej: '%3F', Ol: '%3F', V: 'P1%20PDF%20Viewer%3B%20Portable%20Document%20Format%3B%20internal-pdf-viewer%3A%20Portable%20Document%20Format%3B%20application%2Fpdf%3B%20pdf%3B%20Portable%20Document%20Format%3B%20text%2Fpdf%3B%20pdf%3B%20P2%20Chrome%20PDF%20Viewer%3B%20Portable%20Document%20Format%3B%20internal-pdf-viewer%3A%20Portable%20Document%20Format%3B%20application%2Fpdf%3B%20pdf%3B%20Portable%20Document%20Format%3B%20text%2Fpdf%3B%20pdf%3B%20P3%20Chromium%20PDF%20Viewer%3B%20Portable%20Document%20Format%3B%20internal-pdf-viewer%3A%20Portable%20Document%20Format%3B%20application%2Fpdf%3B%20pdf%3B%20Portable%20Document%20Format%3B%20text%2Fpdf%3B%20pdf%3B%20P4%20Microsoft%20Edge%20PDF%20Viewer%3B%20Portable%20Document%20Format%3B%20internal-pdf-viewer%3A%20Portable%20Document%20Format%3B%20application%2Fpdf%3B%20pdf%3B%20Portable%20Document%20Format%3B%20text%2Fpdf%3B%20pdf%3B%20P5%20WebKit%20built-in%20PDF%3B%20Portable%20Document%20Format%3B%20internal-pdf-viewer%3A%20Portable%20Document%20Format%3B%20application%2Fpdf%3B%20pdf%3B%20Portable%20Document%20Format%3B%20text%2Fpdf%3B%20pdf%3B', S: '%3F', Uh: '%3F', il: { angle: 0, type: 'landscape-primary' }, J: [], f: 0, b: 0, ur: '%3F', js: 1771379972, QI: [ 'bac0ab239', 'aefb46677', '74790af21', '8ae0d7024', '83658c22a', '368a85f78', 'fb46d0505', 'ee4fc9324', 'd4d97e2b6', '6af1ea693' ], fZ: 1, LU: [ 'en-US', 'en' ], sj: '%3F', Wf: '%3F', CI: 0, yr: 0, zs: 0, m: '', ck: 0, So: 0, wz: '%3F', HJ: 0, rY: 0, q: '%3F', Ah: 0, yN: '%3F', Fh: 0, Zc: -60, z: 0, Wz: 14, w: 0, rf: { c: [ '2761d091-fc4c-4b17-b79b-3fa949de3c8b.local' ], id: [ '2122252543', '2105524479', '1686052863' ], ld: [ '0.0.0.0' ], sc: [ '4289f6fb-58c2-4c84-9ac3-159c30ce1633.local', '182.232.227.63' ], sld: [ '0.0.0.0', '182.232.227.63' ], cs: [ 'new' ], scs: [ 'new' ], gs: [ 'complete' ], sgs: [ 'complete' ], rs: 3, hrs: 1, srs: 4, hsrs: 1 }, wq: '%3F', U: 0, A: '0%2C38', F: 0, c: 2, JM: [ 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1 ], ss: 0, Qu: [], pA: 0, HQ: 'abd53265225e11a5b3d07f5ce00141aa', i: 0, D: 0, xr: 0, Z: '%3F', Fs: 'MacIntel', G: '1512%2C517', p: '1512%2C863', vb: '%3F', IU: 'Netscape', N: 0, JY: { locale: 'en', calendar: 'gregory', numberingSystem: 'latn', timeZone: 'Europe/Paris', year: 'numeric', month: 'numeric', day: 'numeric' }, K: 0, fq: '1512x863', cM: '1512x982', GR: 1, tr: '%3F', L: { pd: 0, pn: 0, po: 0 }, GX: 1, tQ: 0, h: 0, e: 2, kP: '%3F', mw: 'fccc0f750c424ff2c78ffeb312075a0f', QO: 0, r: [], I: 5, zG: 0, H: 1, X: '%3F', a: 901, VZ: 'Apple%20M1%2C%20or%20similar', x: 0, tY: 'Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010.15%3B%20rv%3A147.0)%20Gecko%2F20100101%20Firefox%2F147.0', B: 0, Av: 1, Hq: '%3F', jr: '%3F', aC: 0, y: [], uE: 0, cq: { ape: '?', aps: '?' }, YH: 1, em: 0, O: 1, C: 'd79b64d7687571431508c3ad0f86e528', yq: 0, u: 1, Nr: 1, Ir: 0, ph: '', o: 'visible', KL: 1, cY: 1, iA: 0, M: '%3F', E: [], CA: '%3F', g: 1, Y: [], UQ: 1, vE: '%3F', v: 0, l: 'file%3A%2F%2F%2FUsers%2Fantoine%2FDocuments%2FCode%2FDefeat-Anura%2Fshit%2Fnew_anura%2Fanura.html', Q: '0%2C38', Wj: '%3F', j: 0, Ly: 'WebGL%202.0', wr: '%3F', UO: 30, yX: 'visible', yj: '4c67da8241717e724056701871029cc3', vR: 0, bD: '5.0%20(Macintosh)', Bx: 0, t: 0, bb: { '108': 2, '109': 6, '110': 2, '118': 1, '119': 3, '120': 5, '122': 2, '123': 8, '124': 1, '125': 4, '126': 4, '127': 1, '128': 1, '129': 1, '133': 1, '134': 1, '135': 3, '136': 3, '137': 1 }, T: 'file%3A%2F%2F%2FUsers%2Fantoine%2FDocuments%2FCode%2FDefeat-Anura%2Fshit%2Fnew_anura%2Fanura.html', tn: 'en-US', Gb: '%3F', tw: [], cP: '%3F', s: '%3F', d: 0, W: 1, qB: 'file%3A%2F%2F%2FUsers%2Fantoine%2FDocuments%2FCode%2FDefeat-Anura%2Fshit%2Fnew_anura%2Fanura.html', fp: '%3F', k: 0, Tx: 'file%3A%2F%2F%2FUsers%2Fantoine%2FDocuments%2FCode%2FDefeat-Anura%2Fshit%2Fnew_anura%2Fanura.html', Ud: 0, bI: 0, yA: [ 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 3, 1, 1, 3, 3, 3, 3, 1, 3, 3, 3, 2, 1, 1 ], zv: '', kL: { xd: 1, xf: 1, xw: 0, xn: 0, xc: 0, xt: 0, yf: 1, yw: 0, cr: 1 }, n: 1, tf: '', Cx: 0, Kj: 'file%3A', BC: 'eecc3df9f19bdd7bfa7a6dbdb49083b6', cZ: 0, wx: [ 0, 1, 0 ], ts: 30, Uc: {}, ym: '0f3460d2a00d3cc98e84c37de4de199a'}We successfully decoded the payload. Even at a glance, several signals are immediately recognizable: the user agent, the browser language list, the timezone offset, WebRTC ICE candidates, the WebGL version. We will go through each field in detail in Part 3.
Conclusion
We now understand how Anura builds its client payload. The script does not directly construct a JSON object and send it. Instead, it:
- Dispatches a large set of fingerprint probes through an indirection table
- Normalizes and derives additional values
- Applies layered encoding (URI, XOR, base64)
- Serializes everything into a compact form for transport
By reversing this pipeline, we can decode any captured request and inspect the raw signals that drive the antibot decision.
In Part 3, we will analyze each fingerprint field individually: what it measures, why it is there, and how to reproduce it accurately.