反反爬虫 – 腾讯滑块验证码破解

几乎所有的应用程序在涉及到用户信息安全的操作时,都会弹出验证码让用户进行识别,以确保该操作为人类行为,而不是大规模运行的机器。

早期的验证码是由随机字符加入杂点、干扰线后组成的一张图片,用户只需将图片中的字符输入到文本框中即可。该验证码利用了人类的视觉识别能力,将没有识别能力的机器拒之门外,但是这类验证码很快就被OCR技术绕过了。腾讯防水墙滑块验证码是基于视觉识别+用户操作的验证码,长这样:

用户需要先识别出缺口位置,再将方块正确移动到对应的位置上。相比传统的字符验证码,不仅改成了对机器来说更难识别的图片,还加入了用户操作的行为,难度更上一层楼。

网上类似的教程大多数都是Java / Python + Selenium + OpenCV,识别率约90%;
本文提供Javascript + Selenium的简单粗暴解决思路,且识别率99%+。

打开验证码

这里演示打开腾讯防水墙官网的滑块验证码。

const { Builder, By } = require('selenium-webdriver')

;(async () => {
  /**
   * 打开验证码
   */
  // 打开浏览器
  const driver = new Builder().forBrowser('chrome').build()
  // 加载腾讯防水墙官网
  await driver.get('https://007.qq.com/online.html');
  // 点击体验验证码按钮
  (await driver.findElement(By.id('code'))).click()
  // 等验证码加载完
  await driver.sleep(2000)
})

识别缺口

缺口由1px的白色边框 + 深色背景组成,上下左右四个边框可能会凸出来或凹下去,如图所示:

下图标出绿色的部分不会受到凹凸影响,将该部分的某一行二值化后就是:白 + 黑 * 87 + 白,我们按照这个规则就能找到缺口的位置了。

是的,就是这么简单。

// ...

;(async () => {
  // ...

  /**
   * 进入验证码的iframe里
   */
  const frame = await driver.findElement(By.id('tcaptcha_iframe'))
  await driver.switchTo().frame(frame)

  /**
   * 前端计算出对齐缺口需要的偏移值
   */
  let offset = await driver.executeScript(() => {
    // 获取背景图及其宽高
    const bg = document.getElementById('slideBg')
    const w = bg.naturalWidth
    const h = bg.naturalHeight
    // 把背景绘制到canvas中,用于获取每个像素的数据
    const cvs = document.createElement('canvas')
    cvs.width = w
    cvs.height = h
    const ctx = cvs.getContext('2d')
    ctx.drawImage(bg, 0, 0)
    // 获取不会收到凹凸影响的某一行:滑块top * 2 + 方块顶部偏移值(23) + 会受到凹凸影响的高度(16) + 1
    // 在该行中寻找符合规则的索引:白 + 黑*87 + 白
    const y = parseInt($('#slideBlock').css('top')) * 2 + 40
    let lastWhite = -1
    for (let x = w / 2; x < w; x ++) {
      const [r, g, b] = ctx.getImageData(x, y, 1, 1).data
      const grey = (r * 299 + g * 587 + b * 114) / 1000
      // 以150为阈值,大于该值的认定为白色
      if (grey > 150) {
        if (lastWhite === -1 || x - lastWhite !== 88) {
          lastWhite = x
        } else {
          lastWhite /= 2 // 图片缩小了2倍
          lastWhite -= 37 // 滑块left(26) + 方块自身偏移值(23 / 2)
          lastWhite >>= 0 // 移动的像素必须为整数
          return lastWhite
        }
      }
    }
  })
})

此时再看OpenCV的做法:

模拟拖动

除了识别缺口位置外,拖动也是验证中的一部分,有下面行为之一的可能会无法通过

  • 拖动过快,0.1s内完成验证
  • 总是1像素偏差都没有
  • 从头到尾都是均匀移动

按照此规则,制定一个拖动的方案:每次以20ms随机拖动2-10像素,当与目标位置差值在5像素内时停止:

// ...

;(async () => {
  // ...

  /**
   * 执行滑块拖动操作
   */
  // 找到iframe中的滑块元素
  const slide = await driver.findElement(By.id('tcaptcha_drag_thumb'))
  // 将鼠标移动到滑块上并按下左键
  const actions = driver.actions().move({origin: slide}).press()
  // 每次以20ms随机拖动2-10个像素,当与目标位置差值在5像素内时停止
  let current = 0
  while (Math.abs(offset - current) > 5) {
    const distance = Math.round(Math.random() * 8) + 2
    current += distance
    actions.move({origin: slide, x: distance, duration: 20})
    console.log(`moving: ${current} / ${offset}`)
  }
  // 松开鼠标左键
  await actions.release().perform()
})

拖动的效果看起来比较弱智,但是真的管用。

完整代码。

“反反爬虫 – 腾讯滑块验证码破解”上的6条回复

  1. 您好,跨域好像有点问题,顺丰站点提示 UnhandledPromiseRejectionWarning: WebDriverError: : Blocked a frame with origin “https://www.sf-express.com” from accessing a cross-origin frame.能麻烦指点下嘛

    1. 是不是没有禁止同源策略?参考代码:
      const options = new Chrome.Options().addArguments(‘–disable-web-security’)
      const driver = new Builder().forBrowser(‘chrome’).setChromeOptions(options).build()

      1. 感谢大神回复,是从您的源代码改动的,加了的。
        您源码中的网页正常运行,然后改了下地址就不行了

      1. 然后腾讯验证码很狗的加了 滑块儿 白色内发光。。

评论已关闭。

给博主打赏

2元 5元 10元