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

199 天前
 461229187

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

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

本次升级的内容

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

  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% 左右。

总结

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

3750 次点击
所在节点    程序员
20 条回复
qq05629
199 天前
掘金:我谢谢你哈
YVAN7123
199 天前
如果真做局子, 你这样说一句免责就有用吗? 哈哈哈哈哈哈
LiuJiang
199 天前
不错,感谢分享
461229187
199 天前
@YVAN7123 我是不是属于教唆犯罪
wabway
199 天前
感谢分享
MRG0
199 天前
刚在知乎看完
jellyX
199 天前
着实太强啦
461229187
199 天前
titixlq
199 天前
厉害
chi1st
199 天前
大佬平时是搞逆向的么?
461229187
199 天前
@chi1st 就是个普通的前端菜鸡
mightybruce
199 天前
这些操作如果用 python 调用 opencv 几句话就解决了, 并且能处理更复杂的情况
archxm
199 天前
挺强的
yulgang
199 天前
下一个版本 增加生物识别
goxxoo
199 天前
下一版换成人工验证
ishamo
199 天前
有意思。感谢大佬
b821025551b
199 天前
有个想法:现在看起来背景的风景图片二值化后分界清晰,可以和缺口分开,如果遇到这种的,该如何处理呢?
https://pic.imgdb.cn/item/66617e045e6d1bfa05c67567.jpg
drymonfidelia
199 天前
不能提取出协议高并发的话其实意义不大
461229187
199 天前
@b821025551b 重试换一张图
EndlessMemory
198 天前
cv2 的 matchTemplate 能够直接识别,OpenCV 还是太强大了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1047329

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX