【验证码逆向专栏】某验全家桶细节避坑总结
字数 2241 2025-08-11 01:06:30

某验验证码逆向分析与避坑指南

一、某验验证码概述

某验验证码分为多个版本(二代、三代、四代),主要包括以下几种类型:

  • 滑块验证(slide)
  • 点选验证(click):
    • 文字点选(word)
    • 图标点选(icon)
    • 语序点选(phrase)
    • 空间推理(space)
  • 五子棋验证
  • 九宫格验证

二、关键参数与常见错误

1. w值处理

  • 三代验证中多个接口需要w值,除最后一个校验接口ajax.php外,其他接口w可置空
  • 例外情况:三代一键通过模式(无感验证)在请求get.php接口时也校验w值
  • 解决方案:需要获取两次w值,且两次生成方式不同

2. 时间间隔控制

  • 三代验证流程不能过快,生成w值后需随机停留约2秒
  • 错误提示:流程过快会导致验证失败

3. challenge参数

  • 三代滑块验证中:
    • 第一次获取challenge
    • 第二次get.php请求返回新challenge(比第一次多两位数)
    • 必须使用新challenge进行后续请求
  • 错误提示:使用旧challenge会导致验证失败

4. c和s参数

  • 参与w值计算
  • 点选和滑块验证中:
    • 第一次get.php返回c和s
    • 第二次get.php返回新c和s(c通常不变,s会变)
    • 必须使用第二次get.php返回的s生成w值
  • 错误提示:使用错误的s值会导致验证失败

5. 请求顺序要求

  • 三代点选和滑块验证必须按顺序发起请求:
    1. 第一次get.php(返回主题、域名等信息)
    2. 第一次ajax.php(返回验证码类型)
    3. 第二次get.php
    4. 第二次ajax.php
  • 注意:即使前两次请求返回的数据看似无用,也必须发起

三、智能组合验证处理

1. 三代判断逻辑

  1. 第一次ajax.php接口返回验证码类型(click或slide)
  2. 如果是click类型,通过第二次get.php返回的pic_type字段判断具体点选类型:
    • 文字点选(word)
    • 图标点选(icon)
    • 语序点选(phrase)
    • 空间推理(space)

2. 四代判断逻辑

  • load接口的captcha_type字段直接指明验证类型

四、w值算法细节

1. passtime参数

  • 滑块验证:取轨迹最后一个时间值(track[track.length - 1][2])
  • 其他验证:建议使用随机值:Math.floor((Math.random()*500) + 4000)
  • 错误提示:滑块验证中passtime与轨迹时间不一致会导致失败

2. pow_sign和pow_msg(四代特有)

  • pow_msg格式:1|0|md5|datetime|captcha_id|lot_number||随机字符串
  • pow_sign是pow_msg的MD5值
  • 关键点
    • 随机字符串不能真随机,需满足特定条件
    • 通过load接口返回的pow_detail字段判断算法类型(MD5/SHA1/SHA256)

算法实现代码

function get_pow(pow_detail, captcha_id, lot_number) {
  var n = pow_detail.hashfunc;
  var i = pow_detail.version;
  var r = pow_detail.bits;
  var s = pow_detail.datetime;
  var o = "";
  var a = r % 4;
  var u = parseInt(r / 4, 10);
  var c = function g(e, t) { return new Array(t + 1).join(e); }("0", u);
  var _ = i + "|" + r + "|" + n + "|" + s + "|" + captcha_id + "|" + lot_number + "|" + o + "|";
  
  while (1) {
    var h = getRandomString(),
        l = _ + h,
        p = void 0;
    
    switch (n) {
      case "md5": p = CryptoJS.MD5(l).toString(); break;
      case "sha1": p = CryptoJS.SHA1(l).toString(); break;
      case "sha256": p = CryptoJS.SHA256(l).toString();
    }
    
    if (0 == a) {
      if (0 === p.indexOf(c)) return { "pow_msg": _ + h, "pow_sign": p };
    } else if (0 === p.indexOf(c)) {
      var f = void 0, d = p[u];
      switch (a) {
        case 1: f = 7; break;
        case 2: f = 3; break;
        case 3: f = 1;
      }
      if (d <= f) return { "pow_msg": _ + h, "pow_sign": p };
    }
  }
}

3. 随机字符串

  • 16位随机字符串参与w值加密
  • 关键点:同一流程中该字符串需使用两次且保持一致
  • 错误提示
    • 二/三代:随机字符串不一致会导致验证失败
    • 四代:会报特定错误

4. 随机键值对

  • 三四代生成w过程中有随机键值对(如{'h9s9': '1803797734'}
  • 解决方案
    1. 从get.php请求获取gct.js路径
    2. 动态请求gct.js并导出获取键值对的方法

Python实现代码

import re
import execjs
import requests

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
}

# gct js路径
gct_path = "https://static.geetest.com/static/js/gct.b71a9027509bc6bcfef9fc6a196424f5.js"
gct_js = requests.get(gct_path, headers=headers).text

# 正则匹配需要调用的方法名称
function_name = re.findall(r"\)\)\{return", gct_js)[0]

# 查找需要插入全局导出代码的位置
break_position = gct_js.find("return function(t){")

# window.gct全局导出方法
gct_js_new = gct_js[:break_position] + "window.gct=" + function_name + ";" + gct_js[break_position:]

# 添加自定义方法调用window.gct获取键值对
gct_js_new = "window = global;" + gct_js_new + """
function getGct(){
    var e = {"lang": "zh", "ep": "test data"};
    window.gct(e);
    delete e["lang"];
    delete e["ep"];
    return e;
}"""

gct = execjs.compile(gct_js_new).call("getGct")
print(gct)  # 输出示例: {'h9s9': '1803797734'}

五、补环境方法

1. window.crypto.getRandomValues()

window = global;
window.crypto = {
    getRandomValues: getRandomValues_
}

function randoms(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min)
}

function getRandomValues_(buf) {
    var min = 0, max = 255;
    if (buf.length > 65536) {
        var e = new Error();
        e.code = 22;
        e.message = 'Failed to execute \'getRandomValues\' : The ' + 
                    'ArrayBufferView\'s byte length (' + buf.length + 
                    ') exceeds the ' + 'number of bytes of entropy available via this API (65536).';
        e.name = 'QuotaExceededError';
        throw e;
    }
    if (buf instanceof Uint16Array) {
        max = 65535;
    } else if (buf instanceof Uint32Array) {
        max = 4294967295;
    }
    for (var element in buf) {
        buf[element] = randoms(min, max);
    }
    return buf;
}

2. window.performance.timing

function timing() {
    var now = Date.now()
    var tim = {
        "navigationStart": now,
        "unloadEventStart": now + 200,
        "unloadEventEnd": now + 200,
        "redirectStart": 0,
        "redirectEnd": 0,
        "fetchStart": now + 100,
        "domainLookupStart": now + 150,
        "domainLookupEnd": now + 250,
        "connectStart": now + 30,
        "connectEnd": now + 50,
        "secureConnectionStart": now + 52,
        "requestStart": now + 72,
        "responseStart": now + 91,
        "responseEnd": now + 92,
        "domLoading": now + 99,
        "domInteractive": now + 105,
        "domContentLoadedEventStart": now + 105,
        "domContentLoadedEventEnd": now + 111,
        "domComplete": now + 111,
        "loadEventStart": now + 111,
        "loadEventEnd": now + 111,
    }
    return tim
}

六、验证码识别方案

1. OpenCV识别滑块缺口

import cv2
import numpy as np
from pathlib import Path

