文字点选验证码前端安全研究:以网易易盾(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

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

相关推荐
鹏程十八少1 小时前
1.2026金三银四 Android Glide 23连问终极拆解:生命周期、三级缓存、Bitmap复用,大厂面试官到底想听什么?
android·前端·面试
hhhhhh_we1 小时前
预颜美历:AI驱动的私人面部美学与皮肤全周期管理工具
前端·图像处理·人工智能·python·aigc
Cobyte1 小时前
5.响应式系统比对:手写 React 响应式状态库 Mobx
前端·javascript·vue.js
honest_gg1 小时前
潜影【TraceHarvest】:自动化“一键”钓鱼工具
安全·hw·社会工程学·hvv·钓鱼·攻防演练·护网
鹓于1 小时前
PPT VBA随机选题系统实现详解
java·前端·javascript
Flittly2 小时前
【SpringSecurity新手村系列】(1)初识安全框架
java·spring boot·安全·spring·安全架构
前端双越老师2 小时前
OpenClaw 实战记录:前端 VS 全栈 招聘岗位分析
前端·agent·全栈
测试那点事儿2 小时前
Cursor AI技能提示词设计建议:构建全覆盖测试用例生成体系(测试用例设计场景安全性能篇)
人工智能·安全·测试用例·ai辅助测试
Bigger2 小时前
第八章:我是如何剖析 Claude Code 里的“电子宠物”彩蛋的
前端·ai编程·源码阅读