文字点选验证码前端安全研究:以网易易盾(dun.163)为例

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

一、背景介绍

网易易盾是网易旗下的人机验证方案,本文以文字点选验证(picture-click) 为研究对象,分析其前端安全机制。

基本交互逻辑:

  1. 前端携带 idtokencb(动态加密参数)等请求 get 接口,获取背景图(bg)、待点击文字顺序(front)、会话 token
  2. 使用打码平台(超级鹰)识别背景图中各汉字的坐标,按 front 要求的顺序筛选出目标坐标序列
  3. 根据坐标序列生成相邻两点之间的插值轨迹(track_list)及点选列表(point_list
  4. 将 token、轨迹、点选列表传入 SDK JS,经自定义加密函数 处理后构造 data 参数
  5. 携带 datatokencb 等提交 check 接口,服务端返回 result: true/false

研究核心难点:

  • cb 参数动态生成(SDK 内部加密,无法静态提取,需调用 JS 运行时)
  • data 参数加密体系(轨迹数据经自定义函数加密,需逆向 SDK)
  • 文字点选顺序匹配front 字段规定点击顺序,需与 OCR 结果正确对应)

二、整体流程图

复制代码
调用 JS 生成动态 cb 参数
  ↓
get 接口(GET /api/v3/get)
  ↓ 返回 bg URL、front(待点击文字顺序)、token
下载背景图
  ↓
打码平台 OCR 识别 → 返回所有文字坐标(格式:汉字,x,y|汉字,x,y|...)
  ↓
按 front 顺序筛选坐标 → 得到有序坐标序列 xy_list
  ↓
相邻两点插值 → 生成 track_list(完整轨迹)+ point_list(点选列表)
  ↓
调用 JS 加密轨迹 → 得到加密后的 data 参数
再次调用 JS 生成新 cb
  ↓
check 接口(GET /api/v3/check)→ result: true/false

三、抓包分析

3.1 get 接口

请求:

复制代码
GET https://c.dun.163.com/api/v3/get

关键请求参数:

参数 来源 说明
id 业务方配置 绑定具体业务,从前端 JS 中提取,固定值
token 业务方配置 业务会话凭证,固定值(演示页可直接抓取)
dt 固定 业务标识串,固定值
irToken 固定 附加凭证,固定值
zoneId 固定 区域标识,如 CN31
type 固定 3 表示文字点选验证
version 固定 SDK 版本号,如 2.28.5
width 固定 图片宽度,如 320
cb JS 动态生成 核心动态参数,见第四节
callback 固定 JSONP 回调函数名,本地自定义

响应格式(JSONP):

复制代码
__JSONP_xxxxxx_x({"data":{...},"error":0,"msg":"ok"})

响应 data 字段:

字段 说明
bg 背景图 URL 数组(通常含两个 CDN 地址)
front 待点击文字顺序字符串,如 "安扩体"
token 本次验证会话 token(与 check 接口复用)
type 验证类型,3 表示文字点选
waitTime 服务端建议等待时间(ms)
zoneId 区域标识

JSONP 解析注意 :回调函数名(如 __JSONP_awjwj4y_2)长度不固定,不能用硬编码偏移量 (如 text[18:-2])截取,应动态定位括号位置:

python 复制代码
json_str = text[text.index('(') + 1: text.rindex(')')]

3.2 check 接口

请求:

复制代码
GET https://c.dun.163.com/api/v3/check

关键请求参数:

参数 来源 说明
id 同 get 业务方固定值
dt 固定 业务标识串
token get 响应 本次会话 token,原样透传
data JS 加密生成 核心参数,包含加密后的轨迹和点选数据,见第六节
width 固定 320
type 固定 3
version 固定 SDK 版本号
cb JS 动态生成 需在提交前重新生成(每次调用结果不同)
bf 固定 0
callback 固定 JSONP 回调函数名

响应示例(JSONP):

json 复制代码
{
  "data": {
    "result": true,
    "token": "xxx",
    "validate": "yyy"
  },
  "error": 0,
  "msg": "ok"
}

四、cb 参数------动态加密凭证

cb 是每次请求都需要携带的动态参数,由 SDK JS 内部的加密函数生成,无法静态分析或固定。其生成逻辑依赖 SDK 内部状态(时间戳、环境指纹等),需在 Node.js 环境中通过 require 加载 SDK 后调用。