def get_distance(bg, tp, im_show=False, save_path=None):
    """
    :param bg: 背景图路径或二进制
    :param tp: 缺口图路径或二进制
    :param im_show: 是否显示结果
    :param save_path: 保存路径
    :return: 缺口位置
    """
    # 读取图片
    bg_img = cv2_open(bg)
    tp_gray = cv2_open(tp, flag=cv2.COLOR_BGR2GRAY)
    
    # 金字塔均值漂移
    bg_shift = cv2.pyrMeanShiftFiltering(bg_img, 5, 50)
    
    # 边缘检测
    tp_gray = cv2.Canny(tp_gray, 255, 255)
    bg_gray = cv2.Canny(bg_shift, 255, 255)
    
    # 目标匹配
    result = cv2.matchTemplate(bg_gray, tp_gray, cv2.TM_CCOEFF_NORMED)
    
    # 解析匹配结果
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
    distance = max_loc[0]
    
    if save_path or im_show:
        # 绘制矩形框
        tp_height, tp_width = tp_gray.shape[:2]
        x, y = max_loc
        _x, _y = x + tp_width, y + tp_height
        bg_img = cv2_open(bg)
        cv2.rectangle(bg_img, (x, y), (_x, _y), (0, 0, 255), 2)
        
        if save_path:
            cv2.imwrite(str(save_path), bg_img)
        if im_show:
            cv2.imshow('result', bg_img)
            cv2.waitKey(0)
            cv2.destroyAllWindows()
    
    return distance

2. 推荐方案

  1. 深度学习:使用OpenCV等库自行训练模型
  2. ddddocr:支持多种验证码类型和自定义训练
  3. 打码平台:如云码打码平台,支持多种验证码类型

七、轨迹生成方案

1. 基于缓动函数的轨迹生成

import random

def __ease_out_expo(sep):
    """缓动函数easeOutExpo"""
    if sep == 1:
        return 1
    else:
        return 1 - pow(2, -10 * sep)

def get_slide_track(distance):
    """
    根据滑动距离生成滑动轨迹
    :param distance: 需要滑动的距离
    :return: 滑动轨迹[[x,y,t], ...]
    """
    if not isinstance(distance, int) or distance < 0:
        raise ValueError(f"distance类型必须是大于等于0的整数: {distance}")
    
    # 初始化轨迹列表
    slide_track = [
        [random.randint(-50, -10), random.randint(-50, -10), 0],
        [0, 0, 0],
    ]
    
    # 记录次数
    count = 30 + int(distance / 2)
    
    # 初始化滑动时间
    t = random.randint(50, 100)
    
    # 记录上一次滑动的距离
    _x = 0
    _y = 0
    
    for i in range(count):
        # 已滑动的横向距离
        x = round(__ease_out_expo(i / count) * distance)
        # 滑动过程消耗的时间
        t += random.randint(10, 20)
        
        if x == _x:
            continue
            
        slide_track.append([x, _y, t])
        _x = x
    
    slide_track.append(slide_track[-1])
    return slide_track

2. 其他方案

  • 贝塞尔曲线:参考开源项目如gurs
  • tanh和arctan函数:可参考相关技术论坛实现

八、常见错误代码

  1. geetest_xxxxxxxxxxxxx({"status": "error", "error": "illegal challenge", "user_error": "网络不给力", "error_code": "error_23"})

    • 原因:challenge参数不正确
  2. geetest_xxxxxxxxxxxxx({"status": "error", "error": "param decrypt error", "user_error": "网络不给力", "error_code": "error_03"})

    • 原因:w值生成不正确
  3. geetest_xxxxxxxxxxxxx({"status": "error", "error": "not proof", "user_error": "网络不给力", "error_code": "error_21"})

    • 原因:滑动验证缺少轨迹
  4. geetest_xxxxxxxxxxxxx({"success": 0, "message": "fail"})

    • 原因:轨迹、缺口距离或其他参数问题
  5. geetest_xxxxxxxxxxxxx({"success": 0, "message": "forbidden"})

    • 原因:参数问题或验证失败
