【验证码逆向专栏】xx80 邮箱多种类验证码逆向分析

这是「进击的Coder」的第 925 篇分享

作者:K 小哥

来源:K 哥爬虫

阅读本文大概需要 12 分钟。

cf41ebd1b410b802d30d5a4b6b369593.png

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

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

前言

又到了粉丝答疑时间,之前已经分析了两位粉丝存疑的站点,并编写了相应的逆向文章,私信中还有些小伙伴提出了在逆向一些网站的时候碰到的问题,后期仍会选择其中一些,写成文章,以供参考:

1602d9d0002e974969bd96a97d89cd45.png

逆向目标

目标:xx80 邮箱多种类验证码逆向分析

网址:aHR0cHM6Ly93d3cuMjk4MC5jb20vbG9naW4v

这个网站的验证码,会不断变换,非常有意思,堪比一个验证码产品。目前遇到的种类有:滑块、点选、旋转、拼图乱序、钟表,不知道还有没有别的,不过不同类别的验证码加解密操作一样,主要就是明文参数构造的不同。我们就来解决一下它的滑块、点选、旋转验证码,因为这几个比较常见:

4477b553298e20cee8e2de4ec8881481.png

流程分析

我们就以滑块验证码来分析加解密操作,先抓包分析,发现首页加载,验证码加载两处地方都有 debugger

1908893467f5ab9cf2723de808e10a4d.png

发现这两处 debugger 的构造都是一样的,不过在不同的 js 文件中,可以发现它是通过函数的 constructor  来执行 debugger  操作,解决的方法很多,主要讲两种:

  • HOOK

(()=>{
    Function.prototype.__constructor = Function.prototype.constructor;
    Function.prototype.constructor = function(){
        if(arguments && typeof arguments[0]==='string'){
            if("debugger"===arguments[0]){
                return
            }
            return Function.prototype.__constructor.apply(this,arguments);
        }
    }
})()

相关知识,可以阅读 K哥往期文章:JS 逆向之 Hook,吃着火锅唱着歌,突然就被麻匪劫了!

  • 文件替换(推荐),可以直接使用浏览器的替换功能 ,操作如下:

function _0x49fb64(_0x4e04ef) {
    function _0x36c27c(_0x53f377) {
        var _0xbd8c62 = _0x2d52;
        if (typeof _0x53f377 === 'string')
            return function(_0x18f1bb) {}
            [_0xbd8c62(0x2d1)]('while\x20(true)\x20{}')[_0xbd8c62(0x353)](_0xbd8c62(0x222));
        else
            ('' + _0x53f377 / _0x53f377)['length'] !== 0x1 || _0x53f377 % 0x14 === 0x0 ? function() {
                return !![];
            }
            ['constructor'](_0xbd8c62(0x1f8) + 'gger')[_0xbd8c62(0x31a)](_0xbd8c62(0x526)) : function() {
                return ![];
            }
            ['constructor'](_0xbd8c62(0x1f8) + _0xbd8c62(0x25d))[_0xbd8c62(0x353)]('stateObject');
        _0x36c27c(++_0x53f377);
    }
    try {
        if (_0x4e04ef)
            return _0x36c27c;
        //else
        //  _0x36c27c(0x0);
    } catch (_0x5b38c0) {}
}

我们看滑块验证码的图片请求接口:

86581598b858f37af185a5a1ffc23f93.png

发现返回的数据 mes 密文,相关 type 类型如下:

  • 21:滑块;

  • 23:点选;

  • 16:旋转;

  • ......

我们在来看看滑块图片的请求参数:

b404074d32a6335594071b02212d27e0.png

发现有 token 、appid、k 需要解决,我们先搜索,发现  token 、appid 是通过 getBehaviorRegister 接口返回:

e907e623955cc9b1256effaa8f1d96bf.png

a851deb74e56751b8887e59174dbe2e4.png

小结一下

目前我们需要逆向分析的目标有:

  • getBehaviorRegister 接口的 sign 值生成;

  • 图片请求接口的 k 值生成;

  • 图片请求接口的 mes 值解密。

我们一步步来解决。

sign 值

