Как я боролся с анти-читом

15 ИЮНЯ 2022

------------------------------------------------------------------------

Наш телеграм-канал: t.me/@mailsgun_online

Youtube: youtube.com/@mailsgun_online

------------------------------------------------------------------------

Обновить

Из песочницы

Как все началось

Насколько мне известно, в большинстве русскоязычных тестировщиков скорости печати используется метрика CPM, наткнувшись на следующее видео, мне стало интересно посмотреть на свои показатели метрики WPM.

По окончанию тестирования пользователю показывается результат написанный на картинке. И мне она показалась не совсем корректной.

Достаточно обидно
Достаточно обидно

Реализация автонабора для соревновательного режима

На сайте пользователи создают соревнования в которых может участвовать кто угодно, выбор конкретной «комнаты» выглядит следующий образом

Пример соревнований
Пример соревнований

После присоединения пользователя встречает следующее окно:

Слова и поле для ввода
Слова и поле для ввода

Строка со словами меняется динамически по мере введения слов, изначально я думал что это вызовет некоторые проблемы, однако оказалось что словарь слов реализован достаточно простым способом. Код страницы выглядит следующим образом:

Начало словаря
Начало словаря

Из такой структуры можно сделать два вывода: первый — количество слов для набора ограниченно и длина словаря составляет 291 слово, второй — считать все элементы для набора можно следующим образом:

from bs4 import BeautifulSoup
soup = BeautifulSoup(page_source, 'html.parser')
words_list = list()
boundaries = soup.find(attrs={'id': 'row1', 'style': "top: 1px;"})
for span in boundaries.find_all('span'):
		words_list.append(span.text)

Дальше только проще. Я реализовал небольшой класс содержащий методы для запуска/получения/ввода и применил его на случайном испытании:

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time
from bs4 import BeautifulSoup
from webdriver_manager.chrome import ChromeDriverManager
import json
HTML_SOURCE = "https://10fastfingers.com/competition/611216d69046c"
class MonkeyWorker:
    def __init__(self, url, login, password):
        self.__URL = url
        #   Create actual driver
        self.driver = webdriver.Chrome(ChromeDriverManager(version="92.0.4515.107").install())
        self.driver.maximize_window()
        #   Login
        self.driver.get("https://10fastfingers.com/login")
        time.sleep(2)
        self.driver.find_element_by_xpath('//input[@name = "data[User][email]"]').send_keys(login)
        self.driver.find_element_by_xpath('//input[@name = "data[User][password]"]').send_keys(password)
        time.sleep(1)
        self.driver.find_element_by_xpath('//button[@class = "CybotCookiebotDialogBodyButton"]').click()
        self.driver.find_element_by_xpath('//button[@class = "btn btn-info" and @id = "login-form-submit"]').click()
        time.sleep(3)
        #   Open input Link
        self.driver.get(self.__URL)
        time.sleep(5)
        self.soup = BeautifulSoup(self.driver.page_source, 'html.parser')
        self.words_list = list()
        self.input_form = self.driver.find_element_by_xpath('//input[@class = "form-control" and @id = "inputfield"]')
    def get_words(self):
        boundaries = self.soup.find(attrs={'id': 'row1', 'style': "top: 1px;"})
        for span in boundaries.find_all('span'):
            self.words_list.append(span.text)
    def input_words(self):
        for word in self.words_list:
            for symbol in word:
                self.input_form.send_keys(symbol)
            self.input_form.send_keys(Keys.SPACE)
    def close(self):
        self.driver.quit()
with open('config.txt') as file:
    conf = json.load(file)
monke = MonkeyWorker(HTML_SOURCE, conf["login"], conf["password"])
monke.get_words()
monke.input_words()
char = input()
monke.close()

Метод input_words намеренно итерируется по символам в слове с целью имитации ввода пользователем, а так же для возможности добавления случайной задержки перед вводом символа. Применение алгоритма дало следующий результат:

С этого момента и начинается интересная часть поста.

Встреча с анти-читом

После прохождения испытания я заметил что в таблице результатов не произошло изменений, и вот в чем была причина

При переходе по ссылке, пользователя встречает следующая страница. Из нее понятно что у меня есть неограниченное число попыток на «сдачу экзамена», и что триггером послужил аномально большой показатель WPM. Полный код для обхода анти-чит системы будет приложен в конце поста.

При нажатии на кнопку Start Test сразу появляются первые проблемы. Первая из них продемонстрирована ниже:

Возникает необходимость найти способ нажать на кнопку самым простым способом, и как мне показалось он следующий. После нахождения элемента кнопки необходимо вызвать скрипт клика. В Selenium это делается следующим способом:

driver.execute_script("$(arguments[0]).click();", self.driver.find_element_by_xpath('//button[@class = 
                                    "btn btn-large btn-info" and @id = "start-btn"]'))

Было вполне ожидаемо, что после запуска теста меня встретит изображение с текстом который мне прийдется набирать. Мне было необходимо реализовать способ сохранения изображения для того чтобы потом превращать его в список слов. Моя идея заключалась в том, чтобы отыскать ссылку изображения в коде страницы и после загрузить картинку с помощью requests . Выглядеть это должно было примерно следующим образом:

IMAGE_URL = str(self.soup.find('div', attrs={"id": "word-img"}).find_all('img')[0]['src'])
image = requests.get(IMAGE_URL).content()

Однако при попытке сохранить изображение по ссылке я получал обычный белый фон. Я не понимаю чем именно это обусловленно, однако мне было понятно, что способ с сохранением мне не поможет. Перспективным решением мне показалась реализация с помощью скриншота Selenium. Для того чтобы его реализовать мне пришлось вспоминать как создавать новые вкладки средствами ChromeDriver. Выглядит это следующим образом:

def img_getter():
    """
    Function to get image by opening new tab. Doesn't work because 10fastfingers have smart text genearator
    :return:
    """
    #   Getting image
    IMAGE_URL = str(self.soup.find('div', attrs={"id": "word-img"}).find_all('img')[0]['src'])
    self.driver.execute_script("window.open('');")
    self.driver.switch_to.window(self.driver.window_handles[1])
    self.driver.get(IMAGE_URL)
    #   Convert selenium screenshot to Image
    self.image = Image.open(io.BytesIO(self.driver.get_screenshot_as_png()))    # Creating Image
    #   self.driver.close()
    self.driver.switch_to.window(self.driver.window_handles[0])
    self.image.show()
    self.__image_preprocessing()

Обработка изображения представляла из себя следующую функцию. Ее задача состоит в получении границ белого прямоугольника, так как скриншот вкладки содержал в себе обширную область черной заливки:

Мне был нужен только белый прямоугольник
Мне был нужен только белый прямоугольник
def __old_image_preprocessing(self):
    image = self.image.convert('RGB')
    numpy_image = np.array(image)
    original = numpy_image.copy()
    image = image.filter(ImageFilter.MedianFilter(3))
    # Find X,Y coordinates of all white pixels
    whiteY, whiteX = np.where(np.all(numpy_image == [255, 255, 255], axis=2))
    top, bottom = whiteY[0], whiteY[-1]
    left, right = whiteX[0], whiteX[-1]
    # Crop white solid
    ROI = original[top:bottom, left:right]
    final = Image.fromarray(ROI)
    final.show()
    self.image = final

На первый взгляд все работало замечательно, однако все оказалось не так просто. Изображения были разными!

То что мне нужно было вводить
То что мне нужно было вводить
Изображение которое я получал переходя по ссылке
Изображение которое я получал переходя по ссылке

Почему так получалось я так и не понял, но убедившись в том, что такое поведение было заложено при создании анти-чит системы, я начал искать другой способ. Им оказался следующий подход. Я заметил, что генерируемое изображение на сайте помещено в конкретный элемент, что значит что я могу получить параметры поля, и сохранив скриншот с основной вкладки сделать обычный кроп изображения.

def getting_image(self):
    element = self.driver.find_element_by_xpath('//div[@id = "word-img"]')
    location = element.location_once_scrolled_into_view
    size = element.size
    area = (location["x"] * 2, location["y"] * 2, location["x"] * 2 + size["width"] * 2,
            location["y"] * 2 + size["height"] * 2)
    self.image = self.driver.get_screenshot_as_png()
    self.image = Image.open(io.BytesIO(self.image))
    self.image = self.image.crop(area)
    self.__image_preprocessing()
def __image_preprocessing(self):
    """
    Apply SHARPEN filter to make words detecting little better
    :return:
    """
    self.image.show()
    self.image = self.image.filter(ImageFilter.SHARPEN)
    self.image.show()
    self.image = self.image.filter(ImageFilter.SHARPEN)
    self.image.show()

И это сработало! Теперь осталось воспользоваться OCR Tesseract и дело сделано.

import pytesseract as image_engine
from pytesseract import Output
def get_words(self):
    words = image_engine.image_to_data(self.image, lang="eng", output_type=Output.DICT)
    clean_words = list(filter(lambda x: x != '', words["text"]))
    self.words_list = clean_words

При желании можно сделать более тщательную обработку изображения, исправив дисторсию слов, можно лучше настроить Tesseract, можно воспользоваться для определения слов чем-то в духе: https://github.com/courao/ocr.pytorch

Я же хотел просто увидеть результат работы. И он меня не разочаровал:

Так я реализовывал способ обойти анти-чит систему на https://10fastfingers.com/. Понятное дело что сделано это все лишь для веселья и возможности вспомнить основы работы с Selenium. Надеюсь мой пост Вам понравился. Спасибо за внимание!

Анти-чит алгоритм

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time
from bs4 import BeautifulSoup
from webdriver_manager.chrome import ChromeDriverManager
import json
import pytesseract as image_engine
from pytesseract import Output
from PIL import Image, ImageFilter
import io
import numpy as np
HTML_SOURCE = "https://10fastfingers.com/anticheat"
class MonkeyWorker:
    def __init__(self, url, login, password):
        self.__URL = url
        self.driver = webdriver.Chrome(ChromeDriverManager(version="92.0.4515.107").install())
        self.action = webdriver.ActionChains(self.driver)
        self.driver.maximize_window()
        #   Login
        self.driver.get("https://10fastfingers.com/login")
        time.sleep(2)
        self.driver.find_element_by_xpath('//input[@name = "data[User][email]"]').send_keys(login)
        self.driver.find_element_by_xpath('//input[@name = "data[User][password]"]').send_keys(password)
        time.sleep(1)
        #   Bad way is about using execute java script
        self.driver.find_element_by_xpath('//button[@class = 
                                                "CybotCookiebotDialogBodyButton"]').click()    # Apply cookies
        self.driver.find_element_by_xpath('//button[@class = "btn btn-info" and @id = "login-form-submit"]').click()
        time.sleep(1)   # Little sleep in case after login script redirect to another page
        #   Go to anti-cheat page
        self.driver.get(self.__URL)
        time.sleep(1)
        self.driver.find_element_by_xpath('//a[@href= "/anticheat/view/1/1" and @class = "btn btn-info" 
                                            and text() = "Start Test "]').click()
        time.sleep(5)
        #   Button has specific form - so in my opinion the best way to click it - use execute script
        self.driver.execute_script("$(arguments[0]).click();", self.driver.find_element_by_xpath('//button[@class = 
                                    "btn btn-large btn-info" and @id = "start-btn"]'))
        time.sleep(1)
        self.soup = BeautifulSoup(self.driver.page_source, 'html.parser')
        self.words_list = list()
        #   Save element with form where we need to put Keys
        self.input_form = self.driver.find_element_by_xpath('//textarea[@id = "word-input"]')
        self.getting_image()
    def getting_image(self):
        element = self.driver.find_element_by_xpath('//div[@id = "word-img"]')
        location = element.location_once_scrolled_into_view
        size = element.size
        area = (location["x"] * 2, location["y"] * 2, location["x"] * 2 + size["width"] * 2,
                location["y"] * 2 + size["height"] * 2)
        self.image = self.driver.get_screenshot_as_png()
        self.image = Image.open(io.BytesIO(self.image))
        self.image = self.image.crop(area)
        self.__image_preprocessing()
        def not_working():
            """
            Function to get image by opening new tab. Doesn't work because 10fastfingers have smart text genearator
            :return:
            """
            #   Getting image
            IMAGE_URL = str(self.soup.find('div', attrs={"id": "word-img"}).find_all('img')[0]['src'])
            self.driver.execute_script("window.open('');")
            self.driver.switch_to.window(self.driver.window_handles[1])
            self.driver.get(IMAGE_URL)
            #   Convert selenium screenshot to Image
            self.image = Image.open(io.BytesIO(self.driver.get_screenshot_as_png()))    # Creating Image
            #   self.driver.close()
            self.driver.switch_to.window(self.driver.window_handles[0])
            self.image.show()
            self.__image_preprocessing()
    def __image_preprocessing(self):
        """
        Apply SHARPEN filter to make words detecting little better
        :return:
        """
        self.image.show()
        self.image = self.image.filter(ImageFilter.SHARPEN)
        self.image.show()
        self.image = self.image.filter(ImageFilter.SHARPEN)
        self.image.show()
    def __old_image_preprocessing(self):
        image = self.image.convert('RGB')
        numpy_image = np.array(image)
        original = numpy_image.copy()
        image = image.filter(ImageFilter.MedianFilter(3))
        # Find X,Y coordinates of all white pixels
        whiteY, whiteX = np.where(np.all(numpy_image == [255, 255, 255], axis=2))
        top, bottom = whiteY[0], whiteY[-1]
        left, right = whiteX[0], whiteX[-1]
        # Crop white solid
        ROI = original[top:bottom, left:right]
        final = Image.fromarray(ROI)
        final.show()
        self.image = final
    def get_words(self):
        words = image_engine.image_to_data(self.image, lang="eng", output_type=Output.DICT)
        clean_words = list(filter(lambda x: x != '', words["text"]))
        self.words_list = clean_words
    def input_words(self):
        for word in self.words_list:
            for symbol in word:
                self.input_form.send_keys(symbol)
            self.input_form.send_keys(Keys.SPACE)
        self.input_form.send_keys(Keys.TAB, Keys.ENTER)
    def close(self):
        self.driver.quit()
with open('config.txt') as file:
    conf = json.load(file)
monkey = MonkeyWorker(HTML_SOURCE, conf["login"], conf["password"])
monkey.get_words()
monkey.input_words()
char = input()
monkey.close()
Всего голосов 7: ↑7 и ↓0+7

Просмотры893

  • 27 марта 2020 в 16:36
    Всего голосов 4: ↑3 и ↓1+2

    Просмотры3.9K

  • 3 ноября 2016 в 11:30
    Всего голосов 12: ↑10 и ↓2+8

    Просмотры19K

  • 4 июля 2007 в 11:19
    Всего голосов 9: ↑7 и ↓2+5

    Просмотры543

  • вчера в 15:15
    Всего голосов 39: ↑37 и ↓2+35

    Просмотры4.3K

  • вчера в 19:01
    Всего голосов 31: ↑30 и ↓1+29

    Просмотры7.4K

  • вчера в 13:00
    Всего голосов 21: ↑21 и ↓0+21

    Просмотры1.7K

  • вчера в 09:55
    Всего голосов 15: ↑14 и ↓1+13

    Просмотры2.3K

  • вчера в 17:00
    Всего голосов 13: ↑12 и ↓1+11

    Просмотры1.6K

Source: habr1

Ну и напоследок напоминаю вам о нашем новом проекте vkserfing bot.