DEV Community

FeiHua
FeiHua

Posted on

JS Reverse Engineering Case Study #1

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=
Enter fullscreen mode Exit fullscreen mode

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", /* ... */]));
Enter fullscreen mode Exit fullscreen mode

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 analysis and a breakpoint on the get_T call 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>"
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

From this shape you can see the approximate construction of analysis:

  1. Walk the params object (edate / option / sdate) into an array a;
  2. Sort the array and join it with a separator;
  3. Append the URL, a timestamp, and a set of fixed constants;
  4. 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)
Enter fullscreen mode Exit fullscreen mode

A few details:

  • js.call("get_analysis") takes no arguments and returns the full analysis string;
  • 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 option and 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]>
Enter fullscreen mode Exit fullscreen mode

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 analysis and the returned JSON data.


7. Pitfalls & Notes

  1. document must be emulated. get_T reads document.cookie, so a bare execjs.compile throws document is not defined. Forgetting to inject global.document cost me quite some time.

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

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

  4. The signature is bound to the parameters. Change option / sdate and you must recompute analysis.

  5. Use UTF-8 consistently. open(..., encoding="utf-8") matters: on Windows the default encoding can corrupt the JS and break parsing.

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

  1. 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, ...).
  2. The essence of analysis is "sorted params → joined string → hash → symmetric encryption → encoding"; the entry point is the site's own exported get_analysis.
  3. execjs calling an exported JS function + requests with 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)