滑动验证码前端安全研究:以顶象(dingxiang-inc)为例

免责声明: 本文所有分析均基于公开可访问的前端 JS 代码及顶象官网演示页(https://www.dingxiang-inc.com/business/captcha),仅用于安全研究、学习与了解验证码防护机制。文中所有接口地址来自官方公开演示环境,不涉及任何第三方业务系统。请勿将本文技术用于任何未授权的系统,违者后果自负。

一、背景介绍

顶象(dingxiang-inc)是国内主流的人机验证方案提供商,本文以滑动拼图验证码为研究对象,分析其前端安全机制。

基本交互逻辑:

  1. 前端携带 akaidc(动态凭证)等参数请求加载接口,获取乱序背景图路径(p1)、滑块图路径(p2)及会话标识 sid
  2. 还原乱序背景图:根据背景图文件名通过特定置换算法解密列块顺序,重新拼接为正确图像
  3. 使用 OpenCV 多策略模板匹配,识别滑块缺口在背景图中的水平位置(缺口距左边缘的像素距离)
  4. 将坐标传入 Node.js 脚本,通过 SDK 内部逻辑生成加密轨迹参数 ac
  5. 携带 sidaidxyacc 等提交验证接口,服务端返回 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 接口对参数完整性要求较高,缺少任一关键字段(aidakjsv)均会导致返回异常或 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 必须提供完整的页面性能时序对象(navigationStartdomComplete 等约 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.jshook_ac.js 在同一目录

八、JS 环境补全

顶象 SDK(greenseer.js)原本运行于浏览器环境,在 Node.js 中执行需补全所有依赖的浏览器全局对象。环境补全的充分性直接影响 ac 中编码的环境指纹数据质量。

8.1 必须补全的全局对象

对象/方法 Mock 要点 影响
window 映射到 global,同时将各对象挂载到 _win SDK 通过 window.xxx 访问所有浏览器对象
document 需实现 createElementgetElementByIdquerySelectoraddEventListener 等完整接口 getCF 会创建 Canvas 元素;缺失会抛出异常
navigator userAgentcookieEnabledplatformlanguage getDI 和 getCF 依赖;UA 需与请求头 UA 保持一致
screen widthheightavailWidthavailHeightcolorDepth getDI 采集屏幕参数
location hrefhostnameprotocol href 影响环境指纹的 origin 计算
performance.timing 完整的约 15 个时序字段 getTK 依赖,缺失会导致性能数据为空
btoa / atob Buffer 实现 SDK 内部 Base64 编解码
setTimeout / setInterval 直接使用 Node.js 原生 SDK 内部定时器

8.2 Proxy 技巧处理 DOM 节点列表

getElementsByTagNamequerySelectorAll 等接口返回的是类数组对象,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 等)
  • domCompletenavigationStart 约 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

本文技术仅供安全研究与学习,切勿用于任何未授权系统,违者后果自负。

相关推荐
懂懂tty9 小时前
React状态更新流程
前端·react.js
小码哥_常9 小时前
告别繁琐!手把手教你封装超实用Android原生Adapter基类
前端
skywalk81639 小时前
pytest测试的时候这是什么意思?Migrating <class ‘kotti.resources.File‘>
前端·python
一只蝉nahc10 小时前
vue使用iframe内嵌unity模型,并且向模型传递信息,接受信息
前端·vue.js·unity
子兮曰10 小时前
Bun v1.3.12 深度解析:新特性、性能优化与实战指南
前端·typescript·bun
2401_8858850411 小时前
易语言彩信接口怎么调用?E语言Post实现多媒体数据批量下发
前端
a11177611 小时前
Three.js 的前端 WebGL 页面合集(日本 开源项目)
前端·javascript·webgl
Kk.080211 小时前
项目《基于Linux下的mybash命令解释器》(一)
前端·javascript·算法
优泽云安全11 小时前
如何选择IRCS云信息安全管理系统 IRCS云资源评测
linux·服务器·安全·安全架构