JS Reverse Engineering Case Study #1 — Reconstructing an Obfuscated analysis Request Signature (Environment Emulation + execjs)
Part 1 of the "JS Reverse Engineering in Practice — 30 Case Studies" series.
Source code (this case): https://github.com/scool112/openJSreverse (Project1)Note on sanitization: This is a sanitized, educational write-up. Every real domain, endpoint and interface path has been replaced with generic placeholders (
example.com). No production endpoint or site-specific constant is disclosed. The goal is to explain the technique, not to enable abuse of any specific service.
1. Goal & Background
Recently I kept coming across a common anti-crawler pattern on data-driven sites: the request looks plain, but one query parameter is an opaque signature string. If it is missing or wrong, the server rejects the call.
This first case is exactly that. The target is a public app-ranking site (referred to as "Site A" in this article). I wanted the "delisted apps" ranking list, and its list endpoint carries a signature parameter named analysis.
-
Target site:
https://www.example.com(an app-ranking site) -
Target endpoint:
https://api.example.com/<ranking-endpoint> -
Parameter to reconstruct:
analysis(the rest are plain business parameters) -
Difficulty: ⭐⭐⭐⭐ — the algorithm itself is not the hard part. The hard part is that the target bundle is heavily obfuscated, so the pragmatic strategy is: emulate the browser environment and call the site's own exported
get_analysis().
The reverse-engineering target here is a classic parameter signature. Unlike a plain MD5 that you can read at a glance, this one was processed with a javascript-obfuscator-style transformation (string table + member-expression obfuscation). Reading variable names by eye is basically hopeless. So instead of fighting the obfuscation, I chose to emulate the environment and run the original function.
2. Traffic Analysis
Open https://www.example.com in a browser, press F12, switch to the Network panel, filter by Fetch/XHR, and then toggle the ranking tab / flip a page. You will see a request to the list endpoint.
Open that request and look at the Payload (query string):
analysis=<opaque signature string>
option=<n>
sdate=
edate=
option selects the ranking type, and sdate / edate are the date range — all plain text. The only unreadable value is analysis: a mixed-case string that looks like Base64 but behaves like an encrypted result. It changes every time the ranking type or date changes, which means it is tightly bound to the request data — i.e. a real signature.
Now look at the request headers: Origin and Referer both point to Site A, and the request carries login cookies. So this endpoint also requires a logged-in session — a correct analysis alone is not enough.
📷 [Figure 1 — screenshot suggestion] The Network panel: the list request's Payload and the Cookie request header.
Conclusion: The request is a GET with plain parameters; the whole challenge is reconstructing how analysis is generated.
3. Locating the Signature Entry Point
Since this is a parameter signature, I went to the Sources panel and did a global search for the parameter name analysis (or for nearby business fields such as option / sdate). The search lands in a very large obfuscated bundle (the site's main package).
At first I tried to read it directly. Variable names were all single/double letters like n2 / t2 / e2 / R, and strings were packed into a string array, restored through a "index lookup" function such as n(12,13,14,...) — the classic javascript-obfuscator pattern. Restoring it line by line is far too expensive, so I switched strategy: can I just extract the exported function and run it?
Following where analysis is assigned, I found the export:
// Anonymized excerpt from the reconstructed JS (start/end of the bundle)
var AA;
var get_analysis;
// ...
get_analysis = get_T;
}(function (a) {
return function () {
for (var n = arguments, t = "", e = 0, r = n.length; e < r; e++)
t += a[n[e]];
return t;
};
}(["p", "u", "s", "h", "w", "e", "b", "a", "c", "k", /* ... */]));
In other words, after the whole bundle runs, a global get_analysis appears, pointing at the internal, heavily obfuscated get_T. That is exactly the entry point I want.
📷 [Figure 2 — screenshot suggestion] In the Sources panel: the assignment of
analysisand a breakpoint on theget_Tcall stack.
4. Restoring the JS & Emulating the Environment
Early inside get_T, it accesses document.cookie (it mixes session state into the signature), and Node has no document. So the first emulation step is to fake a global.document at the very top of the file and inject the cookies:
// Reconstructed JS (cookies replaced with placeholders — use YOUR OWN session cookies)
var AA;
var get_analysis;
global.document = {
cookie: "SESSIONID=<your-session-id>; TOKEN_A=<your-token-a>; TOKEN_B=<your-token-b>; ts=<your-ts>"
};
Next is the restored core function get_T. It packs the parameter object into an array, sorts and joins it, then applies a hash / symmetric encryption, and finally toString()s the result into analysis. The (obfuscated) shape is:
// Obfuscated variable names kept as-is (anonymized)
function get_T() {
var i = AA(21725), r = AA(9669), o = AA[d7](r), /* ... */ c = AA(C);
// ... build an array `a` from `t.params`, sort it, join with a separator ...
// ... append url + timestamp + fixed constants ...
// e = encode(encrypt(a, key));
return e.toString();
}
get_analysis = get_T;
From this shape you can see the approximate construction of analysis:
- Walk the
paramsobject (edate/option/sdate) into an arraya; - Sort the array and join it with a separator;
- Append the URL, a timestamp, and a set of fixed constants;
- Apply the symmetric encryption / encoding referenced inside the expression, and output
e.toString().
Those constants are hidden behind the string-table restore function n(...). Reversing them one by one by hand is not worth it. So my approach is: don't reconstruct the algorithm — run the whole bundle and call its own get_analysis.
To run it under Node / execjs, I did two emulation things:
- Fake
global.document(shown above); - The bundle already ends with a self-test call to
get_analysis()in a comment, confirming it is meant to be called exactly like that.
5. Python Implementation
The Python script is short: read the JS → execjs.compile → call get_analysis() → put it into the query.
# Anonymized Python (URLs and cookies replaced with placeholders)
import requests
import execjs
with open("bundle.js", encoding="utf-8") as f:
js_code = f.read()
js = execjs.compile(js_code)
analysis = js.call("get_analysis")
headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "en-US,en;q=0.9",
"Origin": "https://www.example.com",
"Referer": "https://www.example.com/",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-site",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
}
cookies = {
"SESSIONID": "<your-session-id>",
"TOKEN_A": "<your-token-a>",
"TOKEN_B": "<your-token-b>",
"ts": "<your-ts>",
}
url = "https://api.example.com/<ranking-endpoint>"
params = {
"analysis": analysis, # computed by the restored JS
"option": "<n>",
"sdate": "",
"edate": "",
}
response = requests.get(url, headers=headers, cookies=cookies, params=params)
print(response.text)
A few details:
-
js.call("get_analysis")takes no arguments and returns the fullanalysisstring; - The cookies must be the same logged-in session as the one you injected into
document.cookie(both are placeholders here — replace them with your own); - Keep
optionand the empty dates consistent with the browser request.
6. Result
Running the script prints the computed params (note analysis differs every run) and then the JSON returned by the endpoint:
{'analysis': '<computed-signature>', 'option': '<n>', 'sdate': '', 'edate': ''}
{"code":0,"msg":"ok","data":{"list":[{"appName":"...","rank":"..."}, ...]}}
<Response [200]>
As long as the server returns code:0 with a populated data.list, the analysis signature is correct. If you get an auth/signature error instead, first suspect an expired cookie or a mismatched parameter.
📷 [Figure 3 — screenshot suggestion] The terminal output: the computed
analysisand the returned JSON data.
7. Pitfalls & Notes
documentmust be emulated.get_Treadsdocument.cookie, so a bareexecjs.compilethrowsdocument is not defined. Forgetting to injectglobal.documentcost me quite some time.Cookies are a live session. The session token is timestamped and expires. The cookies in the JS and in the Python request must match, and both must be yours (placeholders in the repo). An expired cookie will be rejected even if the signature is perfect.
Don't try to de-obfuscate the string table by hand. The
n(12,13,14,...)lookup can be automated with AST tooling, but for a single parameter it is not worth it — emulating the environment and calling the original function is the fastest route.The signature is bound to the parameters. Change
option/sdateand you must recomputeanalysis.Use UTF-8 consistently.
open(..., encoding="utf-8")matters: on Windows the default encoding can corrupt the JS and break parsing.Rate-limit. This is a logged-in data endpoint — keep the request rate low and respect the site's terms and local laws.
8. Takeaways
-
For heavily obfuscated signatures, the best move is usually "emulate the environment and run the original function", not "reconstruct the algorithm" — you only need to fake the few globals it actually touches (
document,window, ...). -
The essence of
analysisis "sorted params → joined string → hash → symmetric encryption → encoding"; the entry point is the site's own exportedget_analysis. -
execjscalling an exported JS function +requestswith a logged-in cookie is the fastest combo for this kind of obfuscated signature.
Disclaimer: This article is for educational and security-research purposes only. Do not use it for any commercial or illegal activity. All endpoints/sites have been anonymized; always respect the target's terms of service and applicable laws.
Top comments (0)