Используйте node + puppeteer, чтобы взломать код подтверждения

внешний интерфейс

Я уже писал статьи о кукловоде

Предварительное исследование кукловода

Практика кукловода в процессе разработки

Я не ожидал, что ты будешь таким SSR

Некоторое время назад LZ получил очередной запрос на сканирование данных о заказе сайта курьерской компании.Сначала я думал, что это просто сканирование данных.Хотя nodejs не очень скользкий, мне все еще сложно сканировать некоторые простые данные ...вид войны пяти мразей.

image

Когда я открыл сайт, ввел данные и уже собирался проанализировать структуру страницы, вдруг выскочил проверочный код слайдера. проклятие...

image

WTF, ты заставил меня залезть на птицу.....

Посмотрев два дня на проверочный код слайдера, я наконец пришел к выводу滑块验证码阻止了人类文明的进步!

продукт каждое утроцветКогда я улыбнулась и спросила о своем прогрессе, мое сердце рухнуло

image

Тяжело чувствовать себя плохо, но дело все равно надо делать. Наконец, я подумал об использовании смоделированного cas (единого входа), разработанного puppeteer, для решения функции автоматического входа в некоторые из наших приложений в средах разработки и тестирования. Теперь я возьму ситуацию в качестве примера, чтобы увидеть, как использовать node + puppeteer для эффективного взлома кода проверки слайдера.

image

Брат писал о Наггетс раньшеВзлом слайдера капчи с кукловодом, то мы будем использовать некоторые другие идеи для расшифровки

Здесь мы также возьмем интерфейсную сеть в качестве примера:

const puppeteer = require("puppeteer");
const fs = require("fs");
const path = require("path");
const pixels = require("image-pixels");
const resemble = require("resemblejs");

let page = null;
const bgImg = path.resolve(__dirname, "bg.png");
const fullbgImg = path.resolve(__dirname, "fullbg.png");

async function run() {
  const browser = await puppeteer.launch({
    headless: false
  });
  page = await browser.newPage();
    
  // 打开前端网
  await page.goto("https://www.qdfuns.com/");
  await page.waitForSelector(".hand");

  await page.click("a[data-type=login]");
  const geetest_btn = ".geetest_btn";
  await page.waitForSelector(geetest_btn);

  await page.click(geetest_btn);
  await page.waitFor(1000);
    
  // 获取滑动距离
  async function getDistance() {
  
    // 获取canvas
    let { bg, fullbg } = await page.evaluate(() => {
      const fullbg = document.querySelector(".geetest_canvas_fullbg");
      const bg = document.querySelector(".geetest_canvas_bg");
      return {
        bg: bg.toDataURL(),
        fullbg: fullbg.toDataURL()
      };
    });

    bg = bg.replace(/^data:image\/\w+;base64,/, "");
    fullbg = fullbg.replace(/^data:image\/\w+;base64,/, "");
    var bgDataBuffer = new Buffer(bg, "base64");
    var fullbgDataBuffer = new Buffer(fullbg, "base64");

    fs.writeFileSync(bgImg, bgDataBuffer);
    fs.writeFileSync(fullbgImg, fullbgDataBuffer);

    // 通过resemble比较背景图和缺口图的不同
    resemble(bgImg)
      .compareTo(fullbgImg)
      .ignoreColors()
      .onComplete(async function(data) {
        fs.writeFileSync(path.resolve(__dirname, `diff.png`), data.getBuffer());
      });

    var { data } = await pixels(path.resolve(__dirname, `diff.png`), {
      cache: false
    });
    
    // 获取缺口距离左边的做小位置,即计为需要滑动的距离
    let arr = [];
    for (let i = 10; i < 150; i++) {
      for (let j = 80; j < 220; j++) {
        var p = 260 * i + j;
        p = p << 2;
        if (data[p] === 255 && data[p + 1] === 0 && data[p + 2] === 255) {
          arr.push(j);
          break;
        }
      }
    }
    return Math.min(...arr);
  }

  const distance = await getDistance();

  const button = await page.$(".geetest_slider_button");
  const box = await button.boundingBox();
  const axleX = Math.floor(box.x + box.width / 2);
  const axleY = Math.floor(box.y + box.height / 2);

  await btnSlider(distance);
  
  // 滑动滑块
  async function btnSlider(distance) {
    await page.mouse.move(axleX, axleY);
    await page.mouse.down();
    await page.waitFor(200);
    await page.mouse.move(box.x + distance / 4, axleY, { steps: 20 });
    await page.waitFor(200);
    await page.mouse.move(box.x + distance / 3, axleY, { steps: 18 });
    await page.waitFor(350);
    await page.mouse.move(box.x + distance / 2, axleY, { steps: 15 });
    await page.waitFor(400);
    await page.mouse.move(box.x + (distance / 3) * 2, axleY, { steps: 15 });
    await page.waitFor(350);
    await page.mouse.move(box.x + (distance / 4) * 3, axleY, { steps: 10 });
    await page.waitFor(350);
    await page.mouse.move(box.x + distance + 30, axleY, { steps: 10 });
    await page.waitFor(300);
    await page.mouse.up();
    await page.waitFor(1000);

   const text = await page.evaluate(() => {
      return document.querySelector(".geetest_result_box").innerText;
    });
    console.log(text);
    let step = 0;
    if (text) {
      // 如果失败重新获取滑块
      if (
        text.includes("怪物吃了拼图") ||
        text.includes("拖动滑块将悬浮图像正确拼合")
      ) {
        await page.waitFor(2000);
        await page.click(".geetest_refresh_1");
        await page.waitFor(1000);
        step = await getDistance();
        await btnSlider(step);
      } else if (text.includes("速度超过")) {
        console.log("success");
      }
    }
  }
}
run();

