【JS逆向】拼多多 anti_content 加密参数逆向(20260424)

一、目标概述

目标站点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_idpagesize3次相同,直接硬编码
固定Headers全部 Headers3次完全一致
无Cookie3次均无Cookie参与
请求动态参数anti_content3次均不同,唯一逆向目标

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 在隔离环境中独立运行。

理由:

  1. SDK 的浏览器指纹采集极其轻量(仅访问 24 个属性),补环境成本低
  2. 无需维护浏览器实例,资源消耗远低于路径B
  3. 可高并发部署

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.userAgentMobile Safari UA设备识别
navigator.languages['zh-CN', 'zh']语言指纹
navigator.plugins{ length: 0 }插件探测
navigator.webdriverfalse自动化检测
screen.availWidth375屏幕指纹
screen.availHeight812屏幕指纹
location.href当前页面URL来源校验
location.port''端口探测
document.cookie''Cookie探测
document.referrer来源URL来源校验
document.createElementFunctionDOM指纹
document.getElementByIdFunctionDOM探测
document.documentElement对象视口信息
document.body对象视口信息
document.documentModeundefinedIE检测
document.addEventListenerFunction事件监听
localStorage.getItem/setItemFunction存储探测
history.backFunction历史记录探测
Element构造函数类型检测关键
4.3.3 关键踩坑
  1. Element 未定义:SDK 内部使用 instanceof Element 做类型检测,必须提供 Element 构造函数
  2. navigator.hasOwnProperty 递归:不能写 function(p) { return this.hasOwnProperty(p) },会无限递归,需用 Object.prototype.hasOwnProperty
  3. location.port 必须为空字符串:HTTPS 默认端口 443 不显示在 URL 中,port 属性应为 ''
  4. 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 可通过服务端校验,数据正常返回。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值