关于python:验证码逆向专栏某验全家桶细节避坑总结

48次阅读

共计 10552 个字符,预计需要花费 27 分钟才能阅读完成。

申明

本文章中所有内容仅供学习交换应用,不用于其余任何目标,不提供残缺代码,抓包内容、敏感网址、数据接口等均已做脱敏解决,严禁用于商业用途和非法用处,否则由此产生的所有结果均与作者无关!

本文章未经许可禁止转载,禁止任何批改后二次流传,擅自应用本文解说的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K 哥爬虫】分割作者立刻删除!

前言

某验的验证码总体来说还是很简略的,然而也有一些细节可能要留神一下,如果你扣完算法发现验证报各种各样的谬误,或者在官网的 demo 能验证通过,在其余网站却验证失败,那么就能够看看本文总结的细节你有没有留神到。

除此之外,本文还分享了一些验证码的辨认计划、轨迹的解决,这些办法大多来自网络上其余大佬的分享,间接百度就能搜到,本文只是做了一个演绎总结。

对于 w 值

三代外面,有几个接口申请都有 w,但除了最初一个校验接口 ajax.php 以外,其余接口的 w 能够置空,但也不齐全都是这样,比方三代的一键通过模式(无感验证),在申请 get.php 接口获取 c 和 s 值的时候,同样校验了 w 值,因而须要获取两次 w 值,而这两次 w 值的生成形式还不太一样,须要本人仔细分两次扣一下。如果你第一次不带 w,或者 w 生成谬误,就会报以下谬误:

{'status': 'error', 'error': 'param decrypt error', 'user_error': '网络不给力', 'error_code': 'error_03'}

对于工夫距离

三代外面,整个流程走得太快了也是不行的,须要在生成 w 值之后,随机停留个 2 秒左右,以三代的点选(文字点选、图标点选、语序点选、空间推理)为例,如果整得太快了验证失败会报以下谬误:

{'status': 'success', 'data': {'result': 'fail', 'msg': ['duration short']}}

对于 challenge

三代外面,有个 challenge 参加了很多接口的申请,三代滑块比拟非凡,第一次获取到了一个 challenge,前面的第二个 get.php 申请返回数据里会有一个新的 challenge,新的 challenge 比第一次的 challenge 多了两位数,后续的申请要用这个新的 challenge 才行,不然的话会报以下谬误:

{'success': 0, 'message': 'fail'}

对于 c 和 s

三代外面,有个 c 和 s 的值参加了 w 的计算,点选系列和滑块,第一次 get.php 申请会返回一个 c 和 s,第二次 get.php 申请也会返回一个 c 和 s,两次的 c 个别是不变的,但 s 会变,生成 w 要用第二次 get.php 返回的 s 才行,不然的话会报以下谬误:

{'success': 0, 'message': 'forbidden'}

对于两次 get.php 和 ajax.php 申请

同样还是三代外面,点选系列和滑块,会有两次以 get.phpajax.php 结尾的申请,第一次的 get.php 返回的是一些主题、域名、提醒文字等信息,第一次的 ajax.php 返回的是验证码的类型,这两次申请返回的数据尽管对咱们没太大用处,然而咱们还是得发动申请,不然后续的申请就不对,必须得依照他这个程序来才行。

对于智能组合验证

智能组合验证说白了就是当时不晓得是什么类型,四代在很多网站都是抉择智能模式,解决办法也很简略,当时把所有类型都筹备好,而后通过接口返回的验证码类型来接入不同的逻辑。

三代判断逻辑:第一次的 ajax.php 接口,返回值会通知你是点选 (click) 还是滑块 (slide),其中点选又分为文字点选、图标点选、语序点选和空间推理,它们的类型都为 click,这个时候就要进行第二次判断,第二次 get.php 返回的 pic_type 字段,会通知你是文字点选 (word)、图标点选 (icon)、语序点选 (phrase) 还是空间推理 (space)。

四代判断逻辑:四代更简洁,load 接口会有一个 captcha_type 字段,会间接通知你是滑块、点选(以及哪种类型的点选)、五子棋还是九宫格等。

对于扣 w 的算法

扣 w 的算法,外面也有一些细节,某些参数也值得注意。

passtime

不论是二代、三代还是四代,生成 w 的时候常常有个 passtime 参加了计算,这个值分为两种状况,如果是滑块,这个值应该是滑动破费的工夫,因为滑块的轨迹里蕴含了工夫,所以应该间接取轨迹的最初一个工夫值即可,即 track[track.length - 1][2],以三代为例,如果这个值和你轨迹里的工夫不统一,就会报以下谬误:

{'success': 0, 'message': 'forbidden'}

除了滑块,其余状况下,这个值写死就行,不过还是倡议写个随机值:Math.floor((Math.random()*500) + 4000)

pow_sign 和 pow_msg

这两个参数是四代里独有的,如果你是在 gt4.geetest.com 进行调试,你会发现 pow_msg 的组成格局如下:

1|0|md5|datetime|captcha_id|lot_number|| 随机字符串

pow_sign 则是 pow_msg 通过 MD5 加密后的值,如下图所示:

这里你可能不留神的话,间接依照这个格局写死了,特地是最初一个随机值,真的随机其实是不行的,真随机就会导致你在某些网站里能通过,某些网站不能通过。搜寻 pow_sign 或者 pow_msg 的 Unicode 值,总共就三个中央,都下个断点,刷新一下网页,断下之后仔细分析,其实是有三种算法的,如下图所示:

上图中第 6819 行的 h 就是随机值,后续会依据不同算法进行计算,判断这个随机值是否满足一些条件,满足才是正确的,能够在 load 接口返回的 pow_detail 字段判断是 MD5、SHA1 还是 SHA256,如下图所示:

这一段的解决逻辑扣进去就是这样的:

var CryptoJS = require("crypto-js");


function getRandomString(){function e(){return (65536 * (1 + Math.random()) | 0).toString(16).substring(1);
    }
    return e() + e() + e() + e();
}

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
                };
        }
    }
}

// 测试用例
// var pow_detail = {
//     bits: 0,
//     datetime: "2023-02-09T11:04:17.687400+08:00",
//     hashfunc: "md5",
//     version: "1"
// }
// var captcha_id = "08c16c99330a5a1d6b7f4371bbd5a978"
// var lot_number = "1417b7e362b748429003c412b3aa300c"
// console.log(get_pow(pow_detail, captcha_id, lot_number))

只有通过这样解决,能力保障 pow_signpow_msg 是正确的,能力适配不同网站、不同算法的验证。

随机变动的字符串

不论是哪一代,都会有一个 16 位随机字符串参加了 w 的加密计算,这个随机字符串个别都会用到两次,这两次要保障是一样的才行。

如果这个字符串两次不一样,二、三代验证会报错如下:

{'status': 'error', 'error': 'param decrypt error', 'user_error': '网络不给力', 'error_code': 'error_03'}

四代验证会报错如下:

{'status': 'error', 'code': '-50002', 'msg': 'param decrypt error', 'desc': {'type': 'defined error'}}

随机变动的键值对

三四代生成 w 的过程中会有一个随机键值对,每隔一段时间就会变动,相似于 {h9s9: '1803797734'},这个键值对写死也能够,貌似不影响,但如果非要和网页一样随机起来应该怎么做呢?

以三代滑块为例,断点到 o 参数生成的中央,后续有个 langep 组成的 s 参数,通过 window[$_CAHJd(744)](s) 解决后,s 里就新增了一个键值对(不同类型略有差异,但生成的地位肯定离 o 不远,认真跟即可),如下图所示:

跟进去,会来到 gct.xxx.js 里,也是通过了一个办法后,就多了这个键值对:

这个 gct 的 js 具体地址能够在后面的 get.php 之类的申请里拿到,因为外面是一直变动的,所以能够采取动静申请这个 js,动静导出获取这个值,一个简略的逻辑如下:

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

补环境中可能用到的办法

补环境可能会遇到 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;
}

// 测试
// var a = new Uint32Array(256);
// console.log(window.crypto.getRandomValues(a))

另外,还有个用到 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
}

对于验证码的辨认

辨认次要有三种办法,第一个是会深度学习的话,本人用 OpenCV 之类的去辨认,第二个当然是十分牛逼的 ddddocr(https://github.com/sml2h3/ddddocr),还反对本人训练,是不错的抉择,当然也有一些其余开源库,这里就不一一举例了,第三个就是打码平台,这里举荐云码打码,可通过我的链接注册:https://www.jfbym.com/register/TG17764,本人去官网看,反对十分多的类型,甚至谷歌验证码都能够,价格也不贵,实测成功率 99%,还是不错的。这里贴一个 OpenCV 辨认滑块的源码(来源于互联网收集),成果还不错:

# CV2 辨认滑块缺口间隔

import cv2
import PIL
import numpy as np
from PIL import Image
from pathlib import Path


def imshow(img, winname='test', delay=0):
    """cv2 展现图片"""
    cv2.imshow(winname, img)
    cv2.waitKey(delay)
    cv2.destroyAllWindows()


def pil_to_cv2(img):
    """
    pil 转 cv2 图片
    :param img: pil 图像, <type 'PIL.JpegImagePlugin.JpegImageFile'>
    :return: cv2 图像, <type 'numpy.ndarray'>
    """
    img = cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)
    return img


def bytes_to_cv2(img):
    """
    二进制图片转 cv2
    :param img: 二进制图片数据, <type 'bytes'>
    :return: cv2 图像, <type 'numpy.ndarray'>
    """
    # 将图片字节码 bytes, 转换成一维的 numpy 数组到缓存中
    img_buffer_np = np.frombuffer(img, dtype=np.uint8)
    # 从指定的内存缓存中读取一维 numpy 数据, 并把数据转换 (解码) 成图像矩阵格局
    img_np = cv2.imdecode(img_buffer_np, 1)
    return img_np


def cv2_open(img, flag=None):
    """对立输入图片格式为 cv2 图像, <type'numpy.ndarray'>
    :param img: <type 'bytes'/'numpy.ndarray'/'str'/'Path'/'PIL.JpegImagePlugin.JpegImageFile'>
    :param flag: 色彩空间转换类型, default: None
        eg: cv2.COLOR_BGR2GRAY(灰度图):return: cv2 图像, <numpy.ndarray>
    """
    if isinstance(img, bytes):
        img = bytes_to_cv2(img)
    elif isinstance(img, (str, Path)):
        img = cv2.imread(str(img))
    elif isinstance(img, np.ndarray):
        img = img
    elif isinstance(img, PIL.Image.Image):
        img = pil_to_cv2(img)
    else:
        raise ValueError(f'输出的图片类型无奈解析: {type(img)}')
    if flag is not None:
        img = cv2.cvtColor(img, flag)
    return img


def get_distance(bg, tp, im_show=False, save_path=None):
    """
    :param bg: 背景图门路或 Path 对象或图片二进制
               eg: 'assets/bg.jpg'、Path('assets/bg.jpg')
    :param tp: 缺口图门路或 Path 对象或图片二进制
               eg: 'assets/tp.jpg'、Path('assets/tp.jpg')
    :param im_show: 是否显示后果, <type 'bool'>; default: False
    :param save_path: 保留门路, <type 'str'/'Path'>; default: None
    :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:
            save_path = Path(save_path).resolve()
            save_path = save_path.parent / f"{save_path.stem}{save_path.suffix}"
            save_path = save_path.__str__()
            cv2.imwrite(save_path, bg_img)
        # 显示缺口辨认后果
        if im_show:
            imshow(bg_img)
    return distance


# with open("./img/slide_bg.jpg", "rb") as f:
#     bg_img = f.read()
# with open("./img/slide_slice.png", "rb") as f:
#     slice_img = f.read()
# distance = get_distance(bg_img, slice_img)
# print(distance)

对于轨迹的生成

轨迹次要是针对滑块的,能够利用贝塞尔曲线、缓动函数等,来生成正确的轨迹,基于贝塞尔曲线的能够参考:https://github.com/2833844911/gurs,吾爱上也有个大佬利用 tanharctan 函数整合生成轨迹的:https://www.52pojie.cn/forum.php?mod=viewthread&tid=1162979

基于缓动函数的能够参考以下代码(来源于互联网收集):

import random


def __ease_out_expo(sep):
    """
    缓动函数 easeOutExpo
    参考:https://easings.net/zh-cn#easeOutExpo
    """
    if sep == 1:
        return 1
    else:
        return 1 - pow(2, -10 * sep)


def get_slide_track(distance):
    """
    依据滑动间隔生成滑动轨迹
    :param distance: 须要滑动的间隔
    :return: 滑动轨迹 <type 'list'>: [[x,y,t], ...]
        x: 已滑动的横向间隔
        y: 已滑动的纵向间隔, 除终点外, 均为 0
        t: 滑动过程耗费的工夫, 单位: 毫秒
    """

    if not isinstance(distance, int) or distance < 0:
        raise ValueError(f"distance 类型必须是大于等于 0 的整数: distance: {distance}, type: {type(distance)}")
    # 初始化轨迹列表
    slide_track = [[random.randint(-50, -10), random.randint(-50, -10), 0],
        [0, 0, 0],
    ]
    # 共记录 count 次滑块地位信息
    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

其余可能的报错

// challenge 不对
geetest_xxxxxxxxxxxxx({"status": "error", "error": "illegal challenge", "user_error": "网络不给力", "error_code": "error_23"})
// w 生成不对
geetest_xxxxxxxxxxxxx({"status": "error", "error": "param decrypt error", "user_error": "网络不给力", "error_code": "error_03"})
// 滑动验证没有轨迹
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"})

正文完
 0