数美滑块验证码分析

小红书、蘑菇街、脉脉、抖鱼等很多都用了数美的验证码。

本文以官网的滑块验证码为例,分析验证过程,完成模拟验证。


@


数美验证码官网:https://www.ishumei.com/trial/captcha.html
在这里插入图片描述


1.验证码申请

打开控制台多看几遍请求过程,就大抵明白请求步骤了,这里就不再细说。

api: 'https://captcha.fengkongcloud.com/ca/v1/conf?'
在这里插入图片描述

params= {
        'organization': 'RlokQwRlVjUrTUlkIqOg',
        'model': 'slide',
        'sdkver': '1.1.3',
        'rversion': '1.0.3',
        'appId': 'default',
        'lang': 'zh-cn',
        'channel': 'YingYongBao',
        'callback': 'sm_{}'.format(int(time.time() * 1000))
    }

该接口返回的js参数,是下一步需要请求的目标。
在这里插入图片描述

2.提取js参数

js地址:https://castatic.fengkongcloud.com/pr/auto-build/v1.0.3-70/captcha-sdk.min.js
需要提取该js中的参数名,会在最后验证的时候使用。
不过一般情况下参数名不会变,也可以省略此处,在后面写死。
在这里插入图片描述
在该js文件中的参数是倒序的
在这里插入图片描述


2.验证码注册

api: https://captcha.fengkongcloud.com/ca/v1/register?
在这里插入图片描述
bg和fg是验证码图片地址。 https://castatic.fengkongcloud.com/crb/+bg
在这里插入图片描述

3.计算滑块位置

根据上一步可以得到验证图片的地址。
验证码图片:https://castatic.fengkongcloud.com/crb/set-000006/v2/a13e3325e9f864fa42a94a6e07cd95fc_bg.jpg
滑块图片:https://castatic.fengkongcloud.com/crb/set-000006/v2/a13e3325e9f864fa42a94a6e07cd95fc_fg.png
在这里插入图片描述

使用opencv查找并匹配图像模板中的滑块。
需要注意的是,这里是以原图计算的,而页面上的图片大小只有(300,150),(应用不同的产品可能大小也不同)
所以需要按比例进行缩小。

4.验证

api:https://captcha.fengkongcloud.com/ca/v2/fverify?

params = {
    'protocol': '70',
    'organization': 'RlokQwRlVjUrTUlkIqOg',
    'rversion': '1.0.3',
    'oe': 'V/QxFC7ISm1=',
    'nj': '1ISY9IKNM+OGjwl0F7LP...省略...7VOki5p4sm+h/qMX2QAhN/4w',
    'zl': '8LwMmaImogs=',
    'sq': '14RHMSbfhJU=',
    'kh': 'qCcI31wL/Fs=',
    'mn': '4AAyKWNy6K0=',
    'rid': '20210113105643e313f68a420c9d240d',
    'ch': 'uiJ+hjbCOka=',
    'yv': 'b9NFDsBKGcg=',
    'ga': 'WssDiJ1wOQI=',
    'ko': '14bDqW72JnI=',
    'callback': 'sm_1610506618948',
    'act.os': 'web_pc',
    'vj': 'IFXKu8Pjb3k=',
    'ostype': 'web',
    'sdkver': '1.1.3'
}

params参数里的 oe,mn,kh等等,都经过了DES加密。

验证后会返回,
在这里插入图片描述
message = success,riskLevel=PASS 说明验证通过


5.完整代码

"""
数美滑块验证码破解验证
"""

import base64
import json
import random
import re
import time
from io import BytesIO
import cv2
import numpy as np
import requests
from pyDes import des, ECB

CAPTCHA_DISPLAY_WIDTH = 310
CAPTCHA_DISPLAY_HEIGHT = 155

p = {}

def pad(b):
    """
    块填充
    """
    block_size = 8
    while len(b) % block_size:
        b += b'\0'
    return b

