PDD 滑块验证链路全流程分析

文档结构分为三部分:

  1. 先按接口流程逐步说明每个接口使用到的参数。
  2. 再给出统一参数表,说明参数来源、是否固定、是否加密。
  3. 最后总结整条链路中使用到的加密类型与具体形式。

最终验证结果
请添加图片描述
请添加图片描述
最终返回True则为验证通过

校验验证码响应:{'code': 0, 'leftover': 5, 'result': True}

1. 整体流程

整条链路可以概括为:

获取服务端时间
-> 生成环境签名 anti_content
-> 预检接口,建立外层会话与 salt
-> 构造准备态行为体 captcha_collect
-> 获取验证码
-> 解密验证码数据
-> 生成 captcha_sign
-> 中间态校验
-> 本地解题
-> 构造验证态行为体 captcha_collect
-> 最终提交 user_verify

整条链路围绕四类核心参数运转:

  • anti_content
  • content
  • captcha_collect
  • captcha_sign

2. 按接口流程讲参数

2.1 获取服务端时间接口

请求:

GET /proxy/api/api/server/_stm
2.1.1 请求参数
参数名 位置 含义 来源 是否固定 是否加密
pdduid query 业务用户标识 外部业务上下文
2.1.2 返回参数
参数名 含义 来源 是否固定 是否加密
server_time 服务端时间基线 服务端返回
2.1.3 这一阶段的作用

server_time 的作用有两层:

  1. 作为第一次环境签名 anti_content 的输入。
  2. 给后续流程提供时间基线。

2.2 生成环境签名

这一阶段本身不是 HTTP 接口,而是为后续接口准备参数。

入口逻辑:

function get_anti(sstime){
    wt = getWt({ serverTime: sstime });
    anti = wt.messagePack()
    return anti
}
2.2.1 输入参数
参数名 含义 来源 是否固定 是否加密
serverTime 环境签名时间基线 上一步服务端返回
2.2.2 输出参数
参数名 含义 来源 是否固定 是否加密
anti_content 浏览器环境签名 本地环境采集与编码结果 属于编码结果,不是普通明文
2.2.3 参数来源说明

anti_content 会综合以下环境要素:

  • 当前页面 URL
  • 屏幕尺寸
  • 浏览器 UA
  • navigator.languages
  • webdriver 的存在方式
  • window.Buffer 是否暴露
  • document.referrer
  • cookie 指纹态
  • 可见性状态
  • 媒体与传感器能力

因此:

  • 它不是固定值。
  • 也不是随机值。
  • 它是“当前环境采集结果”的编码产物。

2.3 预检接口

请求:

POST /proxy/api/api/phantom/vc_pre_ck

这一阶段既要传环境签名,也要建立后续外层加密会话。

2.3.1 先构造业务明文体

业务明文体形态:

{
  "anti_content": "...",
  "sdk_type": 1,
  "client_time": 177...
}

对应参数说明:

参数名 位置 含义 来源 是否固定 是否加密
anti_content 业务明文体 当前环境签名 上一步生成 会进入外层 AES
sdk_type 业务明文体 SDK 类型标识 固定值 1 会进入外层 AES
client_time 业务明文体 当前客户端毫秒时间 本地时间 会进入外层 AES
2.3.2 再构造外层加密参数

外层业务体会用一组随机会话参数进行 AES 加密:

function encryptRequestData(payload, keyIv) {
  const actualKeyIv = keyIv || generateRandomKeyIv();
  return {
    keyIv: actualKeyIv,
    content: encryptContent(JSON.stringify(payload), actualKeyIv.a, actualKeyIv.v)
  };
}

随机会话参数形态:

function generateEy() {
  return {
    a: randomFromCharset(16),
    v: randomFromCharset(16)
  };
}

因此这一步新增了:

参数名 位置 含义 来源 是否固定 是否加密
keyIv.a 本地会话参数 外层 AES key 随机生成 原值不直接上送
keyIv.v 本地会话参数 外层 AES iv 随机生成 原值不直接上送
content 请求 body 外层业务体密文 业务明文体经 AES 得到
2.3.3 还要附带 RSA 包装后的 a / v