Запускаем программу, вывод в консоль следующий (если повезет, то может и один раз, в зависимости от того, как оптимизирована и решена промежуточная обработка)

image

image

Здесь следует отметить следующие моменты

  • 缺口图存在干扰缺口图,resemble在比对的时候需要会得到两个缺口,这里目前没有一个很好的办法来确定到底哪个缺口是我们所需要的(下面我们会提到一个针对该问题的方法来避免该干扰项)
  • 滑动的时候需要控制下滑动速度,具体怎么个滑动法,那就仁者见仁智者见智了

ты думал, что все кончено

image

Во многих случаях код проверки ползунка не дает нам полного фонового изображения. Как мы можем эффективно определить пробел в это время? Здесь мы можем использоватьgmРазмойте наше фоновое изображение ниже, а затем используйтеresemblejsПерейдите к сравнению следующих двух изображений, но в это время изображения будут разными во многих местах.В это время мы можем получить положение маленького изображения ползунка из родительского элемента, чтобы уменьшить диапазон сравнения пикселей ( это может эффективно решить проблему) Мы упоминали выше, чтобы избежать проблемы с отвлекающим фактором)

const puppeteer = require("puppeteer");
const fs = require("fs");
const path = require("path");
const pixels = require("image-pixels");
const resemble = require("resemblejs");
const gm = require("gm");

let page = null;
const bgImg = path.resolve(__dirname, "bg.png");
const bgBlurImg = path.resolve(__dirname, "bgBlur.png");
const bgDiffImg = path.resolve(__dirname, "bgDiff.png");

