D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Автор: Unseen
Специально для XSS.IS
Предыдущая часть: https://xss.is/threads/107381/
Вступление…
Приветствую всех участников нашего форума! В этой части статьи мы рассмотрим один из важных аспектов создания читов для компьютерных игр - сигнатурный поиск(signature scanning) / поиск по паттернам(pattern scanning) / поиск по шаблонам байт(AOB - Array of byte) Scan – Называйте как хотите. Мы углубимся в эту тему, выяснив, как этот метод может быть полезен для разработчиков читов, и какие преимущества он предоставляет в процессе создания программного обеспечения для взлома игр.
Предыстория…
На прошлых частях(№: 3,4,5) мы в основном учились находить смещения(оффсеты) при помощи разных техник. Научились при помощи них вычислять искомые адреса тех или иных переменных (Здоровья, брони, патронов и тд) в игре. И сегодня предлагаю пополнить уже имеющийся список наших техник по поиску смещений(оффсетов) новой техникой.
Снова проблемы…
При создании читов для игр одной из основных задач кодера(нас с вами) является поиск в памяти адресов, соответствующих определенным значениям, таким как количество здоровья, количество патронов и т. д. Традиционный метод поиска адресов на основе найденных оффсетов вручную часто ведет к нестабильным результатам. Помимо этого, мелкие обновления игр(Разработчик решил добавить новое или поменять переменные-поля местами в структуре) и античиты могут привести к тому, что найденные оффсеты становятся недействительными, что делает наш чит неработоспособным. А каждый раз их вычислять занимает время и вообще true программист должен(а может не должен
) стремиться автоматизировать такие нудные процессы! Для хорошего продукта нужна стабильность в работе и скорость в решении проблем при возникновении их.
Новое решение…
Идея поиска самих смещений(оффсетов) на основе сигнатурного поиска представляет собой решение этой проблемы. Вместо того, чтобы просто искать адреса исходя из найденных вручную смещений(оффсетов), мы можем использовать сигнатурный поиск для нахождения этих самих оффсетов динамически, исходя из уникальных шаблонов байтов в памяти процесса.
Сигнатурный поиск (signature scanning) - это метод поиска в памяти процесса определенных шаблонов байтов, которые являются уникальными для конкретного объекта или данных. Этот метод часто используется в области компьютерных игр и программирования читов (cheat), а также в программировании вредоносного ПО и обратной разработке для поиска и изменения значений в памяти процессов. Сигнатурный поиск очень популярен и в том же антивирусе, большинство антивирусных программ используют технологию сигнатурного анализа для обнаружения вредоносных программ, в том числе и вирусов. Когда на малварь вещают сигнатуру.
Сигнатура/Паттерн - это уникальная характеристика или подпись, представляющая собой определенные байтовые последовательности или другие характеристики кода программы. В свою очередь мы можем это использовать и в своих целях, например найти уникальный паттерн в модуле игры и на основе этого паттерна вычислить нужные смещения(оффсеты). Ведь, как мы знаем, что любая программа - это скомпилированный двоичный код и двоичный код может быть представлен в виде байтов. Давайте рассмотрим этот скомпилированный образ программы на примере.
Пример:
Спойлер: Иллюстрация
А паттерн/сигнатура это и есть какой-то уникальный, не повторяющиеся еще в каком-либо участке последовательный массив байт, отрезок можно так сказать. В 16-ном счислении(HEX format). Отметим, что длина сигнатуры может быть любая, но главное не надо сильно фанатеть при создании их =) То есть нам потребуется искать в образе скомпилированной программы или загруженной в данный момент в ОЗУ образе программы уникальный набор байтов, которые называются сигнатурой или же паттерн. Ниже хорошая иллюстрация нашего разговора.
Пример:
Спойлер: Иллюстрация
Принцип работы сигнатурного поиска заключается в том, что вы создаете шаблон байтов, представляющих определенное состояние памяти, которое вы хотите найти в процессе. Затем этот шаблон сравнивается с содержимым памяти процесса, чтобы найти соответствующие участки памяти. Практически мы работаем на уровне байт-кода и все манипуляции будут происходить условно с байтами. Конечно, сигнатурный поиск тоже не идеален, но оффсеты найденные при помощи них будут жить дольше и избавят вас от нудный работы, а именно поиска оффсетов ручками. Обычно этих сигнатур хватает на 3-4 обновления игры, если они не такие серьезные. Лучше раз в неделю искать сигнатуры, чем каждый день надр*чивать руками оффсеты.
Шаблон байтов / сигнатуры / паттерны могут быть сгенерированы, например, путем анализа статического кода программы или путем наблюдения за изменениями в памяти в реальном времени при выполнении программы.
Сигнатурные паттерны могут быть различными по своему составу и характеру, в зависимости от того, что вы ищете и какую информацию вы знаете о целевом объекте в памяти процесса. Вот несколько типов сигнатурных паттернов:
Чтобы продемонстрировать вам, более наглядно, что же из себя представляет сигнатурный поиск и сама сигнатура/паттерн на более простом языке, предлагаю рассмотреть картинку ниже.
P.S. Картинка несет чисто иллюстрационный характер и не надо воспринимать это как содержимое памяти реальной программы!
Пример:
Спойлер: Иллюстрация
Представим, у нас есть память процесса и его байт содержимое будет, как на картинке и мы знаем, что нужные нам данные хранятся в виде байтов - 1 1 1 и расположены примерно в середине памяти. Но! В памяти такой набор байтов будет слишком часто встречаться и здесь нам нужно включить логику и хорошенько проанализировать эту память. Чтобы найти более вариант получше, что бы сделало эти байты уникальными при поиске, предлагаю вариант взять байты 2 1 1 1, но такой набор байтов у нас имеется и выделены они синим, и красным(не считая байты 3 3) условно, то есть - повторяются. А что если взять набор байтов из 2 1 1 1 1 3 3, и да наша с вами теория окажется верной, такой паттерн будет уникальным во всей памяти и мы нехитрым способом сможем вычислить адрес расположения этих самих искомых нами байтов - 1 1 1. Конечно мы могли бы разметить и такой паттерн, как 1 2 1 1 1 1 3 3, но наш алгоритм сигнатурного поиска, который будет проводить все вычисления при поиске окажется намного медленнее. Причина оказывается очень простой, он будет с начала адресного пространства процесса будет идти и сравнивать каждый байт из памяти самого процесса с нашими байтами из найденного паттерна. А здесь первый байт в нашем паттерне равен 1 и он часто будет встречаться в памяти и от этого будет падать скорость алгоритма сигнатурного поиска. Да-да ох уж эти алгоритмы всякие надоели )) Возможно мы могли взять и такой паттерн, как 2 1 1 1 1 3 3 1 2 или вообще замахнуться и разметить такой паттерн 2 1 1 1 1 3 3 1 2 1 1 2 2 3 3 3.
Пример:
Спойлер: Иллюстрация
Спойлер: Иллюстрация
Они все будут уникальны, но сутью было просто рассказать, что нужно найти золотую середину при генерации сигнатуры и не фанатеть, как говорил ранее =) И тогда все будет хорошо!
Выше у нас была простая иллюстрация с фиксированным паттерном, но на деле у нас совсем другие ситуации. Как помним, данные в памяти имеют свойство меняться, то есть допустим те же патроны или уровень здоровья, во время игры они динамичны, то убывают, то увеличиваются! Как тогда размечать для них паттерны? Все просто, мы можем написать алгоритм сигнатурного поиска, таким образом, чтобы некоторое количество байт не нужно было сравнивать, то есть пропускать. Ведь раз байты в каком-то месте будут динамичные и мы не будем знать их текущее «Состояние», то будем просто при сравнении пропускать эти позиции. То есть наш паттерн будет «Маскированным». Предлагаю более детально рассмотреть это на иллюстрации:
Пример:
Спойлер: Иллюстрация
Допустим мы имеем ту же память процесса и при анализе(реверсе) этого участка памяти, мы пришли к выводу, что нужные нам байты хранятся рядом с байтами 2 1 и 3 3, но искомые эти байты динамичны и не имеют одного «Состояния», значит чтобы вычислить их, мы можем точно также взять паттерн 2 1 ? ? ? 3 3 или же 2 1 0 0 0 3 3. Динамичные байты можем размечать, как угодно, кто-то знаками вопроса, кто-то просто нулями, смысл от этого не меняется, главное мы знаем относительное их расположение, то есть эти байты идут после байтов 2 1 или же перед байтами 3 3, и при написании алгоритма можем легко учесть и не сравнивать эти байты конкретно.
Значит наш алгоритм, начнет сравнивание первого байта 2 с нашим байтом 2 из найденного паттерна, далее идет сравнивание 1 и 1, если все верно, то далее сравниваем следующий байт после 1 и если окажется, что сравниваемый байт из паттерна 0 или же ?, то условие все же верно и идем дальше. Вот такой вот простой алгоритм. Надеюсь на простых примерах смог передать суть идеи!
Практическая часть…
Теперь предлагаю взять нашего подопытного -AssaultCube и рассмотреть на нем все сказанное выше. Нашей целью будет поиск тех самых оффсетов, которые могут измениться после обновления. Запустите игру и CheatEngine, затем подключитесь к игре, далее нам нужно будет найти инструкции ассемблера, которые оперируют этими оффсетами для получения доступа к тем или иным данным, в нашем случае здоровье игрока. Значит сначала находим адрес здоровья(Как делали на прошлых частях), далее ловим те самые инструкции:
Спойлер: Скриншот
После того, как нашли адрес – кликаем на нее ПКМ и «Find out what accesses this address». Далее откроется окошко с инструкциями:
Спойлер: Скриншот
Допустим давайте возьмем первую mov ecx, [ebx + 000000EC], как знаем 0xEC это наш оффсет для здоровья и эта инструкция нас устраивает, далее нажимаем «Дизассемблировать». И у нас откроется обозреватель памяти:
Спойлер: Скриншот
Как видим здесь, у нас показаны байтовые представления инструкций и сами команды ассемблера. Как нам известно из прошлых частей про ассемблер, что его инструкциям соответствуют определенные байты. Раз мы знаем, что байты этих инструкций будут статичны, то есть жестко зависят от архитектуры процессора, то они врядли будут меняться и мы можем на основе них сгенерировать сигнатуру/паттерн при помощи которой сможем и вычислять наш оффсет 0xEC. Давайте возьмем байтовое представление инструкции это у нас 8B 8B EC000000. Здесь 8B 8B это сама наша инструкция, а EC000000 это наше смещение(оффсет) и логично предположить, что после обновления это смещение изменится, но зато сама инструкция всегда будет содержать актуальное смещение, а это нам на руку! Возможно, после обновления игры, разработчик добавит новую переменную перед нашей переменной здоровья и соответственно смещение для здоровья уже будет другим, но после перекомпиляции(пересборки) игры, в этой инструкции будет содержаться новое смещение, ведь логика игры не менялась!!! И поэтому сигнатурный поиск будет здорово нас выручать. Конечно, не все вечно, но все же лучше так, чем ничего)))
Раз суть ясна, то как же создать сигнатуру? Ну вариантов много, можно ручками сделать, но потребуется время на анализ памяти. Допустим возьмем байты этой инструкции 8B 8B EC000000, здесь как выяснили EC000000 будет динамичным, то есть имеется шанс, что после обновы это смещение будет другим и значит нам надо сгенерировать «Маскированный паттерн», что как раз делали выше на простых примерах. Это будет выглядеть примерно как 8B 8B 00 00 00 00 или же 8B 8B ?? ?? ?? ?? ну суть поняли, все зависит от того, как будет реализован алгоритм поиска. Но этот паттерн скорее будет не стабильным и уникальным. Ведь в памяти много инструкций вида 8B 8B.
Есть второй вариант, он более прост в использовании и быстрый. Это использование утилиты Sigmaker. Он идет в виде плагина для программ отладки, таких как IDA PRO или же OllyDBG.
Для нашей статьи я взял второй, так как он более прост в использовании для нашей цели. Надеюсь, что с установкой OllyDBG не составит для вас труда.
Ссылка на скачивание OllyDBG - Здесь
А ссылка на скачивание плагина Sigmaker Здесь
Как устанавливать плагины?
Для начала необходимо создать в директории программы папку с названием Plugins. Это позволит в дальнейшем избежать замусоривания основной директории, а в случае необходимости без труда отыскать неработоспособный/конфликтный плагин. Далее копируем DLL-файл плагина в созданную директорию, после чего запускаем отладчик и указываем путь к этой папке:
Спойлер: Скриншот
Далее нам уже CheatEngine не понадобится, можете закрывать его. И да, когда одновременно попытайтесь подключится к одному процессу с OllyDBG и CheatEngine – возникнет конфликт, так что подключайтесь к процессу, когда необходимо. Далее, запускаем(От имени админа) отладчик и подключаемся к процессу игры, через «File» и далее «Attach». В окошке выбираем сам процесс игры – ac_client.exe Дальше у нас все грузится – подождите минуту. Далее в окне дизассемблера кликаем ПКМ на пустом месте, далее «Go To» и «Expression» ну или же Ctrl + G. В поле ввода вставляем адрес, по которому нужно перейти. И у нас рисуется картина похожая на ту из CheatEngine.
Спойлер: Скриншот
Чтобы сгенерировать сигнатуру при помощи плагина Sigmaker, достаточно выделить несколько байтов, давайте возьмем начало паттерна от верхней инструкции и до следующей инструкции после нашей. При помощи Shift выделите этот участок.
Спойлер: Скриншот
Получается это байты 74 97 8B 8B EC 00 00 00 83 F9 19. Вот такой массив байт. Далее для генерации требуется кликнуть по выделенным байтам ПКМ и «MakeSig» далее «Test Sig».
Спойлер: Скриншот
Далее у нас откроется окошко:
Спойлер: Скриншот
Здесь у нас имеется сама сигнатура \x74\x97\x8B\x8B\x00\x00\x00\x00\x83\xF9\x19, как могли заметить плагин автоматически заменил нулями, те части, которые могут быть динамичными. В нашем случае он заменил как раз то смещение для здоровья – EC 00 00 00. 4 байта. У нас «Маскированный паттерн» получается на выходе. Здесь еще есть кнопка «Scan», чтобы проверить уникальность данного паттерна. Нажимаем на нее и видим, что сигнатура обнаружена только по одному адресу и является уникальной, для наших целей она годится!
Теперь, когда мы нашли сигнатуру, то самое время написать алгоритм сигнатурного поиска. Я ее напишу в виде отдельной программы и вы с легкостью сможете «интегрировать» в наш проект из предыдущих частей! Сразу отмечу, реализаций сигнатурного поиска куча, у всех свои подходы и если немного изучите WinApi, то с легкостью можете написать свой алгоритм со своими «фишками» при поиске =)
Спойлер: Весь исходный код программы
C++: Скопировать в буфер обмена
Итак, наш код выполняет следующие действия:
Давайте сразу перейдем к самой функции SignatureScanner и я попытаюсь объяснить, что происходит в этом алгоритме. По сути, мы получаем информацию об начальном адресе модуля, информацию о его размере. То есть если нам известен начальный адрес памяти, то логично будет пройтись циклом по всему модулю, от начальных до конечных адресов памяти. И считывать каждый байт в текущем адресе, затем делать сравнение с байтом из нашего массива сигнатуры. Если первый байт совпал, значит увеличим счетчик counter и сделаем сравнение следующих байтов и так до тех пор, пока мы не сравним все байты и если, счетчик counter будет равняться к размеру сигнатуры – pattern_size, то найдем адрес сигнатуры, иначе сообщим, что сигнатура не найдена и вернем 0. Давайте взглянем, что делает наш алгоритм:
Спойлер: Скриншот
Наш алгоритм начиная с базового адреса, обычно как мы знаем он равен 400000, начинает сравнивать содержимое адреса с первым байтом массива сигнатуры, а именно 0x74. Если нет совпадения идем к следующему адресу 400001 и делаем сравнение с его содержимым и так до конца адресного пространства или же размера модуля. Если обнаружится первое совпадение увеличим счетчик и начнем сравнивать второй байт из нашего массива со следующим содержимым адреса. И так до конца.
Еще один немаловажный момент, раз мы работаем на уровне байт-кода, то найденный адрес сигнатуры будет равняться адресу первого байта в сигнатуре и нашей целью было получить адрес, в котором будет хранится смещение для здоровья и для того, чтобы получить верные результаты, нам нужно вычислить их путем прибавления к найденному адресу смещение 4, ведь наши искомые байты находятся после 4 байт в памяти. Давайте взглянем на это в самой памяти:
Спойлер: Скриншот
Как видим, на скриншоте изображен участок памяти, как раз с нашей сигнатурой. Наш алгоритм в качестве результата вернул бы адрес начала этой сигнатуры, а это на минуточку адрес байта 0x74(Красное выделение). А начало нашего оффсета 0xEC находится совсем по другому адресу , значит по логике мы должны прибавить к найденному адресу, не достающие байты, чтобы в итоге получить нужный нам адрес, в котором будет и наш оффсет для здоровья 0xEC. В данном случае нужно сместиться на 4 байта, и мы получаем нужный нам адрес. В коде этот момент написан как offset_from_offset и передаем туда мы 4.
Давайте скомпилируем нашу программу и запустим!
Спойлер: Скриншот
Как можем наблюдать, все у нас работает! Наш сигнатурный поиск вычислил при помощи паттерна нужный нам адрес и вывел его значение – 0xEC. По сути манипулируя байтами, мы можем сделать много чего, но оставим это на будущее! Попробуйте проделать то же самое и с смещениями патронов. Такая техника поиска оффсетов сильно упрощает поддержку продукта и у нас появляется лишнее время на его совершенствование. Как отмечалось ранее, этот метод очень популярен во многих продуктах)))
Ну что же, кто дошел до конца статьи и сделал какие-либо открытия для себя – Я вас поздравляю! Буду рад, если оставите свой комментарий под этим постом, что было вам понятно или наоборот недопоняли. До следующей части!
Специально для XSS.IS
Предыдущая часть: https://xss.is/threads/107381/
Вступление…
Приветствую всех участников нашего форума! В этой части статьи мы рассмотрим один из важных аспектов создания читов для компьютерных игр - сигнатурный поиск(signature scanning) / поиск по паттернам(pattern scanning) / поиск по шаблонам байт(AOB - Array of byte) Scan – Называйте как хотите. Мы углубимся в эту тему, выяснив, как этот метод может быть полезен для разработчиков читов, и какие преимущества он предоставляет в процессе создания программного обеспечения для взлома игр.
Предыстория…
На прошлых частях(№: 3,4,5) мы в основном учились находить смещения(оффсеты) при помощи разных техник. Научились при помощи них вычислять искомые адреса тех или иных переменных (Здоровья, брони, патронов и тд) в игре. И сегодня предлагаю пополнить уже имеющийся список наших техник по поиску смещений(оффсетов) новой техникой.
Снова проблемы…
При создании читов для игр одной из основных задач кодера(нас с вами) является поиск в памяти адресов, соответствующих определенным значениям, таким как количество здоровья, количество патронов и т. д. Традиционный метод поиска адресов на основе найденных оффсетов вручную часто ведет к нестабильным результатам. Помимо этого, мелкие обновления игр(Разработчик решил добавить новое или поменять переменные-поля местами в структуре) и античиты могут привести к тому, что найденные оффсеты становятся недействительными, что делает наш чит неработоспособным. А каждый раз их вычислять занимает время и вообще true программист должен(а может не должен

Новое решение…
Идея поиска самих смещений(оффсетов) на основе сигнатурного поиска представляет собой решение этой проблемы. Вместо того, чтобы просто искать адреса исходя из найденных вручную смещений(оффсетов), мы можем использовать сигнатурный поиск для нахождения этих самих оффсетов динамически, исходя из уникальных шаблонов байтов в памяти процесса.
Сигнатурный поиск (signature scanning) - это метод поиска в памяти процесса определенных шаблонов байтов, которые являются уникальными для конкретного объекта или данных. Этот метод часто используется в области компьютерных игр и программирования читов (cheat), а также в программировании вредоносного ПО и обратной разработке для поиска и изменения значений в памяти процессов. Сигнатурный поиск очень популярен и в том же антивирусе, большинство антивирусных программ используют технологию сигнатурного анализа для обнаружения вредоносных программ, в том числе и вирусов. Когда на малварь вещают сигнатуру.
Сигнатура/Паттерн - это уникальная характеристика или подпись, представляющая собой определенные байтовые последовательности или другие характеристики кода программы. В свою очередь мы можем это использовать и в своих целях, например найти уникальный паттерн в модуле игры и на основе этого паттерна вычислить нужные смещения(оффсеты). Ведь, как мы знаем, что любая программа - это скомпилированный двоичный код и двоичный код может быть представлен в виде байтов. Давайте рассмотрим этот скомпилированный образ программы на примере.
Пример:
Спойлер: Иллюстрация
А паттерн/сигнатура это и есть какой-то уникальный, не повторяющиеся еще в каком-либо участке последовательный массив байт, отрезок можно так сказать. В 16-ном счислении(HEX format). Отметим, что длина сигнатуры может быть любая, но главное не надо сильно фанатеть при создании их =) То есть нам потребуется искать в образе скомпилированной программы или загруженной в данный момент в ОЗУ образе программы уникальный набор байтов, которые называются сигнатурой или же паттерн. Ниже хорошая иллюстрация нашего разговора.
Пример:
Спойлер: Иллюстрация
Принцип работы сигнатурного поиска заключается в том, что вы создаете шаблон байтов, представляющих определенное состояние памяти, которое вы хотите найти в процессе. Затем этот шаблон сравнивается с содержимым памяти процесса, чтобы найти соответствующие участки памяти. Практически мы работаем на уровне байт-кода и все манипуляции будут происходить условно с байтами. Конечно, сигнатурный поиск тоже не идеален, но оффсеты найденные при помощи них будут жить дольше и избавят вас от нудный работы, а именно поиска оффсетов ручками. Обычно этих сигнатур хватает на 3-4 обновления игры, если они не такие серьезные. Лучше раз в неделю искать сигнатуры, чем каждый день надр*чивать руками оффсеты.
Шаблон байтов / сигнатуры / паттерны могут быть сгенерированы, например, путем анализа статического кода программы или путем наблюдения за изменениями в памяти в реальном времени при выполнении программы.
Сигнатурные паттерны могут быть различными по своему составу и характеру, в зависимости от того, что вы ищете и какую информацию вы знаете о целевом объекте в памяти процесса. Вот несколько типов сигнатурных паттернов:
- Фиксированный паттерн: Это наиболее простая форма сигнатурного паттерна, который состоит из фиксированной последовательности байтов. Этот паттерн используется, когда вы точно знаете, как выглядит объект в памяти и какие байты представляют его. То что было изображено на примере выше : )
- Относительный паттерн: В этом случае сигнатурный паттерн содержит некоторую фиксированную часть, за которой следует неизвестное количество байтов. Это позволяет найти объект в памяти, если его положение относительно известно, но точный адрес неизвестен.
- Маскированный паттерн: Это паттерн, в котором некоторые байты могут быть замаскированы, что означает, что они могут быть любыми значениями. Маскированный паттерн используется, когда вы хотите найти объект, игнорируя определенные байты.
- Шаблонный паттерн: Это более сложный тип сигнатурного паттерна, который позволяет описывать шаблон поиска, включая не только конкретные значения байтов, но и их относительное расположение, количество и другие характеристики.
- Паттерн с отступом: В этом случае сигнатура содержит не только непосредственно искомую последовательность байтов, но и некоторый отступ от начала этой последовательности. Это может быть полезно, если вы знаете, что объект находится рядом с определенным значением или последовательностью байтов.
Чтобы продемонстрировать вам, более наглядно, что же из себя представляет сигнатурный поиск и сама сигнатура/паттерн на более простом языке, предлагаю рассмотреть картинку ниже.

