V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
HexHub
HexHub,一站式SSH、Docker、数据库连接管理工具,支持多种主流数据库、多窗口分屏、智能SQL编辑、极速数据处理、批量命令、云端同步,支持SSH跳板机、命令广播、历史命令、SFTP多端文件互传。
Promoted by xiwh
461229187
V2EX  ›  程序员

掘金滑块验证码安全升级,继续破解

  •  1
     
  •   461229187 ·
    codexu · 2024-06-06 13:25:16 +08:00 · 4196 次点击
    这是一个创建于 396 天前的主题,其中的信息可能已经有所发展或是发生改变。

    去年发过一篇文章,《使用前端技术破解掘金滑块验证码》,我很佩服掘金官方的气度,不但允许我发布这篇文章,还同步发到了官方公众号。最近发现掘金的滑块验证码升级了,也许是我那篇文章起到了一些作用,逼迫官方加强了安全性,这是一个非常好的现象。

    不过,这并不是终点,我们还是可以继续破解。验证码的安全性是在用户体验和安全性之间的一个平衡,如果安全性太高,用户体验就会变差,如果用户体验太好,安全性就会变差。掘金的滑块验证码是一个很好的例子,它的安全性和用户体验之间的平衡做得非常好,并且我们破解的难度体验也非常好。 😄

    本次升级的内容

    掘金的滑块验证码升级了,主要有以下几个方面的改进:

    1. 首先验证码不再是掘金自己的验证码了,而是使用了字节的校验服务,可以看到弹窗是一个 iframe ,并且域名是 bytedance.com

    我们都知道掘金被字节收购了,可以猜测验证码的升级是字节跳动的团队做的。

    1. 验证码的图形不再是拼图,而是随机的不同形状,比如爱心、六角星、圆环、月亮、盾牌等。
    2. 增加了干扰缺口,主要是大小或旋转这种操作。

    下面看一下改版后的滑块验证码:

    我在文章的评论区看到了一些关于这次升级或相关的讨论:

    本文将继续破解这次升级后的滑块验证码,看看这次升级对破解的难度有多大影响,如果你还没有了解过如何破解滑块验证码,请先看我之前的文章。

    iframe

    这次升级,整个滑块都掉用的是外部链接,使用 iframe 呈现,那么在 puppeteer 中如何处理呢?

    await page.waitForSelector('iframe');
    const elementHandle = await page.$('iframe');
    const frame = await elementHandle.contentFrame();
    

    实际上,我们只需要等待 iframe 加载完成,然后获取 iframe 的内容即可。

    Frame 对象和 Page 对象有很多相似的方法,比如 frame.$frame.evaluate 等,我们可以直接使用这些方法来操作 iframe 中的元素。

    验证码的识别

    上一篇文章采用比较简单的判断方式,当时缺口处有明显的白边,所以只需要找到这个白边即可。

    但是本次升级后,缺口不再是白边,而是阴影的效果,并且缺口的形状也不再是拼图,大概率都是曲线的边,所以再判断缺口的方式就不再适用了。

    现在我们可以采用一种新的方式,通过对比滑块图片和缺口区域的像素值相似程度来判断缺口位置。

    首先还是二值化处理,将图片转换为黑白两色:

    可以看到左侧缺口和右侧缺口非常相似,只是做了一点旋转作为干扰。

    再看一下,iframe 中还有一个很重要的东西,就是校验的图片:

    它是一个 png 图片,所以我们可以把它也转换成二值化,简单的方式就是将透明色转换为白色,非透明色转换为黑色,如果想提高识别精度,可以与背景图一样,通过灰度、二值化的转换方式。

    // 获取缺口图像
    const captchaVerifyImage = document.querySelector(
      '#captcha-verify_img_slide',
    ) as HTMLImageElement;
    // 创建一个画布,将 image 转换成 canvas
    const captchaCanvas = document.createElement('canvas');
    captchaCanvas.width = captchaVerifyImage.width;
    captchaCanvas.height = captchaVerifyImage.height;
    const captchaCtx = captchaCanvas.getContext('2d');
    captchaCtx.drawImage(
      captchaVerifyImage,
      0,
      0,
      captchaVerifyImage.width,
      captchaVerifyImage.height,
    );
    const captchaImageData = captchaCtx.getImageData(
      0,
      0,
      captchaVerifyImage.width,
      captchaVerifyImage.height,
    );
    // 将像素数据转换为二维数组,同样处理灰度、二值化,将像素点转换为 0 (黑色)或 1 (白色)
    const captchaData: number[][] = [];
    for (let h = 0; h < captchaVerifyImage.height; h++) {
      captchaData.push([]);
      for (let w = 0; w < captchaVerifyImage.width; w++) {
        const index = (h * captchaVerifyImage.width + w) * 4;
        const r = captchaImageData.data[index] * 0.2126;
        const g = captchaImageData.data[index + 1] * 0.7152;
        const b = captchaImageData.data[index + 2] * 0.0722;
        if (r + g + b > 30) {
          captchaData[h].push(0);
        } else {
          captchaData[h].push(1);
        }
      }
    }
    

    为了对比图形的相似度,二值化后的数据我们页采用二维数组的方式存储,这样可以方便的对比两个图形的相似度。

    如果想观测二值化后的真是效果,可以把二位数组转换为颜色,并覆盖到原图上:

    // 通过 captchaData 0 黑色 或 1 白色 的值,绘制到 canvas 上,查看效果
    for (let h = 0; h < captchaVerifyImage.height; h++) {
      for (let w = 0; w < captchaVerifyImage.width; w++) {
        captchaCtx.fillStyle =
          captchaData[h][w] == 1 ? 'rgba(0,0,0,0)' : 'black';
        captchaCtx.fillRect(w, h, 1, 1);
      }
    }
    captchaVerifyImage.src = captchaCanvas.toDataURL();
    

    数据拿到后,我们可以开始对比两个图形的相似度,这里就采用非常简单的对比方式,从左向右,逐个像素点对比,横向每个图形的像素一致的点数量纪录下来,然后取最大值,这个最大值就是缺口的位置。

    这里我们先优化一下要对比的数据,我们只需要对比缺口的顶部到底部这段的数据,截取这一段,可以减少对比的性能消耗。

    // 获取 captchaVerifyImage 相对于 .verify-image 的偏移量
    const captchaVerifyImageBox = captchaVerifyImage.getBoundingClientRect();
    const captchaVerifyImageTop = captchaVerifyImageBox.top;
    // 获取缺口图像的位置
    const imageBox = image.getBoundingClientRect();
    const imageTop = imageBox.top;
    // 计算缺口图像的位置,top 向上取整,bottom 向下取整
    const top = Math.floor(captchaVerifyImageTop - imageTop);
    // data 截取从 top 列到 top + image.height 列的数据
    const sliceData = data.slice(top, top + image.height);
    

    然后循环对比两个图形的像素点,计算相似度:

    // 循环对比 captchaData 和 sliceData ,从左到右,每次增加一列,返回校验相同的数量
    const equalPoints = [];
    // 从左到右,每次增加一列
    for (let leftIndex = 0; leftIndex < sliceData[0].length; leftIndex++) {
      let equalPoint = 0;
      // 新数组 sliceData 截取 leftIndex - leftIndex + captchaVerifyImage.width 列的数据
      const compareSliceData = sliceData.map((item) =>
        item.slice(leftIndex, leftIndex + captchaVerifyImage.width),
      );
      // 循环判断 captchaData 和 compareSliceData 相同值的数量
      for (let h = 0; h < captchaData.length; h++) {
        for (let w = 0; w < captchaData[h].length; w++) {
          if (captchaData[h][w] === compareSliceData[h][w]) {
            equalPoint++;
          }
        }
      }
      equalPoints.push(equalPoint);
    }
    // 找到最大的相同数量,大概率为缺口位置
    return equalPoints.indexOf(Math.max(...equalPoints));
    

    对比时像素较多,不容易直接看到效果,这里写一个简单的二位数组对比,方便各位理解:

    [
      [0, 1, 0],
      [1, 0, 1],
      [0, 1, 0],
    ]
    [
      [0, 0, 0, 1, 0, 0],
      [0, 0, 1, 0, 1, 0],
      [0, 0, 0, 1, 0, 0],
    ]
    

    循环对比,那么第 3 列开始,匹配的数量可以达到 9 ,所以返回 3 ,这样就是滑块要移动的位置。

    干扰缺口其实对我们这个识别方式没什么影响,最多可能会增加一些失败的概率,我个人测试了一下,识别成功率有 95% 左右。

    总结

    这次升级后,掘金的滑块验证码的安全性有了一定的提升,还是可以继续破解的,只是难度有所增加。最后再奉劝大家不要滥用这个技能,这只是为了学习和研究,不要用于非法用途。如果各位蹲局子,可不关我事啊。 🤔️

    20 条回复    2024-06-07 09:28:39 +08:00
    qq05629
        1
    qq05629  
       2024-06-06 13:50:04 +08:00   ❤️ 1
    掘金:我谢谢你哈
    YVAN7123
        2
    YVAN7123  
       2024-06-06 14:35:44 +08:00
    如果真做局子, 你这样说一句免责就有用吗? 哈哈哈哈哈哈
    LiuJiang
        3
    LiuJiang  
       2024-06-06 15:07:10 +08:00
    不错,感谢分享
    461229187
        4
    461229187  
    OP
       2024-06-06 15:09:49 +08:00
    @YVAN7123 我是不是属于教唆犯罪
    wabway
        5
    wabway  
       2024-06-06 15:12:26 +08:00
    感谢分享
    MRG0
        6
    MRG0  
       2024-06-06 15:20:24 +08:00
    刚在知乎看完
    jellyX
        7
    jellyX  
       2024-06-06 15:27:02 +08:00
    着实太强啦
    titixlq
        9
    titixlq  
       2024-06-06 15:39:17 +08:00
    厉害
    chi1st
        10
    chi1st  
       2024-06-06 15:39:18 +08:00 via Android
    大佬平时是搞逆向的么?
    461229187
        11
    461229187  
    OP
       2024-06-06 15:40:34 +08:00
    @chi1st 就是个普通的前端菜鸡
    mightybruce
        12
    mightybruce  
       2024-06-06 15:47:18 +08:00
    这些操作如果用 python 调用 opencv 几句话就解决了, 并且能处理更复杂的情况
    archxm
        13
    archxm  
       2024-06-06 15:50:37 +08:00
    挺强的
    yulgang
        14
    yulgang  
       2024-06-06 15:53:18 +08:00
    下一个版本 增加生物识别
    goxxoo
        15
    goxxoo  
       2024-06-06 15:54:22 +08:00
    下一版换成人工验证
    ishamo
        16
    ishamo  
       2024-06-06 16:40:47 +08:00
    有意思。感谢大佬
    b821025551b
        17
    b821025551b  
       2024-06-06 17:16:26 +08:00
    有个想法:现在看起来背景的风景图片二值化后分界清晰,可以和缺口分开,如果遇到这种的,该如何处理呢?
    https://pic.imgdb.cn/item/66617e045e6d1bfa05c67567.jpg
    drymonfidelia
        18
    drymonfidelia  
       2024-06-06 17:16:50 +08:00
    不能提取出协议高并发的话其实意义不大
    461229187
        19
    461229187  
    OP
       2024-06-06 21:02:53 +08:00
    @b821025551b 重试换一张图
    EndlessMemory
        20
    EndlessMemory  
       2024-06-07 09:28:39 +08:00
    cv2 的 matchTemplate 能够直接识别,OpenCV 还是太强大了
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3513 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 10:35 · PVG 18:35 · LAX 03:35 · JFK 06:35
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.