预检阶段除了 content,还会单独上传 RSA 包装后的 a / v

function buildEncryptedEyParams(ey) {
  return {
    a: rsaEncryptBase64(ey.a),
    v: rsaEncryptBase64(ey.v)
  };
}

因此请求体最终形态:

{
  "e": true,
  "verify_auth_token": "...",
  "content": "...",
  "a": "...",
  "v": "..."
}

参数说明:

参数名 位置 含义 来源 是否固定 是否加密
e body 当前 body 为加密格式的标志位 固定值 true
verify_auth_token body 当前验证会话令牌 外部业务上下文
content body 外层业务体 AES 密文 前述业务明文体加密得到
a body 会话 key 的 RSA 密文 keyIv.a 经过 RSA
v body 会话 iv 的 RSA 密文 keyIv.v 经过 RSA
pdduid query 业务用户标识 外部业务上下文
2.3.4 预检接口返回参数
参数名 含义 来源 是否固定 是否加密
salt 行为体内层加密种子 服务端返回
server_time 验证码阶段时间基线 服务端返回

其中:

  • salt 后续用于加密 captcha_collect
  • 第二个 server_time 后续作为 ts

2.4 构造准备态行为体并获取验证码

请求:

POST /proxy/api/api/phantom/obtain_captcha

这一阶段会先构造“准备态” captcha_collect

2.4.1 准备态行为体明文

形态:

{
  "v": "c",
  "ts": 177...,
  "t0": 177...,
  "tp": 1,
  "ua": "...",
  "rf": "...",
  "hl": "000000000001010",
  "sc": {"w":2560,"h":1392},
  "ihs": 1,
  "platform": 1
}

参数说明:

参数名 位置 含义 来源 是否固定 是否加密
v 准备态行为体 协议版本位 固定值 "c" 会进入内层 AES
ts 准备态行为体 当前验证码阶段时间基线 预检返回的 server_time 会进入内层 AES
t0 准备态行为体 准备态采集时间 本地时间 会进入内层 AES
tp 准备态行为体 协议类型位 固定值 1 会进入内层 AES
ua 准备态行为体 浏览器 UA 固定浏览器上下文 半固定 会进入内层 AES
rf 准备态行为体 来源页 URL 当前页面上下文 会进入内层 AES
hl 准备态行为体 能力串 固定上下文 会进入内层 AES
sc 准备态行为体 屏幕尺寸 固定上下文 会进入内层 AES
ihs 准备态行为体 固定状态位 固定值 1 会进入内层 AES
platform 准备态行为体 平台位 固定值 1 会进入内层 AES
2.4.2 captcha_collect 的加密

加密逻辑:

function encryptCaptchaCollect(payload, salt) {
  const { aes_key, aes_iv } = deriveSaltKeyIv(salt);
  const binary = gzipToBinary(JSON.stringify(payload));
  return CryptoJS.AES.encrypt(
    binary,
    CryptoJS.enc.Utf8.parse(aes_key),
    { iv: CryptoJS.enc.Utf8.parse(aes_iv) }
  ).toString();
}

也就是:

准备态 captcha_collect 明文
-> JSON.stringify
-> gzip
-> AES(salt派生key/iv)
-> 得到 captcha_collect 密文
2.4.3 获取验证码阶段业务明文体

形态:

{
  "anti_content": "...",
  "captcha_collect": "...",
  "useNativeAntiToken": true,
  "salt": "..."
}

参数说明:

参数名 位置 含义 来源 是否固定 是否加密
anti_content 业务明文体 当前环境签名 本地生成 会进入外层 AES
captcha_collect 业务明文体 准备态行为体密文 前一步内层加密得到 本身已加密,随后还会进入外层 AES
useNativeAntiToken 业务明文体 使用原生 anti 标识 固定值 true 会进入外层 AES
salt 业务明文体 当前行为体加密种子 预检返回 会进入外层 AES

随后这一整块明文体再做外层 AES,形成最终 content

2.5 解密验证码数据

获取验证码接口返回的 content,需要用当前会话 keyIv.a / keyIv.v 解密。

