目标
B 站注册/登录触发极验(GeeTest)v3 滑块验证码时,需要自动化识别缺口位置并模拟拖动完成验证。
本项目采用 Siamese 孪生神经网络 计算图像相似度定位缺口,配合 YOLOv8 做目标检测。
GeeTest v3 协议分析
1. GET /gettype.php?gt={gt} → 获取验证类型
2. GET /get.php?gt={gt}&challenge={ch} → 获取 c/s 值(服务端 AES 密钥相关)
3. GET /pic/{hash}.jpg → 下载背景图 + 缺口图
4. POST /ajax.php → 提交鼠标轨迹 + 验证结果
AES + RSA 协议加密
GeeTest v3 的 ajax 请求需要加密,服务端公钥加密客户端生成的 AES key,然后用 AES-CBC 加密鼠标轨迹:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
import random
# 生成随机 16 字节 AES key(hex string 格式)
aeskey = ''.join(f'{int((1 + random.random()) * 65536):04x}'[1:] for _ in range(4))
# 用 GeeTest 公钥加密 AES key
enc_key = public_key.encrypt(aeskey.encode(), PKCS1v15()).hex()
# 用 AES-CBC 加密鼠标轨迹数据
def aes_encrypt(data: str, key: str) -> str:
cipher = Cipher(algorithms.AES(key.encode()), modes.CBC(key[:16].encode()))
enc = cipher.encryptor()
# PKCS7 padding
padded = data.encode() + b'\x00' * (16 - len(data) % 16)
return (enc.update(padded) + enc.finalize()).hex()
Siamese 孪生网络定位缺口
孪生网络用于计算「缺口小图」和「背景大图」各位置的相似度,输出最高相似位置即为滑块终点:
import onnxruntime as ort
import numpy as np
class Model:
def __init__(self):
self.siamese = ort.InferenceSession('siamese.onnx')
self.yolo = ort.InferenceSession('yolov8s.onnx')
def detect(self, img_bytes: bytes):
# YOLOv8 检测小图和大图区域
img = preprocess(img_bytes)
boxes = self.yolo.run(None, {'images': img})[0]
small_img, big_img = crop_boxes(img, boxes)
return small_img, big_img
def siamese_match(self, small_img, big_img) -> list:
# 在大图上滑窗,找相似度最高的位置
scores = self.siamese.run(None, {
'small': small_img, 'big': big_img
})[0]
return scores.argmax(axis=-1).tolist()
鼠标轨迹模拟
真实鼠标轨迹从录制的 mousepath.json 中读取,然后根据滑动距离等比缩放:
import json
with open('mousepath.json') as f:
mouse_path = json.load(f)
def scale_path(path: list, target_x: float) -> list:
max_x = max(p[0] for p in path)
scale = target_x / max_x
return [[p[0] * scale, p[1], p[2]] for p in path]
验证点坐标编码
GeeTest 使用私有编码格式:
def encode_point(x: float, y: float, img_size: int = 333) -> str:
left = str(round((x + 30) / img_size * 10000))
top = str(round((y + 30) / img_size * 10000))
return f'{left}_{top}'
完整流程
def solve_geetest(gt: str, challenge: str) -> str:
crack = Crack(gt, challenge)
crack.gettype()
crack.get_c_s() # 获取服务端参数
time.sleep(3) # 模拟人工延迟
crack.ajax() # 发起验证请求
model = Model()
for retry in range(6):
pic = crack.get_pic(retry) # 获取图片
small, big = model.detect(pic) # 检测区域
points = model.siamese_match(small, big) # 定位缺口
encoded = [encode_point(*p) for p in points]
time.sleep(2.0) # 模拟识别耗时
result = crack.verify(encoded)
if result['data']['result'] == 'success':
return result['data']['validate']
return None
微信小程序协议伪装
B 站微信小程序端的注册接口风控相对较低,通过伪装 User-Agent 和 Referer 模拟小程序环境:
headers = {
'User-Agent': 'Mozilla/5.0 ... MicroMessenger/7.0.20.1781 ... MiniProgramEnv/Windows',
'xweb_xhr': '1',
'referer': 'https://servicewechat.com/wx7564fd5313d24844/255/page-frame.html',
}