震惊! 滑动验证码竟然能这样破解

17,573 阅读7分钟
原文链接: mp.weixin.qq.com

奇技指南

现在的人工智能过于强大, 大部分扭曲的图形验证码都可以被机器破解, 而且图形验证码体验很差, 输入困难。而滑动验证码操作简便, 破解难度大, 很快就流行起来了。今天来讲讲滑动验证码的破解办法。

本文由 chunpu 在奇舞周刊投稿,已授权转载。

最近西部世界第二季很火, 小编经常分不清谁是机器人谁是真人

为了区分人类和机器, 有个人发明了一种测试, 他叫图灵~

验证码就是一个典型的图灵测试, 英文名 captcha, 全称如下

Completely Automated Public Turing test to tell Computers and Humans Apart

全自动区分计算机和人类的图灵测试

目前主流的验证码有

  • 图形验证码

  • 短信验证码

  • 滑块验证码

  • 图中点选验证码

但现在的人工智能过于强大, 大部分扭曲的图形验证码都可以被机器破解, 已经不再是一个可靠的图灵测试

而且图形验证码体验很差, 输入困难

这时候, 滑动验证码出现了, 最具代表性的就是 Geetest(极验)

滑动验证码对机器破解有两大难点, 第一个是需要通过图像识别知道滑到哪里, 第二个是需要模仿人类做出滑动的手势

滑动验证码 操作简便, 破解难度大, 很快就流行起来了

WebDriver 标准

正好 W3C 近日发布了一个浏览器自动化操作的标准, 名叫 WebDriver

https://www.w3.org/TR/webdriver1/

小编就拿极验滑动验证码开刀, 和大家一起感受 WebDriver 的强大功能

先附上小编成果😝(本视频20秒, 没有声音) 播放

安装 WebDriver

WebDriver 已经是既定标准, 各大浏览器最新版都天然支持 WebDriver 协议, 使用门槛大大降低

小编本次使用的是支持度较好的浏览器 firefox

从 https://github.com/mozilla/geckodriver 官方仓库中即可下载 firefox 的 diver

使用 selenium-webdriver

WebDriver 标准本身只是定义了一系列操作浏览器的 HTTP 协议, 但 selenium 已经为我们封装成了 sdk, 直接调用函数即可

const webdriver = require('selenium-webdriver')

01

打开极验demo页面

我们先用 WebDriver 实现一个最简单的例子: 打开一个网页

const webdriver = require('selenium-webdriver')

!async function() {

  // 新建一个 firefox 的 driver 实例

  let driver = await new webdriver.Builder().forBrowser('firefox').build()

  // 访问极验demo页

  await driver.get('http://www.geetest.com/type/')

  console.log('success')

}()

运行这段代码, 我们能看到浏览器打开了极验的 demo 页

此时浏览器的地址栏是黄色的, 表示该浏览器被控制了, 就好像电视剧里面被心灵控制的人眼睛是红色的

02

选择滑动行为验证

极验第一个标签是智能组合验证, 第二个标签才是我们要破解的滑动验证, 因此需要先切换至滑动验证

我们通过 CSS 选择器选出要点击的标签, 然后使用 WebDriver 点击这个元素

await driver.findElement(webdriver.By.css('.products-content li:nth-child(2)')).click()

03

点击验证按钮

来到了滑动行为验证区域, 接下来就要点击验证按钮, 同样也是先用 CSS 选择器选出按钮, 然后再点击

await driver.findElement(webdriver.By.css('.geetest_radar_tip')).click()

04

区域截图

到这一步, 滑动验证码已经弹出

我们碰到了破解过程中的第一个难点, 图像识别

要知道拼图需要滑动到哪里, 先要知道完整图片以及缺一块的图片, 放一起对比, 才能知道滑块需要滑动到哪里

WebDriver 提供了两种截图方式, 一种是全屏截图, 一种是元素截图

这里我们仅需要获取拼图缺失的背景图, 因此使用元素截图

// 隐藏原图再截图

await driver.executeScript(`document.querySelector('.geetest_canvas_fullbg').style.display = 'none'`)

// 找到验证码背景图元素, 是一个 canvas

