Автор: Unseen
Специально для xss.pro
Предыдущая часть:
Введение…
Доброго времени суток, дорогие читатели! В прошлой статье мы научились находить динамически созданный объект PlayerEntity, то есть структуру нашего игрока. Нашли статический указатель на эту самую структуру. И поисследовали некоторые поля переменные(здоровье, патроны и никнейм) в этом объекте PlayerEntity. Сегодня предлагаю копнуть глубже по теме структур объектов и этот материал будет посвящен программе Reclass.NET – мощному инструменту, предназначенному для работы с памятью и структурами данных, содержащимися в процессах. Этот инструмент стал неотъемлемой частью процесса реверс-инжиниринга игр, обеспечивая удобство и эффективность от начального анализа структур до экспорта размеченных классов.
В данной статье мы проведем подробный обзор Reclass.NET, фокусируясь на его ключевых функциях и применении в контексте разработки читов. Начнем с анализа базовых возможностей инструмента, переходя от непосредственной работы с памятью до структур данных. Пройдем путь от изучения основных функций до рассмотрения вопросов, связанных с использованием Reclass.NET в создании читов и модификации игровых процессов включая использование и чтение размеченных структур из памяти игры.
Определение, общий обзор Reclass…
Следуя из описания проекта на гитхабе можно узнать о том, что ReClass.NET это результат переноса ReClass на платформу .NET с добавлением множества функций. Первоначальные версии были разработаны некими DrUnKeN и ChEeTaH позже работу над ним продолжили CypherPresents и IChooseYou, а в конечном итоге мы будем пользоваться версией от KN4CK3R.
Как вы уже поняли эта программа имеет открытый исходный код, который можно скачать и скомпилировать самостоятельно.
Давайте вкратце пройдемся по функциям, которые были реализованы в самом Reclass:
Предлагаю начать с основных функций, из-за которых стоит использовать эту программу.
Для начала перейдите в релизы и скачайте последний:
https://github.com/ReClassNET/ReClass.NET/releases
Стоит отметить, что есть разница между некоторыми версиями, заключающаяся в представлении указателей на классы. В некоторых версиях нельзя в каком-то классе отметить указатель как указатель на экземпляр класса*(англ. Instance), имейте это ввиду.
*Экземпляр класса (англ. instance) — это описание конкретного объекта в памяти.
Я же скачал последний релиз Reclass.Net v1.2 и советую вам то же самое.
Сегодня так же в пример возьмем нашу подопытную игру Assault Cube. Значит запускаем игру и Reclass.Net. Хочу подметить - если у вас процесс 32 бита, то вам нужно использовать x86 версию, а если 64 бита, то x64 соответственно. В данном случае наша подопытная игра имеет 32бит разрядность. Значит выбираем Reclass x86.
После запуска нас встречает главное окно программы:
ReClass создан для просмотра памяти целевого процесса во время выполнения, что означает, что нам следует подключится к нему. Через меню Файл->Подключение к процессу мы производим выбор целевого процесса:
И в открывшемся меню выбираем нужный процесс, конкретно ac_client.exe
После подключения к процессу мы можем задать адрес в памяти для обзора следующего за ним участка, делается это двойным кликом в поле адреса, которое указано на рисунке. Если точнее, то двойным кликом по адресу 0x40000(Забыл выделить), который рядом с названием класса.
Также на скриншоте указан:
Hex64, Hex32, Hex16 и Hex8 – эти типы данных представляют собой не только инструмент для разметки неизвестных участков структуры, но также служат для заполнения и выравнивания в памяти. Они являются ключевыми при описании данных, когда структура в памяти содержит байты переменной длины. Эти типы можно рассматривать как окна, смотря в которые можно понять, что изменяется / находится в этом участке памяти. Каждый из них позволяет нам взглянуть на соответствующее количество байт в памяти.
Кроме того, они играют важную роль в поддержании выравнивания данных в памяти. Поскольку данные часто выравниваются по определенным границам в зависимости от битности процесса, что способствует более эффективной работе процессора. Как пример переменная типа boolean может занимать как один байт, так и два.
Int8-Int64 это целочисленные типы данных, предоставляющие возможность описания различных целых чисел с разным размером в байтах.
UInt8-Uint64 представляют собой беззнаковые(положительные) целочисленные типы данных, используемые для описания целых чисел различного размера.
Bool - это логический тип данных, который используется для представления булевых значений. Булев тип, или логический тип, может принимать только два значения: истина (true) или ложь (false).
Bitfield представляет собой уникальную структуру данных, где каждый бит в переменной имеет свое собственное значение. Этот тип данных используется для хранения информации в виде битовых флагов, что позволяет эффективно использовать память и представлять различные состояния или параметры в компактной форме. Когда имеется необходимость хранить несколько булевых значений в одной переменной, тип разметки Bitfield становится незаменимым инструментом.
Enum (перечисление) представляет собой тип данных, используемый для хранения именованных констант. Этот тип данных облегчает понимание и восприятие кода, так как позволяет использовать понятные имена вместо числовых значений.
Когда в структуре данных встречаются числовые константы, Enum становится удобным средством для создания читаемых именованных значений. Вместо использования захардкоженых чисел, мы можем использовать Enum для создания именованных констант. В ReClass Enum не только позволяет указывать размер перечисления, но также дает возможность создавать собственные перечисления прямо в проекте ReClass.NET
Float, Double представляют собой типы данных, используемые для представления чисел с плавающей запятой. Float представляет 32-битное число с плавающей запятой одинарной точности, Double представляет 64-битное число с плавающей запятой двойной точности и обеспечивает больший диапазон значений и большую точность, за счет использования большего количества битов, что нужно учитывать при разметке структуры.
Vector2-4 представляют собой типы данных, описывающие векторы определенной размерности. Эти типы данных часто используются для представления координат, направлений или других геометрических величин. При генерации кода эти векторы заменяются на абстрактные структуры. Это означает, что мы должны самостоятельно реализовать эти структуры в нашем софте(чите).
Matrix представляет собой тип данных, описывающий матрицу, такая же ситуация как и с векторами.
UTF8-UTF16 - представление текста в памяти, так же допускается использование дополнительного указателя. Позволяют анализировать и работать с текстовыми данными в различных кодировках.
Pointer представляет собой тип данных, который используется, когда необходимо работать с указателями в памяти.
Array представляет собой тип данных, который используется для описания массивов - упорядоченных коллекций объектов одного типа данных. Возможность использования Array становится неотъемлемой частью анализа структур данных по причине того, что массивы часто встречаются в организации данных, связанных с объектами, персонажами, предметами и так далее. Стоит отметить, что если мы используем External подход, то эта функция будет полезна только для реверс-инжиниринга и анализа, поскольку работа с указателями отличается от Internal подхода, о чём мы поговорим далее.
Union может быть полезным при описании сложных структур данных, когда разные поля могут иметь различные типы в разных сценариях, но используется редко.
Class Instance представляет собой экземпляр класса, причём который может быть расположен как и по указателю, так и линейно в памяти.
VTable Pointer - это указатель, который указывает на VTable (таблицу виртуальных функций), где хранятся адреса соответствующих виртуальных функций для определенного класса. Когда вызывается виртуальная функция через указатель на базовый класс, VTable Pointer используется для определения того, какая конкретная функция должна быть вызвана, исходя из типа объекта во время выполнения.
Function Pointer это указатель на функцию, а Function это сама функция.
Пока возможно вам многое кажется не очень ясным и понятным, но вскоре со временем начнет приходить понимание сути.
Давайте теперь на самом легком примере разберем функционал разметки участка памяти в игре AssautCube, а именно будем отмечать структуру нашего локального игрока. Через CheatEngine я уже нашёл адрес объекта PlayerEntity(локальная сущность игрока). Вам следует так же найти этот адрес, как мы проделывали это в прошлой части №5. В моём случае это 0075F2D0, этот адрес вы можете наблюдать в поле адреса на рисунке(Красное выделение).
Поскольку чтобы до конца имитировать реальную ситуацию нам потребуется тесное взаимодействие с другими программами и отладчиком, я предлагаю при разметке структуры отталкиваться не от подтверждения/опровержения гипотез, а использовать исходный код, который имеет AssautCube. Такое решение обусловлено тем, что объём статьи при использовании другого подхода увеличит ее содержание в десятки раз и не позволит сфокусироваться на поставленной задаче в теме. А открытый исходный код игры это нам на руку!
Вернёмся к нашей структуре. В данный момент у меня указан адрес экземпляра класса PlayerEntity, что следует из вот этой строчки, так же понять, что за класс расположен по адресу можно через обзорщик структур Cheat Engine, но он не обладает таким функционалом как ReClass.
Давайте найдём заголовочный файл entity.h в исходниках AssautCube на GitHub, где описывается класс playerent - https://github.com/assaultcube/AC/blob/master/source/src/entity.h
P.S. Описание класса начинается со строчки 414. Для удобного перемещения пользуйтесь поиском в браузере Ctrl + F.
Как мы можем видеть этот класс playerent наследуется от dynent и playerstate, при том это также указано в самом ReClass. Но как мы можем заметить в исходниках игры(entity.h) первые члены(поля) класса это в любом случае типы int, но в памяти по этому адресу расположены значения больше похожие на float, в данном случае так нам сообщает Reclass.
Это связано с тем, что в начало класса могут попадать переменные базовых классов, от которых происходит наследование, то есть класс playerent наследуется от dynent. Давайте взглянем на этот класс.
Как видим в классе dynent в начальных полях нет переменных с типом float. А так у нас снова наследование от класса physent. Значит идем дальше в описание класса physent.
При переходе к описанию класса physent, что в расшифровке скорее всего означает физическую сущность, мы наблюдаем следующее:
Четыре вектора, и три флота, которые мы скорее всего и наблюдаем в нашей памяти, а именно в классе playerent.
Изменив позицию игрока, мы можем убедится, что по офсету 0x4 у нас находится вектор позиции объекта, изменяем его тип на Vector3 и задаем название Origin:
Как вы можете заметить смещение(оффсет) следующий за вектором поменялся на 0x4+0x4*3=0x10 в 16-ой системе, что логично, ведь float занимает четыре байта, а в векторе три координаты(x,y,z). То есть 3 координаты * по 4 байта = 12 байт + 4 байта = 16 байт в десятичной или же 0x10 в HEX.
Следующим полем в описании класса физической сущности идет скорость, подпрыгнув мы можем убедится в этом, поскольку у вектора изменяется скорость по оси Z - отмечаем это.
Далее отмечаем два вектора(deltapos, newpos) использующихся для логики передвижения
Дальше в исходном коде описаны переменные типа float - yaw, pitch и roll они обычно используются для представления углов ориентации объекта в трехмерном пространстве. Эти углы представляют собой три различных компонента ориентации, которые описывают, как объект повернут относительно своей начальной ориентации, так же вносим соответствующие изменения.
Продолжая размечать структуру, мы можем заметить то, что в документации, комментарий к полю maxspeed гласит, что максимальная скорость для игрока это 24, хотя по факту в памяти по этому значению хранится число 16.0
Когда мы встречаем поля типа int, то мы их отмечаем Int32, потому что int это по стандарту и есть int32_t, если не указано иное.
P.S. Если у вас в классе playerent мало полей, то смело добавляйте еще байтов, допустим 2048. Через ПКМ -> add bytes -> 2048.
Когда нужно задать большому количеству адресов один тип не стесняемся выделять адреса используя Crtl или Shift.
Далее у нас образуется следующая ситуация:
Вроде всё выглядит с первого взгляда корректно размеченным, но почему-то высота последнего прыжка равна нулю. Такие и подобные аномалии могут свидетельствовать, что мы либо где-то допустили ошибку в смещениях и не учли выравнивание памяти. То есть сама по себе переменная scoping хоть и находится по офсету 0x66, но занимает не один байт, как предыдущие переменные, а занимает большее количество. К такому роду выводов можно приходить исходя из самих значений данных в памяти, данных с обзорщика структур в CE или же основываясь на данных с дизассемблера и оффсетов в операндах. Вот к примеру данные из CE, где он автоматически определил, что float следующий после булей должен быть по офсету 0x6C
Давайте воспользуемся встроенным отладчиком в ReClass и посмотрим, что записывает по этому адресу
Так же видим оффсет 0x6C в одной из инструкций. Исходя из этого нам нужно сдвинуть ниже по структуре эти поля путём добавления байтов и изменения их размера. Для этого перед адресом lastjump вставляем четыре байта и задаём тип Hex8, а остальные удаляем
После таких изменений всё встало на свои места.
Следующая ситуация в памяти кажется не очень очевидной и если мы посмотрим на следующий в иерархии наследования класс dynent , то там будет мало данных, которые могут нас интересовать
По этому предлагаю перейти к классу состояния игрока
Находим адрес здоровья или брони в памяти и вычисляем относительно адреса класса игрока оффсет (адреса могут быть другими по причине перезапуска игры)
Начало класса 006C0AD8
Адрес брони 006C0BC8
Оффсет: 0x006C0BC8 - 0x006C0AD8 = 0xF0
Вносим изменения
И далее по списку
А следом идут массивы размера равного количеству оружия у персонажа.
Исходя из логики того, что ammo - это количество патронов общее, mag это количество патронов в текущем магазине, а gunwait переменная связанная с логикой стрельбы начинаем размечать структуру. Кстати говоря, реализация параллельных массивов одинаковой длины для описания какой-то сущности не является хорошей практикой в общем случае и делать так, как это реализовано тут - не стоит. Хорошей реализацией был бы массив экземпляров класса оружия.
Судя по структуре в памяти получается, что оружие имеет не тип - Главное, второстепенное и так далее, а NUMGUNS это количество всех возможных оружий в игре, включая гранаты. Тут мы наглядно видим насколько такая реализация может быть неудобна не только при реверс-инжиниринге, но и в самом коде придется обращаться к оружию по индексу его типа.
С полями класса playerstate всё понятно, переходим к полям основного класса сущности - playerent
Чтобы точно понять, где находятся эти поля и не размечать до конца прошлый класс нужно от чего-то отталкиваться, можно поступить так: найти адрес никнейма и отталкиваться от него.
А далее идем вверх и размечаем поля.
Таким образом мы разметили большинство необходимых полей. Если необходимо разметить класс полностью, то нужно опираться на использующиеся оффсеты в операндах, но в большинстве случаев это не требуется.
С классом сущности игрока(PlayerEntity) мы закончили, давайте рассмотрим ещё несколько видов представления данных и рассмотрим список сущностей(Еще называют EntityList).
Итак, давайте по-быстрому найдем указатель на этот EntityList. Алгоритм действий похож на нахождение указателя для объекта PlayerEntity, как в предыдущих частях.
Значит запускаем игру и создаем одиночную игру с ботами. Советую в правилах игры отключить стрельбу и движения для ботов, чтобы они нам не мешали.
Далее нам нужно будет найти любого бота и получить адрес его здоровья. Как мы делали это с нашим локальным игроком, но поскольку мы не знаем, какой урон будет наноситься ему при стрельбе по нему, предлагаю воспользоваться методом поиска по неизвестному значению. Нам известно точно одно, что при получении урона, его уровень здоровья будет уменьшаться, значит будем сканировать типом поиска «Decreased Value» или же «Значение уменьшилось». Итак, я выбрал бота iLikeCheese и делаю первый поиск с аргументом 100, мы знаем изначально его здоровье равно 100.
Далее наносим урон и делаем поиск с типом «Decreased Value». И так до тех пор, пока не останется один адрес здоровья.
Хорошо мы нашли этот самый адрес, далее кликаем ПКМ на этот адрес и выбираем «Pointer scan for this address»:
Теперь, когда у нас открылось окошко с параметрами поиска, нам нужно выбрать «Pointer must end with specific offsets» или же «указатель должен заканчиваться определенными смещениями», потому что мы знаем, что смещение для здоровья игрока так и ботов одно и то же, а именно 0xEC. Поэтому выставляем этот оффсет в поле. Далее выставляем уровень глубины смещений на 3 и ОК:
После сканирования у нас откроется окно с результатами поиска и нам нужно отсортировать их от наименьшей глубины к наибольшему, можете кликнуть по колонке «Offsets 0». И у нас получится такой вид:
Как известно, EntityList представляет собой массив указателей на сущностей(Ботов), а каждый указатель в 32-битном приложении имеет размер 4 байта, поэтому мы можем предположить, что первое смещение 0x4 будет неким итератором для этого объекта. Значит статическим указателем на EntityList будет ac_client.exe + 0018AC04. Перейдите в CE и добавьте этот адрес вручную, как указатель:
Получается в этом статическом указатели будет хранится сам адрес объекта EntityList, а именно 0C841218 в данном случае у меня.
Когда мы нашли адрес мы можем увидеть следующую картину в обзорщике структур CE(Не обращаем внимание на адреса, статья писалась долго и каждый раз у меня новые адреса, но смысл тот же).
Он автоматически уже определил тип объекта, который находится у нас по указателям.
Теперь переходим в ReClass и создаём новый класс:
Задаем название логичное(EntityList) и меняем адрес:
Чётко видим, что каждые четыре байта у нас лежит указатель, причем все адреса лежат в примерно одной области памяти. Добавляем байтов в структуру, чтобы определить сколько всего сущностей в ней.
При выделении нам говорят, что выделен 31 узел. То есть всего на карте у нас 31 игрок за исключением локального, который отсутствует в этом листе.
Как можем заметить появление указателей в памяти начинается с офсета 0x4, давайте в этом месте отметим начало массива:
Теперь задаём его размер
А далее укажем, что его элементы это указатели нажатием на пункт указанный на картинке
P.S. На этом моменте у меня появились ограничения на количество изображений и далее скрины будут по ссылке.
Теперь уже укажем, что по указателю у нас хранятся экземпляры классов
Скрин
Далее указываем, что это экземпляры именно playerent (хотя по факту это экземпляры класса botent, который наследуется от playerent) По крайней мере так указано в исходниках игры =)
Скрин
Скрин
Теперь мы видим данные первого элемента в этом массиве:
Скрин
Итерацию по массиву можно выполнять используя эти стрелки:
Скрин
В списке классов мы можем заметить, что у нас появился лишний класс:
Скрин
Такое бывает, потому что каждый раз при указании типа экземпляра класса у нас создается класс. Такие классы можно удалить через меню Проект -> удалить неиспользуемые классы.
Скрин
Перед тем, как мы будем экспортировать код я рекомендую вам заглянуть в настройки определения типов
Скрин
Тут вы можете задать имена типов если они отличаются от используемых вами в проекте или у вас специфический стиль нейминга в проекте.
Далее переходим в меню Проект -> Генерация C++ кода.
Скрин
В появившемся окне видим наш огромный класс и все связанные с ним оффсеты в виде комментариев, а также остальные классы.
Скрин
Неразмеченные области у нас заменяются массивом символов, поскольку char занимает в памяти один байт.
Перед тем как перейдём к взаимодействию со сгенерированным кодом стоит упомянуть, что в самом ReClass есть функционал позволяющий искать значения в памяти.
Смысла использовать именно его я не вижу, поскольку такого типа функционал более актуален в среде Cheat Engine, потому что там гораздо больше функционала связано с поиском адресов и работой с ними. Лучше всего использовать ReCalss в связке с CheatEngine.
Использование размеченных структур на практике…
Предлагаю составить план и действовать по нему, так будет легче писать код:
Теперь давайте воспользуемся размеченной структурой памяти. Переходим в Visual Studio или другую среду разработки. Реализовывать будем все это в нашем проекте MemoryManager.
Получившийся сгенерированный код из Reclass, мы копируем в отдельный заголовочный файл game.h в нашем проекте для более удобного доступа к этим полям в классе:
Скрин
P.S. Перед сгенерированным кодом из Reclass добавьте такую строчку кода:
Struct Vector3 { float x,y,z; }; Чтобы C++ знал, что имеется ввиду в ваших полях с типом переменной Vector3 из класса playerent.
Давайте попробуем прочитать объект класса локального игрока.
Для этого предлагаю создать переменную player с типом playerent и сохранить в него наш объект:
И выводим как пример вектор позиции игрока:
После запуска в консольном окне наблюдаем результат работы и видим координаты x y z.
При выводе имени сущности он корректно её отображает - name;
Благодаря тому, что у нас нет дополнительной логики хранения строк в этой игре и это просто массив символов char name[4];
Давайте попробуем прочитать все сущности из списка. Но сначала нужно подменить пару моментов.
Во-первых: Скрин
мы допустили ошибку и не задали имя у списка.
Во-вторых: у нас N00002902 это массив указателей на объекты класса playerent. Пусть название будет items. По логике вещей мы должны будем в коде читать оттуда значения по индексу разыменовывая указатель вот так:
playerent* currentEntPtr = list.items;
Но на деле мы получаем ошибку. Если мы выполняем внешнее чтение памяти из другого процесса, то разыменование указателей напрямую в нашем адресном пространстве это проблема. Связанно это с тем, что указатели, полученные из другого процесса, будут адресами в его пространстве, а не в нашем. Когда мы читаем данные из другого процесса, мы получаем "сырые" данные, которые представляют собой значения в адресном пространстве этого процесса. Если эти данные включают указатели, но мы не можем напрямую разыменовывать их в вашем процессе, чтобы иметь такую возможность нам нужно быть внутри процесса.
Для работы с такими указателями нам придется вручную использовать некоторые смещения для такого рода операций.
Давайте рассмотрим следующую конструкцию:
Здесь мы создали переменную - ptrEntityList, которая будет хранить указатель и переменную - EntityListDynamicAddress, которая будет хранить значение(адрес) которое находится в указатели, в данном случае адрес EntityList. Далее идет переменная EntityQuantity – которая хранит количество существ в данной игре. То есть при перечислении в цикле нам нужно знать текущее количество существ и это нам очень пригодится. Не стал описывать, как найти смещение для этого, ибо думаю вы догадаетесь! В цикле for здесь мы читаем все указатели на сущности и записываем их в переменную текущего адреса. Проводим мы вычисления основываясь на начальном адресе списка и оффсета до первого элемента - 0x4, а далее каждые четыре байта (0x4) у нас расположены указатели. После запуска мы как раз и наблюдаем эти значения
Скрин
Ну а дальше дело остается за малым - нужно зачитать каждый объект класса, зная его адрес в памяти.
Консольный вывод после запуска у нас будет такой:
Скрин
Это говорит о том, что мы всё сделали правильно.
Итог
ReClass - это лучший инструмент для обратного проектирования структур он имеет две основные цели: сначала мы используем его для обратного проектирования классов и структур, затем экспортируем их для использования в своих проектах. И я думаю, что он входит в тройку инструментов для разработки читов по значимости, поэтому очень важно научиться эффективно его использовать. ReClass намного лучше, чем функция Dissect Data/Structures в Cheat Engine, поскольку включает в себя множество функций, отсутствующих в Cheat Engine, что делает его более эффективным и полезным инструментом при поиске различных данных зависящих от структур данных.
Таким образом мы подошли к завершению обзора мощного инструмента Reclass.NET. Который позволяет не только анализировать память игр, но и напрямую участвует в разработке SDK для читов. Я специально не стал выкладывать тот самый сгенерированный кусок кода в Reclass, чтобы каждый мог потренироваться в этой программе. Если будут какие-либо вопросы – смело задавайте!
Надеюсь, что статья была полезна и вы узнали для себя что-то новое! До следующей части!
Специально для xss.pro
Предыдущая часть:
https://xss.pro/threads/106400/
Введение…
Доброго времени суток, дорогие читатели! В прошлой статье мы научились находить динамически созданный объект PlayerEntity, то есть структуру нашего игрока. Нашли статический указатель на эту самую структуру. И поисследовали некоторые поля переменные(здоровье, патроны и никнейм) в этом объекте PlayerEntity. Сегодня предлагаю копнуть глубже по теме структур объектов и этот материал будет посвящен программе Reclass.NET – мощному инструменту, предназначенному для работы с памятью и структурами данных, содержащимися в процессах. Этот инструмент стал неотъемлемой частью процесса реверс-инжиниринга игр, обеспечивая удобство и эффективность от начального анализа структур до экспорта размеченных классов.
В данной статье мы проведем подробный обзор Reclass.NET, фокусируясь на его ключевых функциях и применении в контексте разработки читов. Начнем с анализа базовых возможностей инструмента, переходя от непосредственной работы с памятью до структур данных. Пройдем путь от изучения основных функций до рассмотрения вопросов, связанных с использованием Reclass.NET в создании читов и модификации игровых процессов включая использование и чтение размеченных структур из памяти игры.
Определение, общий обзор Reclass…
Следуя из описания проекта на гитхабе можно узнать о том, что ReClass.NET это результат переноса ReClass на платформу .NET с добавлением множества функций. Первоначальные версии были разработаны некими DrUnKeN и ChEeTaH позже работу над ним продолжили CypherPresents и IChooseYou, а в конечном итоге мы будем пользоваться версией от KN4CK3R.
Как вы уже поняли эта программа имеет открытый исходный код, который можно скачать и скомпилировать самостоятельно.
Давайте вкратце пройдемся по функциям, которые были реализованы в самом Reclass:
- Поддержка как x86 архитектуры, так и x64, что позволяет применять его в процессах любой разрядности(битности). Реализована разными версиями приложения.
- Сохранение и загрузка проектов - крайне полезная, необходимая и очевидная функция.
- Подсвечивание изменяющихся байтов в памяти
- Предпросмотр указателей
- Остановка / возобновление процесса
- Просмотр памяти
- Автоматическое изменение смещений
- Сканер памяти на подобии Cheat Engine + поддержка импорта из него
- Генерация C++/C# кода
- Отладчик по типу что записывает/читает по адресу
- И конечно же помимо возможности напрямую добавить функционал посредством редактирования исходного кода есть возможность написания плагинах на языках C++, C++/CLI, C#, как пример - реализации доступа к памяти через драйвер.
Предлагаю начать с основных функций, из-за которых стоит использовать эту программу.
Для начала перейдите в релизы и скачайте последний:
https://github.com/ReClassNET/ReClass.NET/releases
Стоит отметить, что есть разница между некоторыми версиями, заключающаяся в представлении указателей на классы. В некоторых версиях нельзя в каком-то классе отметить указатель как указатель на экземпляр класса*(англ. Instance), имейте это ввиду.
*Экземпляр класса (англ. instance) — это описание конкретного объекта в памяти.
Я же скачал последний релиз Reclass.Net v1.2 и советую вам то же самое.
Сегодня так же в пример возьмем нашу подопытную игру Assault Cube. Значит запускаем игру и Reclass.Net. Хочу подметить - если у вас процесс 32 бита, то вам нужно использовать x86 версию, а если 64 бита, то x64 соответственно. В данном случае наша подопытная игра имеет 32бит разрядность. Значит выбираем Reclass x86.
После запуска нас встречает главное окно программы:
ReClass создан для просмотра памяти целевого процесса во время выполнения, что означает, что нам следует подключится к нему. Через меню Файл->Подключение к процессу мы производим выбор целевого процесса:
И в открывшемся меню выбираем нужный процесс, конкретно ac_client.exe
После подключения к процессу мы можем задать адрес в памяти для обзора следующего за ним участка, делается это двойным кликом в поле адреса, которое указано на рисунке. Если точнее, то двойным кликом по адресу 0x40000(Забыл выделить), который рядом с названием класса.
Также на скриншоте указан:
- Оранжевое выделение: Список всех размеченных классов в проекте
- Синее выделение: Смещения(оффсеты) относительно модуля/структуры
- Красное выделение: Конечные адреса в памяти
- Черное выделение: Байтовое представление хранящиеся по этим адресам
- Зеленое выделение: Представление в различных вариациях в зависимости от выбранного типа
Hex64, Hex32, Hex16 и Hex8 – эти типы данных представляют собой не только инструмент для разметки неизвестных участков структуры, но также служат для заполнения и выравнивания в памяти. Они являются ключевыми при описании данных, когда структура в памяти содержит байты переменной длины. Эти типы можно рассматривать как окна, смотря в которые можно понять, что изменяется / находится в этом участке памяти. Каждый из них позволяет нам взглянуть на соответствующее количество байт в памяти.
Кроме того, они играют важную роль в поддержании выравнивания данных в памяти. Поскольку данные часто выравниваются по определенным границам в зависимости от битности процесса, что способствует более эффективной работе процессора. Как пример переменная типа boolean может занимать как один байт, так и два.
Int8-Int64 это целочисленные типы данных, предоставляющие возможность описания различных целых чисел с разным размером в байтах.
UInt8-Uint64 представляют собой беззнаковые(положительные) целочисленные типы данных, используемые для описания целых чисел различного размера.
Bool - это логический тип данных, который используется для представления булевых значений. Булев тип, или логический тип, может принимать только два значения: истина (true) или ложь (false).
Bitfield представляет собой уникальную структуру данных, где каждый бит в переменной имеет свое собственное значение. Этот тип данных используется для хранения информации в виде битовых флагов, что позволяет эффективно использовать память и представлять различные состояния или параметры в компактной форме. Когда имеется необходимость хранить несколько булевых значений в одной переменной, тип разметки Bitfield становится незаменимым инструментом.
Enum (перечисление) представляет собой тип данных, используемый для хранения именованных констант. Этот тип данных облегчает понимание и восприятие кода, так как позволяет использовать понятные имена вместо числовых значений.
Когда в структуре данных встречаются числовые константы, Enum становится удобным средством для создания читаемых именованных значений. Вместо использования захардкоженых чисел, мы можем использовать Enum для создания именованных констант. В ReClass Enum не только позволяет указывать размер перечисления, но также дает возможность создавать собственные перечисления прямо в проекте ReClass.NET
Float, Double представляют собой типы данных, используемые для представления чисел с плавающей запятой. Float представляет 32-битное число с плавающей запятой одинарной точности, Double представляет 64-битное число с плавающей запятой двойной точности и обеспечивает больший диапазон значений и большую точность, за счет использования большего количества битов, что нужно учитывать при разметке структуры.
Vector2-4 представляют собой типы данных, описывающие векторы определенной размерности. Эти типы данных часто используются для представления координат, направлений или других геометрических величин. При генерации кода эти векторы заменяются на абстрактные структуры. Это означает, что мы должны самостоятельно реализовать эти структуры в нашем софте(чите).
Matrix представляет собой тип данных, описывающий матрицу, такая же ситуация как и с векторами.
UTF8-UTF16 - представление текста в памяти, так же допускается использование дополнительного указателя. Позволяют анализировать и работать с текстовыми данными в различных кодировках.
Pointer представляет собой тип данных, который используется, когда необходимо работать с указателями в памяти.
Array представляет собой тип данных, который используется для описания массивов - упорядоченных коллекций объектов одного типа данных. Возможность использования Array становится неотъемлемой частью анализа структур данных по причине того, что массивы часто встречаются в организации данных, связанных с объектами, персонажами, предметами и так далее. Стоит отметить, что если мы используем External подход, то эта функция будет полезна только для реверс-инжиниринга и анализа, поскольку работа с указателями отличается от Internal подхода, о чём мы поговорим далее.
Union может быть полезным при описании сложных структур данных, когда разные поля могут иметь различные типы в разных сценариях, но используется редко.
Class Instance представляет собой экземпляр класса, причём который может быть расположен как и по указателю, так и линейно в памяти.
VTable Pointer - это указатель, который указывает на VTable (таблицу виртуальных функций), где хранятся адреса соответствующих виртуальных функций для определенного класса. Когда вызывается виртуальная функция через указатель на базовый класс, VTable Pointer используется для определения того, какая конкретная функция должна быть вызвана, исходя из типа объекта во время выполнения.
Function Pointer это указатель на функцию, а Function это сама функция.
Пока возможно вам многое кажется не очень ясным и понятным, но вскоре со временем начнет приходить понимание сути.
Давайте теперь на самом легком примере разберем функционал разметки участка памяти в игре AssautCube, а именно будем отмечать структуру нашего локального игрока. Через CheatEngine я уже нашёл адрес объекта PlayerEntity(локальная сущность игрока). Вам следует так же найти этот адрес, как мы проделывали это в прошлой части №5. В моём случае это 0075F2D0, этот адрес вы можете наблюдать в поле адреса на рисунке(Красное выделение).
Поскольку чтобы до конца имитировать реальную ситуацию нам потребуется тесное взаимодействие с другими программами и отладчиком, я предлагаю при разметке структуры отталкиваться не от подтверждения/опровержения гипотез, а использовать исходный код, который имеет AssautCube. Такое решение обусловлено тем, что объём статьи при использовании другого подхода увеличит ее содержание в десятки раз и не позволит сфокусироваться на поставленной задаче в теме. А открытый исходный код игры это нам на руку!
Вернёмся к нашей структуре. В данный момент у меня указан адрес экземпляра класса PlayerEntity, что следует из вот этой строчки, так же понять, что за класс расположен по адресу можно через обзорщик структур Cheat Engine, но он не обладает таким функционалом как ReClass.
Давайте найдём заголовочный файл entity.h в исходниках AssautCube на GitHub, где описывается класс playerent - https://github.com/assaultcube/AC/blob/master/source/src/entity.h
P.S. Описание класса начинается со строчки 414. Для удобного перемещения пользуйтесь поиском в браузере Ctrl + F.
Как мы можем видеть этот класс playerent наследуется от dynent и playerstate, при том это также указано в самом ReClass. Но как мы можем заметить в исходниках игры(entity.h) первые члены(поля) класса это в любом случае типы int, но в памяти по этому адресу расположены значения больше похожие на float, в данном случае так нам сообщает Reclass.
Это связано с тем, что в начало класса могут попадать переменные базовых классов, от которых происходит наследование, то есть класс playerent наследуется от dynent. Давайте взглянем на этот класс.
Как видим в классе dynent в начальных полях нет переменных с типом float. А так у нас снова наследование от класса physent. Значит идем дальше в описание класса physent.
При переходе к описанию класса physent, что в расшифровке скорее всего означает физическую сущность, мы наблюдаем следующее:
Четыре вектора, и три флота, которые мы скорее всего и наблюдаем в нашей памяти, а именно в классе playerent.
Изменив позицию игрока, мы можем убедится, что по офсету 0x4 у нас находится вектор позиции объекта, изменяем его тип на Vector3 и задаем название Origin:
Как вы можете заметить смещение(оффсет) следующий за вектором поменялся на 0x4+0x4*3=0x10 в 16-ой системе, что логично, ведь float занимает четыре байта, а в векторе три координаты(x,y,z). То есть 3 координаты * по 4 байта = 12 байт + 4 байта = 16 байт в десятичной или же 0x10 в HEX.
Следующим полем в описании класса физической сущности идет скорость, подпрыгнув мы можем убедится в этом, поскольку у вектора изменяется скорость по оси Z - отмечаем это.
Далее отмечаем два вектора(deltapos, newpos) использующихся для логики передвижения
Дальше в исходном коде описаны переменные типа float - yaw, pitch и roll они обычно используются для представления углов ориентации объекта в трехмерном пространстве. Эти углы представляют собой три различных компонента ориентации, которые описывают, как объект повернут относительно своей начальной ориентации, так же вносим соответствующие изменения.
Продолжая размечать структуру, мы можем заметить то, что в документации, комментарий к полю maxspeed гласит, что максимальная скорость для игрока это 24, хотя по факту в памяти по этому значению хранится число 16.0
Когда мы встречаем поля типа int, то мы их отмечаем Int32, потому что int это по стандарту и есть int32_t, если не указано иное.
P.S. Если у вас в классе playerent мало полей, то смело добавляйте еще байтов, допустим 2048. Через ПКМ -> add bytes -> 2048.
Когда нужно задать большому количеству адресов один тип не стесняемся выделять адреса используя Crtl или Shift.
Далее у нас образуется следующая ситуация:
Вроде всё выглядит с первого взгляда корректно размеченным, но почему-то высота последнего прыжка равна нулю. Такие и подобные аномалии могут свидетельствовать, что мы либо где-то допустили ошибку в смещениях и не учли выравнивание памяти. То есть сама по себе переменная scoping хоть и находится по офсету 0x66, но занимает не один байт, как предыдущие переменные, а занимает большее количество. К такому роду выводов можно приходить исходя из самих значений данных в памяти, данных с обзорщика структур в CE или же основываясь на данных с дизассемблера и оффсетов в операндах. Вот к примеру данные из CE, где он автоматически определил, что float следующий после булей должен быть по офсету 0x6C
Давайте воспользуемся встроенным отладчиком в ReClass и посмотрим, что записывает по этому адресу
Так же видим оффсет 0x6C в одной из инструкций. Исходя из этого нам нужно сдвинуть ниже по структуре эти поля путём добавления байтов и изменения их размера. Для этого перед адресом lastjump вставляем четыре байта и задаём тип Hex8, а остальные удаляем
После таких изменений всё встало на свои места.
Следующая ситуация в памяти кажется не очень очевидной и если мы посмотрим на следующий в иерархии наследования класс dynent , то там будет мало данных, которые могут нас интересовать
По этому предлагаю перейти к классу состояния игрока
Находим адрес здоровья или брони в памяти и вычисляем относительно адреса класса игрока оффсет (адреса могут быть другими по причине перезапуска игры)
Начало класса 006C0AD8
Адрес брони 006C0BC8
Оффсет: 0x006C0BC8 - 0x006C0AD8 = 0xF0
Вносим изменения
И далее по списку
А следом идут массивы размера равного количеству оружия у персонажа.
Исходя из логики того, что ammo - это количество патронов общее, mag это количество патронов в текущем магазине, а gunwait переменная связанная с логикой стрельбы начинаем размечать структуру. Кстати говоря, реализация параллельных массивов одинаковой длины для описания какой-то сущности не является хорошей практикой в общем случае и делать так, как это реализовано тут - не стоит. Хорошей реализацией был бы массив экземпляров класса оружия.
С полями класса playerstate всё понятно, переходим к полям основного класса сущности - playerent
С классом сущности игрока(PlayerEntity) мы закончили, давайте рассмотрим ещё несколько видов представления данных и рассмотрим список сущностей(Еще называют EntityList).
Итак, давайте по-быстрому найдем указатель на этот EntityList. Алгоритм действий похож на нахождение указателя для объекта PlayerEntity, как в предыдущих частях.
Значит запускаем игру и создаем одиночную игру с ботами. Советую в правилах игры отключить стрельбу и движения для ботов, чтобы они нам не мешали.
Когда мы нашли адрес мы можем увидеть следующую картину в обзорщике структур CE(Не обращаем внимание на адреса, статья писалась долго и каждый раз у меня новые адреса, но смысл тот же).
Он автоматически уже определил тип объекта, который находится у нас по указателям.
Теперь переходим в ReClass и создаём новый класс:
Как можем заметить появление указателей в памяти начинается с офсета 0x4, давайте в этом месте отметим начало массива:
Теперь задаём его размер
А далее укажем, что его элементы это указатели нажатием на пункт указанный на картинке
P.S. На этом моменте у меня появились ограничения на количество изображений и далее скрины будут по ссылке.
Теперь уже укажем, что по указателю у нас хранятся экземпляры классов
Скрин
Далее указываем, что это экземпляры именно playerent (хотя по факту это экземпляры класса botent, который наследуется от playerent) По крайней мере так указано в исходниках игры =)
Скрин
Скрин
Теперь мы видим данные первого элемента в этом массиве:
Скрин
Итерацию по массиву можно выполнять используя эти стрелки:
Скрин
В списке классов мы можем заметить, что у нас появился лишний класс:
Скрин
Такое бывает, потому что каждый раз при указании типа экземпляра класса у нас создается класс. Такие классы можно удалить через меню Проект -> удалить неиспользуемые классы.
Скрин
Перед тем, как мы будем экспортировать код я рекомендую вам заглянуть в настройки определения типов
Скрин
Тут вы можете задать имена типов если они отличаются от используемых вами в проекте или у вас специфический стиль нейминга в проекте.
Далее переходим в меню Проект -> Генерация C++ кода.
Скрин
В появившемся окне видим наш огромный класс и все связанные с ним оффсеты в виде комментариев, а также остальные классы.
Скрин
Неразмеченные области у нас заменяются массивом символов, поскольку char занимает в памяти один байт.
Перед тем как перейдём к взаимодействию со сгенерированным кодом стоит упомянуть, что в самом ReClass есть функционал позволяющий искать значения в памяти.
Смысла использовать именно его я не вижу, поскольку такого типа функционал более актуален в среде Cheat Engine, потому что там гораздо больше функционала связано с поиском адресов и работой с ними. Лучше всего использовать ReCalss в связке с CheatEngine.
Использование размеченных структур на практике…
Предлагаю составить план и действовать по нему, так будет легче писать код:
- Откроем наш проект Memory Manager из прошлых частей
- Скопируем полученный код из Reclass в отдельный заголовочный файл VS.
- Попробуем прочитать объект класса локального игрока(playerent) и выведем пару полей из него в консоль нашего софта
- Попробуем прочитать все сущности из списка EntityList
- Сделаем вывод в консоль, таких полей как: имя и уровень здоровья всех сущностей из списка.
C++:
/*
Author Unseen
Source xss.pro
*/
#include <iostream>
#include "game.h"
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>
#include <thread>
#include <chrono>
using namespace std;
/*Глобальные переменные, чтобы ссылать на них, где угодно в коде*/
HANDLE targetProc = NULL;
HWND targetWindow = NULL;
DWORD pID = 0;
DWORD baseModuleAddress = 0;
void GetAccessProcess(char* targetName)
{
targetWindow = FindWindowA(0, targetName);
if (targetWindow == NULL)
{
cout << "Не удалось найти окно игры!" << endl;
}
else
{
GetWindowThreadProcessId(targetWindow, &pID);
if (pID == 0)
{
cout << "Не удалось получить идентификатор процесса!" << endl;
}
else
{
targetProc = OpenProcess(PROCESS_ALL_ACCESS, false, pID);
if (!targetProc)
{
cout << "Не удалось открыть процесс!" << endl;
}
}
}
}
void GetModuleBaseAddress(const wchar_t* moduleName)
{
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pID);
if (hSnap != INVALID_HANDLE_VALUE)
{
MODULEENTRY32 modEntry;
modEntry.dwSize = sizeof(modEntry);
if (Module32First(hSnap, &modEntry))
{
do
{
if (!_wcsicmp(modEntry.szModule, moduleName))
{
baseModuleAddress = (uintptr_t)modEntry.modBaseAddr;
break;
}
} while (Module32Next(hSnap, &modEntry));
}
}
CloseHandle(hSnap);
}
uintptr_t FindTargetAddress(vector<unsigned int>offsets)
{
uintptr_t address = baseModuleAddress;
for (unsigned int i = 0; i < offsets.size(); ++i)
{
ReadProcessMemory(targetProc, (BYTE*)address, &address, sizeof(address), 0);
address += offsets[i];
}
return address;
}
template<class dataType>
void memWrite(DWORD address, dataType value)
{
WriteProcessMemory(targetProc, (PVOID)address, &value, sizeof(dataType), 0);
}
template <class dataType>
dataType memRead(DWORD address)
{
dataType buffer;
ReadProcessMemory(targetProc, (PVOID)address, &buffer, sizeof(dataType), 0);
return buffer;
}
int main() {
setlocale(LC_ALL, "Russian");
GetAccessProcess((char*)"AssaultCube");
cout << "[+]ac_client.exe PID: " << pID << endl;
GetModuleBaseAddress(L"ac_client.exe");
cout << "[+]Адрес базового модуля ac_client.exe: " << hex << baseModuleAddress << endl;
int playerEntity_static_offset = 0x17E254;
DWORD StaticPtrToPlayerEntity = baseModuleAddress + playerEntity_static_offset;
cout << "[+]Статический указатель на объект PlayerEntity:" << hex << StaticPtrToPlayerEntity << endl;
DWORD dynamicAddressPlayerEntity = memRead<DWORD>(StaticPtrToPlayerEntity);
cout << "[+]Динамический адрес PlayerEntity(Начальный адрес структуры), хранящейся в указатели StaticPtrToPlayerEntity: " << hex << dynamicAddressPlayerEntity << endl;
playerent player = memRead<playerent>(dynamicAddressPlayerEntity);
cout << "(" << dec << player.origin.x <<" " << player.origin.y<< " " << player.origin.z << ")" << endl;
DWORD ptrEntityList = baseModuleAddress + 0x0018AC04;
DWORD EntityListDynamicAddress = memRead<DWORD>(ptrEntityList);
int EntityQuantity = memRead<int>(baseModuleAddress + 0x191FD4);
for (unsigned int i = 0; i < EntityQuantity; i++)
{
DWORD currentEntityAddress = memRead<DWORD>(EntityListDynamicAddress + 0x4 + (i * 0x4));
cout << hex << currentEntityAddress << endl;
}
for (unsigned int i = 0; i < EntityQuantity; i++)
{
DWORD currentEntityAddress = memRead<DWORD>(EntityListDynamicAddress + 0x4 + (i * 0x4));
playerent currentEntity = memRead<playerent>(currentEntityAddress);
cout << currentEntity.name << " " << dec << currentEntity.health << endl;
}
cin.get();
CloseHandle(targetProc);
return 0;
}
Получившийся сгенерированный код из Reclass, мы копируем в отдельный заголовочный файл game.h в нашем проекте для более удобного доступа к этим полям в классе:
Скрин
P.S. Перед сгенерированным кодом из Reclass добавьте такую строчку кода:
Struct Vector3 { float x,y,z; }; Чтобы C++ знал, что имеется ввиду в ваших полях с типом переменной Vector3 из класса playerent.
Давайте попробуем прочитать объект класса локального игрока.
Для этого предлагаю создать переменную player с типом playerent и сохранить в него наш объект:
C++:
playerent player = memRead<playerent>(dynamicAddressPlayerEntity);
C++:
cout << "(" << dec << player.origin.x <<" "<< player.origin.y <<" " << player.origin.z << ")" << endl;
После запуска в консольном окне наблюдаем результат работы и видим координаты x y z.
При выводе имени сущности он корректно её отображает - name;
Благодаря тому, что у нас нет дополнительной логики хранения строк в этой игре и это просто массив символов char name[4];
Давайте попробуем прочитать все сущности из списка. Но сначала нужно подменить пару моментов.
Во-первых: Скрин
мы допустили ошибку и не задали имя у списка.
Во-вторых: у нас N00002902 это массив указателей на объекты класса playerent. Пусть название будет items. По логике вещей мы должны будем в коде читать оттуда значения по индексу разыменовывая указатель вот так:
playerent* currentEntPtr = list.items;
Но на деле мы получаем ошибку. Если мы выполняем внешнее чтение памяти из другого процесса, то разыменование указателей напрямую в нашем адресном пространстве это проблема. Связанно это с тем, что указатели, полученные из другого процесса, будут адресами в его пространстве, а не в нашем. Когда мы читаем данные из другого процесса, мы получаем "сырые" данные, которые представляют собой значения в адресном пространстве этого процесса. Если эти данные включают указатели, но мы не можем напрямую разыменовывать их в вашем процессе, чтобы иметь такую возможность нам нужно быть внутри процесса.
Для работы с такими указателями нам придется вручную использовать некоторые смещения для такого рода операций.
Давайте рассмотрим следующую конструкцию:
C++:
DWORD ptrEntityList = baseModuleAddress + 0x0018AC04;
DWORD EntityListDynamicAddress = memRead<DWORD>(ptrEntityList);
int EntityQuantity = memRead<int>(baseModuleAddress + 0x191FD4);
for (unsigned int i = 0; i < EntityQuantity; i++)
{
DWORD currentEntityAddress = memRead<DWORD>(EntityListDynamicAddress + 0x4 + (i * 0x4));
cout << hex << currentEntityAddress << endl;
}
Скрин
Ну а дальше дело остается за малым - нужно зачитать каждый объект класса, зная его адрес в памяти.
C++:
for (unsigned int i = 0; i < EntityQuantity; i++)
{
DWORD currentEntityAddress = memRead<DWORD>(EntityListDynamicAddress + 0x4 + (i * 0x4));
playerent currentEntity = memRead<playerent>(currentEntityAddress);
cout << currentEntity.name << " " << dec << currentEntity.health << endl;
}
Скрин
Это говорит о том, что мы всё сделали правильно.
Итог
ReClass - это лучший инструмент для обратного проектирования структур он имеет две основные цели: сначала мы используем его для обратного проектирования классов и структур, затем экспортируем их для использования в своих проектах. И я думаю, что он входит в тройку инструментов для разработки читов по значимости, поэтому очень важно научиться эффективно его использовать. ReClass намного лучше, чем функция Dissect Data/Structures в Cheat Engine, поскольку включает в себя множество функций, отсутствующих в Cheat Engine, что делает его более эффективным и полезным инструментом при поиске различных данных зависящих от структур данных.
Таким образом мы подошли к завершению обзора мощного инструмента Reclass.NET. Который позволяет не только анализировать память игр, но и напрямую участвует в разработке SDK для читов. Я специально не стал выкладывать тот самый сгенерированный кусок кода в Reclass, чтобы каждый мог потренироваться в этой программе. Если будут какие-либо вопросы – смело задавайте!
Надеюсь, что статья была полезна и вы узнали для себя что-то новое! До следующей части!
Последнее редактирование: