Кликеры

Мы начнём изучение ботов с самого простого для реализации вида – кликеров. В начале этой главы мы рассмотрим широко используемые инструменты для разработки. Затем изучим техники встраивания данных в процесс игрового приложения на уровне ОС, а также перехвата устройства вывода. Чтобы закрепить полученные знания, мы напишем простого бота для игры Lineage 2. Этот небольшой проект поможет нам оценить достоинства и недостатки кликеров. В конце главы мы рассмотрим подходы для обнаружения этого вида ботов системами защиты.

Инструменты для разработки

Вы начинаете писать программу, чтобы решить какую-то проблему. При этом есть большая вероятность, что с подобной задачей кто-то уже сталкивался до вас. Скорее всего, для её решения уже были разработаны специальные инструменты. Поэтому лучшее, что вы можете сделать, перед тем как начать писать свой код, – это изучить существующие языки программирования, фреймворки и библиотеки. Если вам повезёт, вы найдёте несколько готовых решений, которые будет достаточно скомпоновать вместе для получения нужной функциональности. При этом важно не зацикливаться на использовании хорошо знакомых вам инструментов. Скорее всего, с их помощью вы сможете написать практически любое приложение, но на это уйдёт намного больше усилий, чем при использовании более подходящих средств.

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

Язык программирования

AutoIt — один из самых популярных языков программирования для различных задач автоматизации приложений. У него много возможностей, которые ускоряют разработку:

  • Простой для изучения синтаксис.
  • Подробная доступная онлайн документация и поддержка сообщества на форумах.
  • Хорошая интеграция с функциями ОС (WinAPI) и сторонними библиотеками.
  • Встроенный редактор исходного кода.

AutoIt хорошо подходит для изучения программирования с нуля. Все примеры этой главы будут написаны на нём. В комментариях к ним мы рассмотрим WinAPI-функции, вызываемые через AutoIt. Таким образом, вам будет несложно переписать эти примеры на любом другом языке программирования.

AutoHotKey – это ещё один подходящий язык для написания кликеров. У него есть практически все возможности AutoIt. Основное различие этих языков в синтаксисе. Некоторые примеры этой главы будет проще и быстрее реализовать на AutoHotKey. Но этот язык немного более сложен в изучении.

Библиотеки обработки изображений

AutoIt имеет несколько встроенных средств обработки изображений. Но есть две библиотеки, которые значительно расширяют эти возможности.

Библиотека ImageSearch предоставляет функцию поиска указанного фрагмента изображения в окне игрового приложения. С её помощью бот может с высокой точностью и надёжностью определять месторасположение игровых объектов на экране.

Библиотека FastFind предоставляет продвинутые возможности поиска определённой комбинации пикселей в окне приложения. Например, поиск ближайшего к указанной точке пикселя заданного цвета. Это может быть полезно для обнаружения игровых объектов в случаях, когда библиотека ImageSearch не справляется (например, с 3D-графикой).

Инструменты анализа изображений

Для отладки кликеров могут понадобиться средства анализа изображений. Типичная задача отладки заключается в определении точных координат и цвета какого-то пикселя на скриншоте игры. Эта информация позволит проверить данные, поступающие на вход алгоритмов бота.

Существует множество подобных утилит, и вы легко найдёте их с помощью Google. Я предпочитаю приложение ColorPix, в котором есть все необходимое для решения наших задач.

Редакторы исходного кода

В дистрибутив языка AutoIt входит адаптированная версия редактора SciTE. Он хорошо подходит для написания и отладки AutoIt-скриптов. Если же вы планируете использовать другой язык (например, Python или AutoHotKey), вам понадобится более универсальный редактор. Notepad++ будет подходящим решением для разработки небольших скриптов. Для C++ и C# лучше всего использовать Visual Studio Community.

Перехват API

В наших примерах мы будем писать скрипты на высокоуровневом языке программирования AutoIt. Это означает, что каждая инструкция, написанная на нём, скрывает несколько вызовов более низкоуровневых функций, предоставляемых ОС. Для того чтобы лучше понимать алгоритмы бота и исправлять ошибки в них, нам следует изучить внутреннюю работу функций AutoIt. Кроме того, эта информация позволит вам переписать примеры этой главы на другом языке программирования.

Существует несколько инструментов для перехвата вызова функций ОС. Я использовал бесплатное приложение API Monitor v2. У него есть следующие возможности:

  • Фильтрация всех перехваченных вызовов.
  • Сбор информации об анализируемом процессе.
  • Декодирование входных и выходных параметров вызываемых функций.
  • Просмотр памяти процесса.

Список всех возможностей приложения доступен на сайте разработчиков.

Внедрение данных на уровне ОС

Windows API

Главная задача любой ОС – это управление программными и аппаратными ресурсами компьютера, а также предоставление к ним доступа для запущенных процессов. Аппаратные ресурсы мы уже рассматривали. Это – память, процессорное время, периферийные устройства. К программным ресурсам относятся все приложения и компоненты ОС, установленные на компьютере. Примером этого типа ресурсов являются системные библиотеки Windows, предоставляющие алгоритмы для решения различных задач.

В этой книге мы рассматриваем только ОС Windows. На ней вы сможете запустить все приведённые примеры. В дальнейшем для простоты под ОС всегда будем подразумевать Windows.

Иллюстрация 2-1 демонстрирует интерфейс ОС, через который предоставляется доступ к её ресурсам. Каждый запущенный процесс может обратиться к Windows с запросом на выполнение какого-то действия (например создание нового окна, отправки сетевого пакета, выделения дополнительной памяти и т.д.). Для каждого из таких запросов у ОС есть соответствующая функция (или подпрограмма). Функции, которые решают задачи из одной области (например работа с сетью), собраны в отдельные системные библиотеки.

Иллюстрация 2-1. Доступ к ресурсам ОС через WinAPI

Способ, которым процесс может вызвать системную функцию, строго определён, хорошо задокументирован и остаётся неизменным для данной версии ОС. Такое взаимодействие можно сравнить с юридическим договором: если процесс выполняет предварительные условия для вызова функции, ОС гарантирует указанный в документации результат. Такой договор называется интерфейс прикладного программирования Windows (Windows API или WinAPI).

Программное обеспечение очень гибко и легко меняется согласно возникающим требованиям. Так каждое обновление Windows вносит изменения в некоторые детали реализации ОС (например, в какую-то системную библиотеку). Эти детали реализации связаны между собой: типичный случай – одна библиотека вызывает функции другой. Таким образом, даже небольшое изменение может оказать значительное влияние на систему в целом. То же самое справедливо и для игрового приложения. Единственное, что позволяет программному обеспечению работать в этом море постоянных изменений – это надёжные интерфейсы. Именно WinAPI гарантирует согласованное состояние системы и обеспечивает совместимость между новыми версиями ОС и приложения.

На иллюстрации 2-1 приведены два типа приложений. Win32-приложение взаимодействует с подмножеством системных библиотек через WinAPI интерфейс. Win32 – это историческое название, которое возникло в первой 32-битной версии Windows (Windows NT). Библиотеки, доступные через WinAPI, также известные как WinAPI библиотеки, содержат функции, оперирующие сложными абстракциями: элемент управления, файл и т.д.

Второй тип приложений называется нативные (native, поэтому иногда переводится как родной). Они взаимодействуют с более низкоуровневыми библиотеками и ядром Windows через Native API. Преимущество этих библиотек в том, что они становятся доступны на раннем этапе загрузки системы, когда многие функции ОС ещё не работоспособны. Функции этих библиотек оперируют простыми абстракциями, такими как страница памяти, процесс, поток и т.д. Примеры нативных приложений: утилита для разбивки жёсткого диска, антивирус до старта ОС, программа восстановления Windows.

Библиотеки WinAPI вызывают функции низкоуровневых библиотек. Такой подход позволяет составлять сложные абстракции из более простых. Низкоуровневые библиотеки в свою очередь вызывают функции ядра.

Драйвера предоставляют упрощённое представление устройств для системных библиотек. Это представление включает в себя набор функций, которые выполняют характерные для данного устройства действия. WinAPI и низкоуровневые библиотеки обращаются к драйверам через функции ядра.

Слой аппаратной абстракции (Hardware Abstraction Layer или HAL) – это модуль ядра ОС, который предоставляет универсальный доступ к различному аппаратному обеспечению. HAL нужен, чтобы облегчить портирование и сопровождение Windows на новых аппаратных платформах. Функции HAL используются ядром ОС и драйверами устройств.

Симуляция нажатий клавиш

Теперь мы рассмотрим технику симуляции нажатий клавиш. Это наиболее простой метод контроля ботом игрового приложения.

Нажатия клавиш в активном окне

Рассмотрим, какие возможности предлагает AutoIt для решения нашей задачи. В списке доступных функций есть функция Send. Мы воспользуемся ею в тестовом скрипте, который будет нажимать клавишу “a” в окне приложения Notepad (Блокнот).

Алгоритм работы нашего скрипта выглядит следующим образом:

  1. Найти окно Notepad среди всех открытых окон.
  2. Переключится на него.
  3. Симулировать нажатие клавиши “a”.

Для поиска окна приложения мы воспользуемся функцией WinGetHandle. Её первый параметр является обязательным и может быть как заголовком окна, так и его классом. Функция возвращает дескриптор (handle) окна. Дескриптор – это структура данных, которая представляет некоторый ресурс или объект ОС. Большинство функций WinAPI оперируют этими структурами при работе с объектами.

Указывать класс окна при вызове функции WinGetHandle предпочтительнее. Всегда есть вероятность, что окна некоторых работающий приложений будут иметь одинаковые заголовки (например пустые).

Для чтения класса окна Notepad необходимо выполнить следующие шаги:

  1. Запустить приложение Au3Info. Вы можете найти его в каталоге установки AutoIt. Путь к приложению по умолчанию: C:\Program Files (X86)\AutoIt3\Au3Info.exe.
  2. Перетащить иконку “Finder Tool” на окно Notepad и отпустить.

Вы увидите результат, приведённый на иллюстрации 2-2.

Иллюстрация 2-2. Приложение AutoIt3 Info

Класс окна Notepad отображается на панели “Basic Info Window”. Этот класс – “Notepad”.

Скрипт Send.au3, представленный в листинге 2-1, симулирует нажатие клавиши “a”.

Листинг 2-1. Скрипт Send.au3
1 $hWnd = WinGetHandle("[CLASS:Notepad]")
2 WinActivate($hWnd)
3 Send("a")

В первой строке мы получаем дескриптор окна Notepad с помощью функции WinGetHandle. Далее мы переключаем фокус ввода на это окно функцией WinActivate. Последнее действие – симуляция нажатия клавиши “a”.

Чтобы запустить этот скрипт, создайте в вашем редакторе исходного кода файл с именем Send.au3 и скопируйте в него приведённый выше код. Запустите скрипт двойным щелчком по иконке этого файла.

Функция Send

Функция Send представляет собой обёртку над WinAPI вызовом. Мы можем выяснить что это за вызов с помощью приложения API Monitor, которое перехватит все обращения к WinAPI скрипта Send.au3.

Для подключения API Monitor к работающему процессу выполните следующие шаги:

  1. Запустите 32-битную версию API Monitor.
  2. Переключитесь на панель “API Filter” щелчком мыши. Нажмите комбинацию клавиш Ctrl+F, чтобы открыть диалог поиска. Введите в поле “Find what:” текст “Keyboard and Mouse Input” и нажмите кнопку “Find Next”. Закройте диалог поиска и активируйте найденный флажок (check box) “Keyboard and Mouse Input”.
  3. Нажмите Ctrl+M для открытия диалога “Monitor New Process”. Выберите приложение AutoIt3.exe в поле “Process” и нажмите кнопку “OK”. По умолчанию путь к этому приложению должен быть следующий: C:\Program Files (x86)\AutoIt3\AutoIt3.exe.
  4. В открывшемся диалоге “Run Script” выберите скрипт Send.au3. Сразу после этого начнётся его выполнение.
  5. Переключитесь на панель “Summary” окна API Monitor. По нажатию Ctrl+F откройте диалог поиска и с его помощью найдите текст ‘a’ (с одинарными кавычками).

Иллюстрация 2-3 демонстрирует ожидаемый результат. Согласно перехваченным вызовам, VkKeyScanW – это единственная WinAPI-функция, получившая символ “a” в качестве параметра. Если мы обратимся к официальной документации WinAPI, выяснится что эта функция не выполняет нажатия клавиши. Она вместе с функцией MapVirtualKeyW только подготавливает параметры для вызова SendInput, который и симулирует нажатие.

Иллюстрация 2-3. Перехват вызовов WinAPI с помощью API Monitor

Мы узнали достаточно, чтобы симулировать нажатие клавиши “a” напрямую через WinAPI вызовы. Удалим третью строчку скрипта Send.au3 и заменим её новым блоком кода. При этом оставим первые два вызова WinGetHandle и WinActivate без изменений. Листинг 2-2 демонстрирует получившийся результат.

Листинг 2-2. Скрипт SendInput.au3
 1 $hWnd = WinGetHandle("[CLASS:Notepad]")
 2 WinActivate($hWnd)
 3 
 4 Const $KEYEVENTF_UNICODE = 4
 5 Const $INPUT_KEYBOARD = 1
 6 Const $iInputSize = 28
 7 
 8 Const $tagKEYBDINPUT = _
 9     'word wVk;' & _
10     'word wScan;' & _
11     'dword dwFlags;' & _
12     'dword time;' & _
13     'ulong_ptr dwExtraInfo'
14 
15 Const $tagINPUT = _
16     'dword type;' & _
17     $tagKEYBDINPUT & _
18     ';dword pad;'
19 
20 $tINPUTs = DllStructCreate($tagINPUT)
21 $pINPUTs = DllStructGetPtr($tINPUTs)
22 $iINPUTs = 1
23 $Key = AscW('a')
24 
25 DllStructSetData($tINPUTs, 1, $INPUT_KEYBOARD)
26 DllStructSetData($tINPUTs, 3, $Key)
27 DllStructSetData($tINPUTs, 4, $KEYEVENTF_UNICODE)
28 
29 DllCall('user32.dll', 'uint', 'SendInput', _
30         'uint', $iINPUTs, 'ptr', $pINPUTs, 'int', $iInputSize)

Здесь мы используем функцию AutoIt DllCall. С её помощью можно вызвать код динамической библиотеки (DLL), написанной на языке C или C++. В данном случае мы делаем WinAPI вызов SendInput. Его входные параметры должны иметь типы, согласно документации WinAPI. Некоторые из этих типов AutoIt не поддерживает на уровне синтаксиса. Поэтому нам нужны дополнительные шаги, чтобы подготовить эти параметры.

Таблица 2-1 демонстрирует входные параметры функции DllCall.

Таблица 2-1. Входные параметры функции DllCall
Параметр Описание
user32.dll Имя библиотеки, функцию которой требуется вызвать.
uint Тип возвращаемого значения функции.
SendInput Имя функции.
uint, $iINPUTs Пары тип-переменная. Переменные
ptr, $pINPUTs являются входными параметрами
int, $iInputSize функции.

