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 请求参数
参数名位置含义来源是否固定是否加密
pdduidquery业务用户标识外部业务上下文否否
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": "..."
}

参数说明:

参数名位置含义来源是否固定是否加密
ebody当前 body 为加密格式的标志位固定值 true是否
verify_auth_tokenbody当前验证会话令牌外部业务上下文否否
contentbody外层业务体 AES 密文前述业务明文体加密得到否是
abody会话 key 的 RSA 密文keyIv.a 经过 RSA否是
vbody会话 iv 的 RSA 密文keyIv.v 经过 RSA否是
pdduidquery业务用户标识外部业务上下文否否
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_contentbody当前环境签名本地生成否否
sdk_typebodySDK 类型位固定值 1是否
client_timebody当前客户端时间本地时间否否
verify_auth_tokenbody当前验证会话令牌外部业务上下文否否
captcha_signbody滑块附加签名由 semantics[0] 解码得到否否
pdduidquery用户标识外部业务上下文否否

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
deDOM 变化信息本地构造否会进入内层 AES
isbshadowRoot 场景标记本地构造否会进入内层 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": "..."
}

参数说明:

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

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_collect 的 gzip + AES(salt派生key/iv) 正确。
  3. anti_content、verify_code、captcha_sign、行为体本身在业务上自洽。

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

Logo

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

更多推荐