关键特性:

  • get 请求和 check 请求各自独立调用一次,不能复用同一个 cb 值
  • 在 check 提交前必须重新生成,否则服务端会拒绝请求
  • _cb_value 由 SDK 源码在加载时挂载到 window 对象上,每次调用均基于当前时间戳和环境指纹生成新值,因此两次调用结果必然不同

03_main.js 中的封装(骨架):

javascript 复制代码
function get_cb() {
    return window._cb_value();  // 调用 SDK 内部动态生成函数
}

Python 调用示例:

python 复制代码
# get 和 check 各自独立调用,不能复用
cb_get   = call_js_function("dun.163/03_main.js", "get_cb", "")
cb_check = call_js_function("dun.163/03_main.js", "get_cb", "")

五、文字识别与坐标匹配

5.1 OCR 识别

使用第三方打码平台(超级鹰)识别背景图,指定验证码类型为文字点选(codetype 9800)。

识别结果 pic_str 格式为管道符分隔的多段坐标,每段格式为 汉字,x,y

复制代码
安,123,456|扩,234,567|体,345,678|候,456,789|...

图片中通常包含多个汉字(干扰项 + 目标项),并非所有汉字都需要点击。

5.2 按 front 顺序筛选坐标

front 字段规定了需要点击的汉字及点击顺序 ,例如 "安扩体" 表示依次点击"安"→"扩"→"体"。

筛选逻辑:外层按 front 顺序遍历,内层从 OCR 结果中查找匹配汉字的坐标

python 复制代码
xy_list = []
for f in front:               # 外层:按 front 要求的点击顺序
    for item in pic_str_list:
        if item[0] == f:      # item[0] 是 OCR 识别出的汉字
            xy_list.append(item[2:])  # item[2:] 是 "x,y" 部分
            break

常见错误 :若将循环顺序颠倒(外层遍历 OCR 结果,内层匹配 front),xy_list 会按 OCR 随机顺序排列,导致点击顺序错误,验证必然失败。


六、轨迹生成与加密

6.1 轨迹生成

根据有序坐标列表 xy_list 生成两类数据:

  • track_list(完整轨迹):相邻两点之间插值,模拟鼠标移动路径
  • point_list(点选列表):仅包含各目标点坐标

插值算法(线性插值模拟连续移动):

参数 取值 说明
插值点数 随机 30~40 个 相邻两点之间生成的中间轨迹点数量
时间步长 随机 30~60ms 相邻轨迹点的时间差
坐标格式 [x, y, t] x/y 为像素坐标,t 为相对时间(从 3 开始累加)

6.2 轨迹数据加密

轨迹和点选数据需调用 SDK JS 函数进行加密,Python 通过 subprocess 调用 Node.js 执行,传入 tokentrack_listpoint_list,返回加密后的 data 字符串。

Python 调用示例:

python 复制代码
js_data = {
    "token": token,
    "track_list": track_list,
    "point_list": point_list
}
track_data = call_js_function("dun.163/03_main.js", "get_track_data", json.dumps(js_data))
result = json.loads(track_data)  # 包含 m、p、ext 字段

03_main.js 中的封装(骨架):

javascript 复制代码
// 辅助函数:对轨迹列表逐点调用 SDK 加密,返回密文数组
function get_encryValue(token, track_list) {
    // 遍历每个点,格式化为 "x,y,t,0"(第四维固定为 0)
    // 调用 SDK 内部加密函数,逐点生成密文
    // 返回密文字符串数组
}

function get_track_data(data) {
    // 1. 对 track_list 和 point_list 分别调用 get_encryValue
    // 2. 从完整轨迹密文中随机采样 50 条,":"拼接后二次加密 → m
    // 3. 全量点选密文,":"拼接后二次加密 → p
    // 4. 附加元数据(点数等)加密 → ext
    // 返回 JSON.stringify({d: '', m, p, ext})
}

加密链路说明:

步骤 输入 函数类型 输出 字段
1 每个轨迹点转为 x,y,t,0 格式字符串 + token SDK 内置对称加密 单点密文字符串 ---
2a 完整轨迹随机采样 50 条,: 拼接 SDK 内置二次加密 最终密文 m
2b 全部点选数据,: 拼接 SDK 内置二次加密 最终密文 p
2c 轨迹总点数等附加信息 + token 两层加密 最终密文 ext