代码:

function decryptContent(content, key, iv) {
  return CryptoJS.AES.decrypt(
    content,
    CryptoJS.enc.Utf8.parse(key),
    { iv: CryptoJS.enc.Utf8.parse(iv) }
  ).toString(CryptoJS.enc.Utf8);
}
2.5.1 输入参数
参数名 含义 来源 是否固定 是否加密
content 验证码返回体密文 服务端返回
keyIv.a 外层 AES key 预检阶段生成 原值未上送
keyIv.v 外层 AES iv 预检阶段生成 原值未上送
2.5.2 解开后得到的关键字段
参数名 含义 来源 是否固定 是否加密
type 验证码题型编号 服务端返回内容 返回前在外层 AES 中
pictures 图片字段 服务端返回内容 返回前在外层 AES 中
semantics 语义字段数组 服务端返回内容 返回前在外层 AES 中

2.6 生成 captcha_sign

只有滑块题型需要 captcha_sign

其来源是:

  • semantics[0]

解码逻辑:

function decodeSignFromSemantics0(semantics0) {
  const stage1 = decodePddCaptchaField(semantics0);
  if (!stage1) {
    return '';
  }
  return decodeBase64Utf8(stage1);
}

因此参数关系如下:

参数名 含义 来源 是否固定 是否加密
semantics[0] 滑块语义字段 服务端返回 返回前在外层 AES 中
captcha_sign 滑块签名 semantics[0] 解码得到 解码结果本身不是加密参数

2.7 中间态校验接口

请求:

POST /proxy/api/api/phantom/vc_mid_render

这个接口不走 content 包装,而是直接提交明文 JSON:

{
  "anti_content": "...",
  "sdk_type": 1,
  "client_time": 177...,
  "verify_auth_token": "...",
  "captcha_sign": "..."
}

参数说明:

参数名 位置 含义 来源 是否固定 是否加密
anti_content body 当前环境签名 本地生成
sdk_type body SDK 类型位 固定值 1
client_time body 当前客户端时间 本地时间
verify_auth_token body 当前验证会话令牌 外部业务上下文
captcha_sign body 滑块附加签名 semantics[0] 解码得到
pdduid query 用户标识 外部业务上下文

2.8 本地解题阶段

这一阶段不发请求,但会产生后续提交所需的关键参数。

2.8.1 滑块题型

verify_code 来源:

背景图 + 缺口图
-> 图像匹配
-> 缩放修正
-> 偏移修正

关键代码:

verify_code = int(match_bg(bgimg, queimg) * int(self.imageSize['width']) / 320)
verify_code = verify_code + 48 - 14

因此:

参数名 含义 来源 是否固定 是否加密
verify_code 滑块最终答案 图像识别与偏移修正结果 进入外层业务体后会被 AES
2.8.2 空间推理题型

verify_code 是点击坐标数组字符串。

参数名 含义 来源 是否固定 是否加密
verify_code 空间推理答案 本地整理的点击坐标 进入外层业务体后会被 AES
2.8.3 计算题型

verify_code 是人工答案。

参数名 含义 来源 是否固定 是否加密
verify_code 计算题答案 人工输入 进入外层业务体后会被 AES

2.9 构造验证态行为体

验证态 captcha_collect 比准备态复杂得多,核心是补齐真实操作轨迹。

滑块验证态的典型字段如下:

{
  "v": "c",
  "ts": 177...,
  "t1": 177...,
  "t2": 177...,
  "tp": 1,
  "ua": "...",
  "rf": "",
  "platform": 1,
  "hl": "000000000001010",
  "sc": {"w":2560,"h":1392},
  "ihs": 1,
  "imageSize": {"width":512,"height":256},
  "del": [[...]],
  "mel": [[...]],
  "uel": [[...]],
  "mell": [[[...]]],
  "de": [[{"class":"slide-ani","id":null}]],
  "isb": 1
}
2.9.1 轨迹生成相关代码
def generate_dynamic_sequence(steps, start: float, end: float):
    ...

def get_djtime(nowtime, times=1):
    ...

