最近西部世界第二季很火, 小編經常分不清誰是機器人誰是真人
為了區分人類和機器, 有個人發明了一種測試, 他叫圖靈~
驗證碼就是一個典型的圖靈測試, 英文名 captcha, 全稱如下
Completely Automated Public Turing test to tell Computers and Humans Apart
全自動區分計算機和人類的圖靈測試
目前主流的驗證碼有
- 圖形驗證碼
- 簡訊驗證碼
- 滑塊驗證碼
- 圖中點選驗證碼
但現在的人工智慧過於強大, 大部分扭曲的圖形驗證碼都可以被機器破解, 已經不再是一個可靠的圖靈測試
而且圖形驗證碼體驗很差, 輸入困難
這時候, 滑動驗證碼出現了, 最具代表性的就是 Geetest(極驗)
滑動驗證碼對機器破解有兩大難點, 第一個是需要通過影象識別知道滑到哪裡, 第二個是需要模仿人類做出滑動的手勢
滑動驗證碼 操作簡便, 破解難度大, 很快就流行起來了
WebDriver 標準
正好 W3C 近日釋出了一個瀏覽器自動化操作的標準, 名叫 WebDriver
https://www.w3.org/TR/webdriver1/
小編就拿極驗滑動驗證碼開刀, 和大家一起感受 WebDriver 的強大功能
安裝 WebDriver
WebDriver 已經是既定標準, 各大瀏覽器最新版都天然支援 WebDriver 協議, 使用門檻大大降低
小編本次使用的是支援度較好的瀏覽器 firefox
從 https://github.com/mozilla/geckodriver 官方倉庫中即可下載 firefox 的 diver
使用 selenium-webdriver
WebDriver 標準本身只是定義了一系列操作瀏覽器的 HTTP 協議, 但 selenium 已經為我們封裝成了 sdk, 直接呼叫函式即可
1 |
const webdriver = require('selenium-webdriver') |
開啟極驗demo頁面
我們先用 WebDriver 實現一個最簡單的例子: 開啟一個網頁
1 2 3 4 5 6 7 8 |
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 點選這個元素
1 |
await driver.findElement(webdriver.By.css('.products-content li:nth-child(2)')).click() |
點選驗證按鈕
來到了滑動行為驗證區域, 接下來就要點選驗證按鈕, 同樣也是先用 CSS 選擇器選出按鈕, 然後再點選
1 |
await driver.findElement(webdriver.By.css('.geetest_radar_tip')).click() |
區域截圖
到這一步, 滑動驗證碼已經彈出
我們碰到了破解過程中的第一個難點, 影象識別
要知道拼圖需要滑動到哪裡, 先要知道完整圖片以及缺一塊的圖片, 放一起對比, 才能知道滑塊需要滑動到哪裡
WebDriver 提供了兩種截圖方式, 一種是全屏截圖, 一種是元素截圖
這裡我們僅需要獲取拼圖缺失的背景圖, 因此使用元素截圖
1 2 3 4 5 6 7 8 9 |
// 隱藏原圖再截圖 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, 可以完成一系列操作, 比如鍵盤輸入, 滑鼠移動, 點選等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 獲取拼圖滑塊按鈕 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秒後重試
重複好多次, 我們發現
即便完全吻合, 也無法繞過極驗的認證!
低估, 完全的低估!
拼圖吻合只是必要條件, 破解的基礎門檻
真正的難點是拖動過程中的 滑動軌跡!
模仿人類滑動
我們再次想到了西部世界二, 最後的彩蛋威廉在世外山谷絕望的問艾米莉
威廉: Verify what?
艾米莉: Fidelity
Fidelity, 真實度
破解到了這一步, 能否模仿人類的滑動軌跡成了關鍵
我們反覆不斷的調教滑動程式碼, again and again
小編進行了多次嘗試, 比如把 move action 分成多段, 按照不同的速度進行拖動, 甚至加入各種隨機數, 但始終無法通過極驗的軌跡檢查
最後小編仿照微信隨機紅包的方式, 先把要滑動的距離切成幾十份, 並且允許有負數
人在滑動拼圖的時候很容易出現, 滑過了再滑動回去的場景, 因此負數可以增加 Fidelity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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 再次控制瀏覽器
開啟網頁, 點選按鈕, 拖動滑塊, 滑塊曲折前行…
摩擦摩擦, 似魔鬼的步伐, 似老奶奶顫巍巍的手
終於, 極驗顯示出一個清爽綠色的橫幅, 彷彿在向我們招手: 歡迎你, 人類