免责声明: 本文所有分析均基于公开可访问的前端 JS 代码及顶象官网演示页(https://www.dingxiang-inc.com/business/captcha),仅用于安全研究、学习与了解验证码防护机制。文中所有接口地址来自官方公开演示环境,不涉及任何第三方业务系统。请勿将本文技术用于任何未授权的系统,违者后果自负。
一、背景介绍
顶象(dingxiang-inc)是国内主流的人机验证方案提供商,本文以滑动拼图验证码为研究对象,分析其前端安全机制。
基本交互逻辑:
- 前端携带
ak、aid、c(动态凭证)等参数请求加载接口,获取乱序背景图路径(p1)、滑块图路径(p2)及会话标识sid - 还原乱序背景图:根据背景图文件名通过特定置换算法解密列块顺序,重新拼接为正确图像
- 使用 OpenCV 多策略模板匹配,识别滑块缺口在背景图中的水平位置(缺口距左边缘的像素距离)
- 将坐标传入 Node.js 脚本,通过 SDK 内部逻辑生成加密轨迹参数
ac - 携带
sid、aid、x、y、ac、c等提交验证接口,服务端返回success: true/false
研究核心难点:
c参数动态生成(需实时请求 c1 接口获取,有效期短,不可复用)- 背景图乱序还原(列块顺序由文件名加密编码,需逆向置换算法)
- 缺口位置识别(背景图有干扰纹理,单一策略易误匹配,需多策略融合)
ac参数生成(加密轨迹,依赖 SDK JS,需 Node.js 环境调用)
二、整体流程图
获取 c 参数(请求 c1 接口)
↓
加载接口(GET /api/a)
↓ 返回 sid、p1(背景图路径)、p2(滑块图路径)、y 坐标
下载乱序背景图 + 滑块图(RGBA PNG)
↓
从 p1 文件名解密列块顺序 → 还原背景图
↓
OpenCV 多策略识别缺口 x 坐标 → 坐标系换算(400px → 380px)
↓
调用 Node.js 生成 ac 加密轨迹
↓
验证接口(POST /api/v1)→ success: true/false
三、抓包分析
3.1 c 参数获取接口(c1)
请求:
GET https://constid.dingxiang-inc.com/udid/c1
关键请求参数:
| 参数 | 来源 | 说明 |
|---|---|---|
| aid | 客户端生成 | 格式:dx-{时间戳ms}-{8位随机数}-{实例序号},每次不同 |
| ak | 业务方配置 | 应用标识,从前端 JS 提取 |
| jsv | 固定 | SDK 版本号 |
响应字段 c 即为动态凭证,后续请求携带。
注意 :
aid中的时间戳和随机数每次必须变化,否则服务端会识别为重放请求。
3.2 加载接口(load)
请求:
GET https://cap.dingxiang-inc.com/api/a
关键请求参数:
| 参数 | 来源 | 说明 |
|---|---|---|
| aid | 客户端生成 | 同 c1 接口格式,每次刷新重新生成 |
| ak | 业务方配置 | 固定值 |
| c | c1 接口返回 | 动态凭证,每次验证前重新获取 |
| sid | 首次为空 | 服务端返回后用于后续请求 |
| w | 固定 | 渲染宽度,380 |
| h | 固定 | 渲染高度,165 |
| jsv | 固定 | SDK 版本号 |
响应关键字段:
| 字段 | 说明 |
|---|---|
| sid | 会话标识,验证接口必须原样携带 |
| aid | 服务端分配的 aid(需用此值而非客户端生成值提交验证) |
| p1 | 背景图相对路径(含 CDN 域名前缀后拼接) |
| p2 | 滑块图(RGBA PNG)相对路径 |
| y | 滑块的纵坐标,验证时原样传入 |
| const_id | 可选,若存在则优先用于 c 参数透传 |
重要 :验证接口中的
aid必须使用加载接口响应里返回的aid,而非客户端自行生成的值。
3.3 验证接口(verify)
请求:
POST https://cap.dingxiang-inc.com/api/v1
Content-Type: application/x-www-form-urlencoded
关键请求参数:
| 参数 | 来源 | 说明 |
|---|---|---|
| sid | 加载接口返回 | 会话标识,原样携带 |
| aid | 加载接口返回 | 同上 |
| x | OpenCV 识别+换算 | 滑块水平位移(380px 坐标系) |
| y | 加载接口返回 | 滑块纵坐标 |
| ac | Node.js 生成 | 加密轨迹字符串(核心参数) |
| c | c1 接口返回 | 动态凭证 |
| w / h | 固定 | 渲染尺寸 380 / 165 |
响应示例:
json
{
"success": true,
"token": "xxxx...",
"msg": null,
"retry": 0
}
四、c 参数------动态凭证
c 是每次请求必须携带的动态参数,通过请求 c1 接口实时获取,有效期极短(秒级),不可复用、不可静态硬编码。
关键特性:
- 加载接口和验证接口都需携带,但允许在一次完整验证流程中复用同一个 c 值
- 若接口响应中含
const_id字段,应优先将其作为 c 值透传至验证接口 - c1 接口请求失败时可临时回退至最近一次有效抓包值,但建议实现自动重试
c1 接口参数完整性要求:
c1 接口对参数完整性要求较高,缺少任一关键字段(aid、ak、jsv)均会导致返回异常或 c 值失效。aid 必须符合格式规范且包含正确时间戳。
五、背景图乱序还原
5.1 图像格式
- 背景图 :实际尺寸 400×200px,以 WebP 格式下载,内容为列块乱序状态(视觉上为错乱的竖条纹拼接)
- 滑块图 (slice):RGBA PNG,约 68×68px,Alpha 通道定义了滑块形状,RGB 通道的像素颜色直接来自背景图缺口处
5.2 列块置换算法
顶象背景图文件名(.webp 扩展名前的部分)本身编码了列块的置换映射,算法与 360 天御相同:
对文件名前 N 个字符(N = 列块数,通常为 32):
val = (ord(filename[i]) XOR 0x20) % N
若 val 已被使用 → val = (val + 1) % N,直到无冲突
result[i] = val (含义:目标位置 i 的列块来自源图第 val 列)
还原时,将源图第 ranges[i] 列的像素粘贴到目标图第 i 列位置即可。
每列宽度 = 图片宽度 / 列块总数(向下取整),顶象为 400 / 32 = 12.5,取 12px(最后一列略宽)
六、缺口位置识别------OpenCV 多策略融合
6.1 核心问题分析
顶象 slice 图的 RGB 像素颜色直接来自背景图缺口处,这一特性决定了识别策略的选择逻辑:
| 策略 | 原理 | 可靠性分析 |
|---|---|---|
slice_rgb(彩色模板匹配) |
用 slice 的 RGB 像素 + alpha mask 在背景图上做 TM_CCOEFF_NORMED |
不可靠:slice 像素来自背景,背景中其他颜色相似区域(食物、植物等自然纹理)同样会产生高分,误匹配率高 |
alpha_edge(轮廓边缘匹配) |
对 slice 的 alpha 通道做 Canny 边缘检测,与背景图边缘图做模板匹配 | 最可靠:形状轮廓匹配不受颜色影响,缺口形状独特,误匹配少 |
dark_region(暗区列扫描) |
在背景图中间 30%~70% 高度区间内,按列统计平均亮度,找局部最暗列 | 辅助:缺口处通常比周围略暗,但自然图像中可能存在其他暗区干扰 |
dark_border(深色边框匹配) |
构造 alpha 轮廓膨胀后的环形模板,在背景图反色上匹配深色边框 | 辅助:顶象缺口有深色外框特征,可作为 alpha_edge 的补充校验 |
gradient(梯度兜底) |
对背景图水平梯度剖面求最大值列 | 最低优先级:无法区分缺口与背景自然纹理 |
6.2 关键技术细节
alpha_edge 策略实现要点:
- 使用完整 slice(不裁剪透明边框)做 Canny 检测,得到轮廓边缘模板
- 背景图先转灰度再做 Canny(低阈值 20,高阈值 60)
matchTemplate返回的max_loc[0]直接就是缺口左边缘的 x 坐标(因模板与 slice 等大,位置对齐)
slice_rgb 为何不可信(关键陷阱):
直觉上用 slice 的真实像素去匹配背景应该最准,但实际上恰恰相反------正因为 slice 的颜色来自背景,所以背景中任何颜色相似的区域都会产生高相似度分数,导致误匹配。经实测,slice_rgb 分数达到 0.67 的情况下,实际位置可能偏差超过 150px。
6.3 多策略融合决策逻辑
输入:各策略返回 (x坐标, 置信度, 策略名)
1. 过滤异常值:x < 40px 或 x > 365px 的结果排除(缺口不会在边缘极限处)
2. 优先 alpha_edge(置信度阈值 0.07):
- 若同时有 dark_region / dark_border 且差距 ≤ 40px
→ 加权均值(alpha_edge 权重 ×2)
- 否则直接采用 alpha_edge
3. 回落到 dark_region(置信度阈值 0.10):
- 若同时有 dark_border 且差距 ≤ 40px → 取均值
- 否则直接采用 dark_region
4. 回落到 dark_border(置信度阈值 0.10)
5. 最后兜底:gradient 或全策略均值(排除 slice_rgb)
设计哲学 :
slice_rgb完全不参与最终决策,仅作为信息记录。alpha_edge的置信度阈值刻意设低(0.07),是为了在轮廓信号微弱时也能优先信任形状匹配,而非降级到颜色匹配。
6.4 坐标系换算
识别到的 x 坐标基于图片实际尺寸(400px 宽度坐标系),而验证接口要求的是渲染坐标系(380px):
提交 x = round(识别 x × 380 / 400)
七、ac 参数------加密轨迹生成
ac 是验证接口的核心动态参数,由 greenseer SDK(basic-Captcha-js.js)内部加密生成,包含用户行为轨迹、环境指纹、设备信息等多维数据,无法纯 Python 还原,必须通过 Node.js 调用 SDK 生成。
7.1 整体生成流程
初始化 UA 实例(注入 sid)
↓
采集环境信息(CF、DI、EM、JSV、TK 各模块)
↓
模拟鼠标行为(mousedown → mousemove 序列 → 过冲微调)
↓
提交行为数据(sendSA → sendTemp)
↓
getUA() → 返回加密后的 ac 字符串
7.2 SDK 实例初始化
javascript
// SDK 挂载在 window._dx 对象上
const _dx = window['_dx'];
const instance = _dx.UA.init({
token: sid, // 当次验证会话 sid,注入到 ac 内部
});
注意 :UA.init 中的 token 即加载接口返回的 sid,SDK 会将其编码进 ac 字符串,服务端据此将 ac 与当次验证会话绑定,同一 sid 不能被多个 ac 复用。
7.3 环境信息采集模块
初始化后需依次调用各采集模块,SDK 内部会将采集到的数据编码进 ac:
| 方法 | 采集内容 | 说明 |
|---|---|---|
instance.start() |
启动计时器 | 记录实例创建时间,影响后续时间戳计算 |
instance.getCF({}) |
Canvas/WebGL 指纹 | 通过绘图 API 生成硬件渲染特征 |
instance.getDI({}) |
设备信息 | 屏幕尺寸、colorDepth、platform 等 |
instance.getEM({}) |
事件模型检测 | 确认浏览器事件绑定能力 |
instance.getJSV() |
SDK 版本号 | 写入 greenseer 自身版本标识 |
instance.getTK() |
性能时序数据 | 依赖 performance.timing,含页面加载各阶段耗时 |
getTK的关键依赖 :performance.timing必须提供完整的页面性能时序对象(navigationStart、domComplete等约 15 个字段),否则该模块采集数据缺失,可能影响 ac 被服务端接受的概率。Node.js 需手动 mock 一套合理的时序数据,navigationStart建议设为Date.now() - 5000左右以模拟真实页面。
7.4 鼠标轨迹模拟
轨迹生成是 ac 质量的核心,模拟了用户从按下滑块按钮到拖动到位的完整鼠标行为。
坐标系说明:
- 绝对坐标(
pageX/pageY):模拟鼠标在整个页面上的真实像素坐标(如起点约(755, 320)) - 偏移量(
offsetX/offsetY):鼠标相对于目标元素左上角的偏移 - 时间戳(
timeStamp):绝对毫秒时间戳,从约Date.now() - 1500ms开始
三阶段轨迹模型:
阶段1:mousedown(按下)
- 触发 getMD() 事件,记录按下时的位置和时间
- 目标元素为滑块按钮(className: 'dx-captcha-slider-button')
- 时间比起始点早约 100ms
阶段2:mousemove(拖动主体)
- 总采样点数 = 30 + floor(滑动距离 / 2)(距离越长,点越多)
- 每步时间增量:随机 30~50ms
- x 轴位移:easeOutExpo 缓动函数(先快后慢,模拟真实手感)
- y 轴位移:在基准 y ± 5px 范围内随机抖动
- 相同 x 坐标的点不重复记录(避免静止抖动)
- 每 4 个采样点触发一次 getMM() 事件(throttle 控制事件频率)
阶段3:过冲微调(overshoot)
- 滑动结束后继续移动 2~5px(模拟惯性过冲)
- 随后 3 帧逐步回退到目标位置
- 每帧间隔 15~40ms
easeOutExpo 缓动公式:
f(t) = 1 - 2^(-10t),t ∈ [0, 1]
该函数使拖动开始时速度最快,接近终点时速度趋近于零,是最接近人类拖动习惯的缓动曲线。
7.5 提交与输出
javascript
instance.sendSA(); // 提交轨迹序列
instance.sendTemp('x=' + sendX + '&y=' + slideY); // 提交滑动坐标(sendX = 识别x + 20)
const ac = instance.getUA(); // 输出加密后的 ac 字符串
关于 x 值的两层处理:
| 场景 | x 值 | 说明 |
|---|---|---|
| Python 传入 hook_ac.js | 识别x - 20 |
从 dingxiang.py 调用时减 20 |
| hook_ac.js 内部写入 ac | slideX + 20 即恢复为 识别x |
sendTemp 里的 sendX = slideX + 20 |
| 验证接口的 x 参数 | 直接传 识别x |
Python 端 verify_captcha 传入的是换算后的原始 x |
净效果:ac 内部编码的 x 与验证接口传入的 x 一致,两层 ±20 相互抵消,确保服务端校验通过。
7.6 Python 调用方式
python
# 调用 hook_ac.js,传入 (识别x - 20)、y、sid
result = subprocess.run(
['node', 'hook_ac.js', str(x - 20), str(y), sid],
capture_output=True, text=True, timeout=10
)
ac = result.stdout.strip()
注意事项:
ac字符串通常在 200~500 字符范围,若为空或极短则生成失败- 同一
sid对应的 ac 只能使用一次,验证失败后需重新加载获取新 sid - Node.js 版本建议 >= 14,需确保
greenseer.js与hook_ac.js在同一目录
八、JS 环境补全
顶象 SDK(greenseer.js)原本运行于浏览器环境,在 Node.js 中执行需补全所有依赖的浏览器全局对象。环境补全的充分性直接影响 ac 中编码的环境指纹数据质量。
8.1 必须补全的全局对象
| 对象/方法 | Mock 要点 | 影响 |
|---|---|---|
window |
映射到 global,同时将各对象挂载到 _win 上 |
SDK 通过 window.xxx 访问所有浏览器对象 |
document |
需实现 createElement、getElementById、querySelector、addEventListener 等完整接口 |
getCF 会创建 Canvas 元素;缺失会抛出异常 |
navigator |
userAgent、cookieEnabled、platform、language |
getDI 和 getCF 依赖;UA 需与请求头 UA 保持一致 |
screen |
width、height、availWidth、availHeight、colorDepth |
getDI 采集屏幕参数 |
location |
href、hostname、protocol |
href 影响环境指纹的 origin 计算 |
performance.timing |
完整的约 15 个时序字段 | getTK 依赖,缺失会导致性能数据为空 |
btoa / atob |
Buffer 实现 |
SDK 内部 Base64 编解码 |
setTimeout / setInterval |
直接使用 Node.js 原生 | SDK 内部定时器 |
8.2 Proxy 技巧处理 DOM 节点列表
getElementsByTagName、querySelectorAll 等接口返回的是类数组对象,SDK 会对其调用各种方法。使用 Proxy 代理空数组,对任意属性访问都返回 () => false,可避免大量 Cannot read property of undefined 错误:
javascript
const _nodeList = new Proxy([], {
get: (t, k) => k in t ? t[k] : () => false
});
8.3 performance.timing 的 mock 策略
getTK 会读取 performance.timing 各字段计算页面加载耗时,若所有字段都为 0 或不存在,SDK 会记录异常数据。正确的 mock 策略是:
navigationStart=Date.now() - 随机5000~7000ms(模拟 5~7 秒前开始加载页面)- 各阶段时间戳依序递增,且差值符合正常页面加载规律(DNS 解析 20ms、TCP 建立 70ms、首字节 150ms 等)
domComplete距navigationStart约 2500ms(模拟正常 DOM 完成时间)
8.4 环境一致性要求
以下字段会被编码进 ac,服务端可能与请求头做交叉校验:
| 字段 | 说明 |
|---|---|
navigator.userAgent |
必须与 HTTP 请求头 User-Agent 完全一致 |
location.href |
应与 HTTP 请求头 Referer 一致 |
screen.width / height |
建议使用常见分辨率(如 1920×1080) |
九、完整请求流程
| 步骤 | 动作 | 说明 |
|---|---|---|
| 1 | 获取 c 参数 | 请求 c1 接口,传入动态 aid,得到实时 c 值 |
| 2 | 加载验证码 | 携带 ak、aid、c、sid(首次为空)请求加载接口 |
| 3 | 下载图片 | 根据 p1、p2 路径拼接完整 URL,分别下载背景图和滑块图 |
| 4 | 还原背景图 | 从 p1 文件名解密置换映射,重排列块得到正确背景 |
| 5 | 识别缺口 | OpenCV 多策略融合,优先 alpha_edge,输出 x(400px 坐标系) |
| 6 | 坐标换算 | x_submit = round(x × 380 / 400) |
| 7 | 生成 ac | 调用 Node.js hook_ac.js,传入 x_submit - 20、y、sid |
| 8 | 提交验证 | POST 验证接口,携带 sid、aid、x、y、ac、c |
| 9 | 结果处理 | success: true 则完成;否则重新加载(最多 5 轮) |
十、关键知识点总结
| 知识点 | 详情 |
|---|---|
| c 参数 | 实时从 c1 接口获取,不可复用不可硬编码;优先用响应中的 const_id 字段 |
| aid 格式 | dx-{时间戳ms}-{8位随机}-{实例序号},每次请求必须变化 |
| 背景图乱序 | 文件名前 N 字符编码置换映射:val = (char ^ 0x20) % N,冲突时 +1 % N |
| slice 图特性 | RGBA PNG,alpha 通道定义形状,RGB 像素来自背景缺口------此特性导致颜色匹配不可信 |
| 识别策略优先级 | alpha_edge(形状轮廓)> dark_region / dark_border(辅助)> gradient(兜底);slice_rgb 完全排除 |
| 坐标换算 | 图片实际 400px 坐标系 → 渲染 380px 坐标系:round(x × 380/400) |
| ac 参数 | SDK 内部加密轨迹,必须 Node.js 调用,传入 x-20、y、sid |
| 重试机制 | 验证失败后需完整重走流程(重新加载 → 重新识别 → 重新生成 ac),不可复用旧 sid |
十一、与网易易盾(文字点选)的横向对比
| 对比维度 | 顶象(滑动拼图) | 网易易盾(文字点选) |
|---|---|---|
| 验证类型 | 滑动到缺口(拖动) | 按顺序点击指定文字 |
| 图像识别 | OpenCV 多策略模板匹配(纯本地) | 第三方 OCR 打码平台 |
| 图片保护 | 列乱序(32列×约12px,文件名编码映射) | 无乱序,标准 JPEG |
| 动态参数 | c 参数(需实时接口获取)+ ac 轨迹(JS 生成) | cb 参数(SDK JS 动态生成,每次独立) |
| 轨迹格式 | 由 SDK 内部封装,Python 端仅传 x/y/sid | [x, y, t] 三元组列表,t 为相对时间戳 |
| JS 依赖 | ac 参数必须 Node.js 调用 SDK | cb 和 data 均必须 Node.js 调用 SDK |
| 混淆程度 | 中等,hook 后可直接调用关键函数 | 重度混淆(obfuscator 风格) |
| 坐标系 | 双坐标系(图片400px↔渲染380px,需换算) | 单一坐标系(320px,直接传) |
| 识别难点 | slice 颜色来自背景导致颜色匹配不可信,需依赖形状匹配 | 文字识别顺序必须与 front 字段严格对应 |
十二、依赖安装
bash
# Python 依赖
pip install requests opencv-python-headless pillow numpy
# Node.js 环境(用于生成 ac 参数)
node --version # 需已安装 Node.js >= 14
本文技术仅供安全研究与学习,切勿用于任何未授权系统,违者后果自负。