Согласно WinAPI документации, декларация функции SendInput выглядит следующим образом:

1 UINT SendInput(UINT cInputs, LPINPUT pInputs, int cbSize);

Строчку вызова функции DllCall на AutoIt можно представить эквивалентом на языке C++:

1 SendInput(iINPUTs, pINPUTs, iInputSize);

Рассмотрим входные параметры, переданные нами в SendInput:

  1. iINPUTs – количество структур типа INPUT, которые передаются вторым параметром.
  2. pINPUTs – указатель на массив структур типа INPUT из одного элемента. Этот массив подготавливается в несколько этапов. Сначала мы объявляем строки KEYBDINPUT и INPUT с описанием полей соответствующих структур. При этом KEYBDINPUT является вторым полем INPUT. Такое отношение называется вложенные структуры (nested structure). На следующем шаге создаются структуры в формате языка C++ через вызов DllStructCreate. Результат сохраняется в переменной tINPUTs. С помощью функции DllStructGetPtr мы получаем указатель на эту структуру и помещаем его в pINPUTs. Запись значений полей C++ структуры происходит через вызов DllStructSetData. Обратите внимание, что вторым параметром в DllStructSetData передаётся номер поля, начиная с единицы. В случае вложенных структур их поля нумеруются последовательно. То есть элемент 1 соответствует полю dword type структуры INPUT, а элемент 3 – полю word wScan структуры KEYBDINPUT.
  3. iInputSize – размер одной структуры INPUT в байтах. В нашем случае это константное значение, рассчитанное по формуле:
1 dword + (word + word + dword + dword + ulong_ptr) + dword =
2 4 + (2 + 2 + 4 + 4 + 8) + 4 = 28

Слагаемые в скобках – это размеры полей вложенной структуры KEYBDINPUT.

Может быть непонятно, откуда взялись последние четыре байта в приведённой выше формуле. Возможно, вы обратили внимание, что объявленная в скрипте Send.au3 структура INPUT имеет последнее поле типа dword с именем padding (набивка). Оно не используется и служит для выравнивания данных. Рассмотрим это поле подробнее.

Определение структуры INPUT согласно документации WinAPI выглядит следующим образом:

1 typedef struct tagINPUT {
2   DWORD type;
3   union {
4     MOUSEINPUT    mi;
5     KEYBDINPUT    ki;
6     HARDWAREINPUT hi;
7   };
8 } INPUT, *PINPUT;

Вложенная структура KEYBDINPUT на самом деле помещена в блок union с другими структурами MOUSEINPUT и HARDWAREINPUT. Это означает, что под все три структуры будет выделена одна и та же область памяти. Но использоваться она будет только одной из них. Так как область одна, её размер должен соответствовать самой большой структуре, которой является MOUSEINPUT. Она больше KEYBDINPUT на одно поле типа dword, т.е. на четыре байта. Именно из-за него мы добавили padding в наше определение KEYBDINPUT для выравнивания.

Скрипт SendInput.au3 демонстрирует преимущества высокоуровневых языков, таких как AutoIt. Они скрывают от пользователя множество несущественных деталей. Это позволяет оперировать простыми абстракциями и функциями. Кроме того, приложения, написанные на таких языках, короче и яснее.

Нажатия клавиш в неактивном окне

Функция AutoIt Send симулирует нажатия клавиш в активном окне. Другими словами, вы не можете свернуть это окно или переключится на другое, что в некоторых случаях неудобно. Функция ControlSend позволяет обойти такое ограничение. Мы можем переписать скрипт Send.au3 с использованием ControlSend, как демонстрирует листинг 2-3.

Листинг 2-3. Скрипт ControlSend.au3
1 $hWnd = WinGetHandle("[CLASS:Notepad]")
2 ControlSend($hWnd, "", "Edit1", "a")

Третьим параметром в функцию ControlSend передаётся элемент интерфейса (control), который получает симулируемое нажатие клавиши. Указать на него можно несколькими способами. В нашем случае, мы передаём класс элемента “Edit1”. Его можно узнать с помощью утилиты Au3Info точно так же, как и класс окна.

Применив API Monitor, мы узнаем, что ControlSend внутри себя вызывает WinAPI-функцию SetKeyboardState. В качестве упражнения предлагаю вам переписать скрипт ControlSend.au3 так, чтобы он вызывал SetKeyboardState напрямую.

Скрипт ControlSend.au3 работает корректно во всех случаях, кроме симуляции нажатия клавиши в развёрнутом на весь экран окне приложения DirectX. Проблема заключается в том, что такое окно не имеет элементов интерфейса. Чтобы её решить, достаточно просто не указывать третий параметр controlID функции ControlSend. Листинг 2-4 демонстрирует исправленный скрипт.

Листинг 2-4. Скрипт ControlSendDirectX.au3
1 $hWnd = WinGetHandle("Warcraft III")
2 ControlSend($hWnd, "", "", "a")

Этот скрипт ищет окно игры Warcraft 3 по его заголовку и симулирует в нём нажатие клавиши “a”. Узнать заголовок окна DirectX-приложения иногда бывает сложно, потому что не всегда можно выйти из полноэкранного режима. В этом случае утилиты вроде Au3Info вам не помогут. Вместо них с этой задачей справится API Monitor. Если в окне приложения вы наведёте курсор мыши на интересующий вас процесс на панели “Running Process”, вы увидите заголовок окна этого приложения, как показано на иллюстрации 2-4.

Иллюстрация 2-4. Чтение заголовка окна приложения в API Monitor

Если вы не можете найти нужный процесс на панели “Running Process”, попробуйте запустить API Monitor с правами администратора. Если это не помогло и у вас установлена 64-битная версия Windows, надо запустить обе версии API Monitor – 32- и 64-битную. В одной из них процесс должен появиться.

Заголовок некоторых окон в полноэкранном режиме пустой. Из-за этого вы не сможете передать его в функцию WinGetHandle и получить дескриптор. Тогда альтернативным решением будет передавать класс окна. К сожалению, с помощью API Monitor эту информацию не удастся прочитать.

Чтобы получить класс окна, открытого в полноэкранном режиме, вы можете воспользоваться скриптом AutoIt, приведённом в листинге 2-5.

Листинг 2-5. Скрипт GetWindowTitle.au3
1 #include <WinAPI.au3>
2 Sleep(5 * 1000)
3 $handle = WinGetHandle('[Active]')
4 MsgBox(0, "", "Title   : " & WinGetTitle($handle) & @CRLF _
5        & "Class : " & _WinAPI_GetClassName($handle))

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

Рассмотрим подробнее скрипт GetWindowTitle.au3. В первой строке стоит ключевое слово (keyword) include. С его помощью AutoIt включает содержание указанного скрипта WinAPI.au3 в текущий. В WinAPI.au3 реализована нужная нам функция _WinAPI_GetClassName. Она возвращает класс окна по его дескриптору. Далее с помощью функции Sleep скрипт ждёт пять секунд. После этого дескриптор активного в данный момент окна сохраняется в переменную handle. Функция MsgBox создаёт диалоговое окно, в котором выводится результат. Заголовок окна возвращает функция WinGetTitle.

Симуляция действий мыши

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

Действия мыши в активном окне

Мы воспользуемся графическим редактором Microsoft Paint для тестирования скриптов, симулирующих действия мыши. Самое простое действие – это однократный щелчок в указанной точке экрана. Листинг 2-6 демонстрирует соответствующий скрипт.

Листинг 2-6. Скрипт MouseClick.au3
1 $hWnd = WinGetHandle("[CLASS:MSPaintApp]")
2 WinActivate($hWnd)
3 MouseClick("left", 250, 300)

Для тестирования этого скрипта выполните следующее:

  1. Запустите приложение Paint.
  2. Переключитесь на инструмент “Кисти” (Brushes).
  3. Запустите скрипт MouseClick.au3.

Скрипт нарисует чёрную точку по координатам x = 250, y = 300. Их корректность вы можете проверить с помощью утилиты ColorPix.

Для симуляции щелчка мыши мы использовали функцию AutoIt MouseClick. Она принимает пять входных параметров, первые три из которых являются обязательными:

  1. Кнопка мыши для щелчка. Основные варианты: левая (left), правая (right), средняя (middle).
  2. Координата X позиции курсора.
  3. Координата Y позиции курсора.
  4. Число последовательных щелчков.
  5. Скорость мыши для перемещения курсора в указанные координаты.

Внутри себя MouseClick вызывает WinAPI-функцию mouse_event.

Координаты позиции курсора можно задавать в одном из трёх режимов, представленных в таблице 2-2.

Таблица 2-2. Режимы координат, поддерживаемые WinAPI
Режим Описание
0 Координаты относительно левой верхней точки активного окна.
1 Абсолютные координаты экрана. Это режим по умолчанию.
2 Координаты относительно левой верхней точки клиентской части окна (без заголовка, меню и границ).
Иллюстрация 2-5. Режимы координат, поддерживаемые WinAPI

Рассмотрим иллюстрацию 2-5. Каждый номер соответствует режиму координат из таблицы 2-1. Например, точка с номером “0” демонстрирует режим относительно активного окна. Её координаты X0 и Y0.

Функция AutoIt Opt, вызванная с первым параметром MouseCoordMode, позволяет выбрать режим координат для текущего скрипта. Листинг 2-7 демонстрирует выбор координат относительно клиентской части окна в скрипте MouseClick.au3.

Листинг 2-7. Скрипт MouseClick.au3 с выбором режима координат
1 Opt("MouseCoordMode", 2)
2 $hWnd = WinGetHandle("[CLASS:MSPaintApp]")
3 WinActivate($hWnd)
4 MouseClick("left", 250, 300)

Запустив этот скрипт, вы заметите, что координаты чёрной точки, нарисованной в окне Pain, изменились. Выбранный нами режим обеспечивает более точное позиционирование курсора. При разработке кликеров предпочтительнее использовать именно его. Он одинаково хорошо работает для окон в обычном и полноэкранном режимах. Единственный его недостаток заключается в сложности отладки скриптов. Утилиты вроде ColorPix отображают только абсолютные координаты пикселей.

Одно из распространённых действий мышью в компьютерных играх – перетаскивание (drag-and-drop). Для его симуляции AutoIt предоставляет функцию MouseClickDrag. Листинг 2-8 демонстрирует её использование.

Листинг 2-8. Скрипт MouseClickDrag.au3
1 $hWnd = WinGetHandle("[CLASS:MSPaintApp]")
2 WinActivate($hWnd)
3 MouseClickDrag("left", 250, 300, 400, 500)

После запуска скрипт MouseClickDrag.au3 рисует линию в окне Paint. Координаты её начала: x = 250, y = 300. Она заканчивается в точке x = 400, y = 500. Функция AutoIt MouseClickDrag делает внутри себя уже знакомый нам WinAPI вызов mouse_event. Обе AutoIt функции MouseClick и MouseClickDrag симулируют действия мыши только в активном окне.

Действия мыши в неактивном окне

AutoIt предоставляет функцию ControlClick, которая симулирует щелчок мыши в неактивном окне. Пример её использования приведён в листинге 2-9.

Листинг 2-9. Скрипт ControlClick.au3
1 $hWnd = WinGetHandle("[CLASS:MSPaintApp]")
2 ControlClick($hWnd, "", "Afx:00000000FFC20000:81", "left", 1, 250, 300)

Скрипт ControlClick.au3 симулирует щелчок мыши в неактивном или свёрнутом окне Paint. По принципу работы функция ControlClick похожа на ControlSend. Вы должны указать элемент интерфейса по которому будет выполнен щелчок. В нашем случае – это рабочая область окна Paint, в которой пользователь применяет инструменты рисования (например кисти). Согласно информации от утилиты Au3Info, элемент рабочей области имеет класс “Afx:00000000FFC20000:81”.

Ради эксперимента мы можем передать одни и те же координаты курсора в функции MouseClick и ControlClick. В результате щелчки мыши произойдут в разных точках экрана. Причина в том, что входные параметры функции ControlClick – это координаты относительно левого верхнего угла указанного элемента интерфейса. В случае скрипта ControlClick.au3, щелчок произойдёт в точке x = 250, y = 300 относительно левого верхнего угла рабочей области. Тогда как режим координат для функции MouseClick определяется параметром MouseCoordMode.

ControlClick дважды вызывает WinAPI-функцию PostMessageW внутри себя. Иллюстрация 2-6 демонстрирует её вызовы, перехваченные с помощью API Monitor.

Иллюстрация 2-6. Внутренние вызовы функции ControlClick

При вызове функции PostMessageW первый раз, в неё передаётся параметр WM_LBUTTONDOWN. В результате симулируется нажатие кнопки мыши и её удержание. Во втором вызове передаётся параметр WM_LBUTTONUP, что соответствует отпусканию кнопки мыши.

Функция ControlClick работает ненадёжно со свёрнутыми окнами DirectX. В некоторых случаях щелчок мыши полностью игнорируется. Иногда он отрабатывает не в момент вызова ControlClick, а только после того, как свёрнутое окно будет восстановлено.

Выводы

Мы рассмотрели функции AutoIt, которые позволяют симулировать наиболее распространённые действия клавиатуры и мыши в окне игрового приложения. Эти функции делятся на два типа. Первый тип симулирует действия устройства только в активном окне. Второй тип работает как с активными, так и с неактивными или свёрнутыми окнами. Главный недостаток функций второго типа – недостаточная надёжность, поскольку некоторые приложения игнорируют симулируемые ими действия. Поэтому для реализации кликеров рекомендуется использовать функции первого типа.

Перехват устройств вывода

В этом разделе мы познакомимся с методами перехвата данных с устройств вывода. Сначала мы изучим, какие возможности Windows предоставляет приложениям для работы с этими устройствами. Затем рассмотрим способы перехвата выводимых на них изображений.

Интерфейс графических устройств Windows

Интерфейс графических устройств (Graphics Device Interface или GDI) – один из основных компонентов Windows, который отвечает за представление графических объектов и передачу их на устройства вывода. Обычно все элементы интерфейса окна приложения создаются с помощью графических объектов, таких как контекст устройства (device context или DC), битовое изображение (bitmap), кисти, цвета, шрифты.

Ключевая концепция GDI – это контекст устройства. Он представляет собой абстракцию, благодаря которой разработчики могут единообразно работать с графическими объектами независимо от устройства вывода (монитором, принтером, плоттером или графопостроителем и т.д). Сначала все операции по подготовке изображения выполняются над контекстом устройства в памяти. Затем готовый результат отправляется на устройство вывода.

На иллюстрации 2-7 приведены два контекста устройств, которые содержат изображения окон двух приложений A и B. Также на ней представлен DC, соответствующий итоговому изображению всего рабочего стола. ОС может собрать это изображение из всех видимых окон и визуальных элементов рабочего стола (например панели задач). Когда контекст устройства подготовлен в памяти, ОС выводит его содержимое на экран.

Иллюстрация 2-7. Отображение графических объектов на устройства вывода

Предположим, вам нужно напечатать документ, открытый в текстовом редакторе (окно B). В этом случае ОС просто отправляет DC окна этого приложения на принтер. Контексты устройств, связанные с другими открытыми в данный момент окнами игнорируются.

Контекст устройства представляет собой структуру в памяти. Разработчики могу работать с ней только через WinAPI-функции. Каждый DC содержит аппаратно-зависимое битовое изображение (Device Depended Bitmap или DDB). Битовое изображение – это представление поверхности для рисования в памяти. Все операции над графическими объектами в контексте устройства отражаются на соответствующем битовом изображении. Следовательно, оно хранит результат всех этих операций.

Битовое изображение состоит из двух основных частей:

  1. Массив битов, описывающих наименьшие логические элементы изображения, которые называются пикселями.
  2. Метаинформация.

У каждого пикселя есть два параметра: координаты и цвет. Их соответствие задаётся двумерным массивом. Номера элементов массива (индексы) равны координатам пикселя по осям X и Y. Числовое значение элемента массива соответствует коду цвета в палитре, которая связана с данным битовым изображением. Для анализа изображения все элементы двумерного массива должны обрабатываться последовательно.

Когда изображение подготовлено в контексте устройства, оно передаётся на настоящее устройство вывода. Как правило, функции системных библиотек выполняют необходимые преобразования изображения. Например, библиотека vga.dll подготавливает его для вывода на экран. Благодаря им драйвер устройства получает картинку в удобном для него формате.

Функции AutoIt для анализа изображений

AutoIt предоставляет функции для анализа текущего изображения на экране. Все они оперируют объектами GDI. Сейчас мы подробно рассмотрим эти функции.

Анализ отдельного пикселя

Самая простая операция при анализе изображения – это чтение цвета одного пикселя. Для этого необходимо знать его координаты. AutoIt поддерживает несколько режимов координат, которые представлены в таблице 2-3. Они идентичны режимам координат позиционирования курсора мыши из таблицы 2-2.

Таблица 2-3. Режимы координат функций анализа изображений AutoIt
Режим Описание
0 Координаты относительно левой верхней точки активного окна.
1 Абсолютные координаты экрана. Это режим по умолчанию.
2 Координаты относительно левой верхней точки клиентской части окна (без заголовка, меню и границ).

Выбрать нужный режим координат можно с помощью функции Opt, вызванной с первым параметром PixelCoordMode. Например, следующий вызов переключает скрипт во второй режим:

1 Opt("PixelCoordMode", 2)

Функция AutoIt PixelGetColor читает цвет пикселя. Входными параметрами она принимает координаты X и Y пикселя. Функция возвращает код цвета в десятичной системе счисления. Листинг 2-10 демонстрирует её использование.

Листинг 2-10. Скрипт PixelGetColor.au3
1 $color = PixelGetColor(200, 200)
2 MsgBox(0, "", "Цвет пикселя: " & Hex($color, 6))

Скрипт PixelGetColor.au3 читает цвет пикселя с координатами x = 200, y = 200. После этого функция MsgBox выводит диалоговое окно с результатом. После запуска скрипта, вы увидите сообщение вроде: “Цвет пикселя 0355BB”.

Цвет кодируется числом 0355BB в шестнадцатеричной системе счисления. Такое представление широко распространено и называется цветовой моделью RGB. В ней любой цвет представляется координатами в трёхмерном цветовом пространстве. Трём его осям соответствуют цвета: X – красный (Red), Y – зелёный (Green) и Z – синий (Blue). Таким образом, цвет 0355BB из нашего примера соответствует точке с координатами: X = 03, Y = 55, Z = BB. Большинство графических редакторов и утилит используют этот способ кодирования.

Если вы переместите окно Notepad так, чтобы перекрыть им точку с координатой x = 200, y = 200 рабочего стола, результат возвращаемый скриптом PixelGetColor.au3 изменится. Это означает, что он анализирует не конкретное окно, а изображение всего рабочего стола.

Иллюстрация 2-8 демонстрирует перехваченные WinAPI вызовы скрипта PixelGetColor.au3.

Иллюстрация 2-8. WinAPI вызовы скрипта PixelGetColor.au3

Функция PixelGetColor делает внутри себя три WinAPI вызова в следующей последовательности: GetDC, GetPixel, ReleaseDC. GetDC получает входным параметром значение “NULL”. Таким образом мы выбираем контекст устройства всего экрана для дальнейших операций. Если мы передадим в функцию GetDC дескриптор окна, мы получим DC его клиентской области. Благодаря этому наш скрипт сможет анализировать неактивные или перекрытые окна.

Дескриптор окна можно передать третьим параметром в AutoIt функцию PixelGetColor. Листинг 2-11 демонстрирует это решение.

Листинг 2-11. Скрипт PixelGetColorWindow.au3
1 $hWnd = WinGetHandle("[CLASS:MSPaintApp]")
2 $color = PixelGetColor(200, 200, $hWnd)
3 MsgBox(0, "", "Цвет пикселя: " & Hex($color, 6))

Скрипт PixelGetColorWindow.au3 должен вернуть цвет пикселя в окне Paint, даже если оно неактивно. Мы ожидаем прочитать белый цвет с кодом “FFFFFF”, потому что область для рисования по умолчанию пуста.

Скрипт работает корректно, если окно Paint активно. Теперь попробуем перекрыть его окном другого приложения (например интерпретатором командной строки CMD). Скрипт прочитает чёрный цвет вместо белого.

Сравним WinAPI вызовы скриптов PixelGetColorWindow.au3 и PixelGetColor.au3, перехваченные с помощью приложения API Monitor. В обоих случаях функция GetDC получает “NULL” входным параметром. Такое поведение похоже на ошибку в реализации функции PixelGetColor версии 3.3.14.1 AutoIt. Возможно, она будет исправлена в следующий версиях. Попробуем эту ошибку обойти.

Проблема функции PixelGetColor в некорректном параметре при вызове GetDC. Мы знаем, к каким WinAPI-функциям обращается PixelGetColor. Поэтому можем вызвать их напрямую из нашего скрипта, но с корректными параметрами. Результат приведён в листинге 2-12.

Листинг 2-12. Скрипт GetPixel.au3
1 #include <WinAPIGdi.au3>
2 
3 $hWnd = WinGetHandle("[CLASS:MSPaintApp]")
4 $hDC = _WinAPI_GetDC($hWnd)
5 $color = _WinAPI_GetPixel($hDC, 200, 200)
6 MsgBox(0, "", "Цвет пикселя: " & Hex($color, 6))

Скрипт GetPixel.au3 начинается с ключевого слова include. С его помощью мы включаем файл WinAPIGdi.au3, который содержит обёртки _WinAPI_GetDC и _WinAPI_GetPixel для соответствующих WinAPI-функций. Этот скрипт читает цвет пикселя окна Paint, независимо от того перекрыто оно или нет.

У рассмотренного нами решения есть одна проблема. Если вы свернёте окно Paint и запустите скрипт, он вернёт белый цвет. Этот результат выглядит корректным. Теперь попробуем изменить цвет рабочей области Paint, залив её для примера красным. Свернём окно снова, и запустим скрипт. Он опять прочитает белый цвет, хотя мы ожидаем красный. Рассмотрим, почему это происходит.

У каждого окна есть клиентская область. В этой области находятся все элементы интерфейса кроме заголовка окна, его границ и главного меню. Наша проблема с чтением цвета пикселя возникла из-за того, что размер клиентской области свёрнутого окна равен нулю. Следовательно, контекст устройства, связанный с окном, имеет пустое битовое изображение. При попытке чтения несуществующего пикселя, функция WinAPI GetPixel возвращает белый цвет.

Мы можем прочитать размер клиентской области окна с помощью скрипта, представленного в листинге 2-13.

Листинг 2-13. Скрипт GetClientRect.au3
1 #include <WinAPI.au3>
2 
3 $hWnd = WinGetHandle("[CLASS:MSPaintApp]")
4 $tRECT = _WinAPI_GetClientRect($hWnd)
5 MsgBox(0, "Прямоугольник", _
6        "Левый край: " & DllStructGetData($tRECT, "Left") & @CRLF & _
7        "Правый край: " & DllStructGetData($tRECT, "Right") & @CRLF & _
8        "Верхний край: " & DllStructGetData($tRECT, "Top") & @CRLF & _
9        "Нижний край: " & DllStructGetData($tRECT, "Bottom"))

Скрипт GetClientRect.au3 выводит X- и Y-координаты верхней левой и правой нижней точки клиентской области окна Paint. Если оно свёрнуто, все координаты равны нулю. В противном случае мы получим ненулевые числа.

Ограничение при работе со свёрнутым окном крайне неудобно, если вы планируете запускать бота и переключаться на другие приложения. У этой проблемы есть решение. Windows позволяет восстановить свёрнутое окно в прозрачном режиме. После этого можно скопировать битовое изображение его клиентской области в DC, связанный с оперативной памятью, и свернуть окно снова. Для копирования можно воспользоваться WinAPI-функцией PrintWindow. После можно анализировать копию с помощью известной нам AutoIt-обёртки _WinAPI_GetPixel.

Следующая статья подробно рассматривает работу со свёрнутыми окнами.

Анализ изменений картинки

Мы рассмотрели методы чтения цвета отдельно взятого пикселя. Однако, в большинстве случаев точные координаты нужного пикселя в окне игрового приложения неизвестны. Причина этого в том, что мы имеем не статическую картинку, а изображения движущихся игровых объектов. Следовательно, мы должны найти способ анализа изменений на экране. AutoIt предоставляет несколько функций, подходящих для решения этой задачи.

Предположим, что мы ищем конкретный игровой объект на экране. Мы знаем его цвет, но не координаты. Эта задача является обратной той, которую решает функция AutoIt PixelGetColor. Для поиска координат игрового объекта по его цвету можно воспользоваться функцией PixelSearch. Листинг 2-14 демонстрирует пример.

Листинг 2-14. Скрипт PixelSearch.au3
1 $coord = PixelSearch(0, 207, 1000, 600, 0x000000)
2 If @error = 0 then
3     MsgBox(0, "", "Координата чёрной точки: x = " & $coord[0] & _
4            " y = " & $coord[1])
5 else
6     MsgBox(0, "", "Чёрная точка не найдена")
7 endif

Скрипт PixelSearch.au3 ищет пиксель чёрного цвета с кодом 000000 в прямоугольной области экрана с координатами верхнего левого угла x = 0, y = 207 и правого нижнего – x = 1000, y = 600. Если в процессе поиска происходит ошибка, мы обрабатываем её с помощью макроса @error. В этом случае выводится сообщение: “Чёрная точка не найдена”.

Макрос @error можно рассматривать как глобальную переменную. Если в процессе работы AutoIt функции происходит ошибка, её код будет записан в @error. При обработке ошибки важно проверять макрос сразу после вызова функции, поскольку последующие вызовы могут переписать его значение.

Воспользуемся приложением Paint, чтобы протестировать скрипт PixelSearch.au3. Сначала поставим чёрную точку с помощью карандаша или кисти в области для рисования. Затем запустим скрипт. Он выведет координаты точки в диалоговом окне. Если этого не произошло, убедитесь, что Paint не перекрывают другие окна.

Проверим, какие вызовы WinAPI делает функция PixelSearch. Для этого запустим скрипт PixelSearch.au3 из приложения API Monitor. Подождём, пока он отработает. После этого будем искать текст “0, 207” (координаты точки) в окне “Summary”. Вы должны найти вызов WinAPI StretchBlt, как показано на иллюстрации 2-9.

Иллюстрация 2-9. WinAPI-вызовы функции PixelSearch

Функция StretchBlt копирует битовое изображение из DC экрана в контекст устройства памяти, который также известен как совместимый контекст устройства (compatible device context). Чтобы проверить это предположение, сравним входные параметры вызовов GetDC, CreateCompatibleBitmap, CreateCompatibleDC, SelectObject и StretchBlt в окне API Monitor.

Функция GetDC возвращает дескриптор DC экрана, который в нашем случае равен 0x5a011146. Что означает это шестнадцатеричное число? Воспользуемся документацией WinAPI, чтобы уточнить определение типа HDC, соответствующее дескриптору DC:

1 typedef void *PVOID;
2 typedef PVOID HANDLE;
3 typedef HANDLE HDC;

HDC представляет собой указатель на область памяти. Следовательно, 0x5a011146 – это адрес памяти, где хранится дескриптор.

Вызов CreateCompatibleBitmap идёт после GetDC. Он создаёт битовое изображение для работы над ним в памяти. Первым входным параметром CreateCompatibleBitmap принимает дескриптор DC экрана. Далее с помощью CreateCompatibleDC создаётся совместимый контекст устройства. Вызовом SelectObject в него загружается битовое изображение. После этого вызов StretchBlt может выполнить копирование изображения из контекста экрана (дескриптор 0x5a011146) в совместимый DC в памяти.

На следующем шаге AutoIt-функции PixelSearch происходит WinAPI-вызов GetDIBits. Он конвертирует аппаратно-зависимое битовое изображение (DDB) в аппаратно-независимое (DIB). Зачем это нужно? DIB формат более удобен, поскольку позволяет работать с изображениями как с обычным массивом.

Заключительный шаг функции PixelSearch – проход по всем пикселям DIB и сравнение цвета каждого из них с заданным. Для этой операции вызовы WinAPI не нужны.

Пример C++ реализации захвата изображений с экрана доступен в WinAPI документации. Эта реализация демонстрирует копирование битового изображения в совместимый DC и преобразование DDB в DIB.

У функции PixelSearch есть необязательный пятый параметр, через который можно передать дескриптор окна. В этом случае поиск пикселя происходит именно в нём. Если параметр не указан, функция ищет на всём экране.

Листинг 2-15 демонстрирует поиск пикселя в заданном окне.

Листинг 2-15. Скрипт PixelSearchWindow.au3
1 $hWnd = WinGetHandle("[CLASS:MSPaintApp]")
2 $coord = PixelSearch(0, 207, 1000, 600, 0x000000, 0, 1, $hWnd)
3 If @error = 0 then
4     MsgBox(0, "", "Координата чёрной точки: x = " & $coord[0] & _
5            " y = " & $coord[1])
6 else
7     MsgBox(0, "", "Чёрная точка не найдена")
8 endif

Согласно документации AutoIt, скрипт PixelSearchWindow.au3 должен искать пиксель в перекрытом окне Paint, но этого не происходит. Похоже, что мы снова столкнулись с ошибкой, которая проявлялась ранее в функции PixelGetColor. API Monitor подтвердит, что в WinAPI вызов GetDC снова передаётся “NULL” вместо дескриптора окна. По этой причине PixelSearch всегда обрабатывает DC экрана, независимо от своего пятого параметра. Вы можете обойти эту ошибку, если будете работать с WinAPI напрямую. Пример аналогичного решения приведён в листинге 2-12. В этом случае вам необходимо полностью повторить алгоритм функции PixelSearch.

PixelChecksum – это ещё одна функция AutoIt для анализа движущихся изображений. Рассмотренные нами ранее функции PixelGetColor и PixelSearch позволяют получить информацию о единственном пикселе. PixelChecksum работает иначе. Она обнаруживает изменение изображения в заданной области экрана. Это может быть полезно, когда бот должен реагировать на игровые события.

Функция PixelChecksum рассчитывает контрольную сумму (checksum) для пикселей в указанной области. Эта сумма представляет собой число, полученное в результате применения определённого алгоритма к набору данных. Простейшим примером такого алгоритма может быть суммирование кодов цветов пикселей. Если цвет хотя бы одного пикселя поменяется, результирующая контрольная сумма также изменится.

Листинг 2-16 демонстрирует применение функции PixelChecksum.

Листинг 2-16. Скрипт PixelChecksum.au3
1 $checkSum = PixelChecksum(0, 0, 50, 50)
2 
3 while $checkSum = PixelChecksum(0, 0, 50, 50)
4     Sleep(100)
5 wend
6 
7 MsgBox(0, "", "Изображение в области экрана изменилось")

Скрипт PixelChecksum.au3 выводит диалоговое окно, если меняется изображение в области экрана между точками с координатами x = 0, y = 0 и x = 50, y = 50. В скрипте многократно вызывается функция PixelChecksum. Первый раз она вычисляет начальное значение контрольной суммы. После этого функция вызывается каждые 100 миллисекунд в цикле while. Временная задержка выполняется с помощью вызова Sleep. Цикл продолжается до тех пор, пока контрольная сумма не изменится. Как только это происходит, цикл прерывается и выводится диалоговое окно.

Рассмотрим внутренние вызовы функции PixelChecksum. API Monitor покажет нам ту же самую последовательность WinAPI вызовов, что и для функции PixelSearch. Это означает, что AutoIt следует одному и тому же алгоритму для получения DIB из изображения на экране. Однако, последний шаг этих двух функций отличается. PixelChecksum вычисляет контрольную сумму по указанному алгоритму. Вы можете выбрать один из двух доступных алгоритмов: ADLER или CRC32. Рассмотрим их различия.

Любой алгоритм расчёта контрольных сумм имеет коллизии. Коллизия – это два разных набора входных данных, для которых функция возвращает одинаковый результат. Алгоритмы предлагаемые AutoIt отличаются скоростью и надёжностью. CRC32 работает медленнее чем ADLER, но имеет меньше коллизий. Следовательно, надёжность CRC32 выше и использующий его бот будет реже ошибаться.

Все рассмотренные AutoIt функции для анализа пикселей работают в полноэкранных окнах DirectX приложений. Вы можете использовать их для разработки своих ботов без каких либо ограничений.

Библиотеки для анализа изображений

Мы рассмотрели средства AutoIt для анализа изображений на экране. Кроме них есть более мощные функции, предоставляемые сторонними библиотеками. Рассмотрим их подробнее.

Библиотека FastFind

Библиотека FastFind предоставляет мощные функции для анализа изображений, которые хорошо подходят для поиска игровых объектов на экране. Эти функции доступны как из AutoIt скриптов, так и из C++ приложений.

Для вызова функции библиотеки из AutoIt скрипта выполните следующие шаги:

  1. Создайте отдельную папку для вашего скрипта. Для примера назовём её FFDemo.
  2. Скопируйте файл FastFind.au3 из архива FastFind библиотеки в папку FFDemo.
  3. Скопируйте также один из файлов FastFind.dll или FastFind64.dll. Если вы работаете на 64-битной версии Windows, вам нужен файл FastFind64.dll, иначе – FastFind.dll.
  4. Включите файл FastFind.au3 в ваш скрипт с помощью include:
1 #include "FastFind.au3"

Теперь вы можете вызывать функции FastFind в своём скрипте.

Для работы с функциями библиотеки из C++ приложения сделайте следующее:

  1. Скачайте и установите компилятор C++. Это может быть IDE Visual Studio Community с сайта Microsoft, в которую уже встроен компилятор. Альтернативным решением является набор инструментов MinGW.
  2. Если вы используете MinGW, создайте файл с исходным кодом (например test.cpp). В случае Visual Studio, создайте проект “Win32 Console Application”.
  3. Скопируйте код из листинга 2-17 в свой CPP-файл.
  4. Скопируйте из архива библиотеки файл FastFind.dll в папку вашего проекта. FastFind64.dll следует копировать только в том случае, если вы собираетесь компилировать 64-битные исполняемые файлы.
  5. Если вы используете MinGW, создайте файл с именем Makefile и следующим содержанием:
1 all:
2     g++ test.cpp -o test.exe

6. В случае использования MinGW, скомпилируйте приложение с помощью команды make, запущенной в командной строке CMD. Для Visual Studio достаточно нажать горячую клавишу F7.

Листинг 2-17. Файл test.cpp
 1 #include <iostream>
 2 
 3 #define WIN32_LEAN_AND_MEAN
 4 #include <windows.h>
 5 
 6 using namespace std;
 7 
 8 typedef LPCTSTR(CALLBACK* LPFNDLLFUNC1)(void);
 9 
10 HINSTANCE hDLL;               // Дескриптор DLL-библиотеки
11 LPFNDLLFUNC1 lpfnDllFunc1;    // Указатель на функцию
12 LPCTSTR uReturnVal;
13 
14 int main()
15 {
16     hDLL = LoadLibraryA("FastFind");
17     if (hDLL != NULL)
18     {
19         lpfnDllFunc1 = (LPFNDLLFUNC1)GetProcAddress(hDLL, "FFVersion");
20         if (!lpfnDllFunc1)
21         {
22             // Обработка ошибки
23             FreeLibrary(hDLL);
24             cout << "error" << endl;
25             return 1;
26         }
27         else
28         {
29             // Вызов функции из библиотеки
30             uReturnVal = lpfnDllFunc1();
31             cout << "version = " << uReturnVal << endl;
32         }
33     }
34     return 0;
35 }

После компиляции вы получите исполняемый EXE-файл. Запустив его, вы увидите вывод версии библиотеки FastFind в консоль:

1 version = 2.2

В нашем примере мы использовали явную компоновку библиотеки (explicit library linking) для доступа к её функциям. Также возможно альтернативное решение – неявная компоновка библиотеки (implicit library linking). Вы можете применять любой подход для работы с FastFind. Но во втором случае вам придётся использовать тот же компилятор C++ (желательно и той же версии), что и разработчики FastFind.

Какие задачи мы сможем решить с помощью библиотеки? Прежде всего, у нас появился более надёжный метод поиска игрового объекта. Функция FFBestSpot ищет область экрана, которая содержит максимальное число пикселей заданного цвета. Рассмотрим пример её использования.

На иллюстрации 2-10 приведён снимок экрана (или скриншот) популярной MMORPG-игры Lineage 2. На нём вы видите модели двух персонажей. В правой части расположен персонаж игрока с именем “Zagstruk”. Слева от него находится монстр “Wretched Archer”. Чтобы определить его координаты, применим функцию FFBestSpot.

Сначала нам нужно выбрать подходящий цвет для поиска. Лучше всего для этой цели подойдёт цвет текста над персонажами. При их перемещении, эти надписи не меняют свою геометрию. Также они не зависят от световых эффектов, приближения и угла поворота камеры. В этом случае поиск функцией FFBestSpot будет достаточно надёжным. Монстр, в отличие от игрока, имеет дополнительный текст зелёного цвета. Именно его мы и будем искать.

Иллюстрация 2-10. Скриншот известной MMORPG-игры Lineage 2

В некоторых случаях для поиска у нас нет статичных элементов интерфейса, таких как надписи над игровыми объектами. Тогда приходится искать модели персонажей. Функция FFBestSpot может оказаться недостаточно надёжной для этой задачи и часто давать ошибочный результат. Причина заключается в том, что тени и световые эффекты могут менять цвета моделей.

Листинг 2-18 демонстрирует поиск текста зелёного цвета с помощью функции FFBestSpot.

Листинг 2-18. Скрипт FFBestSpot.au3
 1 #include "FastFind.au3"
 2 
 3 Sleep(5 * 1000)
 4 
 5 const $sizeSearch = 80
 6 const $minNbPixel = 50
 7 const $optNbPixel = 200
 8 const $posX = 700
 9 const $posY = 380
10 
11 $coords = FFBestSpot($sizeSearch, $minNbPixel, $optNbPixel, _
12                      $posX, $posY, 0xA9E89C, 10)
13 
14 if not @error then
15     MsgBox(0, "Coords", $coords[0] & ", " & $coords[1])
16 else
17     MsgBox(0, "Coords", "Текст не найден")
18 endif

Если вы запустите скрипт FFBestSpot.au3 и переключитесь на окно с иллюстрацией 2-10, появится диалоговое окно с координатами текста. После старта, скрипт ждёт пять секунд, в течение которых вы должны переключиться на скриншот игры. Функция FFBestSpot отработает после этой задержки. Таблица 2-4 описывает её входные параметры.

Таблица 2-4. Входные параметры функции FFBestSpot
Параметр Описание
sizeSearch Ширина и высота квадратной области экрана для поиска.
minNbPixel Минимальное число пикселей, которое должно быть в искомой области.
optNbPixel Оптимальное число пикселей, которое должно быть в искомой области.
posX Примерная координата X искомой области.
posY Примерная координата Y искомой области.
0xA9E89C Искомый цвет в шестнадцатеричной системе счисления.
10 Допустимое отклонение цвета от каждого из основных цветов (красный, зелёный, синий). Значение должно быть в диапазоне от 0 до 255.

Функция FFBestSpot возвращает массив из трёх элементов, если ей удаётся найти указанную область. В противном случае возвращается ноль и выставляется макрос @error с кодом ошибки. Первые два элемента массива с результатом – это X и Y координаты найденной области. Третий элемент равен числу пикселей указанного цвета в ней. Более подробную информацию о функции вы можете найти в файле документации FastFind.chm из архива библиотеки.

Функция FFBestSpot хорошо подходит для поиска элементов интерфейса, таких как индикатор здоровья, иконки, окна и текст. Кроме того, с её помощью можно успешно искать игровые объекты в 2D играх.

Вторая задача, которую хорошо решает FastFind, заключается в обнаружении изменений изображения на экране. Функция FFLocalizeChanges реализует подходящий алгоритм. Для демонстрации её работы воспользуемся окном приложения Notepad.

Скрипт FFLocalizeChanges.au3, приведённый в листинге 2-19, определяет координаты текста, который вы введёте в окне Notepad.

Листинг 2-19. Скрипт FFLocalizeChanges.au3
 1 #include "FastFind.au3"
 2 
 3 Sleep(5 * 1000)
 4 FFSnapShot (0, 0, 0, 0, 0)
 5 
 6 MsgBox(0, "Info", "Измените изображение")
 7 
 8 Sleep(5 * 1000)
 9 FFSnapShot (0, 0, 0, 0, 1)
10 
11 $coords = FFLocalizeChanges(0, 1, 10)
12 
13 if not @error then
14     MsgBox(0, "Coords", "x1 = " & $coords[0] & ", y1 = " & $coords[1] & _
15            " x2 = " & $coords[2] & ", y2 = " & $coords[3])
16 else
17     MsgBox(0, "Coords", "Изменения не обнаружены")
18 endif

Для тестирования скрипта FFLocalizeChanges.au3 выполните следующие шаги:

  1. Запустите приложение Notepad и разверните его окно на весь экран.
  2. Запустите скрипт.
  3. Переключитесь на окно Notepad.
  4. Ожидайте диалоговое окно с сообщением “Измените изображение”.
  5. Введите несколько символов в Notepad в течение пяти секунд.
  6. Ожидайте диалоговое окно с координатами введённого текста.

Функции библиотеки FastFind оперируют абстракцией SnapShot (снимок). SnapShot – это копия текущего изображения на экране в память. По сути такой снимок очень похож на DIB. Когда мы использовали функцию FFBestSpot, она создавала SnapShot неявно. Затем на нём отрабатывал алгоритм поиска нужной области.

Функция FFLocalizeChanges принимает входными параметрами два SnapShot: до и после изменения. Она не знает, в какой момент времени произошло изменение. Поэтому SnapShot должен создавать пользователь библиотеки с помощью функции FFSnapShot. Получившиеся снимки будут сохранены в массиве, индексы которого начинаются с нуля. По умолчанию после каждого вызова FFSnapShot индекс инкриминируется. Но его можно указать и явно в пятом параметре функции. Первые четыре параметра FFSnapShot – это координаты X и Y верхнего левого и правого нижнего углов сохраняемой области. Если все координаты равны нулю, скопировано будет изображение всего экрана.

Рассмотрим алгоритм скрипта FFLocalizeChanges.au3. После пятисекундной задержки вызывается функция FFSnapShot, которая создаёт SnapShot экрана с первоначальным изображением окна Notepad. Затем выводится сообщение “Измените изображение”, после которого пользователь вводит текст. Спустя пять секунд, скрипт делает ещё один SnapShot. Оба SnapShot передаются в функцию FFLocalizeChanges, которая вычисляет координаты изменившейся области.

Входные параметры FFLocalizeChanges приведены в таблице 2-5.

Таблица 2-5. Входные параметры функции FFLocalizeChanges
Параметр Описание
0 Индекс первого SnapShot для сравнения.
1 Индекс второго SnapShot для сравнения.
10 Допустимое отклонение цвета. Этот параметр работает так же, как и для функции FFBestSpot.

Функция FFLocalizeChanges возвращает массив из пяти элементов. Первые четыре из них – это координаты X и Y верхнего левого и правого нижнего углов изменённой области. Пятый элемент хранит число отличающихся пикселей. FFLocalizeChanges представляет собой хорошую альтернативу AutoIt функции PixelChecksum, потому что реже ошибается и предоставляет больше информации об обнаруженном изменении.

Функции библиотеки FastFind работают с перекрытыми окнами, но не со свёрнутыми. Большинству из них можно передать дескриптор окна через необязательный входной параметр. Также все функции работают корректно с полноэкранными окнами DirectX-приложений.

Библиотека ImageSearch

Библиотека ImageSearch решает одну единственную задачу. Она ищет заданный фрагмент изображения в указанной области экрана.

Для вызова функций библиотеки из AutoIt скрипта выполните следующие шаги:

  1. Создайте папку для проекта (например с именем ImageSearchDemo).
  2. Скопируйте в неё файлы ImageSearch.au3 и ImageSearchDLL.dll из архива библиотеки.
  3. Включите файл ImageSearch.au3 в ваш скрипт:
1 #include "ImageSearch.au3"

После этого все функции библиотеки станут доступны.

Если вы разрабатываете приложение на C++ и планируете работать с ImageSearch, необходимо выполнить явную компоновку библиотеки. Пример этого метода приведён в предыдущем разделе, посвящённом FastFind.

Для демонстрации возможностей ImageSearch напишем скрипт для поиска иконки приложения Notepad на экране. Для начала подготовим фрагмент изображения, который будем искать. В нашем случае это иконка, приведённая на иллюстрации 2-11.

Иллюстрация 2-11. Иконка Notepad

Вы можете создать эту иконку с помощью приложения Paint. Для этого запустите приложение Notepad, сделайте скриншот окна, вставьте его в Paint и вырежьте иконку. Сохраните результат в файл с именем notepad-logo.bmp в папку с проектом ImageSearchDemo.

Листинг 2-20 демонстрирует скрипт Search.au3 для поиска иконки на экране.

Листинг 2-20. Скрипт Search.au3
 1 #include <ImageSearch.au3>
 2 
 3 Sleep(5 * 1000)
 4 
 5 global $x = 0, $y = 0
 6 $search = _ImageSearch('notepad-logo.bmp', 0, $x, $y, 20)
 7 
 8 if $search = 1 then
 9     MsgBox(0, "Coords", $x & ", " & $y)
10 else
11     MsgBox(0, "Coords", "Фрагмент изображения не найден")
12 endif

Чтобы протестировать скрипт, выполните следующие шаги:

  1. Запустите приложение Notepad.
  2. Запустите скрипт Search.au3.
  3. Сделайте активным окно Notepad.
  4. Ожидайте сообщения с координатами иконки.

Если у вас возникли проблемы с последними версиями библиотеки, вы можете воспользоваться более старой, но стабильной сборкой.

Параметры функции _ImageSearch приведены в таблице 2-6.

Таблица 2-6. Входные параметры функции _ImageSearch
Параметр Описание
notepad-­logo.bmp Путь к файлу с фрагментом изображения для поиска.
0 Флаг для выбора точки, координаты которой вернёт функция. Значение 0 соответствует верхнему левому углу фрагмента. Значение 1 – координатам его центра.
x Переменная для записи X-координаты найденного фрагмента.
y Переменная для Y-координаты.
20 Допустимое отклонение цвета.

В случае успешного поиска, функция возвращает нулевое значение. Если же произошла ошибка, возвращается её код.

Функция _ImageSearch ищет фрагмент изображения на всём экране. Библиотека также предоставляет функцию _ImageSearchArea для поиска только в указанной области экрана. Пример её вызова выглядит следующим образом:

1 $search = _ImageSearchArea('notepad-logo.bmp', 0, 100, 150, 400, 450, $x, $y, 20)

Четыре дополнительных параметра функции (со второго по шестой) – это координаты области экрана для поиска. В нашем примере она ограничена точками x = 100, y = 150 и x = 400, y = 450. _ImageSearchArea возвращает такой же результат, как и функция _ImageSearch: код ошибки и координаты найденного фрагмента через седьмой и восьмой входной параметр.

Функции библиотеки ImageSearch работают только с текущим изображением на экране. Это значит, что вы не можете перекрыть или свернуть окно анализируемого приложения. Полноэкранные окна DirectX-приложений обрабатываются корректно.

Библиотека ImageSearch – это надёжный инструмент для поиска статичных фрагментов в окне игрового приложения. Она хорошо подходит для обнаружения элементов интерфейса и 2D-объектов.

Выводы

Мы рассмотрели функции AutoIt для анализа пикселей изображения на экране и для обнаружения изменений этого изображения.

Мы изучили основные возможности библиотек FastFind и ImageSearch. Первая из них предоставляет более мощные функции анализа пикселей. Вторая позволяет найти фрагмент изображения на экране.

Пример кликера для Lineage 2

Напишем простого бота-кликера для MMORPG Lineage 2, чтобы закрепить полученные знания о техниках внедрения данных на уровне ОС и перехвате устройств вывода.

Обзор игры Lineage 2

Игровой процесс Lineage 2 типичен для жанра RPG. Сначала надо выбрать расу и класс для своего персонажа. Для получения новых умений и покупки предметов игрок должен выполнять задания (или квесты) и охотиться на монстров. Этот процесс получения ресурсов называется фарминг (farming). При этом у игроков всегда есть возможность общаться и взаимодействовать между собой, как и в любой MMORPG. Они могут помогать или мешать друг другу. Если несколько игроков хотят получить один и тот же ресурс, они должны сражаться за него. Этот элемент соперничества представляет наиболее привлекательную часть игрового процесса. Поэтому пользователи стремятся как можно быстрее и лучше развить своего персонажа, чтобы сражаться между собой.

Самый прямолинейный путь развития персонажа – это охота на монстров. После убийства каждого из них, игрок получает очки опыта для улучшения умений персонажа, а также золото для покупки новых предметов. Мы попытаемся автоматизировать именно этот процесс, поскольку он ведёт к разностороннему развитию героя. Однако, есть и другие пути получения игровых ресурсов: торговля, рыбалка, создание предметов и выполнение заданий.

На иллюстрации 2-12 приведён скриншот игры. Рассмотрим на нём элементы игрового интерфейса, помеченные номерам:

  1. Окно состояния с параметрами персонажа игрока. К наиболее важным из них относятся очки здоровья (health points или HP) и мана (mana points или MP).
  2. Окно цели с информацией о выделенном в данный момент монстре. В нём есть полоска с HP цели.
  3. Панель горячих клавиш с иконками возможных действий и доступных умений.
  4. Окно чата для ввода команд и отправки сообщений другим игрокам.
Иллюстрация 2-12. Интерфейс Lineage 2

Тщательное изучение интерфейса поможет вам разработать наиболее простой и эффективный алгоритм взаимодействия бота с игрой. Более подробно интерфейс Lineage 2 описан на вики-странице.

В интернете есть множество серверов Lineage 2. Они отличаются версией игры, дополнительными возможностями и системами защиты, которые предотвращают использование ботов. Наиболее эффективная защита работает на официальных серверах, которые поддерживают разработчики игры. Кроме них есть так называемые пиратские сервера, которые поддерживаются энтузиастами. Как правило, их защита значительно слабее. В нашем примере мы будем подключаться к серверу РПГ-Клуб.

Реализация бота

Чтобы лучше понять механику игры, попробуйте зарегистрироваться на сервере РПГ-Клуб, создать персонажа и убить нескольких монстров. Вы заметите, что почти всё время нажимаете одни и те же кнопки на панели горячих клавиш.

Теперь составим список действий, которые надо автоматизировать. Предлагаю следующий вариант:

1. Выбрать монстра для атаки. Это можно сделать двумя способами: левым щелчком мыши по нему или ввести в окно чата команду “/target”. Например:

1 /target ИмяМонстра

Полный список игровых команд приведён на официальном сайте. Их можно комбинировать в одно действие с помощью макросов.

  1. Атаковать монстра. Для этого можно нажать кнопку “атака” на панели горячих клавиш или горячую клавишу F1.
  2. Ожидать пока персонаж убьёт монстра.
  3. Подобрать выпавшие из монстра предметы и золото. Опять же можно щёлкнуть мышью по действию на панели горячих клавиш или нажать F8.

Рассмотренные нами действия выглядят достаточно просто и прямолинейно. По сути у нас получился алгоритм работы бота. Напишем скрипт, который будет по нему работать.

Слепой бот

Начнём с того, что будем строго следовать нашему алгоритму охоты на монстров. На каждом его шаге бот должен симулировать нажатие одной клавиши. Такой кликер можно считать слепым, поскольку он не получает никакой информации о состоянии игровых объектов.

Перед тем как начать писать код, рассмотрим конфигурацию панели горячих клавиш. Вам нужно настроить её так же как на иллюстрации 2-13.

Иллюстрация 2-13. Панель горячих клавиш

Таблица 2-7 описывает конфигурацию панели.

Таблица 2-7. Действия и соответствующие им горячие клавиши
Клавиша Действие
F1 Атаковать выделенного в данный момент монстра.
F2 Использовать наступательное умение по текущей цели.
F5 Использовать зелье лечения для восстановления HP.
F8 Подобрать с земли предметы, лежащие около персонажа.
F9 Макрос с командой /target ИмяМонстра для выбора цели.
F10 Выбор ближайшего монстра.

Теперь стало очевидно, как надо связать горячие клавиши с шагами алгоритма бота. Скрипт BlindBot.au3, приведённый в листинге 2-21, демонстрирует это.

Листинг 2-21. Скрипт BlindBot.au3
 1 #RequireAdmin
 2 
 3 Sleep(2000)
 4 
 5 while true
 6     Send("{F9}")
 7     Sleep(200)
 8     Send("{F1}")
 9     Sleep(5000)
10     Send("{F8}")
11     Sleep(1000)
12 wend

В первой строчке скрипта стоит ключевое слово #RequireAdmin. Благодаря ему при старте скрипт потребует предоставить ему права администратора. Получив эти права, он сможет взаимодействовать с другими приложениями независимо от того, какой пользователь их запустил. Некоторые клиенты Lineage 2 при старте также требуют прав администратора. Поэтому к ним не смогут получить доступ скрипты AutoIt, запущенные от имени пользователя с меньшими правами. Я рекомендую всегда использовать #RequireAdmin в ваших кликерах.

Скрипт начинает своё выполнение с двухсекундной задержки. Она нужна для того, чтобы вы успели переключиться на окно Lineage 2. Текущая версия бота работает только с активным окном игры.

После вызова Sleep идёт бесконечный цикл while, в котором выполняются все действия бота:

  1. Send("{F9}") – выбрать монстра с помощью макроса, настроенного на клавишу F9.
  2. Sleep(200) – подождать 200 миллисекунд. Это время требуется клиенту Lineage 2, чтобы выделить монстра и отрисовать окно цели.
  1. Send("{F1}") – атаковать выбранного монстра.
  2. Sleep(5000) – ожидать пять секунд, пока персонаж не подбежит к монстру и не убьёт его.
  3. Send("{F8}") – подобрать один выпавший предмет.
  1. Sleep(1000) – ждать одну секунду, пока персонаж подбирает предмет.

В нашем примере последовательность действий бота строго определена. Поэтому каждое действие может завершиться успешно только в том случае, если предыдущее также было успешно. Это значит, что макрос выбора монстра должен отработать правильно. Если первый шаг не удался, все дальнейшие действия не имеют смысла. Затем персонаж должен успеть подбежать к монстру и убить его за пять секунд. Очевидно, это время может меняться в зависимости от расстояния до цели. Наконец, бот ожидает, что из монстра выпадет только один предмет. Наш скрипт отработает правильно только тогда, когда все перечисленные условия выполнятся, иначе неизбежны ошибки.

Попробуйте запустить скрипт и проверить его работу. Часто бот будет совершать не те действия, которые нужны в данный момент. Причина в том, что одно из условий его работы нарушено. С другой стороны, все его ошибки не критичны, поскольку он продолжает свою работу. Это возможно благодаря особенности команды /target и механизму атаки цели. Если выполнить макрос /target дважды, бот будет атаковать уже выбранного монстра. Таким образом он всегда будет добивать цель. Даже если монстр выжил после первой итерации цикла while, атака на него продолжится в следующих итерациях. Кроме того, команда “поднять предмет” не прерывает атаку, если поблизости от персонажа нет предметов. Поэтому он будет продолжать бить цель и после пятисекундной задержки, отведённой на убийство монстра.

Единственная проблема, которую бот не сможет решить, заключается в подбирании выпадающих предметов. Число их случайно и зависит от вида монстра. Поэтому иногда они будут оставаться лежать на земле и персонаж недополучит свои ресурсы. В такой ситуации повторение действия “поднять” несколько раз будет лучшим, что можно придумать без дополнительных проверок. Даже если зачастую число нажатий будет больше необходимого, персонаж подберёт все выпавшие ресурсы.

Можно сделать скрипт более удобным для чтения и модификации, если вынести каждый шаг алгоритма в отдельную функцию с говорящим названием. Результат такого улучшения приведён в скрипте BlindBotFunc.au3 из листинга 2-22.

Листинг 2-22. Скрипт BlindBotFunc.au3
 1 #RequireAdmin
 2 
 3 func SelectTarget()
 4     Send("{F9}")
 5     Sleep(200)
 6 endfunc
 7 
 8 func Attack()
 9     Send("{F1}")
10     Sleep(5000)
11 endfunc
12 
13 func Pickup()
14     Send("{F8}")
15     Sleep(1000)
16 endfunc
17 
18 Sleep(2000)
19 
20 while true
21     SelectTarget()
22     Attack()
23     Pickup()
24 wend

Теперь скрипт выглядит намного понятнее. Он начинает свою работу с вызова Sleep(2000). Выше этой строчки находятся только объявления пользовательских функций, которые определены разработчиком для своих целей. Их код будет выполнен только в местах вызова, то есть в цикле while. Обратите внимание, что несмотря на изменившуюся структуру кода, алгоритмы скриптов BlindBotFunc.au3 и BlindBot.au3 остались идентичны.

Бот с условиями

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

Перед тем как мы продолжим, было бы полезно добавить к текущей реализации бота механизм вывода диагностических сообщений. Техника вывода сообщений в местах принятия программой важных решений известна как трассировка (tracing). С её помощью мы сможем отследить, какие решения принимает бот в ходе своей работы.

Реализация функции вывода сообщений в файл представлена в листинге 2-23.

Листинг 2-23. Реализация функции LogWrite
1 global const $LogFile = "debug.log"
2 
3 func LogWrite($data)
4     FileWrite($LogFile, $data & chr(10))
5 endfunc
6 
7 LogWrite("Hello world!")

После выполнения этого скрипта в одной папке с ним будет создан файл debug.log, содержащий строку “Hello world!”. Функция LogWrite является обёрткой над AutoIt вызовом FileWrite. Она будет удобна, если вам понадобиться отключить вывод в лог-файл. Для этого достаточно будет закомментировать в ней вызов FileWrite. Вы можете изменить путь до лог-файла и его имя с помощью константы LogFile.

Первое условие, которое бот должен проверить, – это результат выбора цели. Попробуйте несколько раз выделить монстров с помощью мыши. Заметили ли вы элемент интерфейса, который отличается при наличии и отсутствии цели? Я имею в виду окно цели. Оно появляется каждый раз при выборе цели и пропадает при её убийстве или отмене по клавише Esc. Наш бот может найти это окно на экране с помощью функции FFBestSpot библиотеки FastFind.

Чтобы отличить окно цели от остальных, нам нужно выбрать уникальный для него цвет. Другими словами, надо найти такой цвет, который встречается только в окне цели. Для этого подошёл бы красный цвет полосы HP монстра. Код из листинга 2-24 проверяет, есть ли окно цели на экране.

Листинг 2-24. Функция IsTargetExist
 1 func IsTargetExist()
 2     const $SizeSearch = 80
 3     const $MinNbPixel = 3
 4     const $OptNbPixel = 10
 5     const $PosX = 688
 6     const $PosY = 67
 7 
 8     $coords = FFBestSpot($SizeSearch, $MinNbPixel, $OptNbPixel, _
 9                          $PosX, $PosY, 0x871D18, 10)
10 
11     const $MaxX = 800
12     const $MinX = 575
13     const $MaxY = 100
14 
15     if not @error then
16         if $MinX < $coords[0] and $coords[0] < $MaxX _
17            and $coords[1] < $MaxY then
18 
19             LogWrite("IsTargetExist() - Success, coords = " & _
20                      $coords[0] & ", " & $coords[1] & " pixels = " & _
21                      $coords[2])
22             return True
23         else
24             LogWrite("IsTargetExist() - Fail #1")
25             return False
26         endif
27     else
28         LogWrite("IsTargetExist() - Fail #2")
29         return False
30     endif
31 endfunc

Рассмотрим функцию IsTargetExist подробнее. Константы PosX и PosY – это примерные координаты полосы HP цели. Мы передаём их и красный цвет полосы (равный 871D18) в функцию FFBestSpot в качестве входных параметров. Она ищет указанную область по всему экрану.

Внимательный читатель заметит, что вместо окна цели может быть найдено окно состояния персонажа. Ведь в нём тоже встречается красный цвет на полоске HP персонажа. В таком случае бот всегда будет делать вывод, что цель есть. Чтобы избежать этой ошибки, мы проверяем координаты области, найденной функцией FFBestSpot. Сравниваем их (coords[0] и coords[1]) с максимальными (MaxX и MaxY) и минимальными (MinX) допустимыми значениями. Эти значения задают область экрана, в которой ожидается появление окна цели. Они зависят от разрешения экрана и конфигурации интерфейса игры. Поэтому вам придётся подбирать их самостоятельно.

В каждой ветви операторов if мы вызываем функцию LogWrite, чтобы отследить принятые решения. Благодаря этому мы сможем обнаружить возможные ошибки, связанные с несоответствием входных и выходных данных функции IsTargetExist.

IsTargetExist позволяет нам решить сразу две задачи:

  1. Проверка успешности выбора цели в функции SelectTarget.
  2. Проверка состояния атакуемого ботом монстра (жив или нет).

Скрипт AnalysisBot.au3, представленный в листинге 2-25, использует функцию IsTargetExist для проверки наличия цели.

Листинг 2-25. Скрипт AnalysisBot.au3
 1 #include "FastFind.au3"
 2 
 3 #RequireAdmin
 4 
 5 Sleep(2000)
 6 
 7 global const $LogFile = "debug.log"
 8 
 9 func LogWrite($data)
10     FileWrite($LogFile, $data & chr(10))
11 endfunc
12 
13 func IsTargetExist()
14     ; Смотрите реализацию в листинге 2-24
15 endfunc
16 
17 func SelectTarget()
18     LogWrite("SelectTarget()")
19     while not IsTargetExist()
20         Send("{F9}")
21         Sleep(200)
22     wend
23 endfunc
24 
25 func Attack()
26     LogWrite("Attack()")
27     while IsTargetExist()
28         Send("{F1}")
29         Sleep(1000)
30     wend
31 endfunc
32 
33 func Pickup()
34     Send("{F8}")
35     Sleep(1000)
36 endfunc
37 
38 while True
39     SelectTarget()
40     Attack()
41     Pickup()
42 wend

Обратите внимание на новую реализацию функций SelectTarget и Attack. В SelectTarget бот пытается выделить цель в цикле до тех пор, пока функция IsTargetExist не вернёт значение True. Только после этого он переходит в функцию Attack. В ней бот продолжает атаковать монстра (выбирая действие “атака” по клавише F1) до тех пор, пока тот жив.

Мы печатаем в лог-файл названия функций SelectTarget и Attack, когда они получают управление. Этот вывод позволяет определить, которая из них вызывает IsTargetExist.

Дальнейшие улучшения

Теперь наш кликер выбирает действие, согласно игровой ситуации. Тем не менее, по-прежнему возможны случаи, когда бот допустит критическую ошибку и умрёт.

Первая проблема заключается в агрессивных монстрах. Большинство из них неагрессивны. Они остаются в одной и той же области карты, не реагируя на приближение игрока. Но некоторые из них в такой ситуации атакуют и преследуют.

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

Чтобы решить эту проблему, воспользуемся командой “выбор ближайшей цели”. На нашей панели горячих клавиш она доступна по нажатию F10. Ближайшая цель находится на минимальной (по сравнению с другими) дистанции от бота. Это значительно уменьшит время бота в пути, а значит и вероятность встречи с агрессивными монстрами.

Листинг 2-26 демонстрирует дополненную версию функции SelectTarget.

Листинг 2-26. Функция SelectTarget
 1 func SelectTarget()
 2     LogWrite("SelectTarget()")
 3 
 4     while not IsTargetExist()
 5         Send("{F10}")
 6         Sleep(200)
 7 
 8         if IsTargetExist() then
 9             exitloop
10         endif
11 
12         Send("{F9}")
13         Sleep(200)
14     wend
15 endfunc

Теперь бот в первую очередь пытается найти ближайшую цель по клавише F10. Только тогда, когда ему это не удалось, он использует команду /target. Таким образом бот всегда стремится выбрать ближайшего к нему монстра. Если тот окажется агрессивным, то побежит навстречу и будет ближе всего.

Вторую серьёзную проблему для бота представляют собой преграды на карте. При движении к цели он может зацепиться за камень или дерево и застрять. Самое простое решение заключается в тайм-ауте на атаку. Если отведённое время на убийство монстра прошло, а цель осталась жива, можно предположить, что бот застрял. Тогда для обхода препятствия ему помогут случайные перемещения. Новые версии функций Move и Attack из листинга 2-27 демонстрируют это решение.

Листинг 2-27. Функции Move и Attack
 1 func Move()
 2     SRandom(@MSEC)
 3     MouseClick("left", Random(300, 800), Random(170, 550), 1)
 4 endfunc
 5 
 6 func Attack()
 7     LogWrite("Attack()")
 8 
 9     const $TimeoutMax = 10
10     $timeout = 0
11     while IsTargetExist() and $timeout < $TimeoutMax
12         Send("{F1}")
13         Sleep(2000)
14 
15         Send("{F2}")
16         Sleep(2000)
17 
18         $timeout += 1
19     wend
20 
21     if $timeout == $TimeoutMax then
22         Move()
23     endif
24 endfunc

Мы добавили счётчик timeout в функцию Attack. На каждой итерации цикла while он инкрементируется и сравнивается с пороговым значением константы TimeoutMax. Когда счётчик достигает TimeoutMax, бот делает вывод, что застрял. В этом случае вызывается функция Move, которая симулирует щелчок левой кнопки мыши по точке со случайной координатой. Чтобы получить случайное число, используются функции AutoIt SRandom и Random. Первая из них инициализирует генератор псевдослучайных чисел. Вторая возвращает следующее число из очереди сгенерированных. В качестве параметров функция Random принимает границы интервала для случайного числа.

Возможно, вы заметили дополнительное действие, появившееся в новой функции Attack. Это симуляция нажатия клавиши F2. Мы можем назначить на неё любое атакующее умение персонажа, и бот будет применять его в сражении. Благодаря этому он сможет быстрее убивать монстров.

Теперь наш кликер способен самостоятельно работать достаточно долгое время. Он умеет обходить препятствия и первым атаковать агрессивных монстров. Но есть одно улучшение, способное значительно увеличить выживаемость бота. Речь идёт об использовании зелья восстановления здоровья, которое привязано к горячей клавише F5. Чтобы правильно его применять, необходимо анализировать полосу HP персонажа в окне состояния. Вы можете реализовать этот механизм самостоятельно в качестве упражнения. Алгоритм чтения уровня HP будет похож на функцию IsTargetExist.

Выводы

Мы реализовали кликера для игры Lineage 2. Он использует самые распространённые техники симуляции действий и анализа окна игрового приложения. Попробуем оценить их эффективность и обобщить результат на всех ботов этого типа.

Преимущества кликеров:

  1. Простота разработки, отладки и расширения функциональности.
  2. Просто адаптировать под любую версию игры, даже если её интерфейс поменялся.
  3. Защититься от этого типа ботов достаточно сложно.

Недостатки кликеров:

  1. Каждому пользователю приходится подгонять цвета и координаты искомых пикселей под своё разрешение экрана.
  2. Бот может зависнуть в некоторых непредвиденных случаях (смерть персонажа, отключение от сервера и т.д.).
  3. Тайм-ауты на симулируемые действия часто приводят к потере времени и низкой эффективности.
  4. При анализе изображений на экране возможны ошибки. Поэтому в некоторых случаях бот будет выбирать неподходящие действия.

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

Методы защиты от кликеров

Мы познакомились с основными принципами работы кликеров. Теперь рассмотрим этот тип ботов с точки зрения разработчика систем защиты. Как можно их обнаружить и помешать симулировать действия игрока? На этот вопрос мы найдём ответ в этом разделе.

В первой главе мы рассмотрели архитектуру типичной онлайн-игры. Как вы помните, её приложение состоит из двух частей: клиентской и серверной. Зачастую, система защиты придерживается такой же архитектуры и разделена на две части. Клиентская часть контролирует точки перехвата и внедрения данных на стороне пользователя (драйвера, ОС, приложение). Серверная часть следит за взаимодействием игрового приложения и сервером. Большинство техник по обнаружению кликеров работают на клиентской стороне.

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

  1. Уведомить администратора игрового сервера о подозрительных действиях игрока. Для этого достаточно сделать запись в лог-файл на стороне сервера.
  2. Разорвать соединение между подозрительным пользователем и сервером.
  3. Заблокировать игрока по IP-адресу. Это предотвратит его дальнейшие попытки подключения к серверу.

Мы рассмотрим только алгоритмы обнаружения ботов, но не способы блокировки их работы. Поскольку самыми надёжными мерами пресечения нарушений будут не технические приёмы, а административные действия (например, блокировка аккаунта или штрафное время ожидания подключения к серверу).

Тестовое приложение

Для тестирования алгоритмов обнаружения ботов, мы воспользуемся приложением Notepad. Предположим, что это игровой клиент, который мы должны защитить. Напишем простейший AutoIt-скрипт, который выполняет роль кликера и вводит текст в Notepad. Тогда наша цель заключается в его обнаружении.

Листинг 2-28 демонстрирует скрипт SimpleBot.au3, который печатает буквы “a”, “b”, “c” в окне Notepad.

Листинг 2-28. Скрипт SimpleBot.au3
 1 $hWnd = WinGetHandle("[CLASS:Notepad]")
 2 WinActivate($hWnd)
 3 
 4 Sleep(200)
 5 
 6 while true
 7     Send("a")
 8     Sleep(1000)
 9     Send("b")
10     Sleep(2000)
11     Send("c")
12     Sleep(1500)
13 wend

Для тестирования запустите Notepad, а затем скрипт SimpleBot.au3. Он переключится на нужное окно и будет вводить буквы в бесконечном цикле.

Скрипт SimpleBot.au3 служит отправной точкой нашего исследования. Его цель в том, чтобы отличить симулируемые ботом нажатия клавиш от действий пользователя в окне Notepad. Прототипы алгоритмов защиты мы будем писать на AutoIt. Благодаря этому получится простой и компактный код для изучения. В реальных системах защиты предпочтительнее использовать компилируемые языки вроде C или C++.

Анализ действий игрока

Вычисление временных задержек

Скрипт SimpleBot.au3 симулирует одни и те же действия в цикле. Их систематичность – это первое, что бросается в глаза при анализе работы бота. Ещё раз обратимся к его коду. Между каждым действием и предыдущим стоят строго определённые задержки. Человек не может действовать в таких точных временных интервалах. Более того, такие чёткие остановки не имеет никакого смысла в компьютерной игре, потому что зачастую пользователь должен реагировать на различные случайные ситуации. Если кто-то ведёт себя подобным образом, очень вероятно что это программа.

Алгоритм защиты может замерять задержки между двумя одинаковыми действиями. Если они повторяются через одни и те же интервалы времени с разницей не более 100 миллисекунд, их наверняка выполняет бот. Попробуем реализовать такую защиту.

Наш скрипт защиты должен выполнять две задачи: перехватывать действия пользователя и измерять временные задержки между ними. Код в листинге 2-29 реализует перехват нажатия клавиш.

Листинг 2-29. Перехват нажатия клавиш
 1 global const $gKeyHandler = "_KeyHandler"
 2 
 3 func _KeyHandler()
 4     $keyPressed = @HotKeyPressed
 5 
 6     LogWrite("_KeyHandler() - asc = " & asc($keyPressed) & " key = " & $keyPressed)
 7     AnalyzeKey($keyPressed)
 8 
 9     HotKeySet($keyPressed)
10     Send($keyPressed)
11     HotKeySet($keyPressed, $gKeyHandler)
12 endfunc
13 
14 func InitKeyHooks($handler)
15     for $i = 0 to 255
16         HotKeySet(Chr($i), $handler)
17     next
18 endfunc
19 
20 InitKeyHooks($gKeyHandler)
21 
22 while true
23     Sleep(10)
24 wend

Мы применили функцию AutoIt HotKeySet, чтобы назначить обработчик (handler или hook) для нажатий клавиш. Она принимает на вход два параметра: код перехватываемой клавиши и ссылку на функцию-обработчик. Чтобы пройти по всем кодам от 0 до 255, в пользовательской функции InitKeyHooks используется цикл for. Обработчик _KeyHandler назначается для всех клавиш. Алгоритм его работы выглядит следующим образом:

  1. Вызвать функцию AnalyzeKey и передать ей код нажатой клавиши. Этот код хранится в макросе @HotKeyPressed.
  2. Выключить перехват следующего нажатия обрабатываемой клавиши. Для этого снова вызывается функция HotKeySet. Данный шаг нужен, чтобы последующее нажатие обработало приложение Notepad, а не наш скрипт.
  3. Вызвать функцию Send для симуляции нажатия обрабатываемой клавиши в Notepad. Этот шаг нужен, поскольку нажатие пользователя получил скрипт, а не Notepad.
  4. Включить перехват скриптом последующих нажатий с помощью HotKeySet.

Листинг 2-30 демонстрирует код функции AnalyzeKey.

Листинг 2-30. Функция AnalyzeKey
 1 global $gTimeSpanA = -1
 2 global $gPrevTimestampA = -1
 3 
 4 func AnalyzeKey($key)
 5     local $timestamp = (@SEC * 1000 + @MSEC)
 6     LogWrite("AnalyzeKey() - key = " & $key & " msec = " & $timestamp)
 7     if $key <> 'a' then
 8         return
 9     endif
10 
11     if $gPrevTimestampA = -1 then
12         $gPrevTimestampA = $timestamp
13         return
14     endif
15 
16     local $newTimeSpan = $timestamp - $gPrevTimestampA
17     $gPrevTimestampA = $timestamp
18 
19     if $gTimeSpanA = -1 then
20         $gTimeSpanA = $newTimeSpan
21         return
22     endif
23 
24     if Abs($gTimeSpanA - $newTimeSpan) < 100 then
25         MsgBox(0, "Alert", "Обнаружен бот-кликер!")
26     endif
27 endfunc

В функции AnalyzeKey мы измеряем задержки между нажатиями клавиши “a”. Две глобальные переменные хранят текущее состояние алгоритма:

  1. gPrevTimestampA – это момент времени (timestamp) первого нажатия.
  2. gTimeSpanA – это задержка между первым и вторым нажатиями.

При старте скрипта обоим переменным присваивается значение -1, которое соответствует неинициализированному состоянию. Нашему алгоритму требуется перехватить как минимум три нажатия клавиш, чтобы обнаружить бота. Первое нажатие инициализирует переменную gPrevTimestampA:

1     if $gPrevTimestampA = -1 then
2         $gPrevTimestampA = $timestamp
3         return
4     endif

Момент времени второго нажатия мы используем для расчёта переменной gTimeSpanA. Она равна разности между временем первого и второго нажатий:

1     local $newTimeSpan = $timestamp - $gPrevTimestampA
2     $gPrevTimestampA = $timestamp
3 
4     if $gTimeSpanA = -1 then
5         $gTimeSpanA = $newTimeSpan
6         return
7     endif

После третьего нажатия мы можем вычислить задержку второй раз (переменная newTimeSpan) и сравнить её с предыдущей (значение gTimeSpanA):

1     if Abs($gTimeSpanA - $newTimeSpan) < 100 then
2         MsgBox(0, "Alert", "Clicker bot detected!")
3     endif

Если разница между первой и второй задержкой меньше 100 миллисекунд, алгоритм защиты выводит сообщение об обнаружении бота.

Полный код защиты представлен в скрипте TimeSpanProtection.au3 из листинга 2-31. В нём мы опустили реализацию функций _KeyHandler и AnalyzeKey, поскольку рассмотрели их ранее.

Листинг 2-31. Скрипт TimeSpanProtection.au3
 1 global const $gKeyHandler = "_KeyHandler"
 2 global const $kLogFile = "debug.log"
 3 
 4 global $gTimeSpanA = -1
 5 global $gPrevTimestampA = -1
 6 
 7 func LogWrite($data)
 8     FileWrite($kLogFile, $data & chr(10))
 9 endfunc
10 
11 func _KeyHandler()
12     ; См листинг 2-29
13 endfunc
14 
15 func InitKeyHooks($handler)
16     for $i = 0 to 256
17         HotKeySet(Chr($i), $handler)
18     next
19 endfunc
20 
21 func AnalyzeKey($key)
22     ; См листинг 2-30
23 endfunc
24 
25 InitKeyHooks($gKeyHandler)
26 
27 while true
28     Sleep(10)
29 wend

Анализ последовательности действий

Мы можем незначительно изменить скрипт SimpleBot.au3, чтобы обойти защиту TimeSpanProtection.au3. Для этого заменим фиксированные задержки между действиями на случайные. Листинг 2-32 демонстрирует исправленную версию бота.

Листинг 2-32. Скрипт RandomDelayBot.au3
 1 SRandom(@MSEC)
 2 
 3 $hWnd = WinGetHandle("[CLASS:Notepad]")
 4 WinActivate($hWnd)
 5 
 6 Sleep(200)
 7 
 8 while true
 9     Send("a")
10     Sleep(Random(800, 1200))
11     Send("b")
12     Sleep(Random(1700, 2300))
13     Send("c")
14     Sleep(Random(1300, 1700))
15 wend

Каждый раз, в вызов Sleep мы передаём случайное число, полученное из функции Random. Попробуйте протестировать нового бота вместе с защитой TimeSpanProtection.au3. Теперь она не обнаружит кликера. Можем ли мы её улучшить?

У скрипта RandomDelayBot.au3 по-прежнему есть закономерность, которая сразу видна человеку, следящему за его работой. Речь идёт о последовательности нажимаемых кнопок. Очевидно, что игрок не способен безошибочно повторять свои действия десятки и сотни раз. Даже если он и захочет это сделать, в какой-то момент он ошибётся и нажмёт не ту клавишу.

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

Листинг 2-33. Функция AnalyzeKey
 1 global const $gActionTemplate[3] = ['a', 'b', 'c']
 2 global $gActionIndex = 0
 3 global $gCounter = 0
 4 
 5 func Reset()
 6     $gActionIndex = 0
 7     $gCounter = 0
 8 endfunc
 9 
10 func AnalyzeKey($key)
11     LogWrite("AnalyzeKey() - key = " & $key);
12 
13     $indexMax = UBound($gActionTemplate) - 1
14     if $gActionIndex <= $indexMax and $key <> $gActionTemplate[$gActionIndex] then
15         Reset()
16         return
17     endif
18 
19     if $gActionIndex < $indexMax and $key = $gActionTemplate[$gActionIndex] then
20         $gActionIndex += 1
21         return
22     endif
23 
24     if $gActionIndex = $indexMax and $key = $gActionTemplate[$gActionIndex] then
25         $gCounter += 1
26         $gActionIndex = 0
27 
28         if $gCounter = 3 then
29             MsgBox(0, "Alert", "Обнаружен бот-кликер!")
30             Reset()
31         endif
32     endif
33 endfunc

Новый вариант функции AnalyzeKey использует глобальную константу и две переменные:

  1. gActionTemplate – это массив с последовательностью действий, которую выполняет предполагаемый бот.
  2. gActionIndex – индекс массива gActionTemplate, который соответствует последнему перехваченному нажатию.
  3. gCounter – число обнаруженных повторений последовательности действий.

В функции AnalyzeKey есть три основных условия для обработки нажатия клавиши. Первое из них выполняется, если нажатие не соответствует ни одному элементу массива gActionTemplate:

1     $indexMax = UBound($gActionTemplate) - 1
2     if $gActionIndex <= $indexMax and $key <> $gActionTemplate[$gActionIndex] then
3         Reset()
4         return
5     endif

В этом случае мы вызываем функцию Reset, которая сбрасывает в ноль значения переменных gActionIndex и gCounter. После этого мы выходим из AnalyzeKey.

Второе условие обработки нажатия выполняется, когда перехваченное действие встречается в массиве gActionTemplate, кроме того этот элемент не последний и его индекс равен gActionIndex:

1     if $gActionIndex < $indexMax and $key = $gActionTemplate[$gActionIndex] then
2         $gActionIndex += 1
3         return
4     endif

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

Третье условие выполняется, когда перехваченное нажатие соответствует последнему элементу массива gActionTemplate:

1 if $gActionIndex = $indexMax and $key = $gActionTemplate[$gActionIndex]
2 then
3         $gCounter += 1
4         $gActionIndex = 0
5         if $gCounter = 3 then
6             MsgBox(0, "Alert", "Clicker bot detected!")
7             Reset()
8         endif
9     endif

В этом случае мы инкрементируем счётчик совпадения последовательностей gCounter и сбрасываем значение gActionIndex. Таким образом мы готовы к обнаружению следующих действий бота.

Если ожидаемая последовательность действий происходит три раза подряд, скрипт делает вывод, что её симулирует бот. Тогда пользователю выдаётся соответствующее сообщение. В этом случае счётчик gCounter сбрасывается в ноль и алгоритм защиты начинает свою работу сначала.

Вы можете запустить скрипты ActionSequenceProtection.au3 и RandomDelayBot.au3 для тестирования защиты. Теперь бот будет обнаружен.

Очевидно, что рассмотренный алгоритм может ошибиться. Он примет игрока за бота, если тот трижды повторит одни и те же действия. Вероятность такой ошибки можно уменьшить, если мы увеличим пороговое значение для счётчика $gCounter в следующем условии:

1         if $gCounter = 3 then
2             MsgBox(0, "Alert", "Clicker bot detected!")
3             Reset()
4         endif

К сожалению, у скрипта защиты ActionSequenceProtection.au3 есть и другой серьёзный недостаток. Он способен обнаружить только бота, который запрограммирован на последовательность нажатий “a”, “b”, “c”. Если кликер вместо этого будет выполнять “a”, “c”, “b”, то алгоритм не сможет его обнаружить.

Изменим нашего бота согласно листингу 2-34. Это позволит ему обойти защиту ActionSequenceProtection.au3.

Листинг 2-34. Скрипт RandomActionBot.au3
 1 SRandom(@MSEC)
 2 
 3 $hWnd = WinGetHandle("[CLASS:Notepad]")
 4 WinActivate($hWnd)
 5 
 6 Sleep(200)
 7 
 8 while true
 9     Send("a")
10     Sleep(1000)
11 
12     if Random(0, 9, 1) < 5 then
13         Send("b")
14         Sleep(2000)
15     endif
16 
17     Send("c")
18     Sleep(1500)
19 wend

Теперь симулируемая ботом последовательность действий случайна. Он пропускает нажатие клавиши “b” после “a” с вероятностью порядка 50%. Это приводит к тому, что условия функции AnalyzeKey на обнаружение бота перестают выполняться. Каждый раз, когда бот пропускает “b”, алгоритм защиты сбрасывает счётчик gCounter в ноль. Таким образом, он никогда не достигает порогового значения.

Мы можем обнаружить бота RandomActionBot.au3, если немного изменим защитный алгоритм. Вместо проверки нажатий клавиш “на лету”, он должен записывать их в один большой файл. Когда этот файл достигнет максимально допустимого размера, скрипт должен его прочитать и проверить на наличие часто повторяющихся последовательностей действий. Если они встречаются, это может быть сигналом о том, что их выполняет программа. В случае бота RandomActionBot.au3, такими последовательностями будут:

  1. “a”, “c”.
  2. “a”, “b”, “c”.

Сканирование процессов

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

Скрипт ProcessScanProtection.au3, приведённый в листинге 2-35, демонстрирует этот подход.

Листинг 2-35. Скрипт ProcessScanProtection.au3
 1 global const $kLogFile = "debug.log"
 2 
 3 func LogWrite($data)
 4     FileWrite($kLogFile, $data & chr(10))
 5 endfunc
 6 
 7 func ScanProcess($name)
 8     local $processList = ProcessList($name)
 9 
10     if $processList[0][0] > 0 then
11         LogWrite("Name: " & $processList[1][0] & " PID: " & $processList[1][1])
12         MsgBox(0, "Alert", "Обнаружен бот-кликер!")
13     endif
14 endfunc
15 
16 while true
17     ScanProcess("AutoHotKey.exe")
18     Sleep(5000)
19 wend

Мы можем получить список запущенных в данный момент процессов с помощью AutoIt функции ProcessList. У неё есть единственный необязательный параметр: имя процесса, который нужно найти. Если его передать, функция вернёт список из одного элемента в случае успешного поиска. Предположим, что защита ищет процесс интерпретатора AutoHotKey.exe, который выполняет скрипт бота. ProcessList возвращает двумерный массив, представленный в таблице 2-8.

Таблица 2-8. Элементы массива, возвращаемого ProcessList
Элемент массив Описание
processList[0][0] Количество найденных процессов.
processList[1][0] Имя первого процесса в списке.
processList[1][1] Идентификатор (ID или PID) первого процесса в списке.

Если элемент массива processList[0][0] не равен нулю, процесс AutoHotKey.exe в данный момент запущен и работает.

Почему мы ищем процесс AutoHotKey.exe, а не AutoIt.exe? Дело в том, что мы не сможем протестировать скрипт ProcessScanProtection.au3 на нашем тестовом боте SimpleBot.au3. Оба скрипта написаны на языке AutoIt. Это значит, что как только мы щёлкнем два раза мышью по иконке ProcessScanProtection.au3, ОС запустит процесс интерпретатора AutoIt.exe, который и выполнит наш скрипт. Из-за этого алгоритм защиты будет всегда находить процесс AutoIt.exe, независимо от того запущен бот или нет.

Перепишем нашего тестового бота на языке AutoHotKey. Результат приведён в листинге 2-36.

Листинг 2-36. Скрипт SimpleBot.ahk
 1 WinActivate, Untitled - Notepad
 2 Sleep, 200
 3 
 4 while true
 5 {
 6     Send, a
 7     Sleep, 1000
 8     Send, b
 9     Sleep, 2000
10     Send, c
11     Sleep, 1500
12 }

Вы можете сравнить скрипты SimpleBot.ahk и SimpleBot.au3. Они выглядят похоже. Единственное отличие заключается в синтаксисе вызова функций. В AutoHotKey параметры указываются не в скобках, а через запятую и пробел после имени функции.

Теперь мы можем протестировать скрипт защиты ProcessScanProtection.au3. Для этого выполните следующие шаги:

  1. Запустите приложение Notepad.
  2. Запустите скрипт ProcessScanProtection.au3.
  3. Запустите тестового бота SimpleBot.ahk. Не забудьте перед этим установить на свой компьютер интерпретатор AutoHotKey.
  4. Ожидайте, пока алгоритм защиты не обнаружит бота. Когда это случится, откроется диалоговое окно с сообщением.

Есть несколько методов для обхода такого типа защиты. Самый простой из них заключается в применении компилятора скриптов. Компилятор собирает скрипт и интерпретатор AutoHotKey.exe в единый исполняемый EXE-файл. Имя этого файла будет соответствовать имени процесса в списке, возвращаемом функцией ProcessList. Таким образом, алгоритм защиты ProcessScanProtection.au3 не сработает.

Для компилирования скрипта SimpleBot.ahk выполните следующие шаги:

1. Запустите приложение компилятора AutoHotKey. Его окно выглядит как на иллюстрации 2-14. Путь к нему по умолчанию:

1 C:\Program Files (x86)\AutoHotkey\Compiler\Ahk2Exe.exe
  1. Выберите скрипт SimpleBot.ahk в качестве исходного файла. Диалоговое окно для выбора файла открывается по кнопке “Browse” напротив текста “Source (script file)”.
  2. Не указывайте имя выходного файла в поле “Destination (.exe file)”. В этом случае в той же папке, где находится скрипт, будет создан EXE-файл с таким же именем.
  3. Нажмите кнопку “> Convert <”. После окончания процесса компиляции вы увидите сообщение.
Иллюстрация 2-14. Окно компилятора AutoHotKey

Попробуйте запустить полученный EXE-файл с именем SimpleBot.exe. Он ведёт себя точно так же, как и скрипт SimpleBot.ahk. Единственное отличие в том, что алгоритм защиты ProcessScanProtection.au3 не может его обнаружить. Это происходит из-за того, что процесс бота теперь называется SimpleBot.exe, а не AutoHotKey.exe.

Вычисление хэш-суммы запускаемого файла

Можем ли мы усовершенствовать скрипт ProcessScanProtection.au3 так, чтобы он обнаруживал скомпилированную версию бота SimpleBot.exe? Как мы выяснили, имя исполняемого файла легко поменять в отличие от его содержания. EXE-файл представляет собой машинный код, который выполняется процессором, и заголовки с метаинформацией. Неверное изменение любой из этих частей приведёт к ошибке приложения при старте.

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

  1. Рассчитывать хэш-суммы исполняемых файлов всех запущенных процессов и сравнивать их с предопределёнными значениями.
  2. Проверять последовательность байт в определённом месте каждого из этих исполняемых файлов.
  3. Искать заданную последовательность байт по всему файлу.

Попробуем реализовать первый подход. Скрипт Md5ScanProtection.au3 из листинга 2-37 считает хэш-сумму по алгоритму MD5 для исполняемого файла каждого из запущенных процессов. Если она совпала с искомой, алгоритм делает вывод о наличии работающего бота.

Листинг 2-37. Скрипт Md5ScanProtection.au3
 1 #include <Crypt.au3>
 2 
 3 global const $kLogFile = "debug.log"
 4 global const $kCheckMd5[2] = ["0x3E4539E7A04472610D68B32D31BF714B", _
 5                               "0xD960F13A44D3BD8F262DF625F5705A63"]
 6 
 7 func LogWrite($data)
 8     FileWrite($kLogFile, $data & chr(10))
 9 endfunc
10 
11 func _ProcessGetLocation($pid)
12     local $proc = DllCall('kernel32.dll', 'hwnd', 'OpenProcess', 'int', _
13                           BitOR(0x0400, 0x0010), 'int', 0, 'int', $pid)
14 
15     if $proc[0] = 0 then
16         return ""
17     endif
18 
19     local $struct = DllStructCreate('int[1024]')
20     DllCall('psapi.dll', 'int', 'EnumProcessModules', 'hwnd', $proc[0], _
21             'ptr', DllStructGetPtr($struct), 'int', _
22             DllStructGetSize($struct), 'int_ptr', 0)
23 
24     local $return = DllCall('psapi.dll', 'int', 'GetModuleFileNameEx', _
25         'hwnd', $proc[0], 'int', DllStructGetData($struct, 1), _
26         'str', '', 'int', 2048)
27 
28     if StringLen($return[3]) = 0 then
29         return ""
30     endif
31 
32     return $return[3]
33 endfunc
34 
35 func ScanProcess()
36     local $processList = ProcessList()
37 
38     for $i = 1 to $processList[0][0]
39         local $path = _ProcessGetLocation($processList[$i][1])
40         local $md5 = _Crypt_HashFile($path, $CALG_MD5)
41         LogWrite("Name: " & $processList[$i][0] & " PID: " & _
42                  $processList[$i][1] & " Path: " & $path & " md5: " & $md5)
43 
44         for $j = 0 to Ubound($kCheckMd5) - 1
45             if $md5 == $kCheckMd5[$j] then
46                 MsgBox(0, "Alert", "Обнаружен бот-кликер!")
47             endif
48         next
49     next
50 endfunc
51 
52 while true
53     ScanProcess()
54     Sleep(5000)
55 wend

Рассмотрим скрипт Md5ScanProtection.au3 подробнее. Весь алгоритм обнаружения бота реализован в функции ScanProcess, которая вызывается в цикле while каждые пять секунд. В ней читается список запущенных процессов с помощью AutoIt-вызова ProcessList. Его результат сохраняется в переменную processList. После этого цикл for проходит по полученному списку. Для каждого его элемента функция _ProcessGetLocation читает путь к исполняемому файлу, машинный код которого был загружен в память процесса. Полученный путь передаётся в AutoIt-функцию _Crypt_HashFile, которая считает хэш-сумму по содержимому всего файла. На заключительном шаге алгоритма происходит сравнение рассчитанной хэш-суммы с искомыми значениями из глобального массива kCheckMd5. В нашем примере этот массив содержит MD5-суммы файлов SimpleBot.exe и AutoHotKey.exe.

Рассмотрим функцию _ProcessGetLocation. В ней происходит три WinAPI-вызова через AutoIt обёртку DllCall:

  1. OpenProcess
  2. EnumProcessModules
  3. GetModuleFileNameEx

Первый вызов OpenProcess возвращает дескриптор процесса по его идентификатору. С помощью дескриптора можно запросить дополнительную информацию о процессе через WinAPI.

Следующая функция EnumProcessModules читает список модулей процесса в массив struct. Обычно процесс состоит из нескольких модулей. Каждый из них содержит машинный код исполняемого файла или динамической библиотеки DLL. Этот код загружается в память процесса при старте. Первый модуль в списке всегда соответствует исполняемому файлу. Его мы и передаём в функцию GetModuleFileNameEx. Она извлекает из метаинформации модуля путь к соответствующему ему файлу.

Попробуйте запустить скрипт Md5ScanProtection.au3 и оба варианта бота: SimpleBot.ahk и SimpleBot.exe. Новый алгоритм должен их обнаружить.

Может случиться так, что скрипт SimpleBot.ahk не будет обнаружен. Это означает, что ваша версия интерпретатора AutoHotKey отличается от моей. Чтобы это исправить, добавьте в массив kCheckMd5 его хэш-сумму. Вы можете узнать её из лог-файла debug.log с отладочной информацией. В него пишутся все прочитанные защитой Md5ScanProtection.au3 процессы и их MD5-суммы.

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

  1. Сделать незначительное изменение в коде скрипта SimpleBot.ahk (например, уменьшить задержку на пару миллисекунд) и скомпилировать его по новой.
  2. Изменить заголовок с метаинформацией исполняемого файла AutoHotKey.exe. Для этого можно воспользоваться редактором HT Editor.

Изменение машинного кода, записанного в исполняемый файл, чревато повреждением приложения. В этом случае оно завершится с ошибкой при старте. Но метаинформация, хранимая в заголовке COFF (Common Object File Format), не так чувствительна к изменениям. У заголовка есть несколько стандартных полей. Одно из них – время создания файла. Очевидно, изменение этого поля никак не повлияет на функциональность приложения. В то же время исправленное время создания файла приведёт к другому результату расчёта MD5-суммы. В результате алгоритм защиты Md5ScanProtection.au3 не сможет обнаружить бота.

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

  1. Запустите приложение HT Editor с правами администратора. Для удобства скопируйте сначала исполняемый файл редактора в папку к AutoHotKey.exe.
  2. В окне редактора нажмите клавишу F3, чтобы вызвать диалог открытия файла (“open file”).
  3. Нажмите клавишу Tab, чтобы перейти к списку файлов (“files”). Найдите в нём AutoHotKey.exe и нажмите Enter.
  4. Нажмите F6, чтобы открыть диалог выбора режима редактирования (“select mode”). В нём включите режим “- pe/header”. После этого вы увидите список заголовков файла AutoHotKey.exe.
  5. Выберите пункт “COFF header” и нажмите Enter. Перейдите на поле “time-data stamp” заголовка.
  6. Нажмите F4 для редактирования поля и измените его. Иллюстрация 2-15 демонстрирует эту операцию.
  7. Нажмите F4 и выберите вариант “Yes” в диалоге подтверждения сохранения изменений.
Иллюстрация 2-15. Изменение времени создания файла в HT editor

В результате мы получим файл AutoHotKey.exe, содержание которого отличается от исходного. Попробуйте запустить его и указать нашего бота SimpleBot.ahk в диалоге открытия скрипта. Алгоритм защиты Md5ScanProtection.au3 не сможет его обнаружить.

Можно исправить алгоритм защиты так, чтобы он игнорировал все заголовки исполняемого файла при подсчёте хэш-суммы. Тогда разработчику бота придётся менять его машинный код. Альтернативное решение для усиления защиты – считать MD5 не для всего содержания файла, а только для небольшого набора байтов из строго определённого места. В этом случае для обхода алгоритма надо будет точно знать это место.

Проверка состояния клавиатуры

Windows предоставляет механизм уровня ядра, который позволяет отличить реальное нажатие клавиши от симулируемого. Рассмотрим, как можно использовать этот механизм для обнаружения кликеров.

Прежде всего, мы должны перехватить событие нажатия клавиши на низком уровне. В этом нам поможет WinAPI-вызов SetWindowsHookEx. Принцип его работы похож на функцию AutoIt HotKeySet: он устанавливает обработчик для различных типов событий ОС. Первый входной параметр SetWindowsHookEx определяет этот тип. В нашем случае он должен быть равен WH_KEYBOARD_LL, что соответствует событиям клавиатуры.

Теперь мы должны реализовать функцию-обработчик события. Она получает входным параметром структуру типа KBDLLHOOKSTRUCT, которая содержит полную информацию о перехваченном событии. У этой структуры есть поле flags. Если в нём присутствует флаг (то есть бит в определённой позиции) LLKHF_INJECTED, перехваченное нажатие клавиши было симулировано WinAPI-функцией SendInput или keybd_event. Если флага LLKHF_INJECTED нет, источником события является клавиатура. Подменить поле flags структуры KBDLLHOOKSTRUCT достаточно сложно, поскольку оно выставляется на уровне ядра ОС.

Скрипт KeyboardCheckProtection.au3 из листинга 2-38 демонстрирует проверку флага LLKHF_INJECTED.

Листинг 2-38. Скрипт KeyboardCheckProtection.au3
 1 #include <WinAPI.au3>
 2 
 3 global const $kLogFile = "debug.log"
 4 global $gHook
 5 
 6 func LogWrite($data)
 7     FileWrite($kLogFile, $data & chr(10))
 8 endfunc
 9 
10 func _KeyHandler($nCode, $wParam, $lParam)
11     if $nCode < 0 then
12         return _WinAPI_CallNextHookEx($gHook, $nCode, $wParam, $lParam)
13     endIf
14 
15     local $keyHooks = DllStructCreate($tagKBDLLHOOKSTRUCT, $lParam)
16 
17     LogWrite("_KeyHandler() - keyccode = " & DllStructGetData($keyHooks, "vkCode"));
18 
19     local $flags = DllStructGetData($keyHooks, "flags")
20     if $flags = $LLKHF_INJECTED then
21         MsgBox(0, "Alert", "Обнаружен бот-кликер!")
22     endif
23 
24     return _WinAPI_CallNextHookEx($gHook, $nCode, $wParam, $lParam)
25 endfunc
26 
27 func InitKeyHooks($handler)
28     local $keyHandler = DllCallbackRegister($handler, "long", "int;wparam;lparam")
29     local $hMod = _WinAPI_GetModuleHandle(0)
30     $gHook = _WinAPI_SetWindowsHookEx($WH_KEYBOARD_LL, _
31         DllCallbackGetPtr($keyHandler), $hMod)
32 endfunc
33 
34 InitKeyHooks("_KeyHandler")
35 
36 while true
37     Sleep(10)
38 wend

Алгоритм назначения обработчика нажатий клавиш похож на тот, который мы применяли в скриптах TimeSpanProtection.au3 и ActionSequenceProtection.au3. Только в данном случае мы делаем вызов WinAPI через AutoIt-обёртку _WinAPI_SetWindowsHookEx в функции InitKeyHooks. Таким образом мы инициализируем обработчик _KeyHandler, который будет перехватывать все события клавиатуры.

Функция InitKeyHooks выполняет следующие шаги:

  1. Регистрирует обработчик _KeyHandler через AutoIt-функцию DllCallbackRegister. Это позволит передать его в вызовы WinAPI.
  2. Читает в переменную hMod дескриптор первого модуля (нумерация начинается с нуля) текущего процесса через обёртку _WinAPI_GetModuleHandle. Не забудьте, что наш скрипт выполняется в интерпретаторе AutoIt.
  3. Добавляет _KeyHandler в цепочку обработчиков через WinAPI-вызов SetWindowsHookEx. В неё мы должны передать дескриптор модуля, в котором этот обработчик реализован. В нашем случае это переменная hMod.

Алгоритм проверки флага LLKHF_INJECTED в обработчике _KeyHandler выглядит следующим образом:

  1. Проверить значение параметра nCode. Если оно меньше нуля, мы передаём событие дальше по цепочке обработчиков. В этом случае оно не содержит нужной нам структуры KBDLLHOOKSTRUCT.
  2. Если параметр nCode не равен нулю, вызвать функцию DllStructCreate и передать в неё lParam. Таким образом мы получаем структуру KBDLLHOOKSTRUCT.
  3. Прочитать поле flags из KBDLLHOOKSTRUCT с помощью функции DllStructGetData.
  4. Проверить наличие флага LLKHF_INJECTED. Если он присутствует, нажатие клавиши было симулировано ботом.

Для тестирования защиты KeyboardCheckProtection.au3 запустите Notepad и бота SimpleBot.au3. Как только он выполнит первое нажатие клавиши, вы увидите сообщение об его обнаружении.

Есть несколько способов обойти подобную защиту. Для этого надо симулировать нажатия так, чтобы ядро ОС воспринимало их идущими от клавиатуры. Эти способы следующие:

  1. Использовать виртуальную машину (virtual machine или VM).
  2. Использовать специальный драйвер клавиатуры вместо WinAPI-функций SendInput и keybd_event для симуляции нажатий. Пример такого драйвера – InpOut32.
  3. Эмулировать клавиатуру или мышь на специальном устройстве. Мы рассмотрим этот подход в пятой главе.

Самый простой в реализации вариант – использование виртуальной машины. У неё есть виртуальные драйверы устройств. Они решают две задачи: эмулируют устройства для гостевой ОС (запущенной внутри VM) и предоставляют доступ к реальным устройствам. Все события симулируемые на хост-системе (на которой запускается VM) и идущие от реальных устройств проходят через виртуальные драйверы. Из-за этого гостевая ОС не может отличить их источник. Поэтому симулируемые ботом нажатия не будут иметь флага LLKHF_INJECTED.

Для запуска VM и нашего тестового бота выполните шаги:

  1. Установите одну из следующих виртуальных машин:
    * Virtual Box
    * VMWare Player
    * Windows Virtual PC
  2. Установите Windows в качестве гостевой ОС.
  3. Запустите в ней Notepad и скрипт KeyboardCheckProtection.au3.
  4. Запустите скрипт VirtualMachineBot.au3 на хост-системе.

Скрипт VirtualMachineBot.au3 из листинга 2-39 представляет адаптированную версию нашего бота.

Листинг 2-39. Скрипт VirtualMachineBot.au3
 1 Sleep(2000)
 2 
 3 while true
 4     Send("a")
 5     Sleep(1000)
 6     Send("b")
 7     Sleep(2000)
 8     Send("c")
 9     Sleep(1500)
10 wend

Скрипт VirtualMachineBot.au3 отличается от SimpleBot.au3 процедурой переключения на окно Notepad. Теперь бот не может самостоятельно его найти, поскольку Notepad запущен на гостевой ОС. Мы добавили двухсекундную задержку после старта скрипта, чтобы у вас было время переключиться на окно VM и Notepad внутри неё. Алгоритм защиты KeyboardCheckProtection.au3 не сможет обнаружить скрипт VirtualMachineBot.au3.

Выводы

Мы рассмотрели методы обнаружения кликеров. Каждый из них имеет свои достоинства и недостатки. Чтобы ваш бот смог обойти защиту, вы должны хорошо изучить её алгоритм. Следующие подходы помогут вам в этом:

  1. Перехват выполняемых защитой WinAPI-вызовов. Для этой цели подойдёт приложение API Monitor.
  2. Применение методов реверс-инжиниринга для изучения исполняемых файлов и DLL-библиотек системы защиты.
  3. Тестирование различных механизмов симуляции нажатий клавиш. Это поможет выяснить, на что именно реагирует защита.

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