Пример:
Спойлер: Иллюстрация
Представим, у нас есть память процесса и его байт содержимое будет, как на картинке и мы знаем, что нужные нам данные хранятся в виде байтов - 1 1 1 и расположены примерно в середине памяти. Но! В памяти такой набор байтов будет слишком часто встречаться и здесь нам нужно включить логику и хорошенько проанализировать эту память. Чтобы найти более вариант получше, что бы сделало эти байты уникальными при поиске, предлагаю вариант взять байты 2 1 1 1, но такой набор байтов у нас имеется и выделены они синим, и красным(не считая байты 3 3) условно, то есть - повторяются. А что если взять набор байтов из 2 1 1 1 1 3 3, и да наша с вами теория окажется верной, такой паттерн будет уникальным во всей памяти и мы нехитрым способом сможем вычислить адрес расположения этих самих искомых нами байтов - 1 1 1. Конечно мы могли бы разметить и такой паттерн, как 1 2 1 1 1 1 3 3, но наш алгоритм сигнатурного поиска, который будет проводить все вычисления при поиске окажется намного медленнее. Причина оказывается очень простой, он будет с начала адресного пространства процесса будет идти и сравнивать каждый байт из памяти самого процесса с нашими байтами из найденного паттерна. А здесь первый байт в нашем паттерне равен 1 и он часто будет встречаться в памяти и от этого будет падать скорость алгоритма сигнатурного поиска. Да-да ох уж эти алгоритмы всякие надоели )) Возможно мы могли взять и такой паттерн, как 2 1 1 1 1 3 3 1 2 или вообще замахнуться и разметить такой паттерн 2 1 1 1 1 3 3 1 2 1 1 2 2 3 3 3.
Пример:
Спойлер: Иллюстрация
Спойлер: Иллюстрация
Они все будут уникальны, но сутью было просто рассказать, что нужно найти золотую середину при генерации сигнатуры и не фанатеть, как говорил ранее =) И тогда все будет хорошо!
Выше у нас была простая иллюстрация с фиксированным паттерном, но на деле у нас совсем другие ситуации. Как помним, данные в памяти имеют свойство меняться, то есть допустим те же патроны или уровень здоровья, во время игры они динамичны, то убывают, то увеличиваются! Как тогда размечать для них паттерны? Все просто, мы можем написать алгоритм сигнатурного поиска, таким образом, чтобы некоторое количество байт не нужно было сравнивать, то есть пропускать. Ведь раз байты в каком-то месте будут динамичные и мы не будем знать их текущее «Состояние», то будем просто при сравнении пропускать эти позиции. То есть наш паттерн будет «Маскированным». Предлагаю более детально рассмотреть это на иллюстрации:
Пример:
Спойлер: Иллюстрация
Допустим мы имеем ту же память процесса и при анализе(реверсе) этого участка памяти, мы пришли к выводу, что нужные нам байты хранятся рядом с байтами 2 1 и 3 3, но искомые эти байты динамичны и не имеют одного «Состояния», значит чтобы вычислить их, мы можем точно также взять паттерн 2 1 ? ? ? 3 3 или же 2 1 0 0 0 3 3. Динамичные байты можем размечать, как угодно, кто-то знаками вопроса, кто-то просто нулями, смысл от этого не меняется, главное мы знаем относительное их расположение, то есть эти байты идут после байтов 2 1 или же перед байтами 3 3, и при написании алгоритма можем легко учесть и не сравнивать эти байты конкретно.
Значит наш алгоритм, начнет сравнивание первого байта 2 с нашим байтом 2 из найденного паттерна, далее идет сравнивание 1 и 1, если все верно, то далее сравниваем следующий байт после 1 и если окажется, что сравниваемый байт из паттерна 0 или же ?, то условие все же верно и идем дальше. Вот такой вот простой алгоритм. Надеюсь на простых примерах смог передать суть идеи!
Практическая часть…
Теперь предлагаю взять нашего подопытного -AssaultCube и рассмотреть на нем все сказанное выше. Нашей целью будет поиск тех самых оффсетов, которые могут измениться после обновления. Запустите игру и CheatEngine, затем подключитесь к игре, далее нам нужно будет найти инструкции ассемблера, которые оперируют этими оффсетами для получения доступа к тем или иным данным, в нашем случае здоровье игрока. Значит сначала находим адрес здоровья(Как делали на прошлых частях), далее ловим те самые инструкции:
Спойлер: Скриншот
После того, как нашли адрес – кликаем на нее ПКМ и «Find out what accesses this address». Далее откроется окошко с инструкциями:
Спойлер: Скриншот
Допустим давайте возьмем первую mov ecx, [ebx + 000000EC], как знаем 0xEC это наш оффсет для здоровья и эта инструкция нас устраивает, далее нажимаем «Дизассемблировать». И у нас откроется обозреватель памяти:
Спойлер: Скриншот
Как видим здесь, у нас показаны байтовые представления инструкций и сами команды ассемблера. Как нам известно из прошлых частей про ассемблер, что его инструкциям соответствуют определенные байты. Раз мы знаем, что байты этих инструкций будут статичны, то есть жестко зависят от архитектуры процессора, то они врядли будут меняться и мы можем на основе них сгенерировать сигнатуру/паттерн при помощи которой сможем и вычислять наш оффсет 0xEC. Давайте возьмем байтовое представление инструкции это у нас 8B 8B EC000000. Здесь 8B 8B это сама наша инструкция, а EC000000 это наше смещение(оффсет) и логично предположить, что после обновления это смещение изменится, но зато сама инструкция всегда будет содержать актуальное смещение, а это нам на руку! Возможно, после обновления игры, разработчик добавит новую переменную перед нашей переменной здоровья и соответственно смещение для здоровья уже будет другим, но после перекомпиляции(пересборки) игры, в этой инструкции будет содержаться новое смещение, ведь логика игры не менялась!!! И поэтому сигнатурный поиск будет здорово нас выручать. Конечно, не все вечно, но все же лучше так, чем ничего)))
Раз суть ясна, то как же создать сигнатуру? Ну вариантов много, можно ручками сделать, но потребуется время на анализ памяти. Допустим возьмем байты этой инструкции 8B 8B EC000000, здесь как выяснили EC000000 будет динамичным, то есть имеется шанс, что после обновы это смещение будет другим и значит нам надо сгенерировать «Маскированный паттерн», что как раз делали выше на простых примерах. Это будет выглядеть примерно как 8B 8B 00 00 00 00 или же 8B 8B ?? ?? ?? ?? ну суть поняли, все зависит от того, как будет реализован алгоритм поиска. Но этот паттерн скорее будет не стабильным и уникальным. Ведь в памяти много инструкций вида 8B 8B.
Есть второй вариант, он более прост в использовании и быстрый. Это использование утилиты Sigmaker. Он идет в виде плагина для программ отладки, таких как IDA PRO или же OllyDBG.
Для нашей статьи я взял второй, так как он более прост в использовании для нашей цели. Надеюсь, что с установкой OllyDBG не составит для вас труда.
Ссылка на скачивание OllyDBG - Здесь
А ссылка на скачивание плагина Sigmaker Здесь
Как устанавливать плагины?
Для начала необходимо создать в директории программы папку с названием Plugins. Это позволит в дальнейшем избежать замусоривания основной директории, а в случае необходимости без труда отыскать неработоспособный/конфликтный плагин. Далее копируем DLL-файл плагина в созданную директорию, после чего запускаем отладчик и указываем путь к этой папке:
- OllyDBG v1.10: выбираем Options -> Appearance и закладку Directories. В пункте Plugin path указываем путь к папке с плагинами.
- OllyDBG v2.01h: выбираем Options -> Options и раздел Directories. В пункте Plugin directory указываем путь к папке с плагинами.
Спойлер: Скриншот
Далее нам уже CheatEngine не понадобится, можете закрывать его. И да, когда одновременно попытайтесь подключится к одному процессу с OllyDBG и CheatEngine – возникнет конфликт, так что подключайтесь к процессу, когда необходимо. Далее, запускаем(От имени админа) отладчик и подключаемся к процессу игры, через «File» и далее «Attach». В окошке выбираем сам процесс игры – ac_client.exe Дальше у нас все грузится – подождите минуту. Далее в окне дизассемблера кликаем ПКМ на пустом месте, далее «Go To» и «Expression» ну или же Ctrl + G. В поле ввода вставляем адрес, по которому нужно перейти. И у нас рисуется картина похожая на ту из CheatEngine.
Спойлер: Скриншот
Чтобы сгенерировать сигнатуру при помощи плагина Sigmaker, достаточно выделить несколько байтов, давайте возьмем начало паттерна от верхней инструкции и до следующей инструкции после нашей. При помощи Shift выделите этот участок.
Спойлер: Скриншот
Получается это байты 74 97 8B 8B EC 00 00 00 83 F9 19. Вот такой массив байт. Далее для генерации требуется кликнуть по выделенным байтам ПКМ и «MakeSig» далее «Test Sig».
Спойлер: Скриншот
Далее у нас откроется окошко:
Спойлер: Скриншот
Здесь у нас имеется сама сигнатура \x74\x97\x8B\x8B\x00\x00\x00\x00\x83\xF9\x19, как могли заметить плагин автоматически заменил нулями, те части, которые могут быть динамичными. В нашем случае он заменил как раз то смещение для здоровья – EC 00 00 00. 4 байта. У нас «Маскированный паттерн» получается на выходе. Здесь еще есть кнопка «Scan», чтобы проверить уникальность данного паттерна. Нажимаем на нее и видим, что сигнатура обнаружена только по одному адресу и является уникальной, для наших целей она годится!
Теперь, когда мы нашли сигнатуру, то самое время написать алгоритм сигнатурного поиска. Я ее напишу в виде отдельной программы и вы с легкостью сможете «интегрировать» в наш проект из предыдущих частей! Сразу отмечу, реализаций сигнатурного поиска куча, у всех свои подходы и если немного изучите WinApi, то с легкостью можете написать свой алгоритм со своими «фишками» при поиске =)
Спойлер: Весь исходный код программы
C++: Скопировать в буфер обмена
Код:
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
// Оффсет для структуры игрока
#define PlayerEntityOffset 0x18AC00
// Определение структуры TargetProc
struct TargetProc
{
private:
const wchar_t* p_name; // Приватное член-данные - указатель на имя процесса
public:
// Конструктор класса TargetProc
TargetProc(const wchar_t* p_name)
{
this->p_name = p_name; // Устанавливаем значение указателя на имя процесса
}
// Функция для получения PID процесса
int GetProcPid()
{
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); // Создаем снимок процессов
PROCESSENTRY32 proc; // Структура для информации о процессе
ZeroMemory(&proc, sizeof(proc)); // Обнуляем структуру
proc.dwSize = sizeof(proc); // Устанавливаем размер структуры
// Перебираем процессы
if (Process32First(hSnap, &proc))
{
do
{
if (wcscmp(proc.szExeFile, p_name) == 0) // Сравниваем имена процессов
{
CloseHandle(hSnap); // Закрываем дескриптор снимка процессов
return proc.th32ProcessID; // Возвращаем PID процесса
}
} while (Process32Next(hSnap, &proc)); // Получаем информацию о следующем процессе
}
}
// Функция для получения информации о модуле процесса по имени модуля
MODULEENTRY32 GetProcModuleInfo(const wchar_t* mod_name)
{
int pid = this->GetProcPid(); // Получаем PID процесса
// Создаем снимок модулей процесса
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
MODULEENTRY32 mod; // Структура для информации о модуле
ZeroMemory(&mod, sizeof(mod)); // Обнуляем структуру
mod.dwSize = sizeof(mod); // Устанавливаем размер структуры
// Перебираем модули процесса
if (Module32First(hSnap, &mod))
{
do
{
if (wcscmp(mod_name, mod.szModule) == 0) // Сравниваем имена модулей, если совпало:
{
CloseHandle(hSnap); // Закрываем дескриптор снимка модулей
return mod; // Возвращаем информацию о модуле
}
} while (Module32Next(hSnap, &mod)); // Получаем информацию о следующем модуле, пока не найдем модуль
}
}
};
// Функция для сканирования памяти процесса и поиска сигнатуры
DWORD SignatureScanner(BYTE* mod_buffer, DWORD mod_size, DWORD mod_base, BYTE* pattern, int pattern_size, DWORD offset_from_offset)
{
int counter = 0; // Счетчик совпадений сигнатуры
// Проходим по всему буферу модуля памяти
for (int i = 0; i < mod_size; i++)
{
// Проверяем, совпадает ли текущий байт с первым байтом сигнатуры
if (mod_buffer[i] == pattern[0] || pattern[0] == 0x00)
{
// Если совпадение найдено, начинаем проверять последующие байты сигнатуры
for (int p = 0; p < pattern_size; p++)
{
// Проверяем, совпадает ли текущий байт в буфере с текущим байтом сигнатуры
if (mod_buffer[i + p] == pattern[p] || pattern[p] == 0x00)
{
counter++; // Увеличиваем счетчик совпадений
// Если все байты сигнатуры найдены:
if (counter == pattern_size)
{
DWORD start_address_sig = i + mod_base; // Вычисляем адрес сигнатуры | Базовый адрес + количество шагов в памяти
DWORD address_of_offset_hp = start_address_sig + offset_from_offset; // Вычисляем адрес с учетом смещения | Добавляя к началу адреса сигнатуры 4 байта, получаем конечный искомый адрес
return address_of_offset_hp; // Возвращаем адрес
}
}
else {
counter = 0; // Если текущие байты не совпадают, сбрасываем счетчик
}
}
}
}
// Если сигнатура не найдена, возвращаем нулевое значение
std::cout << "Signature not found!" << std::endl;
return 0;
}
// Основная функция программы
int main()
{
setlocale(LC_ALL, "Russian"); // Устанавливаем локаль для вывода на русском языке
TargetProc Attach(L"ac_client.exe"); // Создаем объект класса TargetProc для работы с процессом
MODULEENTRY32 mod = Attach.GetProcModuleInfo(L"ac_client.exe"); // Получаем информацию о модуле процесса
HANDLE handleTargetProc = OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE, false, Attach.GetProcPid()); // Открываем процесс для чтения и записи
if (handleTargetProc == NULL)
{
std::cout << "Не удалось открыть процесс!" << std::endl;
return 0;
}
DWORD mod_BaseAddress = (DWORD)mod.modBaseAddr; // Получаем базовый адрес модуля
std::cout << "ModBase address: " << std::hex << mod_BaseAddress << std::endl; // Выводим адрес модуля в шестнадцатеричном формате
DWORD mod_BaseSize = mod.modBaseSize; // Получаем размер модуля
std::cout << "ModBase size: " << std::hex << mod_BaseSize << std::endl; // Выводим размер модуля в шестнадцатеричном формате
DWORD pLocalPlayerEntity_Addresss = (DWORD)(mod_BaseAddress + PlayerEntityOffset); // Вычисляем адрес объекта игрока
DWORD LocalPlayerEntity_BaseAddress;
if (!ReadProcessMemory(handleTargetProc, (LPCVOID)pLocalPlayerEntity_Addresss, &LocalPlayerEntity_BaseAddress, sizeof(DWORD), 0))
{
std::cout << "Не удалось прочитать данные из указателя pLocalPlayerEntity_Address" << std::endl;
return 0;
}
BYTE pattern_offset_hp[]{ "\x74\x97\x8B\x8B\x00\x00\x00\x00\x83\xF9\x19" }; // Определяем сигнатуру для поиска оффсета здоровья
int pattern_size = sizeof(pattern_offset_hp) - 1; // Определяем размер сигнатуры
BYTE* mod_buffer = new BYTE[mod_BaseSize]{ 0 }; // Выделяем память для буфера модуля
if (!ReadProcessMemory(handleTargetProc, (LPCVOID)mod_BaseAddress, mod_buffer, mod_BaseSize, 0))
{
return 0;
}
DWORD result = SignatureScanner(mod_buffer, mod_BaseSize, mod_BaseAddress, pattern_offset_hp, pattern_size, 4); // Выполняем сканирование памяти процесса
DWORD Offset;
if (!ReadProcessMemory(handleTargetProc, (LPCVOID)result, &Offset, sizeof(DWORD), 0)) // Читаем данные по найденному адресу
{
return 0;
}
std::cout << "Offset for health address = 0x" << std::hex << Offset << std::endl; // Выводим смещение адреса здоровья
DWORD healthAddr = LocalPlayerEntity_BaseAddress + Offset; // Вычисляем адрес здоровья игрока
int health = 0;
if (!ReadProcessMemory(handleTargetProc, (LPCVOID)healthAddr, &health, sizeof(health), 0)) { // Читаем значение здоровья
return 0;
}
std::cout << "Health player: " << std::dec << health << std::endl; // Выводим значение здоровья в десятичном формате
delete[]mod_buffer; // Освобождаем выделенную память для буфера модуля
}
Итак, наш код выполняет следующие действия:
- Определяет PID процесса по его имени и получает информацию о его модулях.
- Открывает процесс для чтения и записи памяти.
- Находит базовый адрес модуля и его размер.
- Ищет сигнатуру в памяти процесса.
- Считывает смещение из найденного адреса сигнатуры.
- Рассчитывает и считывает адрес переменной (например, здоровья) и выводит ее значение.
Давайте сразу перейдем к самой функции SignatureScanner и я попытаюсь объяснить, что происходит в этом алгоритме. По сути, мы получаем информацию об начальном адресе модуля, информацию о его размере. То есть если нам известен начальный адрес памяти, то логично будет пройтись циклом по всему модулю, от начальных до конечных адресов памяти. И считывать каждый байт в текущем адресе, затем делать сравнение с байтом из нашего массива сигнатуры. Если первый байт совпал, значит увеличим счетчик counter и сделаем сравнение следующих байтов и так до тех пор, пока мы не сравним все байты и если, счетчик counter будет равняться к размеру сигнатуры – pattern_size, то найдем адрес сигнатуры, иначе сообщим, что сигнатура не найдена и вернем 0. Давайте взглянем, что делает наш алгоритм:
Спойлер: Скриншот
Наш алгоритм начиная с базового адреса, обычно как мы знаем он равен 400000, начинает сравнивать содержимое адреса с первым байтом массива сигнатуры, а именно 0x74. Если нет совпадения идем к следующему адресу 400001 и делаем сравнение с его содержимым и так до конца адресного пространства или же размера модуля. Если обнаружится первое совпадение увеличим счетчик и начнем сравнивать второй байт из нашего массива со следующим содержимым адреса. И так до конца.
Еще один немаловажный момент, раз мы работаем на уровне байт-кода, то найденный адрес сигнатуры будет равняться адресу первого байта в сигнатуре и нашей целью было получить адрес, в котором будет хранится смещение для здоровья и для того, чтобы получить верные результаты, нам нужно вычислить их путем прибавления к найденному адресу смещение 4, ведь наши искомые байты находятся после 4 байт в памяти. Давайте взглянем на это в самой памяти:
Спойлер: Скриншот
Как видим, на скриншоте изображен участок памяти, как раз с нашей сигнатурой. Наш алгоритм в качестве результата вернул бы адрес начала этой сигнатуры, а это на минуточку адрес байта 0x74(Красное выделение). А начало нашего оффсета 0xEC находится совсем по другому адресу , значит по логике мы должны прибавить к найденному адресу, не достающие байты, чтобы в итоге получить нужный нам адрес, в котором будет и наш оффсет для здоровья 0xEC. В данном случае нужно сместиться на 4 байта, и мы получаем нужный нам адрес. В коде этот момент написан как offset_from_offset и передаем туда мы 4.
DWORD address_of_offset_hp = start_address_sig + offset_from_offset;
В итоге к полученному адресу прибавляем это смещение и возвращаем его, как результат.Давайте скомпилируем нашу программу и запустим!
Спойлер: Скриншот
Как можем наблюдать, все у нас работает! Наш сигнатурный поиск вычислил при помощи паттерна нужный нам адрес и вывел его значение – 0xEC. По сути манипулируя байтами, мы можем сделать много чего, но оставим это на будущее! Попробуйте проделать то же самое и с смещениями патронов. Такая техника поиска оффсетов сильно упрощает поддержку продукта и у нас появляется лишнее время на его совершенствование. Как отмечалось ранее, этот метод очень популярен во многих продуктах)))
Ну что же, кто дошел до конца статьи и сделал какие-либо открытия для себя – Я вас поздравляю! Буду рад, если оставите свой комментарий под этим постом, что было вам понятно или наоборот недопоняли. До следующей части!