目标

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',
}