async function run() {
  const browser = await puppeteer.launch({
    headless: false
  });
  page = await browser.newPage();

  await page.goto(
    "https://x.tongdun.cn/onlineExperience/slidingPuzzle?source=baidu&plan=%E5%8F%8D%E6%AC%BA%E8%AF%88&unit=%E6%99%BA%E8%83%BD%E9%AA%8C%E8%AF%81&keyword=%E6%99%BA%E8%83%BD%E9%AA%8C%E8%AF%81%E7%A0%81&e_creative=24659987438&e_adposition=cl1&e_keywordid=101045415224&e_keywordid2=101045415224&audience=236369"
  );
  await page.waitForSelector("#loginBtn");

  await page.click("#loginBtn");
  const slidetrigger = ".td-pop-slidetrigger";
  await page.waitForSelector(slidetrigger);

  await page.click(slidetrigger);
  await page.waitFor(1000);
  const slideIdentity = ".td-pop-slide-identity";
  await page.waitFor(slideIdentity);

  // 获取小滑块的top值,来减少比对范围
  const top = await page.evaluate(() => {
    const identity = document.querySelector(".td-pop-slide-identity");
    return identity.offsetTop;
  });

  async function getDistance() {
    // 获取缺口图片
    let { bg } = await page.evaluate(() => {
      const bg = document.querySelector(".td-bg-img");
      return {
        bg: bg.toDataURL()
      };
    });
    bg = bg.replace(/^data:image\/\w+;base64,/, "");
    var bgDataBuffer = new Buffer(bg, "base64");

    fs.writeFileSync(bgImg, bgDataBuffer);

    // 图片模糊
    gm(bgImg)
      .blur(1)
      .write(bgBlurImg, function(err) {
        if (!err) console.log("done");
      });

    // 图片对比
    resemble(bgImg)
      .compareTo(bgBlurImg)
      .ignoreColors()
      .onComplete(async function(data) {
        fs.writeFileSync(bgDiffImg, data.getBuffer());
      });

    var { data } = await pixels(bgDiffImg, {
      cache: false
    });
    let arr = [];

    // 比对范围内的像素点
    for (let i = top; i < top + 44; i++) {
      for (let j = 60; j < 320; j++) {
        var p = 320 * i + j;
        p = p << 2;
        if (data[p] === 255 && data[p + 1] === 0 && data[p + 2] === 255) {
          arr.push(j);
          break;
        }
      }
    }
    const { maxStr } = getMoreNum(arr);
    return Number(maxStr);
  }

  const distance = await getDistance();
  const button = await page.$(slidetrigger);
  const box = await button.boundingBox();
  const axleX = Math.floor(box.x + box.width / 2);
  const axleY = Math.floor(box.y + box.height / 2);
  console.log(distance, "distance");
  console.log(box.x + distance);

  await btnSlider(distance);
  async function btnSlider(distance) {
    await page.mouse.move(axleX, axleY);
    await page.mouse.down();
    await page.waitFor(200);
    await page.mouse.move(box.x + distance / 4, axleY, { steps: 20 });
    await page.waitFor(200);
    await page.mouse.move(box.x + distance / 3, axleY, { steps: 18 });
    await page.waitFor(350);
    await page.mouse.move(box.x + distance / 2, axleY, { steps: 15 });
    await page.waitFor(400);
    await page.mouse.move(box.x + (distance / 3) * 2, axleY, { steps: 15 });
    await page.waitFor(350);
    await page.mouse.move(box.x + (distance / 4) * 3, axleY, { steps: 10 });
    await page.waitFor(350);
    await page.mouse.move(box.x + distance + 20, axleY, { steps: 10 });
    await page.waitFor(300);
    await page.mouse.up();
    await page.waitFor(1000);
  }
}

run();

function getMoreNum(arr) {
  var obj = {};
  var arr1 = [];
  for (var i = 0; i < arr.length; i++) {
    if (arr1.indexOf(arr[i]) == -1) {
      obj[arr[i]] = 1;
      arr1.push(arr[i]);
    } else {
      obj[arr[i]]++;
    }
  }
  var max = 0;
  var maxStr;
  for (var i in obj) {
    if (max < obj[i]) {
      max = obj[i];
      maxStr = i;
    }
  }
  return { max, maxStr };
}

В примере не добавлена ​​логика повторного проскальзывания после ошибки

image

Проблемы с этим методом

  • 自己模糊化背景图片就行像素比较,成功率较低,需优化(亦可以通过比对的图片通过其灰度值来锁定区域)

Выше мы представили два метода взлома кода проверки слайдера. Кроме того, LZ также пытался использовать метод бинаризации изображения для обнаружения разрыва изображения.Успешность этого метода намного выше, чем у второго метода.Конкретный метод реализации не будет написан, и читатели могут изучить его самостоятельно.

Пример кода доступен по адресуgithubПроверить

image