一、目标概述
目标站点: https://m.pinduoduo.com/subject/girlclothes
目标接口: https://apiv2.pinduoduo.com/api/gindex/tf/query_tf_goods_info
加密参数: anti_content
接口类型: GET 请求,参数通过 Query String 传递
二、加密参数特征分析
2.1 3次 Curl 差异分析
通过对比3次同接口 curl 请求,将参数分为三类:
| 分类 | 参数 | 证据 |
|---|---|---|
| 固定参数 | tf_id, page, size | 3次相同,直接硬编码 |
| 固定Headers | 全部 Headers | 3次完全一致 |
| 无Cookie | — | 3次均无Cookie参与 |
| 请求动态参数 | anti_content | 3次均不同,唯一逆向目标 |
2.2 anti_content 字符串特征
0arWfxUkBwVeXqjyXbKt_FKccGEYidHqXxGtuy6PHJFwsYibctFHndFgoJyYttu3fYNFKYd...
- 固定前缀:
0ar - 长度: 约 420-440 字符
- 字符集:
A-Za-z0-9_-(URL-safe Base64 变体) - 每次请求均变化,与业务参数无关
三、加密算法逆向过程
3.1 源码定位
通过浏览器 DevTools XHR 断点 + Call Stack 步入,定位到加密执行器所在文件:
https://mcdn.pinduoduo.com/_next/static/chunks/91b07fdebf35642aa31ae96eddea663f9e2f29c0.9721ffb62aaf725f5ad2.js
文件为 Next.js Webpack Chunk,约 112KB,包含多个 webpack 模块。
3.2 模块依赖追踪
通过断点调试,发现加密值的产生在messagePackSync调用之后

通过源码分析,追踪到完整的依赖链路:
eL/d 模块 (业务调用入口)
│
│ 异步获取服务器时间: GET /api/server/_stm → server_time
│ 首次调用时: c=true, 获取时间后 new n({serverTime: o})
│ 后续调用: e.messagePackSync()
│
├─> r("rx36") ← 透传模块
│ 源码: r.r(n); var e = r("fbeZ"); n.default = e
│ 即: rx36.default === fbeZ的导出
│
└─> r("fbeZ") ← 核心SDK模块(内嵌独立webpack bundle)
导出: 一个匿名函数 function(){...}
该函数 return ut (内部预创建的 it 单例实例)
关键发现:
| 代码位置 | 功能 |
|---|---|
eL/d 模块 (line 383-495) | 业务层调用入口,初始化SDK并调用 messagePackSync() |
rx36 模块 (line 7135-7140) | 透传层,仅 n.default = r("fbeZ") |
fbeZ 模块 (line 496-7134) | 核心SDK,内含独立的 webpack bundle,约 6600 行 |
3.3 原型方法(运行时确认)
这是整个逆向中最重要的部分,是一个加密的代码,调试时可以解密出来:
// 方法通过混淆属性名动态挂载:
it[H("54^6", -278)][H("N)xu", -581) + at(639, "tt&(")] = function(t) {
d = m.now(); W = t;
};
// ↑ 即: it.prototype.updateServerTime = function(t) { ... }
it[H("1[03", -97)][H(")8Bu", -232)] = h;
// ↑ 即: it.prototype.init = h(空函数)
it[H("WWJ$", -661)][at(388, "QYdW")] = h;
// ↑ 即: it.prototype.clearCache = h(同一个空函数)
it[at(658, "QYdW")][at(912, "EGti") + "k"] = function() {
return T.counter++, { PpEgG: function(t) { return t() } }[at(603, "(f2U")](et);
};
// ↑ 即: it.prototype.messagePack = function() { T.counter++; return et(); }
it[H("q]CY", -10)][H("hIzm", -377) + H("SlDP", -87)] = function() {
return new Promise(function(n) {
T.counter++;
n(et());
});
};
// ↑ 即: it.prototype.messagePackSync = function() { return Promise.resolve(et()) }
3.4 加密流程还原
et() 函数(line 3802-3937)内部调用了 20 个数据采集器,按固定顺序拼接,通过调用它生成anti_content。
anti_content 的生成流程如下:
┌─────────────────────────────────────────────────┐
│ 1. 浏览器指纹采集 (20+ Collector) │
│ ├─ Q[]: navigator 信息 │
│ ├─ x[]: screen 分辨率信息 │
│ ├─ O[]: document 信息 │
│ ├─ G[]: location 信息 │
│ ├─ q[], z[]: Canvas/WebGL 指纹 │
│ ├─ N[], J[]: 性能计时数据 │
│ ├─ K[], U[]: 存储探测 │
│ ├─ B[], F[]: DOM 特征 │
│ ├─ I[], D[]: 事件/行为数据 │
│ └─ A[], Y[], V[], T[], M[], j[], Z[], E[], L[]│
└──────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ 2. 数据拼接 (et函数) │
│ ├─ 调用所有 Collector 的 encode 方法 │
│ ├─ 结果拼接成字节数组 │
│ ├─ 构建头部标记 (版本号+数据长度编码) │
│ └─ 拼接: [header] + [1,1,0] + [length] + [data]│
└──────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ 3. 压缩编码 (rt函数 + pako) │
│ ├─ 使用 pako/deflate 压缩字节数组 │
│ ├─ 与 rt() 生成的随机前缀拼接 │
│ └─ URL-safe Base64 编码 │
└──────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ 4. 输出 │
│ "0ar" + Base64Url(compressed_fingerprint_data)│
└─────────────────────────────────────────────────┘
3.5 混淆保护机制
SDK 代码使用了双层字符串混淆保护:
外层混淆(fbeZ 模块级别):
- 字符串数组函数
s(),约 300+ 条目 - RC4 解码器
u(t, n) - 包装函数
c(),d() - 数组旋转 IIFE(line 706-737),通过数学恒等式确定旋转次数
内层混淆(SDK 核心逻辑级别):
- 字符串数组函数
ot(),713 条目 - RC4 解码器
tt(t, n):Base64 解码 → RC4 解密 - 包装函数
H(t, n)=tt(n + 908, t) - 包装函数
at(t, n)=tt(t - 104, n) - 数组旋转 IIFE(line 2172-2201),目标值 866438
四、逆向方案:白盒对抗
4.1 方案选型
白盒对抗与深度环境伪造:
在 Node.js 纯后端部署,通过 VM 沙盒 + 精准补环境的方式,让 SDK 在隔离环境中独立运行。
理由:
- SDK 的浏览器指纹采集极其轻量(仅访问 24 个属性),补环境成本低
- 无需维护浏览器实例,资源消耗远低于路径B
- 可高并发部署
4.2 模块提取
从原始 webpack chunk 中提取 fbeZ 模块,构造 Mini-Loader:
// 1. 在 VM 沙盒中加载原始 JS 文件
const script = new vm.Script(rawSource);
script.runInContext(context);
// 2. 从 webpackJsonp 中提取 fbeZ 模块
const chunks = window.webpackJsonp;
for (const chunk of chunks) {
const modules = chunk[1];
if (modules && modules.fbeZ) {
const moduleObj = { exports: {} };
modules.fbeZ(moduleObj, moduleObj.exports, () => undefined);
const SDK = moduleObj.exports.default;
// SDK 即为 AntiContentSDK 类
}
}
4.3 环境伪造
4.3.1 需求探测
使用 Proxy 拦截网记录 SDK 实际访问的浏览器属性:
function createLoggingProxy(name, target) {
return new Proxy(target, {
get(obj, prop) {
accessedProps.add(`${name}.${String(prop)}`);
return obj[prop];
}
});
}
4.3.2 探测结果
SDK 仅访问以下 24 个浏览器属性:
| 属性 | 返回值 | 用途 |
|---|---|---|
navigator.userAgent | Mobile Safari UA | 设备识别 |
navigator.languages | ['zh-CN', 'zh'] | 语言指纹 |
navigator.plugins | { length: 0 } | 插件探测 |
navigator.webdriver | false | 自动化检测 |
screen.availWidth | 375 | 屏幕指纹 |
screen.availHeight | 812 | 屏幕指纹 |
location.href | 当前页面URL | 来源校验 |
location.port | '' | 端口探测 |
document.cookie | '' | Cookie探测 |
document.referrer | 来源URL | 来源校验 |
document.createElement | Function | DOM指纹 |
document.getElementById | Function | DOM探测 |
document.documentElement | 对象 | 视口信息 |
document.body | 对象 | 视口信息 |
document.documentMode | undefined | IE检测 |
document.addEventListener | Function | 事件监听 |
localStorage.getItem/setItem | Function | 存储探测 |
history.back | Function | 历史记录探测 |
Element | 构造函数 | 类型检测关键 |
4.3.3 关键踩坑
Element未定义:SDK 内部使用instanceof Element做类型检测,必须提供Element构造函数navigator.hasOwnProperty递归:不能写function(p) { return this.hasOwnProperty(p) },会无限递归,需用Object.prototype.hasOwnPropertylocation.port必须为空字符串:HTTPS 默认端口 443 不显示在 URL 中,port属性应为''- Proxy 深度拦截导致爆栈:嵌套 Proxy 对象的
Symbol.toPrimitive调用链会触发栈溢出,改用具体 mock 对象
4.4 最终方案架构
┌──────────────────────────────────────────────┐
│ Node.js 进程 │
│ │
│ ┌────────────────────────────────────┐ │
│ │ VM 沙盒 (vm.createContext) │ │
│ │ │ │
│ │ mockWindow (浏览器环境伪造) │ │
│ │ ├─ navigator (UA/language/...) │ │
│ │ ├─ screen (375x812) │ │
│ │ ├─ location (当前URL) │ │
│ │ ├─ document (createElement/...) │ │
│ │ ├─ Element/HTMLElement/Node │ │
│ │ ├─ performance (timing) │ │
│ │ ├─ localStorage/sessionStorage │ │
│ │ └─ crypto (getRandomValues) │ │
│ │ │ │
│ │ anti_content.js (原始SDK) │ │
│ │ └─> messagePack() → "0ar..." │ │
│ └────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ HTTPS 请求 + anti_content │
│ │ │
│ ▼ │
│ apiv2.pinduoduo.com │
│ → HTTP 200, 数据正常返回 │
└──────────────────────────────────────────────┘
五、验证结果
5.1 Node.js 生成 anti_content
Anti-content result: {
"ok": true,
"value": "0arWfxUkBwVeXqjyXbKt_...",
"length": 422,
"startsWith0ar": true
}
5.2 API 请求验证
HTTP Status: 200
error_code: 1000000 ← 成功
error_msg: N/A
result: [{"goods_id":590800921896,
"goods_name":"真维斯紫色短袖t恤女2025新款夏季半袖小个子超好看印花短款上衣",...}]
结论:Node.js 纯后端生成的 anti_content 可通过服务端校验,数据正常返回。
&spm=1001.2101.3001.5002&articleId=160481648&d=1&t=3&u=1bdf09c3ca5f45a9bb553cc612c40416)
374

被折叠的 条评论
为什么被折叠?



