[验证码逆向]某手势验证码逆向分析

查看 117|回复 10
作者:s1lencee   
[验证码逆向]某手势验证码逆向分析

本文章所有内容仅供学习和研究使用,本人不提供具体模型和源码。若有侵权,请联系我立即删除!维护网络安全,人人有责。

前言
手势验证码是一种绘制轨迹进行人机的验证码,我曾经遇到过几次。
查了查网上没有较好的教程,便打算自己写一篇文章。
目标网址:
aHR0cHM6Ly93d3cudmFwdGNoYS5jb20vI2RlbW8=
手势验证码长这样


1.png (246.65 KB, 下载次数: 0)
下载附件
1
2024-2-8 00:51 上传

我将该验证码的协议逆向分析和验证码识别分为2篇文章,本文主要研究该验证码的逆向分析。
目录

  • 请求分析

  • 准备工作

  • 4种请求

  • config请求

  • get请求

  • validate请求

  • 获取验证码

  • en参数分析

  • fp指纹分析

  • fp指纹分析2

  • 构造en参数

  • 获取数据

  • 图片还原

  • 还原代码分析

  • 获取图片还原顺序

  • python还原图片

  • 图片识别

  • 关于图片识别

  • 构造请求

  • 请求分析

  • 轨迹分析

  • 轨迹加密

  • 请求验证

  • 总结

    请求分析
    准备工作
    首先,每个验证码平台都有唯一的业务标识,用于区分不同网站。我们应该养成看文档的好习惯,一般情况下直接在该验证码的官网找到Web端开发文档


    2.png (101.16 KB, 下载次数: 0)
    下载附件
    2
    2024-2-8 00:51 上传

    可以看到可以看到验证码初始化的代码,vid就是唯一标识,然后在验证码出现的页面全局搜索vaptcha(


    3.png (165.06 KB, 下载次数: 0)
    下载附件
    3
    2024-2-8 00:51 上传

    然后记住该id为59b252ed57f5a2111xxxxxxx(为了脱敏,部分真实数据我将用xxx代替)
    4种请求
    该验证码没有debugger反爬措施,直接打开浏览器开发者工具抓包即可。


    4.png (30.97 KB, 下载次数: 0)
    下载附件
    4
    2024-2-8 00:51 上传

    可以看到一共有4种请求,其中59b252ed57f5a2111xxxxxxx接口就是用户的vid,用于获取验证服务器。


    5.png (40.94 KB, 下载次数: 0)
    下载附件
    5
    2024-2-8 00:51 上传

    返回的api就是下面3个请求的域名。
    config请求
    该请求是获取验证码配置的,即一些sdk地址和版本。
    请求数据


    6.png (23.44 KB, 下载次数: 0)
    下载附件
    6
    2024-2-8 00:52 上传

    响应数据


    7.png (34.09 KB, 下载次数: 0)
    下载附件
    7
    2024-2-8 00:52 上传

    vi参数就是vid
    其中我们只需要响应数据的knock值
    那么u值是怎么生成的呢?
    我们开始跟栈


    8.png (34.24 KB, 下载次数: 0)
    下载附件
    8
    2024-2-8 00:52 上传

    getConfig这个函数名那么明显,我们进去看看


    9.png (34.54 KB, 下载次数: 0)
    下载附件
    9
    2024-2-8 00:52 上传

    可以明显看到,u值的来源是localStorage中的vaptchanu,那么意味着可以是空值。
    当把该页面的缓存清空后,可以发现u值为空,这里我就不放图了,有兴趣大家试试。
    get请求
    顾名思义,该请求是获取验证码数据的。
    请求数据


    10.png (25.32 KB, 下载次数: 0)
    下载附件
    10
    2024-2-8 00:52 上传

    响应数据


    11.png (31.68 KB, 下载次数: 0)
    下载附件
    12
    2024-2-8 00:53 上传

    可以看到k参数就是config接口返回的knock
    而en参数包含一些环境检测,我将在下文分析
    validate请求
    该请求用于验证是否正确。
    请求数据


    12.png (41.08 KB, 下载次数: 0)
    下载附件
    13
    2024-2-8 00:53 上传

    正确的响应数据


    13.png (17.29 KB, 下载次数: 0)
    下载附件
    14
    2024-2-8 00:53 上传

    返回的token值就是我们的目标,我们携带该值请求需要验证的接口就可以返回数据了。
    获取验证码
    en参数分析
    我们进入post的前面的栈


    14.png (38.83 KB, 下载次数: 0)
    下载附件
    15
    2024-2-8 00:53 上传



    16.png (112.48 KB, 下载次数: 0)
    下载附件
    16
    2024-2-8 00:53 上传

    由于switch的存在,代码是分步执行的,我们在switch打上记录点,记录执行顺序


    15.png (122.37 KB, 下载次数: 0)
    下载附件
    11
    2024-2-8 00:53 上传

    可以看到,获取了一些localStorage的内容,
    hex_md5其实就是md5加密 (严格来说不算加密,因为md5无法解密)
    我们清空缓存后重新请求可以发现执行顺序。
    6
    0
    1
    2
    5
    我们只需要把执行过的函数扣下来即可。
    我只说几个重点
    [ol]

  • 获取了localStorage的变量都是三元运算符,当localStorage里的值为空时就会设置默认值

  • _0x502684['GenerateFP']和_0x5d0164['GenerateFP']都是浏览器指纹,但是后者是Promise异步任务,我接下来会分析这两个的区别。

  • _0x4f54b3['secretC']是一个固定值。
    [/ol]


    17.png (48.13 KB, 下载次数: 0)
    下载附件
    17
    2024-2-8 00:54 上传
  • _0x4f54b3['globalMd5']是config返回内容处理后的md5加密值。
    [/ol]


    18.png (81.34 KB, 下载次数: 0)
    下载附件
    18
    2024-2-8 00:54 上传



    19.png (144.83 KB, 下载次数: 0)
    下载附件
    19
    2024-2-8 00:54 上传

    将返回值的值相加即可得到该字符串。由于js的相加和python不太一样,我们可以使用以下代码在python中实现
    def sort_dict_by_key(input_dict):
        """字典按照键排序"""
        sorted_keys = sorted(input_dict.keys())
        sorted_dict = {key: input_dict[key] for key in sorted_keys}
        return sorted_dict
    def splicing_obj(obj: dict):
        obj = sort_dict_by_key(obj)
        result = ""
        for k, v in obj.items():
            if isinstance(v, str):
                result += v
            elif isinstance(v, int):
                result += str(v)
            elif isinstance(v, float):
                result += str(v)
            elif isinstance(v, bool):
                result += "true" if v else "false"
            elif isinstance(v, list):
                result += ",".join(v)
            else:
                result += "[object Object]"
        return result
    fp指纹分析
    _0x502684['GenerateFP']函数主要通过canvas绘制特定字符再转换为base64来计算浏览器指纹。
    传入一个参数,该参数将会绘制在canvas上。


    20.png (101.97 KB, 下载次数: 0)
    下载附件
    20
    2024-2-8 00:53 上传

    该方法在不同浏览器上的结果不相同,这就意味着我们可以不用canvas来计算指纹,但是要求同一参数的计算值相同。
    我们可以使用加盐的哈希算法来计算,在同一次请求中盐值应当相同(要注意CRC32校验,否则验证不通过)。
    由于本文是教程,所以我就直接使用canvas来计算指纹(nodejs安装canvas库有些麻烦)


    21.png (98.96 KB, 下载次数: 0)
    下载附件
    21
    2024-2-8 00:55 上传

    其他就是一些转换了,包括CRC32校验,直接扣下来即可。
    fp指纹分析2
    _0x5d0164['GenerateFP']是个异步函数


    22.png (10.75 KB, 下载次数: 0)
    下载附件
    22
    2024-2-8 00:55 上传

    我们跟进函数内查看,在返回部分打个断点重新请求。


    23.png (160.2 KB, 下载次数: 0)
    下载附件
    23
    2024-2-8 00:55 上传

    可以看到,_0x33ff5b是浏览器环境,经过hashComponents后返回一个哈希值(我没见过这种加密,有懂的大佬可以说说)
    查看代码得知该GenerateFP函数可以传入两个参数:
  • 第一个参数将被设置进_0x33ff5b中,和浏览器环境一起加密
  • 第二个参数是个布尔值,当为true时返回全部哈希值,false时截取前面8为返回

    // _0x41154d存在时将加入浏览器环境
    if (_0x41154d) {
      _0x33ff5b = _0x3633c3(_0x3633c3({}, _0x2d65f5['components']), {
        'param': {
          'value': _0x41154d,
          'duration': 0x0
        }
      });
    } else {
      _0x33ff5b = _0x3633c3({}, _0x2d65f5['components']);
    }
    // 计算哈希值
    _0x4c267d = _0x5bf32f['hashComponents'](_0x33ff5b);
    // 是否截取前八位,_0x596253是回调函数
    if (_0x5799b7)
      _0x596253(_0x4c267d);
    else
      _0x596253(_0x4c267d['slice'](0x0, 0x8));
    _0x596253是回调函数,在下一步代码的_0x4f1db8['sent']()可以获取该值


    24.png (125.17 KB, 下载次数: 0)
    下载附件
    24
    2024-2-8 00:55 上传

    我们直接将hashComponents全部扣下来即可。
    关于浏览器环境
    就是检测一些字体、插件、设置等,完全可以弄成定值,同样的相同的参数返回的值应该相同。
    构造en参数
    encryFunc和selectFrom没有特殊含义,直接扣下来即可
    最后,把扣下来的代码整合可以得到en值


    25.png (98.01 KB, 下载次数: 0)
    下载附件
    25
    2024-2-8 00:55 上传

    获取数据
    最后把en携带参数去请求服务器即可获得验证码数据


    26.png (70.76 KB, 下载次数: 0)
    下载附件
    26
    2024-2-8 00:48 上传

    图片还原
    还原代码分析


    27.png (224.05 KB, 下载次数: 0)
    下载附件
    27
    2024-2-8 00:48 上传

    可以看到,接口返回的图片是乱序的。
    我们可以在事件侦听器中勾选canvas创建,因为浏览器还原乱序图片肯定会用到canvas。


    28.png (20.9 KB, 下载次数: 0)
    下载附件
    28
    2024-2-8 00:48 上传

    跳过几个检测指纹的canvas,我们成功断住了还原图片的地方


    29.png (65.38 KB, 下载次数: 0)
    下载附件
    29
    2024-2-8 00:48 上传

    我们只需要注意传入的_0x8782d1即可,该变量就是图片顺序。
    获取图片还原顺序
    向前跟栈找到生成顺序的地方


    30.png (68.87 KB, 下载次数: 0)
    下载附件
    30
    2024-2-8 00:48 上传

    大体就是一个计算得到的整数_0x19be99和接口返回的img_order经过一个_0x2f94f6['Decrypt']方法得到
    和en一样,找到switch的执行顺序以后可以得到0x19be99的值
    _0x19be99 = hex2int(GenerateFP(ha)) + hex2int(hashComponents(hb)) + parseInt(secretC) + pow(r);
    r、ha和hb就是get接口返回的内容
    当有一项为空值时,就直接设为0.
    pow为work2.js返回的内容,直接扣下来即可


    31.png (89.71 KB, 下载次数: 0)
    下载附件
    31
    2024-2-8 00:48 上传

    整合后
    function get_order(img_order, r, hb, ha){
        let hb_en = 0;
        let ha_en = 0;
        if (hb !== "" && hb) {
            hb_en = hex2int(hashComponents(hb))
        }
        if (ha !== "" && ha) {
            ha_en = hex2int(GenerateFP(ha))
        }
        let _0x28c598 = ha_en + hb_en + 8549731620 + pow(r);
        return Decrypt(img_order, _0x28c598)
    }
    python还原图片
    from PIL import Image
    from io import BytesIO
    def restore(img, order):
        """还原图片"""
        img = Image.open(BytesIO(img))
        new_img = Image.new('RGB', (400, 230))
        width = 80
        height = 115
        def drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight):
            """
            param image:
            param sx:       开始剪切的 x 坐标位置。
            param sy:       开始剪切的 y 坐标位置。
            param sWidth:   被剪切图像的宽度。
            param sHeight:  被剪切图像的高度。
            param dx:       在画布上放置图像的 x 坐标位置。
            param dy:       在画布上放置图像的 y 坐标位置。
            param dWidth:   要使用的图像的宽度
            param dHeight:  要使用的图像的高度
            """
            split_img = image.crop((sx, sy, sx + sWidth, sy + sHeight))
            new_img.paste(split_img, (dx, dy))
        for i in range(10):
            if i
    传入2个参数即可还原,img是图片二进制数据,order为图片还原顺序
    图片识别
    关于图片识别
    图片识别我将在另一个专题详细讲
    大家可以稍等
    构造请求
    请求分析
    和get请求一样,在post前面断住


    32.png (123.06 KB, 下载次数: 0)
    下载附件
    32
    2024-2-8 00:48 上传

    我们可以看到,和get请求的en加密基本相同,只不过多了几个参数。
    其中_0x13cb8b变量包含验证信息:
  • dt: 用时,不到4位数用0补上
  • ch: canvas的高
  • cw: canvas的宽
  • v:  加密后的轨迹值



    33.png (100.46 KB, 下载次数: 0)
    下载附件
    33
    2024-2-8 00:48 上传

    轨迹分析
    而_0x491ed2就是轨迹信息。


    34.png (118.02 KB, 下载次数: 0)
    下载附件
    34
    2024-2-8 00:49 上传

    可以看到这个轨迹有点迷糊,有很多小数。我们找到生成轨迹的地方。


    35.png (29.26 KB, 下载次数: 0)
    下载附件
    35
    2024-2-8 00:49 上传

    可以看到,就是鼠标位置减去绘制的起始位置。
    这里有个坑。


    36.png (219.85 KB, 下载次数: 0)
    下载附件
    36
    2024-2-8 00:49 上传

    这里区域实际上比事件位置多出30px,(验证图片原高230px,宽400px)
    所以我们在原来的位置加上30px即可。
    其他注意事项:
    当轨迹x或y间隔小于5时,不会加入轨迹列表


    37.png (49.89 KB, 下载次数: 0)
    下载附件
    37
    2024-2-8 00:49 上传



    38.png (42.12 KB, 下载次数: 0)
    下载附件
    38
    2024-2-8 00:49 上传

    轨迹加密
    assemblyCoordData就是轨迹加密函数


    39.png (73.07 KB, 下载次数: 0)
    下载附件
    39
    2024-2-8 00:49 上传

    我们直接扣下来即可


    40.png (94.28 KB, 下载次数: 0)
    下载附件
    40
    2024-2-8 00:49 上传

    验证请求
    这样,我们使用识别图片的轨迹加密生成en后去请求。


    41.png (47.85 KB, 下载次数: 0)
    下载附件
    41
    2024-2-8 00:49 上传

    总结
    在这个验证码飞速发展的时代,可能我今天的文章,明天就过期了。
    所以,不要只一昧的CV,我们要学习新的思路,这样知识才是自己的。
    如果文章有什么不足的欢迎各位大佬们补充。

    下载次数, 下载附件

  • wangxd   

    请教一下,有些APP的比如某宝的滑动验证码,手动右滑就可以验证成功,但是用js代码的swipe滑 能右滑过去了但是验证不给过,这是什么原理呢?
    443566434   

    请教一下,有些APP的比如某宝的滑动验证码,手动右滑就可以验证成功,但是用js代码的swipe滑 能右滑过去了但是验证不给过,这是什么原理呢?
    s1lencee
    OP
      

    我先在这提前祝大家新年快乐
    海是倒过来的天   

    凌晨发帖,分析得这么详细,学习了
    次谐波   

    进来学习下。
    ztqddj007   

    牛逼大佬支持一波
    lhfcsm   

    呵呵,正在学习中
    序列号001   

    进来学习下
    xuelinghua   

    进来学习下
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部