利用puppeteer破解極驗的滑動驗證

YDJFE發表於2018-02-23

上一篇文章寫了一個puppeteer的簡單入門,出於好奇想了一個問題,puppeteer能破解驗證碼嗎???於是,正好就拿前端網來試試 (純粹出於學習)

基本的流程

1. 開啟前端網,點選登入。

2. 填寫賬號,密碼。

3. 點解驗證按鈕,通過滑動驗證,最後成功登陸。

程式碼實現

github上可以checkout。

run.js

const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6 Plus'];
let timeout = function (delay) {
     return new Promise((resolve, reject) => {   
           setTimeout(() => {   
                  try {
                      resolve(1)
                  } catch (e) {
                      reject(0)
                   }
           }, delay);
     })
 }

 let page = null
 let btn_position = null
 let times = 0 // 執行重新滑動的次數
 const distanceError = [-10,2,3,5] // 距離誤差

 async function run() {
  const browser = await puppeteer.launch({
      headless:false //這裡我設定成false主要是為了讓大家看到效果,設定為true就不會開啟瀏覽器
  });
  page = await browser.newPage();

  // 1.開啟前端網
  await page.emulate(iPhone);
  await page.goto('https://www.qdfuns.com/');
  await timeout(1000);
  
  // 2.開啟登入頁面
  page.click('a[data-type=login]')
  await timeout(1000);

  // 3.輸入賬號密碼
  page.type('input[data-type=email]','你的賬號')
  await timeout(500);
  page.type('input[placeholder=密碼]','你的密碼')
  await timeout(1000);
  
  // 4.點選驗證
  page.click('.geetest_radar_tip')
  await timeout(1000);

  btn_position = await getBtnPosition();

  // 5.滑動
  drag(null)
 }

 /**
  * 計算按鈕需要滑動的距離 
  * */ 
 async function calculateDistance() {
  const distance = await page.evaluate(() => {

    // 比較畫素,找到缺口的大概位置
    function compare(document) {
      const ctx1 = document.querySelector('.geetest_canvas_fullbg'); // 完成圖片
      const ctx2 = document.querySelector('.geetest_canvas_bg');  // 帶缺口圖片
      const pixelDifference = 30; // 畫素差
      let res = []; // 儲存畫素差較大的x座標

      // 對比畫素
      for(let i=57;i<260;i++){
        for(let j=1;j<160;j++) {
          const imgData1 = ctx1.getContext("2d").getImageData(1*i,1*j,1,1)
          const imgData2 = ctx2.getContext("2d").getImageData(1*i,1*j,1,1)
          const data1 = imgData1.data;
          const data2 = imgData2.data;
          const res1=Math.abs(data1[0]-data2[0]);
          const res2=Math.abs(data1[1]-data2[1]);
          const res3=Math.abs(data1[2]-data2[2]);
              if(!(res1 < pixelDifference && res2 < pixelDifference && res3 < pixelDifference)) {
                if(!res.includes(i)) {
                  res.push(i);
                }
              }  
        }
      }
      // 返回畫素差最大值跟最小值,經過除錯最小值往左小7畫素,最大值往左54畫素
      return {min:res[0]-7,max:res[res.length-1]-54}
    }
    return compare(document)
  })
  return distance;
 }

 /**
  * 計算滑塊位置
 */
 async function getBtnPosition() {
  const btn_position = await page.evaluate(() => {
    const {clientWidth,clientHeight} = document.querySelector('.geetest_popup_ghost')
    return {btn_left:clientWidth/2-104,btn_top:clientHeight/2+59}
  })
  return btn_position;
 }

 /**
  * 嘗試滑動按鈕
  * @param distance 滑動距離
  * */  
 async function tryValidation(distance) {
  //將距離拆分成兩段,模擬正常人的行為
  const distance1 = distance - 10
  const distance2 = 10

  page.mouse.click(btn_position.btn_left,btn_position.btn_top,{delay:2000})
  page.mouse.down(btn_position.btn_left,btn_position.btn_top)
  page.mouse.move(btn_position.btn_left+distance1,btn_position.btn_top,{steps:30})
  await timeout(800);
  page.mouse.move(btn_position.btn_left+distance1+distance2,btn_position.btn_top,{steps:20})
  await timeout(800);
  page.mouse.up()
  await timeout(4000);
  
  // 判斷是否驗證成功
  const isSuccess = await page.evaluate(() => {
    return document.querySelector('.geetest_success_radar_tip_content') && document.querySelector('.geetest_success_radar_tip_content').innerHTML
  })
  await timeout(1000);
  // 判斷是否需要重新計算距離
  const reDistance = await page.evaluate(() => {
    return document.querySelector('.geetest_result_content') && document.querySelector('.geetest_result_content').innerHTML
  })
  await timeout(1000);
  return {isSuccess:isSuccess==='驗證成功',reDistance:reDistance.includes('怪物吃了拼圖')}
 }

 /**
  * 拖動滑塊
  * @param distance 滑動距離
  * */ 
 async function drag(distance) {
  distance = distance || await calculateDistance();
  const result = await tryValidation(distance.min)
  if(result.isSuccess) {
    await timeout(1000);
    //登入
    console.log('驗證成功')
    page.click('#modal-member-login button')
  }else if(result.reDistance) {
    console.log('重新計算滑距離錄,重新滑動')
    times = 0
    await drag(null)
  } else {
    if(distanceError[times]){
      times ++
      console.log('重新滑動')
      await drag({min:distance.max,max:distance.max+distanceError[times]})
    } else {
      console.log('滑動失敗')
      times = 0
      run()
    }
  }
 }

 run()


複製程式碼

package.json

{
  "name": "demo",
  "version": "1.0.0",
  "dependencies": {
    "puppeteer": "^1.0.0"
  }
}

複製程式碼

執行

1. 將這個兩個檔案儲存到資料夾下面,終端切換到當前路徑下

2. npm i

3. 補上前端網的賬號,密碼

4. node run

演示

下圖演示可以分為四步:

1. 開啟登陸頁面,輸入事先寫好的 賬號密碼

2. 第一次拖動滑塊提示“ 被怪獸吃了”,所以重新計算了新的圖片的缺口距離。

3. 第二,三次拖動提示 “沒正確合拼”,所以重新拖動。

4. 驗證成功,登入

(請將滑鼠放到gif上檢視演示效果,或者請拖到新視窗開啟gif)

利用puppeteer破解極驗的滑動驗證

說明

1. 滑動驗證有三個canvas,其中只需要 classname為 ‘geetest_canvas_fullbg’ 以及 ‘geetest_canvas_bg’ 的進行畫素差對比。ps: 前者是完整圖片,後者是帶缺口的圖片。

利用puppeteer破解極驗的滑動驗證

2. 每個帶缺口的圖片都有一塊誤導的陰影,所以對比畫素差的時候,計算出的距離分別是誤導陰影以及缺口的。因此,滑動距離的取值,我取‘{min:res[0]-7,max:res[res.length-1]-54}’。當缺口比誤導陰影靠左時,min(距離最小值)值就是滑動距離,否則就是max(距離最大值)減去滑塊寬度

利用puppeteer破解極驗的滑動驗證

3. 滑動結果分三種情況:驗證成功被吃了失敗“被吃了”會重新請求圖片,所以重新計算了距離再滑動;“失敗”則重新滑動,如果執行4次依然失敗,則重新run整個流程。

整個流程大概就是這樣,有興趣的朋友可以交流交流

相關文章