本文记录了对某电商数据分析平台 API 响应加密机制的完整逆向过程,涵盖抓包侦察、参数验证、解密定位、算法识别与本地还原。该加密方案采用AES-ECB + PKCS7 + gzip的组合模式,密钥通过多层 toString(36) 编码混淆,属于典型的前端响应加密防护,适合作为响应解密类逆向的入门案例。
一、目标概况
| 项目 | 说明 |
|---|---|
| 目标平台 | 某电商数据分析平台 |
| 页面入口 | aHR0cHM6Ly93d3cuY2hhbm1hbWEuY29tL3Byb21vdGlvblJhbmsv |
| 数据接口 | aHR0cHM6Ly9hcGktc2VydmljZS5jaGFubWFtYS5jb20vdjYvcHJvZHVjdC9zZWFyY2g= |
| 鉴权方式 | 请求头固定参数:x-client-hash/x-client-id/x-encrypt-version/x-platform-id |
| 加密方式 | 响应体加密:AES-ECB + PKCS7 解密 → WordArray 转 Uint8Array → gzip 解压 → JSON |
| 分析日期 | 2026.07.29 |
URL 采用 Base64 编码脱敏,解码后可得原始地址。
逆向目标:还原接口响应中密文数据的解密逻辑,使本地可正常获取明文数据。
二、抓包侦察
2.1 捕获目标请求
打开浏览器 DevTools → Network 面板,通过翻页操作触发列表数据请求,定位到search接口。
请求头(关键部分):
x-client-hash: <固定哈希值> x-client-id: <固定ID> x-client-version: 1 x-encrypt-version: 2 x-platform-id: 10000请求体(明文 JSON):
{"multi_category_id":"-1","keyword":"","page":2,"size":50,"sort":"duration_volume","day_type":1,"platform":"<平台标识>","order_by":"desc"}响应体(密文,状态码 200):
2.2 初步判断
通过观察请求体与响应体,可以得出两个关键结论:
- 数据层存在加密:响应体为密文信息,需逆向解密方法才能获取明文数据
- 请求层无动态鉴权:请求头中的参数均为固定值,无需逆向生成逻辑
2.3 参数固定性验证
拦截多页请求,对比各参数变化情况:
| 参数 | 多次请求对比 | 结论 |
|---|---|---|
x-client-hash | 始终相同 | 固定值(硬编码在代码中) |
x-client-id | 始终相同 | 固定值 |
x-encrypt-version | 始终相同 | 固定值 |
x-platform-id | 始终相同 | 固定值 |
结论:请求头参数均为固定值,无需逆向生成逻辑。核心逆向目标为响应体解密。
三、解密定位
3.1 Hook 定位
在 DevTools Sources 面板中,注入 Hook 脚本拦截JSON.parse方法的调用,定位解密入口:
Hook 触发后出现明文信息,根据堆栈调用信息向上追溯,找到解密方法的调用位置。
3.2 断点验证
在解密方法处设置断点,观察变量值:
此时可以清晰看到完整的解密链路:
密文 e → AES 解密 → WordArray a → u(a) 转换 → Uint8Array o → ungzip 解压 → 明文 r → JSON.parse四、解密逻辑分析
4.1 源码还原
从断点位置提取的关键代码如下:
// AES 密钥推导n=s.enc.Utf8.parse(27..toString(36).toLowerCase().split("").map(function(t){returnString.fromCharCode(t.charCodeAt()+-39);}).join("")+24901..toString(36).toLowerCase()+33..toString(36).toLowerCase().split("").map(function(t){returnString.fromCharCode(t.charCodeAt()+-39);}).join("")+976..toString(36).toLowerCase()+20..toString(36).toLowerCase().split("").map(function(t){returnString.fromCharCode(t.charCodeAt()+-39);}).join("")+(function(){vart=Array.prototype.slice.call(arguments),e=t.shift();returnt.reverse().map(function(t,n){returnString.fromCharCode(t-e-24-n);}).join("");})(10,127,154,91,151,91,136)+11..toString(36).toLowerCase()+13..toString(36).toLowerCase().split("").map(function(t){returnString.fromCharCode(t.charCodeAt()+-13);}).join(""));// AES-ECB 解密a=s.AES.decrypt(e,n,{mode:s.mode.ECB,padding:s.pad.Pkcs7});// WordArray → Uint8Arrayo=u(a);// gzip 解压r=c.ungzip(o,{to:"string"});// JSON 解析returnJSON.parse(r);4.2 逻辑拆解
将上述代码逐步拆解,解密流程如下:
┌─────────────────────────────────────────────────────────┐ │ Step 1: 推导 AES 密钥 │ │ key = 多层 toString(36) 编码 + charCode 偏移拼接 │ │ 最终结果: 16字节字符串 (AES-128) │ ├─────────────────────────────────────────────────────────┤ │ Step 2: AES-ECB 解密 │ │ plaintext = AES.decrypt(ciphertext, key, { │ │ mode: ECB, padding: PKCS7 │ │ }) │ │ // 输出为 CryptoJS WordArray 对象 │ ├─────────────────────────────────────────────────────────┤ │ Step 3: WordArray → Uint8Array │ │ bytes = u(wordArray) │ │ // 逐字提取 4 字节,按大端序拆分 │ ├─────────────────────────────────────────────────────────┤ │ Step 4: gzip 解压 │ │ jsonStr = pako.ungzip(bytes, { to: "string" }) │ ├─────────────────────────────────────────────────────────┤ │ Step 5: JSON 解析 │ │ data = JSON.parse(jsonStr) │ └─────────────────────────────────────────────────────────┘4.3 子函数验证
密钥推导:将混淆代码在 Node.js 中执行,得到 AES 密钥为 16 字节字符串,对应 AES-128。
u 方法(WordArray → Uint8Array 转换):
functionu(t){vare,n,i=t.words.length,a=newUint8Array(t.sigBytes),o=0;for(n=0;n<i;n++)e=t.words[n],a[o++]=e>>24,a[o++]=e>>16&255,a[o++]=e>>8&255,a[o++]=255&e;returna;}该函数将 CryptoJS 的 WordArray(32 位整数数组)按大端序逐字节拆分为 Uint8Array,属于标准转换操作。
AES 解密验证:使用推导出的密钥对密文进行 AES-ECB + PKCS7 解密,解密后的数据经u方法转换后为 gzip 压缩的二进制数据,符合预期。
gzip 解压验证:使用pako库对解密后的 Uint8Array 进行解压,得到明文 JSON 字符串,与浏览器中观察到的明文数据完全一致。
五、本地还原
5.1 Node.js 实现
constCryptoJS=require('crypto-js');constpako=require('pako');functionu(t){vare,n,i=t.words.length,a=newUint8Array(t.sigBytes),o=0;for(n=0;n<i;n++)e=t.words[n],a[o++]=e>>24,a[o++]=e>>16&255,a[o++]=e>>8&255,a[o++]=255&e;returna;}functiondecryptResponse(ciphertext){constkey=CryptoJS.enc.Utf8.parse('<推导出的AES密钥>');constdecrypted=CryptoJS.AES.decrypt(ciphertext,key,{mode:CryptoJS.mode.ECB,padding:CryptoJS.pad.Pkcs7});constbytes=u(decrypted);constjsonStr=pako.ungzip(bytes,{to:'string'});returnJSON.parse(jsonStr);}constciphertext='...';// 接口返回的密文console.log(decryptResponse(ciphertext));5.2 Python 实现
fromCrypto.CipherimportAESfromCrypto.Util.Paddingimportunpadimportgzipimportjsonimportbase64defdecrypt_response(ciphertext_b64):key=b'<推导出的AES密钥>'ciphertext=base64.b64decode(ciphertext_b64)cipher=AES.new(key,AES.MODE_ECB)plaintext=unpad(cipher.decrypt(ciphertext),AES.block_size)json_str=gzip.decompress(plaintext).decode('utf-8')returnjson.loads(json_str)ciphertext='...'# 接口返回的密文(Base64编码)print(decrypt_response(ciphertext))5.3 验证结果
通过本地还原的解密方法,能够正常解密接口返回的密文信息,得到明文数据:
六、总结
加密方案特征
| 维度 | 分析 |
|---|---|
| 算法类型 | 标准 AES-128-ECB + PKCS7(无魔改),配合 gzip 压缩 |
| 密钥来源 | 前端 JS 中通过 toString(36) + charCode 偏移多层混淆,但可本地执行还原 |
| 防护强度 | 较低——密钥推导逻辑硬编码在前端,ECB 模式无 IV,算法均为标准实现 |
| 压缩层 | gzip 压缩(服务端加密前压缩,客户端解密后解压) |
| 请求鉴权 | 无动态鉴权参数,请求头均为固定值 |
逆向思路回顾
抓包侦察 → 参数验证(固定性)→ Hook JSON.parse 定位 → 断点确认解密链路 → 源码提取 → 密钥推导还原 → 子函数验证(u方法/ungzip)→ 本地还原 → 一致性验证本案例属于前端响应加密的入门级难度,核心难点在于定位解密入口和还原混淆的密钥推导逻辑。解密链路清晰(AES → WordArray转换 → gzip解压 → JSON),算法均为标准实现,还原过程非常直接。
免责声明:本文仅用于技术学习与研究目的,请勿将相关技术用于非法用途。数据采集行为应遵守《网络安全法》等相关法律法规,尊重网站的服务条款和 robots.txt 协议。