滑块验证码前端安全研究:以极验 GT4(第四代)为例

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

一、背景介绍

极验 GT4(第四代行为验证)是国内应用最广泛的人机验证方案之一,支持滑动拼图、文字点选、一点即过、消消乐等多种验证形式。本文以滑动拼图验证(slide) 为研究对象,分析其前端安全机制。

基本交互逻辑:

  1. 前端携带 captcha_id + 动态 challenge 请求 load 接口,获取背景图(bg)、滑块图(slice)、会话标识(lot_number)及工作量证明参数(pow_detail)
  2. 用计算机视觉识别滑块应移动到的缺口位置,得到 distance
  3. 前端将滑动距离、耗时、设备信息等核心字段组装为 w_data,经 AES-CBC + RSA 双层加密后构造参数 w
  4. 携带 w 及会话参数提交 verify 接口,服务端返回 result: success/fail

研究核心难点:

  • 识别缺口距离(计算机视觉)
  • 还原 w 参数的加密体系(AES-CBC 对称加密 + RSA 非对称加密密钥)
  • 工作量证明(PoW)参数计算(动态 bits/hashfunc 适配)

二、整体流程图

复制代码
本地生成 challenge(UUID v4)+ callback(时间戳)
  ↓
load 接口(GET /load)
  ↓ 返回 lot_number、bg、slice、pow_detail、payload、process_token
下载背景图 + 滑块图
  ↓
ddddocr 识别缺口距离 distance
  ↓
计算工作量证明 pow_msg / pow_sign
  ↓
组装 w_data(distance、passtime、userresponse 等字段)
  ↓
AES-CBC 加密 w_data → aes_data
RSA 加密 AES 密钥 → rsa_data
w = aes_data + rsa_data
  ↓
verify 接口(GET /verify)→ result: success/fail

三、抓包分析

3.1 load 接口

请求:

复制代码
GET https://gcaptcha4.geetest.com/load

关键请求参数:

参数 来源 说明
callback 本地生成 'geetest_' + Date.now(),用于 JSONP 回调
captcha_id 业务方配置 绑定某个具体业务,从前端 JS 中提取,固定值
challenge 本地生成 UUID v4 格式,每次请求动态生成(源码:`config.challenge
client_type 固定 web
risk_type 固定 slide 表示滑动拼图,不同验证形式对应不同值
lang 固定 zh

challenge 在 GT4 的 gt4.js 源码中明确写为 config.challenge || uuid(),即业务方不传时自动生成 UUID,因此可本地用 uuid.uuid4() 替代。

响应示例(JSONP 格式):

json 复制代码
geetest_xxxxxxxxxx({
  "status": "success",
  "data": {
    "lot_number": "614c7e56806748cfad5d2eeca241293f",
    "captcha_type": "slide",
    "bg": "captcha_v4/xxx/bg/xxx.jpg",
    "slice": "captcha_v4/xxx/slice/xxx.png",
    "ypos": 14,
    "pow_detail": {
      "version": "1",
      "bits": 0,
      "datetime": "2026-04-15T21:20:34.369357+08:00",
      "hashfunc": "md5"
    },
    "payload": "AgFD8g...",
    "process_token": "ee684e49...",
    "payload_protocol": 1
  }
})

响应为 JSONP 格式,需动态定位括号位置解析,不能硬编码偏移量(callback 名长度随时间戳变化)。

3.2 verify 接口

请求:

复制代码
GET https://gcaptcha4.geetest.com/verify

关键请求参数:

参数 来源 说明
captcha_id 同 load 业务方固定值
lot_number load 响应 本次会话唯一标识
risk_type 固定 slide
payload load 响应 服务端加密载荷,原样透传
process_token load 响应 会话凭证,原样透传
payload_protocol load 响应 协议版本,原样透传
pt load 响应 原样透传
w 本地加密生成 核心参数,见第四节

四、JS 逆向------w 参数加密体系

w 是 verify 接口的核心参数,其生成逻辑可直接在极验 CDN 分发的 gt4.js(未混淆)中阅读。

4.1 w 参数结构

复制代码
w = AES_CBC_Encrypt(JSON.stringify(w_data), aes_key)  [hex]
  + RSA_Encrypt(aes_key)  [hex]

即:AES 加密后的数据 hex 字符串 拼接 RSA 加密密钥 hex 字符串 ,服务端先用私钥解出 AES 密钥,再解密出 w_data

4.2 w_data 核心字段

字段 含义 说明
setLeft 滑块移动像素 ddddocr 识别结果
passtime 滑动总耗时(ms) 随机范围 1300~2000
userresponse 实际响应距离 distance / 1.0059466... + 2,固定换算公式
lot_number 会话标识 来自 load 响应
pow_msg PoW 消息 见第五节
pow_sign PoW 签名 见第五节
gee_guard 环境检测结果 固定结构,包含多项 bot 检测标志
em 环境检测补充 固定结构

4.3 AES 加密

算法:AES-128-CBC,PKCS7 填充,输出 hex

密钥处理规则(来自 gt4.js 逆向):

  • key_str 长度 < 16:取其 MD5 digest(16 字节)作为密钥
  • key_str 长度 ≥ 16:截取前 16 字节
  • IV 处理规则相同,默认 IV 为 "0000000000000000"
python 复制代码
def AES_Encrypt(word: str, key_str: str, iv_str: str = "0000000000000000") -> str:
    """AES-128-CBC 加密,PKCS7 填充,输出 hex"""
    key_bytes = (hashlib.md5(key_str.encode()).digest()
                 if len(key_str) < 16
                 else key_str.encode()[:16])
    iv_bytes  = (hashlib.md5(iv_str.encode()).digest()
                 if len(iv_str) < 16
                 else iv_str.encode()[:16])
    cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
    return cipher.encrypt(pad(word.encode(), AES.block_size)).hex()

4.4 RSA 加密

算法:RSA PKCS1 v1.5,公钥通过模数(hex)+ 固定指数(65537)构造,输出 hex

RSA 公钥的模数(modulus)虽以 hex 字符串形式存在于 gt4.js 中,但实际使用时需通过调试断点或 Hook 加密函数的方式动态确认其与当前 SDK 版本的对应关系,不建议直接静态读取硬编码。加密的内容是随机生成的 AES 密钥字符串,服务端持有私钥还原后用于解密 w_data

python 复制代码
def RSA_Encrypt(plaintext: str) -> str:
    """RSA PKCS1 v1.5 加密,modulus 需通过调试从 gt4.js 中获取,输出 hex"""
    modulus = int("<调试获取的 modulus hex 字符串>", 16)
    key = RSA.construct((modulus, 65537))
    return PKCS1_v1_5.new(key).encrypt(plaintext.encode()).hex()

4.5 AES 密钥生成

每次请求随机生成 16 字节(4 × 4 位随机 hex),保证每次 w 不重复:

python 复制代码
def get_random_key() -> str:
    """生成 16 字节随机 hex 字符串,作为 AES 会话密钥"""
    return ''.join(format(random.getrandbits(16), '04x') for _ in range(4))

五、工作量证明(PoW)机制

GT4 在 load 响应中下发 pow_detail,要求客户端提交满足条件的哈希值,用于防刷。

5.1 pow_detail 结构

字段 说明
version 协议版本,固定 "1"
bits 要求哈希结果前导零 bit 数;bits=0 表示无要求,任意值即可
hashfunc 哈希算法,md5sha256
datetime 服务端当前时间戳,计入消息体防重放

5.2 pow_msg 格式

复制代码
pow_msg = f"1|{bits}|{hashfunc}|{datetime}|{captcha_id}|{lot_number}||{random_key}"
pow_sign = hashfunc(pow_msg)

bits > 0 时,需循环碰撞直到 pow_sign 满足前导零条件:

python 复制代码
def get_pow_info(pow_detail: dict, captcha_id: str, lot_number: str):
    """
    动态计算 PoW,适配 bits=0(直接返回)和 bits>0(碰撞循环)两种场景。
    """
    bits     = pow_detail.get('bits', 0)
    datetime = pow_detail.get('datetime', '')
    hashfunc = pow_detail.get('hashfunc', 'md5')
    hash_fn  = hashlib.sha256 if hashfunc == 'sha256' else hashlib.md5
    prefix   = '0' * (bits // 4)   # 需要多少个前导零 hex 字符

    while True:
        key      = get_random_key()
        pow_msg  = f'1|{bits}|{hashfunc}|{datetime}|{captcha_id}|{lot_number}||{key}'
        pow_sign = hash_fn(pow_msg.encode()).hexdigest()
        if bits == 0 or pow_sign.startswith(prefix):
            return pow_msg, pow_sign

六、图像识别------计算缺口距离

使用开源库 ddddocr 进行滑块缺口匹配,支持直接传入图片二进制,无需写入磁盘:

python 复制代码
from ddddocr import DdddOcr

def ddddocr_get_distance(bg: bytes, tp: bytes) -> int:
    """
    利用 ddddocr 识别滑块缺口位置,返回滑动距离(像素)。
    :param bg: 背景图二进制(带缺口的完整背景)
    :param tp: 滑块图二进制(带透明通道的拼图块)
    :return: 缺口 x 坐标(像素)
    """
    det = DdddOcr(det=False, ocr=False, show_ad=False)
    res = det.slide_match(tp, bg, simple_target=True)
    return int(res['target'][0])

res['target'] 返回 [left, top, right, bottom]left 即缺口的 x 坐标。

极验 GT4 图片为标准分辨率(非 Retina 2x),无需额外 /2 校正,与数美等 SDK 不同。

备选方案:OpenCV 模板匹配

若 ddddocr 识别不准,可改用 Canny 边缘检测 + 模板匹配:

python 复制代码
import cv2

def opencv_get_distance(bg_path: str, slice_path: str) -> int:
    """OpenCV Canny 边缘检测 + 模板匹配识别缺口距离"""
    bg    = cv2.imread(bg_path)
    tp    = cv2.imread(slice_path, cv2.IMREAD_UNCHANGED)
    # 统一灰度化
    bg_gray = cv2.cvtColor(bg, cv2.COLOR_BGR2GRAY)
    tp_gray = (cv2.cvtColor(tp, cv2.COLOR_BGRA2GRAY)
               if tp.shape[2] == 4
               else cv2.cvtColor(tp, cv2.COLOR_BGR2GRAY))
    # 边缘检测后模板匹配
    result = cv2.matchTemplate(
        cv2.Canny(bg_gray, 50, 150),
        cv2.Canny(tp_gray, 50, 150),
        cv2.TM_CCOEFF_NORMED
    )
    return cv2.minMaxLoc(result)[3][0]   # max_loc[0] 即 x

七、JSONP 响应解析

GT4 的 load/verify 接口均返回 JSONP 格式,回调函数名包含毫秒级时间戳,长度不固定,不能用固定偏移量(如 [22:-1])截取,应动态定位括号:

python 复制代码
def parse_jsonp(text: str) -> dict:
    """动态解析 JSONP 响应,提取 JSON 部分"""
    json_str = text[text.index('(') + 1: text.rindex(')')]
    return json.loads(json_str)

八、完整请求流程概述

步骤 动作 说明
1 生成会话标识 本地生成 challengeuuid.uuid4())和 callbackgeetest_ + 时间戳)
2 调用 load 接口 获取 lot_number、背景图/滑块图路径、pow_detailpayloadprocess_token
3 下载图片 https://static.geetest.com/ 下载 bg 和 slice
4 识别缺口距离 ddddocr slide_match 得到 distance
5 计算 PoW 根据 pow_detail.bitshashfunc 动态碰撞,得到 pow_msg/pow_sign
6 组装 w_data 填入 distance、passtime、userresponse、pow 等字段
7 加密构造 w AES_CBC(w_data, aes_key) + RSA(aes_key) 拼接
8 调用 verify 接口 携带 w 及会话参数,解析返回 result: success/fail

九、关键知识点总结

知识点 详情
challenge 来源 本地 uuid.uuid4() 生成,GT4 源码明确支持(`config.challenge
captcha_id 业务方配置的固定值,从前端 JS 提取,不会每次变化
图像识别库 ddddocr 滑块匹配,GT4 无 Retina 缩放,无需 /2
AES 加密 AES-128-CBC,PKCS7 填充,输出 hex;密钥短于 16 字节时取 MD5
RSA 加密 PKCS1 v1.5,公钥参数可从 gt4.js 明文读取,输出 hex
w 结构 AES(w_data) hex + RSA(aes_key) hex 直接拼接
PoW 机制 动态适配 bits/hashfunc,bits=0 时直接返回,bits>0 时循环碰撞
JSONP 解析 动态定位 ( ) 位置,不能硬编码偏移量
Session 管理 使用 requests.Session 自动携带服务端下发的 Cookie(如 captcha_v4_user

十、与数美 SDK(protocol=206)的横向对比

对比维度 极验 GT4 数美 SDK(新版)
加密算法 AES-CBC + RSA,输出 hex DES-ECB,输出 Base64
加密参数数量 合并为 1 个 w 参数 分散为 12 个独立字段
密钥来源 本地随机生成 AES Key + 固定公钥 硬编码 Key 逆向 + 动态派生 __key
工作量证明 有 PoW 机制(pow_detail 动态下发) 无 PoW
图片分辨率 标准分辨率,无缩放 Retina 2x,需 /2 校正
序列化 标准 json.dumps 自定义 smStringify(差异处理 undefined)
JSONP 解析 需动态解析 需动态解析
JS 混淆程度 gt4.js 基本可读 obfuscator.io 重度混淆,需 webcrack 反混淆

十一、依赖安装

bash 复制代码
pip install requests pycryptodome ddddocr opencv-python

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

相关推荐
恋恋风尘hhh4 小时前
滑块验证码前端安全研究:以数美(Ishumei)风控 SDK 为例
状态模式
前端不太难20 小时前
从 OpenClaw 到端侧 AI:低算力智能体架构设计
人工智能·状态模式
阿珊和她的猫20 小时前
使用 TypeScript 实现数组类型判断方法
javascript·typescript·状态模式
Smoothcloud润云2 天前
从“预测下一个词”到“预测下一个世界状态”:世界模型作为AGI新范式的深度分析报告
人工智能·测试工具·微服务·容器·github·状态模式·agi
GISer_Jing2 天前
前端视频技术全解析:从编解码到渲染优化
前端·音视频·状态模式
ZC跨境爬虫2 天前
海南大学交友平台开发实战 day10(后端向前端输出_前端读取数据全流程联调+日志调试落地)
前端·python·sqlite·html·状态模式
前端不太难2 天前
OpenClaw 的设备架构设计:如何在有限算力上跑复杂智能体
状态模式·openclaw
Rabbit_QL3 天前
【服务出错问题排查记录】从一个“点击失败”开始:为什么“系统异常”其实是最差的错误设计
状态模式
老神在在0015 天前
Spring Boot 全局异常处理器(GlobalExceptionHandler)
spring boot·spring·java-ee·状态模式·