某验验证码逆向分析与避坑指南 一、某验验证码概述 某验验证码分为多个版本(二代、三代、四代),主要包括以下几种类型: 滑块验证(slide) 点选验证(click): 文字点选(word) 图标点选(icon) 语序点选(phrase) 空间推理(space) 五子棋验证 九宫格验证 二、关键参数与常见错误 1. w值处理 三代验证中多个接口需要w值,除最后一个校验接口ajax.php外,其他接口w可置空 例外情况:三代一键通过模式(无感验证)在请求get.php接口时也校验w值 解决方案 :需要获取两次w值,且两次生成方式不同 2. 时间间隔控制 三代验证流程不能过快,生成w值后需随机停留约2秒 错误提示 :流程过快会导致验证失败 3. challenge参数 三代滑块验证中: 第一次获取challenge 第二次get.php请求返回新challenge(比第一次多两位数) 必须使用新challenge 进行后续请求 错误提示 :使用旧challenge会导致验证失败 4. c和s参数 参与w值计算 点选和滑块验证中: 第一次get.php返回c和s 第二次get.php返回新c和s(c通常不变,s会变) 必须使用第二次get.php返回的s 生成w值 错误提示 :使用错误的s值会导致验证失败 5. 请求顺序要求 三代点选和滑块验证必须按顺序发起请求: 第一次get.php(返回主题、域名等信息) 第一次ajax.php(返回验证码类型) 第二次get.php 第二次ajax.php 注意 :即使前两次请求返回的数据看似无用,也必须发起 三、智能组合验证处理 1. 三代判断逻辑 第一次ajax.php接口返回验证码类型(click或slide) 如果是click类型,通过第二次get.php返回的pic_ type字段判断具体点选类型: 文字点选(word) 图标点选(icon) 语序点选(phrase) 空间推理(space) 2. 四代判断逻辑 load接口的captcha_ type字段直接指明验证类型 四、w值算法细节 1. passtime参数 滑块验证 :取轨迹最后一个时间值(track[ track.length - 1][ 2 ]) 其他验证 :建议使用随机值: Math.floor((Math.random()*500) + 4000) 错误提示 :滑块验证中passtime与轨迹时间不一致会导致失败 2. pow_ sign和pow_ msg(四代特有) pow_ msg格式: 1|0|md5|datetime|captcha_id|lot_number||随机字符串 pow_ sign是pow_ msg的MD5值 关键点 : 随机字符串不能真随机,需满足特定条件 通过load接口返回的pow_ detail字段判断算法类型(MD5/SHA1/SHA256) 算法实现代码 3. 随机字符串 16位随机字符串参与w值加密 关键点 :同一流程中该字符串需使用两次且保持一致 错误提示 : 二/三代:随机字符串不一致会导致验证失败 四代:会报特定错误 4. 随机键值对 三四代生成w过程中有随机键值对(如 {'h9s9': '1803797734'} ) 解决方案 : 从get.php请求获取gct.js路径 动态请求gct.js并导出获取键值对的方法 Python实现代码 五、补环境方法 1. window.crypto.getRandomValues() 2. window.performance.timing 六、验证码识别方案 1. OpenCV识别滑块缺口 2. 推荐方案 深度学习 :使用OpenCV等库自行训练模型 ddddocr :支持多种验证码类型和自定义训练 打码平台 :如云码打码平台,支持多种验证码类型 七、轨迹生成方案 1. 基于缓动函数的轨迹生成 2. 其他方案 贝塞尔曲线 :参考开源项目如gurs tanh和arctan函数 :可参考相关技术论坛实现 八、常见错误代码 geetest_xxxxxxxxxxxxx({"status": "error", "error": "illegal challenge", "user_error": "网络不给力", "error_code": "error_23"}) 原因 :challenge参数不正确 geetest_xxxxxxxxxxxxx({"status": "error", "error": "param decrypt error", "user_error": "网络不给力", "error_code": "error_03"}) 原因 :w值生成不正确 geetest_xxxxxxxxxxxxx({"status": "error", "error": "not proof", "user_error": "网络不给力", "error_code": "error_21"}) 原因 :滑动验证缺少轨迹 geetest_xxxxxxxxxxxxx({"success": 0, "message": "fail"}) 原因 :轨迹、缺口距离或其他参数问题 geetest_xxxxxxxxxxxxx({"success": 0, "message": "forbidden"}) 原因 :参数问题或验证失败