def split_args(s):
    """
    分割js参数
    """
    r = []
    a = ''
    i = 0
    while i < len(s):
        c = s[i]
        if c == ',' and (a[0] != '\'' or len(a) >= 2 and a[-1] == '\''):
            r.append(a)
            a = ''
        elif c:
            a += c
        i += 1
    r.append(a)
    return r

def find_arg_names(script):
    """
    通过js解析出参数名
    """
    names = {}
    a = []
    for r in re.findall(r'function\((.*?)\)', script):
        if len(r.split(',')) > 100:
            a = split_args(r)
            break

    r = re.search(r';\)(.*?)\(}', script[::-1]).group(1)
    v = split_args(r[::-1])

    d = r'{%s}' % ''.join([((',' if i else '') + '\'k{}\':([_x0-9a-z]*)'.format(i + 1)) for i in range(15)])

    k = []
    r = re.search(d, script)
    for i in range(15):
        k.append(r.group(i + 1))

    n = int(v[a.index(re.search(r'arguments;.*?,(.*?)\);', script).group(1))], base=16)

    for i in range(n // 2):
        v[i], v[n - 1 - i] = v[n - 1 - i], v[i]

    for i, b in enumerate(k):
        t = v[a.index(b)].strip('\'')
        names['k{}'.format(i + 1)] = t if len(t) > 2 else t[::-1]

    return names

def get_encrypt_content(message, key, flag):
    """
    接口参数的加密、解密
    """
    des_obj = des(key.encode(), mode=ECB)
    if flag:
        content = pad(str(message).replace(' ', '').encode())
        return base64.b64encode(des_obj.encrypt(content)).decode('utf-8')
    else:
        return des_obj.decrypt(base64.b64decode(message)).decode('utf-8')

def get_random_ge(distance):
    """
    生成随机的轨迹
    """
    ge = []

    y = 0
    v = 0
    t = 1
    current = 0
    mid = distance * 3 / 4
    exceed = 20
    z = t

    ge.append([0, 0, 1])

    while current < (distance + exceed):
        if current < mid / 2:
            a = 15
        elif current < mid:
            a = 20
        else:
            a = -30
        a /= 2
        v0 = v
        s = v0 * t + 0.5 * a * (t * t)
        current += int(s)
        v = v0 + a * t

        y += random.randint(-5, 5)
        z += 100 + random.randint(0, 10)

        ge.append([min(current, (distance + exceed)), y, z])

    while exceed > 0:
        exceed -= random.randint(0, 5)
        y += random.randint(-5, 5)
        z += 100 + random.randint(0, 10)
        ge.append([min(current, (distance + exceed)), y, z])

    return ge

def make_mouse_action_args(distance):
    """
    生成鼠标行为相关的参数
    """
    ge = get_random_ge(distance)
    args = {
        p['k']['k5']: round(distance / CAPTCHA_DISPLAY_WIDTH, 2),
        p['k']['k6']: get_random_ge(distance),
        p['k']['k7']: ge[-1][-1] + random.randint(0, 100),
        p['k']['k8']: CAPTCHA_DISPLAY_WIDTH,
        p['k']['k9']: CAPTCHA_DISPLAY_HEIGHT,
        p['k']['k11']: 1,
        p['k']['k12']: 0,
        p['k']['k13']: -1,
        'act.os': 'android'
    }
    return args

def get_distance(fg, bg):
    """
    计算滑动距离
    """
    target = cv2.imdecode(np.asarray(bytearray(fg.read()), dtype=np.uint8), 0)
    template = cv2.imdecode(np.asarray(bytearray(bg.read()), dtype=np.uint8), 0)
    result = cv2.matchTemplate(target, template, cv2.TM_CCORR_NORMED)
    _, distance = np.unravel_index(result.argmax(), result.shape)
    return distance

def update_protocol(protocol_num, js_uri):
    """
    更新协议
    """
    global p
    r = requests.get(js_uri, verify=False)
    names = find_arg_names(r.text)
    p = {
        'i': protocol_num,
        'k': names
    }

def conf_captcha(organization):
    """
    获取验证码设置
    """
    url = 'https://captcha.fengkongcloud.com/ca/v1/conf'

    args = {
        'organization': organization,
        'model': 'slide',
        'sdkver': '1.1.3',
        'rversion': '1.0.3',
        'appId': 'default',
        'lang': 'zh-cn',
        'channel': 'YingYongBao',
        'callback': 'sm_{}'.format(int(time.time() * 1000))
    }

    r = requests.get(url, params=args, verify=False)
    resp = json.loads(re.search(r'{}\((.*)\)'.format(args['callback']), r.text).group(1))
    return resp

def register_captcha(organization):
    """
    注册验证码
    """
    url = 'https://captcha.fengkongcloud.com/ca/v1/register'

    args = {
        'organization': organization,
        'channel': 'YingYongBao',
        'lang': 'zh-cn',
        'model': 'slide',
        'appId': 'default',
        'sdkver': '1.1.3',
        'data': '{}',
        'rversion': '1.0.3',
        'callback': 'sm_{}'.format(int(time.time() * 1000))
    }

    r = requests.get(url, params=args, verify=False)
    resp = json.loads(re.search(r'{}\((.*)\)'.format(args['callback']), r.text).group(1))

    return resp

def verify_captcha(organization, rid, key, distance):
    """
    提交验证
    """
    url = 'https://captcha.fengkongcloud.com/ca/v2/fverify'
    args = {
        'organization': organization,
        p['k']['k1']: 'default',
        p['k']['k2']: 'YingYongBao',
        p['k']['k3']: 'zh-cn',
        'rid': rid,
        'rversion': '1.0.3',
        'sdkver': '1.1.3',
        'protocol': p['i'],
        'ostype': 'web',
        'callback': 'sm_{}'.format(int(time.time() * 1000))
    }

    args.update(make_mouse_action_args(distance))

    key = get_encrypt_content(key, 'sshummei', 0)

    for k, v in args.items():
        if len(k) == 2:
            args[k] = get_encrypt_content(v, key, 1)
    print(args)
    r = requests.get(url, params=args, verify=False)
    resp = json.loads(re.search(r'{}\((.*)\)'.format(args['callback']), r.text).group(1))

    return resp

def get_verify(organization):
    """
    进行验证
    """
    resp = conf_captcha(organization)
    protocol_num = re.search(r'build/v1.0.3-(.*?)/captcha-sdk.min.js', resp['detail']['js']).group(1)

    if not p.get('id') or protocol_num != p['i']:
        update_protocol(protocol_num, ''.join(['https://', resp['detail']['domains'][0], resp['detail']['js']]))

    resp = register_captcha(organization)

    rid = resp['detail']['rid']
    key = resp['detail']['k']

    domain = resp['detail']['domains'][0]
    fg_uri = resp['detail']['fg']
    bg_uri = resp['detail']['bg']

    fg_url = ''.join(['http://', domain, fg_uri])
    bg_url = ''.join(['http://', domain, bg_uri])

    r = requests.get(fg_url, verify=False)
    fg = BytesIO(r.content)

    r = requests.get(bg_url, verify=False)
    bg = BytesIO(r.content)

    distance = get_distance(fg, bg)
    print(distance)
    r = verify_captcha(organization, rid, key, int(distance / 600 * 310))

    return rid, r

def test():
    # 表示小红书
    organization = 'eR46sBuqF0fdw7KWFLYa'

    # rid是验证过程中响应的标示,r是最后提交验证返回的响应
    rid, r = get_verify(organization)
    print(rid, r)

    # riskLevel为PASS说明验证通过
    if r['riskLevel'] == 'PASS':
        # 这里需要向小红书提交rid
        # 具体可抓包查看,接口:/api/sns/v1/system_service/slide_captcha_check
        pass

if __name__ == '__main__':
    test()

原文链接:https://blog.csdn.net/weixin_43582101/article/details/112553479

点赞

发表回复