const bgCanvas = await driver.findElement(webdriver.By.css('.geetest_canvas_bg'))

// 获得一个 base64 格式的 png 截图

05

找到滑动点

小编并不懂图像识别, 为了降低实现难度, 用了一个简单取巧的方法

因为拼图缺失的区域会有 阴影, 而阴影一般比较黑

因此我们把题目从 寻找拼图丢失区 改成了 寻找比较黑的点

当然这个草率的规则正确率并不高, 但为了demo演示已经足够了

那怎么定义 比较黑 呢? 最简单的方法就是挨个读取图像中的像素

我们把 R, G, B 三值相加, 数字越小就认为越黑, 最黑的 rgb(0, 0, 0) 就是0

附上小编用的读取像素库 get-pixels

06

开始滑动

滑动操作使用了 WebDriver 中的 actions api, 可以完成一系列操作, 比如键盘输入, 鼠标移动, 点击等

// 获取拼图滑块按钮

const button = await driver.findElement(webdriver.By.css('.geetest_slider_button'))

// 获取按钮位置等信息

const buttonRect = await button.getRect()

// 初始化 action

let actions = driver.actions({async: true})

// 把鼠标移动到滑块上, 然后点击

actions = actions.move({

  x: x + 10,

  y: y + 10,

  duration: 100

}).press()

// 花一秒钟把滑块拖动至拼图缺失区, 松开鼠标

await actions.move({

  x: x + 10 + point.x - 5,

  y: y + 10,

  duration: 1000

}).release().perform()

写完代码, 执行, 看着拼图顺畅的向前滑动, 等待着奇迹的发生

然而奇迹并没有发生, 只有一行黄字映入眼帘

怪物吃了拼图, 请3秒后重试

重复好多次, 我们发现

即便完全吻合, 也无法绕过极验的认证!

低估, 完全的低估!

拼图吻合只是必要条件, 破解的基础门槛

真正的难点是拖动过程中的 滑动轨迹!

07

模仿人类滑动

我们再次想到了西部世界二, 最后的彩蛋威廉在世外山谷绝望的问艾米莉

威廉: Verify what?

艾米莉: Fidelity

Fidelity, 真实度

破解到了这一步, 能否模仿人类的滑动轨迹成了关键

我们反复不断的调教滑动代码, again and again

小编进行了多次尝试, 比如把 move action 分成多段, 按照不同的速度进行拖动, 甚至加入各种随机数, 但始终无法通过极验的轨迹检查

最后小编仿照微信随机红包的方式, 先把要滑动的距离切成几十份, 并且允许有负数

人在滑动拼图的时候很容易出现, 滑过了再滑动回去的场景, 因此负数可以增加 Fidelity

const count = 30 // 小编分成30步进行滑动

const steps = getSteps(distance, count)

const totalDuration = 8000 // 一共耗时8秒, 慢才能充实轨迹~

_.reduce(steps, (actions, step) => {

  return actions.move({

    x: x + 10 + step,

    y: y + 10 + _.random(-5, 40), // 加上y轴随机数

    duration: parseInt(_.random(totalDuration / count / 2, totalDuration / count * 2)) // 加上时长随机数

  })

}, actions)

// 随机拆成n份

function getRandomDistribution(total, count) {

  let item = total / count

  item = item + _.random(-item * 2, item * 3)

  item = parseInt(item)

  if (count === 1) {

    return [total]

  } else {

    return [item].concat(getRandomDistribution(total - item, count - 1))

  }

}

// 获取每次滑动的X坐标

function getSteps(total, count) {

  let distribution = getRandomDistribution(total, count)

  return _.map(distribution, (item, i) => {

    return _.sum(distribution.slice(0, i + 1))

  })

}

保存, 执行

小编放下鼠标, 端起桌上的马克杯, 看着 WebDriver 再次控制浏览器

打开网页, 点击按钮, 拖动滑块, 滑块曲折前行...

摩擦摩擦, 似魔鬼的步伐, 似老奶奶颤巍巍的手

终于, 极验显示出一个清爽绿色的横幅, 仿佛在向我们招手: 欢迎你, 人类