NOW_MS = int(time.time() * 1000)
start_point = [random.randint(50, 100), random.randint(200, 500)]
end_point = [start_point[0] + verify_code, start_point[0] + random.randint(-10, 10)]

mels = generate_dynamic_sequence(50, start_point[0], end_point[0])
mel = []
for i, rx in enumerate(mels):
    ry = start_point[1]
    ry += random.randint(-5, 5)
    mel.append([rx, ry, get_djtime(NOW_MS, i)])
2.9.2 滑块验证态行为体参数说明
参数名 含义 来源 是否固定 是否加密
t1 行为开始时间 本地时间 会进入内层 AES
t2 行为结束时间 本地时间 会进入内层 AES
del 按下起点 本地行为构造 会进入内层 AES
mel 主移动轨迹 本地行为构造 会进入内层 AES
uel 抬起点 本地行为构造 会进入内层 AES
mell 分组轨迹 本地行为构造 会进入内层 AES
de DOM 变化信息 本地构造 会进入内层 AES
isb shadowRoot 场景标记 本地构造 会进入内层 AES

计算题还会额外出现:

参数名 含义 来源 是否固定 是否加密
cel 输入行为事件 人工答案构造 会进入内层 AES
2.9.3 验证态行为体的加密

和准备态一致,仍然是:

验证态 captcha_collect 明文
-> JSON.stringify
-> gzip
-> AES(salt派生key/iv)
-> 得到 captcha_collect 密文

2.10 最终提交接口

请求:

POST /proxy/api/api/phantom/user_verify
2.10.1 最终业务明文体

形态:

{
  "anti_content": "...",
  "useNativeAntiToken": true,
  "salt": "...",
  "captcha_collect": "...",
  "verify_code": "...",
  "captcha_sign": "..."
}

参数说明:

参数名 位置 含义 来源 是否固定 是否加密
anti_content 业务明文体 当前环境签名 本地生成 会进入外层 AES
useNativeAntiToken 业务明文体 使用原生 anti 标志 固定值 true 会进入外层 AES
salt 业务明文体 当前行为体加密种子 预检返回 会进入外层 AES
captcha_collect 业务明文体 验证态行为体密文 内层加密结果 本身已加密,随后再进入外层 AES
verify_code 业务明文体 当前题目答案 本地解题结果 会进入外层 AES
captcha_sign 业务明文体 滑块签名 语义字段解码结果 滑块题型下不固定,其他题型为空 会进入外层 AES
2.10.2 最终请求体
{
  "e": true,
  "verify_auth_token": "...",
  "content": "..."
}

参数说明:

参数名 位置 含义 来源 是否固定 是否加密
e body 加密标志位 固定值 true
verify_auth_token body 当前验证会话令牌 外部业务上下文
content body 最终业务体 AES 密文 最终业务明文体经外层 AES 得到
pdduid query 用户标识 外部业务上下文

3. 参数总表

下面把整条链路中的核心参数统一汇总。

