AI逆向 PDD 滑块验证链路全流程分析
PDD 滑块验证链路全流程分析
文档结构分为三部分:
- 先按接口流程逐步说明每个接口使用到的参数。
- 再给出统一参数表,说明参数来源、是否固定、是否加密。
- 最后总结整条链路中使用到的加密类型与具体形式。
最终验证结果

最终返回True则为验证通过
校验验证码响应:{'code': 0, 'leftover': 5, 'result': True}
1. 整体流程
整条链路可以概括为:
获取服务端时间
-> 生成环境签名 anti_content
-> 预检接口,建立外层会话与 salt
-> 构造准备态行为体 captcha_collect
-> 获取验证码
-> 解密验证码数据
-> 生成 captcha_sign
-> 中间态校验
-> 本地解题
-> 构造验证态行为体 captcha_collect
-> 最终提交 user_verify
整条链路围绕四类核心参数运转:
anti_contentcontentcaptcha_collectcaptcha_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 的作用有两层:
- 作为第一次环境签名
anti_content的输入。 - 给后续流程提供时间基线。
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.languageswebdriver的存在方式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. 结论
这条链路的核心不是单点加密,而是三层校验同时成立:
- 外层
content的 AES 会话正确。 - 内层
captcha_collect的gzip + AES(salt派生key/iv)正确。 anti_content、verify_code、captcha_sign、行为体本身在业务上自洽。
如果只复现其中一层,而另外两层不匹配,最终都很难通过。
更多推荐

所有评论(0)