免责声明: 本文所有分析均基于公开可访问的前端 JS 代码及网易易盾官网演示页(https://dun.163.com/trial/picture-click),仅用于安全研究、学习与了解验证码防护机制。文中所有接口地址来自官方公开演示环境,不涉及任何第三方业务系统。请勿将本文技术用于任何未授权的系统,违者后果自负。
一、背景介绍
网易易盾是网易旗下的人机验证方案,本文以文字点选验证(picture-click) 为研究对象,分析其前端安全机制。
基本交互逻辑:
- 前端携带
id、token、cb(动态加密参数)等请求get接口,获取背景图(bg)、待点击文字顺序(front)、会话 token - 使用打码平台(超级鹰)识别背景图中各汉字的坐标,按
front要求的顺序筛选出目标坐标序列 - 根据坐标序列生成相邻两点之间的插值轨迹(
track_list)及点选列表(point_list) - 将 token、轨迹、点选列表传入 SDK JS,经自定义加密函数 处理后构造
data参数 - 携带
data、token、cb等提交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])截取,应动态定位括号位置:
pythonjson_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 执行,传入 token、track_list、point_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(返回预定义桩对象,含 getAttribute、setAttribute、style、addEventListener 等属性)、getElementById、addEventListener 等接口,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 |
实现 createElement、getElementById、addEventListener 等,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
本文技术仅供安全研究与学习,切勿用于任何未授权系统,违者后果自负。