参数名 所在阶段 含义 来源 是否固定 是否加密 加密位置 备注
pdduid 全流程 用户标识 外部业务上下文 所有接口 query
verify_auth_token 核心请求 验证会话令牌 外部业务上下文 所有关键 body 都会带
server_time 时间获取、预检返回 服务端时间基线 服务端返回 用于 anti 与 ts
anti_content 多个关键请求 环境签名 本地环境采集 特殊编码 本地生成 anti 时 非普通明文
sdk_type 预检、中间态 SDK 类型位 固定值 1 预检中会被 AES,中间态不加密 预检外层业务体
client_time 预检、中间态 客户端毫秒时间 本地时间 预检中会被 AES,中间态不加密 预检外层业务体
keyIv.a 外层会话 外层 AES key 随机生成 原值不上送 本地构造外层会话时
keyIv.v 外层会话 外层 AES iv 随机生成 原值不上送 本地构造外层会话时
a 预检请求 keyIv.a 的 RSA 密文 keyIv.a 派生 预检请求出站前
v 预检请求 keyIv.v 的 RSA 密文 keyIv.v 派生 预检请求出站前
content 核心请求 外层业务体密文 业务明文体外层 AES 每次核心请求出站前
salt 预检返回、后续业务体 行为体加密种子 服务端返回 不直接加密 行为体内层加密前参与派生;之后随外层业务体再次 AES 但会再进入外层 AES
v 行为体 协议版本位 固定值 "c" 内层 AES 行为体内层加密前
ts 行为体 验证码阶段时间基线 预检返回时间 内层 AES 行为体内层加密前
t0 准备态行为体 准备态时间 本地时间 内层 AES 行为体内层加密前
t1 验证态行为体 行为开始时间 本地时间 内层 AES 行为体内层加密前
t2 验证态行为体 行为结束时间 本地时间 内层 AES 行为体内层加密前
tp 行为体 协议位 固定值 1 内层 AES 行为体内层加密前
ua 行为体 浏览器 UA 固定浏览器上下文 半固定 内层 AES 行为体内层加密前
rf 行为体 来源页 页面上下文或空 内层 AES 行为体内层加密前
hl 行为体 能力串 固定上下文 内层 AES 行为体内层加密前
sc 行为体 屏幕尺寸 固定上下文 内层 AES 行为体内层加密前
ihs 行为体 状态位 固定值 1 内层 AES 行为体内层加密前
platform 行为体 平台位 固定值 1 内层 AES 行为体内层加密前
imageSize 滑块行为体 验证码组件尺寸 固定上下文 内层 AES 行为体内层加密前
del 滑块行为体 按下起点 本地构造 内层 AES 行为体内层加密前
mel 滑块行为体 主移动轨迹 本地构造 内层 AES 行为体内层加密前
uel 滑块行为体 抬起点 本地构造 内层 AES 行为体内层加密前
mell 滑块行为体 分组轨迹 本地构造 内层 AES 行为体内层加密前
de 滑块行为体 DOM 变化信息 本地构造 内层 AES 行为体内层加密前
isb 滑块行为体 shadowRoot 标记 本地构造 内层 AES 行为体内层加密前
cel 计算题行为体 输入行为事件 人工答案构造 内层 AES 行为体内层加密前
captcha_collect 获取验证码、最终提交 行为体密文 行为体明文经 gzip + AES 获取验证码前、最终提交前 之后还会进外层 AES
type 验证码数据 题型编号 服务端返回 返回前在外层 AES 中 服务端返回验证码数据时
pictures 验证码数据 图片字段 服务端返回 返回前在外层 AES 中 服务端返回验证码数据时 还需再做字段级解码
semantics 验证码数据 语义字段数组 服务端返回 返回前在外层 AES 中 服务端返回验证码数据时
captcha_sign 滑块题型 滑块签名 semantics[0] 解码 最终进入外层 AES 中间态接口前为明文;最终提交前进入外层业务体 中间态时明文
verify_code 最终提交 题目答案 本地解题结果 最终进入外层 AES 最终提交业务体构造时
useNativeAntiToken 获取验证码、最终提交 原生 anti 标志 固定值 true 会进入外层 AES 获取验证码、最终提交业务体构造时
e 核心请求 加密标志位 固定值 true

4. 使用到的加密与编码总结

整条链路里实际用了四类技术。

4.1 外层业务包 AES

形式:

业务 JSON
-> JSON.stringify
-> AES-CBC/PKCS7
-> content

使用位置:

  • 预检接口
  • 获取验证码接口
  • 最终提交接口

加密位置:

  • 每次核心接口发送前,把当前业务明文体整体做外层 AES。

部分加密代码组成:

function buildContentFromR(r, keyIv) {
  const encrypted = encryptRequestData(r, keyIv);
  return {
    keyIv: encrypted.keyIv,
    content: encrypted.content
  };
}

function encryptRequestData(payload, keyIv) {
  const actualKeyIv = keyIv || generateRandomKeyIv();
  return {
    keyIv: actualKeyIv,
    content: encryptContent(JSON.stringify(payload), actualKeyIv.a, actualKeyIv.v)
  };
}

作用:

  • 保护业务参数整体不明文传输。

4.2 会话参数 a / v 的 RSA 包装

形式:

随机生成的会话 key/iv
-> RSA 公钥加密
-> a / v

使用位置:

  • 预检接口

加密位置:

  • 预检接口发送前,对当前外层 AES 会话参数 keyIv.a / keyIv.v 做 RSA 包装。

部分加密代码组成:

function rsaEncryptBase64(plaintext) {
  return crypto.publicEncrypt(
    {
      key: RSA_PUBLIC_KEY,
      padding: crypto.constants.RSA_PKCS1_PADDING
    },
    Buffer.from(plaintext, 'utf8')
  ).toString('base64');
}

function buildEncryptedEyParams(ey) {
  return {
    a: rsaEncryptBase64(ey.a),
    v: rsaEncryptBase64(ey.v)
  };
}

作用:

  • 把外层 AES 的 key / iv 交给服务端。

4.3 行为体 captcha_collect 的内层加密

形式:

captcha_collect
-> JSON.stringify
-> gzip
-> AES-CBC/PKCS7

key/iv 来源:

  • 不是随机 ey
  • 而是服务端 salt 派生

使用位置:

  • 获取验证码前的准备态行为体
  • 最终提交前的验证态行为体

加密位置:

  • 准备态行为体构造完成后、放入取验证码业务体之前。
  • 验证态行为体构造完成后、放入最终提交业务体之前。

部分加密代码组成:

function encryptCaptchaCollect(payload, salt) {
  const { aes_key, aes_iv } = deriveSaltKeyIv(salt);
  const binary = gzipToBinary(JSON.stringify(payload));
  return CryptoJS.AES.encrypt(
    binary,
    CryptoJS.enc.Utf8.parse(aes_key),
    { iv: CryptoJS.enc.Utf8.parse(aes_iv) }
  ).toString();
}

salt 派生部分代码组成:

function deriveSaltKeyIv(salt) {
  const result = {
    aes_key: DEFAULT_SALT_KEY,
    aes_iv: DEFAULT_SALT_IV
  };
  ...
  return result;
}

作用:

  • 保护行为体细节,并增加行为体协议强度。

4.4 环境签名 anti_content 的编码

形式:

环境采集
-> 按固定顺序打包
-> 字节化
-> deflate
-> 自定义字符编码
-> 0as 前缀

使用位置:

  • 预检接口
  • 获取验证码接口
  • 中间态校验接口
  • 最终提交接口

加密位置:

  • 每次需要提交环境签名时,先在本地采集环境,再生成 anti_content,随后作为业务字段参与请求。

部分代码组成:

function get_anti(sstime){
    wt = getWt({ serverTime: sstime });
    anti = wt.messagePack()
    return anti
}

结构组成:

serverTime
-> messagePack()
-> 环境 collector 打包
-> deflate
-> 自定义编码
-> 0as...

作用:

  • 证明当前环境像真实浏览器。

4.5 字段级自定义解码

这不属于传统加密,但属于协议编码的一部分。

图片字段

形式:

自定义字符表编码
-> 8 字符块解码
-> 5 字节输出

作用:

  • 还原验证码图片内容。

部分代码组成:

function decodePicture(encoded) {
  if (!encoded || encoded.length % 8 !== 0) {
    return null;
  }
  ...
  return out
    .join('')
    .replace('#', '')
    .replace('@?', '')
    .replace('*&%', '')
    .replace('<$|>', '');
}
captcha_sign

形式:

semantics[0]
-> 自定义字段解码
-> base64 utf8 decode
-> captcha_sign

作用:

  • 还原滑块签名。

部分代码组成:

function decodeSignFromSemantics0(semantics0) {
  const stage1 = decodePddCaptchaField(semantics0);
  if (!stage1) {
    return '';
  }
  return decodeBase64Utf8(stage1);
}

5. 结论

这条链路的核心不是单点加密,而是三层校验同时成立:

  1. 外层 content 的 AES 会话正确。
  2. 内层 captcha_collectgzip + AES(salt派生key/iv) 正确。
  3. anti_contentverify_codecaptcha_sign、行为体本身在业务上自洽。

如果只复现其中一层,而另外两层不匹配,最终都很难通过。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