关键细节:

  • 每个轨迹点在转为字符串时需追加第四维 0(格式为 x,y,t,0),这是 SDK 协议要求,缺少会导致验证失败
  • 从完整轨迹中随机采样 50 条构成 m 字段,而非全量传输,防止服务端通过全量数据逆推轨迹规律
  • 以上加密函数均为 SDK 内部混淆实现,通过逆向 SDK 源码可确认其为自定义对称加密,实际使用时以 Node.js 调用方式直接复用,无需彻底还原算法

七、JS 环境补全

SDK JS(02_source.js)原本运行于浏览器环境,在 Node.js 中执行需要补全浏览器 API。01_env.js 负责建立以下 Mock 对象,核心一步是 window = global ,使 SDK 内部所有 window.xxx 的读写都实际操作 Node.js 的 global 对象,从而让 _cb_value 等函数正确挂载并可被调用:

document 需实现 createElement(返回预定义桩对象,含 getAttributesetAttributestyleaddEventListener 等属性)、getElementByIdaddEventListener 等接口,SDK 初始化时会创建 div/iframe 做沙箱检测,缺失任何属性都可能抛出异常。

01_env.js 整体结构(骨架):

javascript 复制代码
window = global               // 核心:将 global 映射为 window

document = {
    createElement: function(tag) { /* 返回对应的 div/iframe 桩对象 */ },
    getElementById: function() {},
    addEventListener: function() {},
    body: { appendChild: function() {} },
    // ... 其他 DOM 方法
}

navigator = {
    userAgent: '...',         // 需与 HTTP 请求头 UA 保持一致
    // cookieEnabled, platform, language ...
}

location = {
    href: 'https://...',      // 需与实际业务页面地址一致,影响 origin 指纹
    // origin, hostname, pathname ...
}

setTimeout  = function() {}   // 置空:防止进程因 SDK 内部定时器挂起
setInterval = function() {}
对象/方法 Mock 要点 影响
window 直接映射为 Node.js global SDK 所有 window.xxx 读写均操作 global,使加密函数正确挂载
document 实现 createElementgetElementByIdaddEventListener 等,createElement('div'/'iframe') 返回预定义桩对象 SDK 初始化时会创建 DOM 元素做沙箱检测,缺失会抛出异常
navigator.userAgent 固定为 Chrome UA 字符串 影响 SDK 浏览器识别逻辑,修改可能导致 cb 生成异常
location 固定为演示页完整 URL 对象(含 href、origin、hostname、pathname 等字段) 影响 SDK 计算 origin 指纹,需与实际业务页面地址一致
setTimeout / setInterval 置为空函数 SDK 内部定时器(上报、心跳)在脚本化场景无需执行,置空防止进程挂起

三文件加载顺序不可颠倒: 02_source.js 依赖 01_env.js 建立的全局环境,03_main.js 再封装调用接口。顺序错误则 SDK 初始化必然报错。

文件 职责
01_env.js 浏览器环境 Mock,建立 window/document/navigator/location
02_source.js SDK 源码,加载后将加密函数挂载到 window
03_main.js 胶水层,封装 Python 可调用的函数,通过 process.argv 分发

八、Python 调用 JS 的通用模式

由于 SDK 加密函数难以纯 Python 还原,采用 Python + Node.js 混合调用 方案:通过 subprocess 执行 node 03_main.js <函数名> <参数>,Python 读取 stdout 获得结果。

python 复制代码
def call_js_function(js_file_path, function_name, arg):
    result = subprocess.run(
        ['node', js_file_path, function_name, arg],
        capture_output=True, text=True, check=True,
        encoding='utf-8'  # Windows 下必须显式指定,否则 GBK 默认编码导致乱码
    )
    return result.stdout.strip()

03_main.js 胶水层的工作原理(骨架):

javascript 复制代码
require('./01_env');    // 第1步:补全浏览器环境
require('./02_source'); // 第2步:加载 SDK,挂载 _cb_value 等函数到 window

// 第3步:根据 process.argv 分发调用
const functionName = process.argv[2];  // 'get_cb' 或 'get_track_data'
const data         = process.argv[3];  // JSON 参数字符串

