chunpu / blog

personal blog render by jekyll
MIT License
51 stars 8 forks source link

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

Open chunpu opened 6 years ago

chunpu commented 6 years ago

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

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

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

Completely Automated Public Turing test to tell Computers and Humans Apart 全自动区分计算机和人类的图灵测试

目前主流的验证码有

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

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

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

image

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

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

WebDriver 标准

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

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

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

先附上成果

https://v.qq.com/x/page/y0715zqcz7f.html

安装 WebDriver

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

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

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

使用 selenium-webdriver

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

const webdriver = require('selenium-webdriver')

打开极验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 页

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

选择滑动行为验证

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

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

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

点击验证按钮

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

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

区域截图

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

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

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

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 截图
const bgPng = await bgCanvas.takeScreenshot()

找到滑动点

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

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

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

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

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

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

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

开始滑动

滑动操作使用了 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秒后重试

image

重复好多次, 我们发现: 即便完全吻合, 也无法绕过极验的认证!

低估, 完全的低估!

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

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

模仿人类滑动

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

威廉: Verify what? 艾米莉: Fidelity

image

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) {
  var 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) {
  var distribution = getRandomDistribution(total, count)
  return _.map(distribution, (item, i) => {
    return _.sum(distribution.slice(0, i + 1))
  })
}

保存, 执行

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

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

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

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

image