V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
461229187
V2EX  ›  程序员

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

  •  1
     
  •   461229187 · 199 天前 · 3750 次点击
    这是一个创建于 199 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    本次升级的内容

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

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