JS 逆向:PEDATA 资讯接口加密解密与 zlib.gunzipSync 应用
1. 逆向目标
- 目标系统:某投资领域 SaaS 系统 PEDATA MAX 资讯模块
- 请求地址:
https://max.pedata.cn/api/q4x/newsflash/list - 返回特征:响应数据中的
data字段为经过多层加密的字符串,包含exor异或密钥和ts时间戳。 - 核心难点:数据经过 Base64、XOR 异或及 Gzip 压缩处理,需还原明文 JSON。
2. 抓包分析
在页面加载时,通过开发者工具 XHR 面板筛选出 newsflash/list 请求。观察请求参数与响应结构:
2.1 请求参数
Payload 主要包含翻页信息(如页码、每页数量),无明显加密特征。重点在于请求头:
HTTP-X-TOKEN:用于标识用户身份,替代 Cookie 进行鉴权。Cookie:部分广告统计参数(如Hm_lvt_xxx)可忽略。
2.2 响应结构
{
"data": "L+o+YmIyNDE...",
"exor": "01",
"ts": 1708632000
}
其中 data 为加密串,exor 为后续解密所需的异或密钥。
3. 加密逻辑逆向
3.1 定位解密函数
在浏览器调试器中搜索键名 exor,定位到唯一引用处。该处代码调用了对象方法 Object(p["y"])(e.data.data, e.data.exor)。跟进该方法发现其内部调用了名为 M 的函数。
3.2 关键函数解析
核心解密逻辑位于 M(t, n) 函数中:
function M(t, n) {
var a = L(Object(s["a"])(), n);
var r = Y(B(t), a);
var c = o.a.gunzipSync(e.from(r)).toString("utf-8");
return JSON.parse(c);
}
各步骤含义如下:
- 获取 Token:
Object(s["a"])()获取登录凭证(即 HTTP-X-TOKEN)。 - 生成异或数组:
L(token, exor)根据 token 和 exor 生成异或密钥数组。 - Base64 解码:
B(t)将加密字符串转为普通字符串。 - XOR 异或:
Y(decoded_str, key_array)执行异或运算得到二进制流。 - 解压数据:
gunzipSync对二进制流进行 Gunzip 解压。 - 解析 JSON:最后将解压后的 UTF-8 字符串解析为 JSON 对象。
3.3 zlib.gunzipSync 说明
zlib.gunzipSync 是 Node.js zlib 模块提供的同步解压方法。它支持 Buffer、TypedArray 等类型输入。在浏览器环境中通常无法直接使用,但在爬虫脚本(Node.js/Python via execjs)中可直接调用。
4. 完整实现方案
4.1 JavaScript 核心算法
将逆向得到的 JS 函数提取为独立模块,便于在其他语言中复现。
var zlib = require('zlib');
// 生成异或密钥数组
function L(e, t) {
if ("1" == t) return [7, 65, 75, 31, 71, 101, 57, 0];
for (var n = [], a = 0, r = t.length; a < r; a += 2)
n.push(e.substr(1 * t.substr(a, 2), 1).charCodeAt());
return n;
}
// XOR 异或运算
function Y(e, t) {
for (var n, a = new Uint8Array(e.length), r = 0, c = e.length; r < c; r++)
n = t[r % t.length], a[r] = e[r].charCodeAt() ^ n;
return a;
}
// Base64 解码辅助
function B(e) {
var t, n, a, r, c, u, i, o = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", s = "", f = 0;
e = e.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while (f < e.length)
r = o.indexOf(e.charAt(f++)), c = o.indexOf(e.charAt(f++)),
u = o.indexOf(e.charAt(f++)), i = o.indexOf(e.charAt(f++)),
t = r << 2 | c >> 4, n = (15 & c) << 4 | u >> 2,
a = (3 & u) << 6 | i, s += String.fromCharCode(t),
64 != u && (s += String.fromCharCode(n)), 64 != i && (s += String.fromCharCode(a));
return s;
}
// 主解密入口
function getDecryptedData(encryptedData, exor, loginToken) {
var a = L(loginToken, exor);
var r = Y(B(encryptedData), a);
var decryptedData = zlib.gunzipSync(r).toString();
return decryptedData;
}
4.2 Python 调用示例
使用 execjs 调用上述 JS 逻辑,结合 requests 发起请求。
import execjs
import requests
import json
# 配置项
NEWS_URL = "https://max.pedata.cn/api/q4x/newsflash/list"
LOGIN_TOKEN = "YOUR_LOGIN_TOKEN_HERE" # 替换为实际 Token
headers = {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"Host": "max.pedata.cn",
"HTTP-X-TOKEN": LOGIN_TOKEN,
"Origin": "https://max.pedata.cn",
"Referer": "https://max.pedata.cn/client/news/newsflash",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
def load_js_context():
with open('pedata_decrypt.js', 'r', encoding='utf-8') as f:
return execjs.compile(f.read())
def decrypt_data(encrypted_data, exor, ctx):
return ctx.call('getDecryptedData', encrypted_data, exor, LOGIN_TOKEN)
def fetch_news():
payload = {
"type": "",
"module": "LP",
"page": {"currentPage": 1, "pageSize": 10}
}
resp = requests.post(NEWS_URL, headers=headers, json=payload)
resp.raise_for_status()
data = resp.json()
encrypted_data = data.get("data")
exor = data.get("exor")
if not encrypted_data or not exor:
raise ValueError("Missing encryption parameters")
ctx = load_js_context()
decrypted_json = decrypt_data(encrypted_data, exor, ctx)
return json.loads(decrypted_json)
if __name__ == '__main__':
try:
news_list = fetch_news()
print(json.dumps(news_list, ensure_ascii=False, indent=2))
except Exception as e:
print(f"Error: {e}")
5. 注意事项
- Token 有效期:
HTTP-X-TOKEN可能有时效性,若请求失败请重新获取。 - 环境依赖:Python 端需安装
requests和execjs库。 - Buffer 兼容性:Node.js
zlib处理Uint8Array时需确保版本 >= v8.0.0。 - 合规性:本方案仅供技术学习,请勿用于非法爬取或商业用途。