if (functionName === 'get_cb') {
    console.log(get_cb());
}
if (functionName === 'get_track_data') {
    console.log(get_track_data(JSON.parse(data)));
}
  • 通过 process.argv 接收 Python 传入的函数名(get_cb / get_track_data)和 JSON 参数
  • 调用对应函数后将结果 console.log 输出,Python 读取 stdout 获得返回值
调用场景 函数名 参数 返回值
生成 get/check 请求的 cb get_cb 空字符串 cb 字符串
加密轨迹数据 get_track_data {token, track_list, point_list} JSON 含 m/p/ext 字段的 JSON 字符串

编码注意 :调用 subprocess.run 时须显式指定 encoding='utf-8',否则在 Windows 环境下因默认 GBK 编码可能导致数据乱码,进而使 SDK 加密结果错误。


九、完整请求流程概述

步骤 动作 说明
1 生成 cb 调用 JS get_cb(),每次生成唯一动态参数
2 调用 get 接口 获取 bg URL、front(点击顺序)、token
3 下载背景图 取 bg 数组第一个 URL 下载
4 OCR 识别 调用打码平台(超级鹰 codetype=9800)识别所有文字坐标
5 坐标排序 按 front 顺序从 OCR 结果中筛选目标坐标,构建 xy_list
6 生成轨迹 线性插值生成 track_list 和 point_list
7 加密轨迹 调用 JS get_track_data(),传入 token + 轨迹数据,返回加密后的 data
8 重新生成 cb check 接口需用新 cb,重新调用 JS 生成
9 调用 check 接口 携带 data、token、cb 等参数提交,解析 JSONP 响应

十、关键知识点总结

知识点 详情
cb 参数 SDK 内部动态生成,get 和 check 各需独立调用一次,不可复用
JSONP 解析 回调名长度不固定,必须动态定位括号 index('(') 解析,不能硬编码偏移量
front 字段 规定点击文字的顺序,筛选坐标时外层遍历 front,内层从 OCR 结果中查找
轨迹格式 [x, y, t] 三元组列表,t 为相对时间(从 3 开始累加 30~60ms 步长)
数据加密 SDK 内部混淆函数(_0x8b4875_0x59b784),采用 Python 调用 Node.js 的方式复用,无需逆向
data 结构 {d: '', m: 采样轨迹加密, p: 点选列表加密, ext: 附加信息加密} 的 JSON 字符串
JS 环境补全 Node.js 中需 Mock window/document/navigator/location 等浏览器 API
打码平台 超级鹰 codetype 9800 对应文字点选,返回格式 `汉字,x,y

十一、与 360 天御(滑块)的横向对比

对比维度 网易易盾(文字点选) 360 天御(滑块)
验证类型 文字点选(识别+顺序点击) 滑动拼图(拖动到缺口)
图像识别 打码平台 OCR(超级鹰) OpenCV 多策略模板匹配
加密方案 SDK JS 混淆函数,Python 调用 Node.js 复用 RSA PKCS1_v1_5 纯 Python 还原
动态参数 cb(每次调用 JS 生成) 无动态加密参数(sign 为 MD5 可纯 Python 复现)
图片保护 无乱序,标准 JPEG 直接下载 列乱序(32列×17px,基于文件名置换还原)
响应格式 JSONP,需动态定位括号解析 标准 JSON
轨迹格式 [x, y, t] 三元组,相对时间 {"index": {"t": 绝对时间戳, "y": 纵坐标}}
JS 依赖 必须依赖(cb 和 data 均无法纯 Python 还原) 无需 JS 依赖(全部纯 Python 实现)
混淆程度 重度混淆(obfuscator 风格,函数名如 _0x8b4875 中等,关键逻辑可读

十二、依赖安装

bash 复制代码
# Python 依赖
pip install requests

# Node.js 环境
node --version  # 需已安装 Node.js

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

相关推荐
mldong3 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排3 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60614 小时前
如何自己造一个时间处理库
前端·javascript·github
三8444 小时前
redis防御加固与安全基线
数据库·redis·安全·应急响应·防御
走到天涯海角5 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!5 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰5 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
乐迪信息5 小时前
如何通过AI防爆摄像机精准判断船舶超速?
大数据·人工智能·算法·安全·目标跟踪
luiyarch6 小时前
汽车电子ISO 26262功能安全系列(第35期):配置管理与变更管理——给安全系统装上“时空穿梭机”
安全·汽车
mayaairi7 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js