爬虫逆向之滑块验证码加密分析(轨迹和坐标)

本文章中所有内容仅供学习交流使用,不用于其他任何目的。否则由此产生的一切后果均与作者无关!

在爬虫开发过程中,滑块验证码常常成为我们获取数据的一大阻碍。而滑块验证码的加密方式多种多样,其中轨迹加密和坐标加密是比较常见的两种方式。本文将详细介绍这两种加密方式的原理以及如何进行逆向分析。

验证码逆向过程分析

第一步,找生成图片的接口(接口可能有加密参数)获取图片url或者图片base64编码,可能还有id,token等值。

第二步,用识别工具识别图片,获取缺口坐标(用函数模拟轨迹)

第三步,获取缺口坐标(轨迹)(可能会进行加密)和第一次接口获取id、token等(可能会进行加密),可能直接携带加密坐标(轨迹)和token、id直接请求页面。也可能作为请求体或者cookie、请求头进行另一个接口请求(验证接口可能不止一个,甚至多级),请求成功返回一个成功的token(可能有时间效期,token可能只能用一次,也可能用多次)。携带token请求你要请求的页面,成功请求。

最好用seesion = requests.session()进行请求

接口不一定一次性返回 图片+id+token

 识别后并不一定直接拿“缺口坐标”(轨迹)就能用,可能会进行加密

验证接口可能不止一个,甚至多级

还有一个典型的「状态依赖」问题:

验证码接口依赖会话状态

/captcha/image 只做一件事:根据当前会话生成一张图并返回其token。

紧接着调 /captcha/check 也返回一个token

两个token一模一样

但如果只请求/captcha/image获取token,没有经过/captcha/check 直接去携带token请求页面,是通过不了的。

以下两个例子:

/captcha/image返回的token和/captcha/check返回的token一模一样

但是如果跳过/captcha/check,直接用/captcha/image返回的token去请求页面。是没法通过的。一句话:token 必须被 /captcha/check 把状态从 issued 变成 verified 才能继续用,否则服务端会判定“未经验证的验证码”。

/captcha/image接口

/captcha/check接口

返回响应内容

要请求的接口内容

这三个token一样,但是跳过/captcha/check接口直接用/captcha/image接口的token是请求不通过的。

1.坐标加密

目标网址:aHR0cHM6Ly96YnRiLmdkLmdvdi5jbi8jL2p5Z2c=

大于五页后都会有滑块验证码

抓包分析,图片接口

图片接口请求头字段加密字段

找加密位置非常简单,xhr跟栈就行了

回调第一个就是加密的位置

事实上是搜不到的,对字段进行打乱重组,但是跟栈也轻轻松松

这个值是要求的

求出来这种格式像什么

CryptoJSWordArray 对象的内部格式,哈希加密,.toString()

测试,这个就是原型的SHA256加密,直接用原生库即可

内部进行字符串拼接

最后一个参数需要传验证码接口的请求体

输出为请求体的拼接

直接写死即可(这网站请求详情,则不能写死,因为页数变化,时间变化)

简简单单请求出参数。获取两张图片

在验证码接口一共四个参数有用到

两张图片

secretKey为密钥

token为验证接口携带

用ddddcor求出x距离

ocr = ddddocr.DdddOcr()
img_1 = base64.b64decode(response.json()['data']['repData']['jigsawImageBase64'])
img_2 = base64.b64decode(response.json()['data']['repData']['originalImageBase64'])
token = response.json()['data']['repData']['token']
secretKey = response.json()['data']['repData']['secretKey']
print(ocr.slide_match(img_1,img_2)['target'][0])

第二个接口check

直接搜,比较简单

这个X对于是距离。

进去NO函数。非常容易的AES加密

def aes_encrypted(w, L):
    data = json.dumps(w, separators=(',', ':')).encode('utf-8')
    key = L.encode('utf-8')[:16].ljust(16, b'\0')
    cipher = AES.new(key, AES.MODE_ECB)
    ct = cipher.encrypt(pad(data, AES.block_size))
    return base64.b64encode(ct).decode('ascii')

对坐标加密

有个非常巨大的坑,表面L已经写好了,其实传参L不是这个值

恶心。实际L值是第一个接口返回secretKey

最后成功获取token

在详情页携带token,就能顺利请求内容了。

2.轨迹加密

目标网址:aHR0cHM6Ly9janljLmhiYmlkZGluZy5jb20uY24vaHViZWl5dGgvanl4eC90cmFkZV9pbmZvci5odG1s

验证码接口直接请求即可,非常友好(后面你就知道有一个巨大的坑)

图片处理请求距离

img_1 = base64.b64decode(response.json()['captcha']['templateImage'].split('base64,')[1])
img_2 = base64.b64decode(response.json()['captcha']['backgroundImage'].split('base64,')[1])
ocr = ddddocr.DdddOcr()
x = ocr.slide_match(img_1,img_2)['target'][0]
print(x)

非常友好,瞬间出来

看看第二个验证接口

一眼看出,id是第一个接口请求的,data加密很明显是base64加密

响应cookies为第二个接口求这个值

base64解密看一下

解析一下

'bgImageWidth': 260,
'bgImageHeight': 159,
'sliderImageWidth': 49,
'sliderImageHeight': 159,这四个参数为两张验证码大小

"startSlidingTime":"2025-08-14T09:55:48.568Z","endSlidingTime":"2025-08-14T09:55:50.110Z",

startSlidingTime开始时间,endSlidingTime点击到验证码验证时间

trackList就是轨迹

一大串轨迹,跟栈调试一下

x代表移动距离,y代表上下多动,由x的变化看出,是先慢后快,在慢,t则是时间

轨迹生成的函数

    def gen_track( gap_x, gap_y=0, seed=None):
        # ran_x = random.randint(19, 40)
        """
        模拟轨迹生成
        生成「慢→快→慢」三段式轨迹
        gap_x : 缺口 x 像素
        gap_y : y 轴最大抖动像素
        seed  : 随机种子,方便调试
        """
        if seed:
            random.seed(seed)

        # 总步数 & 总耗时
        steps = random.randint(40, 60)  # 步数少一点更平滑
        total_t = random.randint(2800, 3500)  # 总耗时 2.8~3.5 s

        track = []
        x0, y0 = 0, 0
        t0 = 2383  # 起始时间戳
        gap_x = gap_x
        for i in range(steps + 1):
            # 1. 三段式 S 曲线映射
            t = i / steps
            # 三次贝塞尔缓动:慢→快→慢
            ratio = 3 * t ** 2 - 2 * t ** 3
            if i != 0:
                # 2. 计算本次坐标
                x = int(round(x0 + gap_x * ratio)) + 1
                y = y0 + random.randint(-1, 1)
            if i == 0:
                x = 0
                y = 0

            # 3. 时间分布也按 S 曲线:开始稀疏、中间密集、末尾稀疏
            dt = int(total_t * (0.8 + 1.2 * (1 - math.sin(math.pi * t))) / steps)
            t0 += dt

            # 4. 事件类型
            if i == 0:
                ev_type = "down"
            elif i == steps:
                ev_type = "up"
            else:
                ev_type = "move"

            track.append({"x": x, "y": y, "type": ev_type, "t": t0})

            # 提前到达终点就停
            if x >= gap_x:
                track[-1]['x'] = gap_x
                track[-1]['type'] = "up"
                break
        return track

y上下抖动,X先慢后快在慢,t时间不能太快,传入x距离即可,t实际为毫秒,随机累加

ocr = ddddocr.DdddOcr()
x = ocr.slide_match(img_1,img_2)['target'][0]
track = gen_track(x)

在把时间整理一下

tart_iso = datetime.datetime.utcnow().isoformat(timespec='milliseconds') + 'Z'
end_iso = (datetime.datetime.utcnow() +
           datetime.timedelta(milliseconds=track[-1]['t'])).isoformat(timespec='milliseconds') + 'Z'

全部求出来,再用base64编码

ef gen_track(gap_x, gap_y=0, seed=None):
    # ran_x = random.randint(19, 40)
    """
    模拟轨迹生成
    生成「慢→快→慢」三段式轨迹
    gap_x : 缺口 x 像素
    gap_y : y 轴最大抖动像素
    seed  : 随机种子,方便调试
    """
    if seed:
        random.seed(seed)

    # 总步数 & 总耗时
    steps = random.randint(40, 60)  # 步数少一点更平滑
    total_t = random.randint(2800, 3500)  # 总耗时 2.8~3.5 s

    track = []
    x0, y0 = 0, 0
    t0 = 2383  # 起始时间戳
    gap_x = gap_x
    for i in range(steps + 1):
        # 1. 三段式 S 曲线映射
        t = i / steps
        # 三次贝塞尔缓动:慢→快→慢
        ratio = 3 * t ** 2 - 2 * t ** 3
        if i != 0:
            # 2. 计算本次坐标
            x = int(round(x0 + gap_x * ratio)) + 1
            y = y0 + random.randint(-1, 1)
        if i == 0:
            x = 0
            y = 0

        # 3. 时间分布也按 S 曲线:开始稀疏、中间密集、末尾稀疏
        dt = int(total_t * (0.8 + 1.2 * (1 - math.sin(math.pi * t))) / steps)
        t0 += dt

        # 4. 事件类型
        if i == 0:
            ev_type = "down"
        elif i == steps:
            ev_type = "up"
        else:
            ev_type = "move"

        track.append({"x": x, "y": y, "type": ev_type, "t": t0})

        # 提前到达终点就停
        if x >= gap_x:
            track[-1]['x'] = gap_x
            track[-1]['type'] = "up"
            break
    return track