通过启动器查找,或者 xhr 断点很快就能找到如下生成位置:

c5e8d3146618b8810459255eea48f332.png

更进 i 函数后,就会很明显的看出其加密方式为 SHA256,明文 i 的构造比较简单,就不分析了:

2aaa68ad96fffa95495b31f69cea6c3a.png

k 值

同样的方法很快就能找到如下生成位置:

2574a9799ab870dea315f5864d9e5899.png

跟进去:

b96ef2e410b03e439c868cc1e1734129.png

c93eeb0d4968f6bc1557359e39d6c14e.png

很明显是 AES 加密,加密模式为 ECB:

  • ECB:Electronic Code Book(电子码本模式),是一种基础的加密方式,密文被分割成分组长度相等的块(不足补齐),然后单独一个个加密,一个个输出组成密文。

后面传入的参数分别是明文以及 key 值。明文部分为 fingerPrinterList[0x2] + ',' + fingerPrinterSon,都是指纹信息加密生成,就不具体分析,生成位置如下:

  • fingerPrinterList[0x2]:

de1bb6d55a7cfd9edca40baa9705ac47.png

  • fingerPrinterSon:

bc8f2d8c7d56635ce22f8e9765575fe1.png

key 值 :

(_0x37cb01 + _0x134810)['substring'](8, 24)
  • _0x37cb01:在 js 文件中,可以固定;

  • _0x134810 :时间戳,与图片接口请求时 params 中的 t 值对应。

mes 值解密

这个我们有许多方法定位,比如打 xhr 断点跟值或者 HOOK 等等。其实上面分析 k 值的生成时 AES 加密的下面就是 AES 解密算法的位置,我们其实都可以猜到。这里选择跟值,找到 'success' : 里面的操作,很快就能定位到解密函数:

d485c4329e8acc6097169ee3e9186738.png


f0b4ff0b3ea0b76ac1b5313b829a514e.png

第一个参数就是 mes 值,第二个参数 key 跟上面 k 值加密的 key 保持一致。

解密后的数据类似如下:

'{"dif":"0","answer":["9","未","雨"],"bg":"https://csmoss.duoyi.com/19/124bf7b8f82292","num":3,"sn":"9a363bc74a8a","type":23,"list":[]}'
  • 滑块的图片时乱序的,需要还原,其他的不需要,附上还原图片测试代码:

    def split_and_reorder_image(image_path, reorder_array, split_ratios=(10, 1)):
        """
            
        :param image_path: 乱序的图片路径
        :param reorder_array: 滑块图片接口返回的 mes 解密后的 list
        :param split_ratios: 
        """
        # 打开并读取原始图片
        original_image = Image.open(image_path)
        width, height = original_image.size
    
        # 按照指定的比例分割图片
        split_width = width // split_ratios[0]
        split_height = height // split_ratios[1]
        images = [(i * split_width, j * split_height, (i + 1) * split_width, (j + 1) * split_height)
                  for i in range(split_ratios[0]) for j in range(split_ratios[1])]
    
        images = [original_image.crop(box) for box in images]
    
        reordered_images = ["" for _ in range(len(images))]
        # 遍历reorder_array,根据其中的索引将images列表中的元素添加到reordered_images列表的相应位置
        for i, new_index in enumerate(reorder_array):
            reordered_images[new_index] = images[i]
    
        # 将重新排序的子图片拼接回原始图片
        new_width = split_width * split_ratios[0]
        new_height = split_height * split_ratios[1]
        new_image = Image.new('RGB', (new_width, new_height))
        for i, img in enumerate(reordered_images):
            x = (i % split_ratios[0]) * split_width
            y = (i // split_ratios[0]) * split_height
            new_image.paste(img, (x, y))
            
        new_image.save(image_path)

验证请求

成功:{"message":{"pass":"cd5201bdb47748fe8b9114769d7122cb"},"code":1};

失败:{"message":"not match","code":0}。

请求参数:

c219169035acd7b3cb9967b74cfc4f4b.png

分析一下:

  • token 、appid 上面接口返回,跟图片接口保持一致;

  • portion、cn、timestamp、signature 需要逆向分析;

  • sn 图片接口解密后返回;

  • 请求负载 需要逆向分析,这也是不同验证码类型的唯一区别。

直接从启动器入手,点击进去就可以发现生成位置:

0f2c6af53d828bfb9b34d9d3ef2efe65.png

  • portion、cn、timestamp、signature 分析如下:

portion = Math['random']()['toFixed'](2);

cn = md5('num' + parseInt(10000000 + Math['random']() * 1000000) + 'time' + new Date()['getTime']());

timestamp =  new Date()['getTime']();

// token 、sn 上面接口返回; :5b350044ac092f7bf3c2bc791638ca2f 文件写死
signature = md5(md5(portion + ':' + timestamp + ':' + token) + ':' + sn + ':1:' + cn + ':auth' + ":5b350044ac092f7bf3c2bc791638ca2f")
  • 请求负载:就是上面图片中的 'data' 操作;发现也是进入到之前的 AES 加密函数中,明文由一堆环境加验证码相关信息,JSON.stringify 之后生成;key 由固定值 + signature, 截取 8 到 24 生成。

我们来分析一下不同验证码的明文有什么差异,是如何生成的:

点选关键参数

  • slide:点选轨迹;

  • click_behavior:[点选坐标 + 时间 + 顺序];

  • portion,计算后点选坐标,计算如下,就是和图片宽高的一个比例:

# center_points 点选坐标 
portion = []
for i in range(len(center_points)):
    portion.append([str(format(center_points[i][0]/320, '.5f')), str(round((center_points[i][1]+0.83332824707031)/200, 5))])

滑块关键参数

  • slide:滑动轨迹;

  • portion:计算后的滑块距离,比较特殊点, 计算如下, 原理就是不断滑动滑动条,当计算出来的识别距离与我们真实的识别距离接近时,返回结果:

/**
 * 
 * @param x : ddddocr / cv2 识别距离
 * @param startposition : 这是获取图片接口返回的startPosition参数
 * @returns {{portion: number, value: number}}  : value: 滑动条滑动的距离, portion: 计算后的距离
 */
function get_slide_distance(x, startposition) {
    var _0x3c1ee0 = 72;
    var _0x3d74b9 = 1.11;
    var _0x4b4e13 = 0.74;
    var _0x1fda62 = startposition; // 这是获取图片接口返回的startPosition参数
    var _0x101145 = 1;
    var _0x3d3596;

    for (var i = 0; i < 999; i++) {
        _0x101145++;
        var _0x3d3596 = Math["pow"](_0x3c1ee0 * _0x101145, 0.4 * Math['abs'](Math['sin'](0.025 * _0x101145))) + Math["pow"](_0x101145, _0x3d74b9) * _0x4b4e13 + _0x1fda62;
  // _0x3d3596 :计算出来的识别距离
        if (Math.abs(_0x3d3596 - x) <= 1) {
            break;
        }
    }
    var portion = (Math["pow"](_0x3c1ee0 * _0x101145, 0.4 * Math['abs'](Math['sin'](0.025 * _0x101145))) + Math["pow"](_0x101145, _0x3d74b9) * _0x4b4e13) / 320;

   var  res={"portion":portion,"value":_0x101145}
    return res;
}

旋转关键参数

  • portion:识别角度;

  • slide,旋转滑动轨迹,是根据滑动条滑动的距离来计算轨迹,滑动条滑动的距离计算如下:

    int(portion * 0.6)

轨迹参考代码

# 轨迹
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 = [
        [0, 0, -(distance * 20)]
    ]
    # 共记录count次滑块位置信息
    count = 50 + int(distance / 2)
    # 初始化滑动时间
    t = slide_track[0][2]
    # 记录上一次滑动的距离
    _x = 0
    _y = slide_track[0][1]
    for i in range(count):
        _y -= 1 if i % 9 == 0 else 0

        # 已滑动的横向距离
        x = slide_track[0][0] + 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

结果验证

  • 滑块:

6c19397d617d43c52421f7fc27489bc9.png

  • 点选:

0bd3db5d4c79c4753f24adf7584709f2.png

  • 旋转:

98a58d443acb13a1b641e09b59ce6740.png

好文和朋友一起看~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值