免责声明: 本文所有分析均基于公开可访问的前端 JS 代码及极验官网演示页(https://www.geetest.com/adaptive-captcha),仅用于安全研究、学习与了解验证码防护机制。文中所有敏感参数(RSA 公钥模数、captcha_id 等)均来自极验官方公开演示环境,不涉及任何第三方业务系统。请勿将本文技术用于任何未授权的系统,违者后果自负。
一、背景介绍
极验 GT4(第四代行为验证)是国内应用最广泛的人机验证方案之一,支持滑动拼图、文字点选、一点即过、消消乐等多种验证形式。本文以滑动拼图验证(slide) 为研究对象,分析其前端安全机制。
基本交互逻辑:
- 前端携带
captcha_id+ 动态challenge请求load接口,获取背景图(bg)、滑块图(slice)、会话标识(lot_number)及工作量证明参数(pow_detail) - 用计算机视觉识别滑块应移动到的缺口位置,得到
distance - 前端将滑动距离、耗时、设备信息等核心字段组装为
w_data,经 AES-CBC + RSA 双层加密后构造参数w - 携带
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 | 哈希算法,md5 或 sha256 |
| 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 | 生成会话标识 | 本地生成 challenge(uuid.uuid4())和 callback(geetest_ + 时间戳) |
| 2 | 调用 load 接口 | 获取 lot_number、背景图/滑块图路径、pow_detail、payload、process_token |
| 3 | 下载图片 | 从 https://static.geetest.com/ 下载 bg 和 slice |
| 4 | 识别缺口距离 | ddddocr slide_match 得到 distance |
| 5 | 计算 PoW | 根据 pow_detail.bits 和 hashfunc 动态碰撞,得到 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
本文技术仅供安全研究与学习,切勿用于任何未授权系统,违者后果自负。