ocr = ddddocr.DdddOcr()
x = ocr.slide_match(img_1,img_2)['target'][0]
track = gen_track(x)
tart_iso = datetime.datetime.utcnow().isoformat(timespec='milliseconds') + 'Z'
end_iso = (datetime.datetime.utcnow() +
           datetime.timedelta(milliseconds=track[-1]['t'])).isoformat(timespec='milliseconds') + 'Z'
payload = {
    'bgImageWidth': 260,
    'bgImageHeight': 159,
    'sliderImageWidth': 49,
    'sliderImageHeight': 159,
    'startSlidingTime': tart_iso,
    'endSlidingTime': end_iso,
    'trackList': track
}
data = base64.b64encode(json.dumps(payload, separators=(',', ':')).encode()).decode()
url = "https://cjyc.hbbidding.com.cn/captcha/check2"
payload = {
    "id": id ,
    "data":data
}
response = requests.post(url, headers=headers, cookies=cookies, data=payload)
print(response.text)

最终结果请求失败,请求多次还是失败

我在想,这么完美的请求方式,为什么错了,最后调试好久,对比距离得出

最后x的距离是网页验证码的距离,不是实际下载图片的距离

实际下载图片这么大

最终的大小按页面大小算

所以还得把图片修改一下

用opencv,改一下图片大小,再识别距离

 def _resize(b64: str, w: int, h: int) -> str:
        """把 base64 图片缩放成指定宽高后再 base64 编码"""
        img = cv2.imdecode(np.frombuffer(base64.b64decode(b64), np.uint8), cv2.IMREAD_COLOR)
        img = cv2.resize(img, (w, h), interpolation=cv2.INTER_AREA)
        return base64.b64encode(cv2.imencode('.jpg', img)[1]).decode()

以下为核心代码。图片大小,轨迹生成

response = requests.get(url, headers=headers, cookies=cookies, params=params)
id = response.json()['id']

def _resize(b64: str, w: int, h: int) -> str:
    """把 base64 图片缩放成指定宽高后再 base64 编码"""
    img = cv2.imdecode(np.frombuffer(base64.b64decode(b64), np.uint8), cv2.IMREAD_COLOR)
    img = cv2.resize(img, (w, h), interpolation=cv2.INTER_AREA)
    return base64.b64encode(cv2.imencode('.jpg', img)[1]).decode()
slider_b64 = _resize(response.json()['captcha']['templateImage'].split(',', 1)[-1], 49, 159)
bg_b64 = _resize(response.json()['captcha']['backgroundImage'].split(',', 1)[-1], 260, 159)
target_bytes = base64.b64decode(slider_b64)
bg_bytes = base64.b64decode(bg_b64)
img_1 = base64.b64decode(slider_b64)
img_2 = base64.b64decode(bg_b64)
def gen_track(gap_x, gap_y=0, seed=None):
    # ran_x = random.randint(19, 40)
    """
    模拟轨迹生成
    生成「慢→快→慢」三段式轨迹
    gap_x : 缺口 x 像素
    gap_y : y 轴最大抖动像素
    seed  : 随机种子,方便调试
    """
    if seed:
        random.seed(seed)

    # 总步数 & 总耗时
    steps = random.randint(40, 60)  # 步数少一点更平滑
    total_t = random.randint(2800, 3500)  # 总耗时 2.8~3.5 s

    track = []
    x0, y0 = 0, 0
    t0 = 2383  # 起始时间戳
    gap_x = gap_x
    for i in range(steps + 1):
        # 1. 三段式 S 曲线映射
        t = i / steps
        # 三次贝塞尔缓动:慢→快→慢
        ratio = 3 * t ** 2 - 2 * t ** 3
        if i != 0:
            # 2. 计算本次坐标
            x = int(round(x0 + gap_x * ratio)) + 1
            y = y0 + random.randint(-1, 1)
        if i == 0:
            x = 0
            y = 0

        # 3. 时间分布也按 S 曲线:开始稀疏、中间密集、末尾稀疏
        dt = int(total_t * (0.8 + 1.2 * (1 - math.sin(math.pi * t))) / steps)
        t0 += dt

        # 4. 事件类型
        if i == 0:
            ev_type = "down"
        elif i == steps:
            ev_type = "up"
        else:
            ev_type = "move"

        track.append({"x": x, "y": y, "type": ev_type, "t": t0})

        # 提前到达终点就停
        if x >= gap_x:
            track[-1]['x'] = gap_x
            track[-1]['type'] = "up"
            break
    return track
ocr = ddddocr.DdddOcr()
x = ocr.slide_match(img_1,img_2)['target'][0]
track = gen_track(x)

请求失败

多试几次就请求成功了。请求概率挺高的

请求成功

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值