Продолжение крутой статьи:Информация - Чит/трейнер своими руками. Хакер. Часть 1
Автор реально постарался всё расписать, респект:
Сегодня мы с тобой напишем чит для сетевого шутера. Мы реализуем хаки типа extrasensory perception (ESP) и aimbot. ESP отображает информацию об игроках над их головами. Здесь может быть здоровье игрока, имя или текущее используемое оружие. Aimbot автоматически нацеливается на других игроков.
Также создадим локальный сервер. Нужно будет настроить его, а именно задать время игры, чтобы таймер не торопил нас. Конфиг лежит по следующему пути:
C:\Program Files (x86)\AssaultCube 1.3.0.2\config\maprot.cfg
Поменяем время на максимальное — 100 минут.
А после уже запускаем сам сервер.
И подключаемся к нему.
Запускаем
Будем наносить урон до тех пор, пока не найдем адрес, по которому хранится показатель здоровья нашего игрока.
На этом все знакомые по прошлой статье действия в Cheat Engine заканчиваются и начинаются новые. Наша цель — реализовать extrasensory perception и aimbot. Для этого нам нужно узнать класс игрока и его статический адрес. Чтобы найти класс, кликаем правой кнопкой мыши по нашему адресу и выбираем Find out what writes to this address (можно просто нажать F6).
Появится новое окно, где будут отображаться инструкции, которые производят запись по нашему адресу. Чтобы они появились, снова наносим урон вторым игроком первому.
Показатель здоровья записывает всего одна инструкция, она расположена по смещению 0xEC. Значит, значение, хранящееся в регистре edx, — это адрес класса игрока (но адрес в куче, а не статический). Поэтому добавим его вручную.
Как видишь, у нас есть множество параметров для поиска указателя, но нас интересует Max level. Это значение отвечает за то, сколько раз будет разыменован наш указатель (статический адрес). Оно‑то и поможет нам получить искомый адрес.
Исполняемый файл игры занимает мало, и кода в нем тоже негусто. Это наводит на мысль, что классов и структур в игре не так много и не будет большого количества смещений. Поэтому глубину поиска мы установим равной единице.
Мы видим следующий результат. Об адресах вида "ac_client.exe"+YYYYYYYY мы уже писали в прошлой статье. Именно они нам и понадобятся. А вот адреса формата "THREADSTACKX"-YYYYYYYY говорят нам о том, что искомый нами адрес был найден в стеке.
Добавим найденные пять адресов в список.
И выставим для каждого адреса смещение до показателей здоровья.
Далее перезапускаем игру, и Cheat Engine предложит сохранить наши адреса. Мы их сохраняем, чтобы загрузить при повторном подключении.
После переподключения к игре в списке адресов Cheat Engine видим, что только два адреса указывают на показатель здоровья: "ac_client.exe"+0017E0A8 и "ac_client.exe"+0018AC00.
Попробуем повторно получить урон и посмотреть, что будет с другими адресами. Как видим, еще в двух адресах появился наш показатель здоровья. Значит, в списке мы оставляем только два упомянутых ранее адреса.
Добавим статический адрес "ac_client.exe"+0017E0A8 под именем Player_ptr_1.
Добавим статический адрес "ac_client.exe"+0018AC00 под именем Player_ptr_2.
К счастью, у Cheat Engine есть инструмент, который позволяет нам лучше визуализировать структуры памяти, а не просматривать байты в дампе. Просто жми правой кнопкой мыши по выделенному байту и выбирай Open in dissect data/structure.
Откроется новое окно с адресом выбранного байта. Нажми Structures, затем Define new structure (или Ctrl-N).
Назовем структуру Player. Остальные настройки оставим по умолчанию: предположительный тип поля и размер структуры (4096).
Поставив галочку Guess field type, мы попросили Cheat Engine угадать тип поля. И он неплохо с этим справился.
Перейдем по смещению 0xEC, чтобы убедиться, что там находится показатель здоровья нашего игрока.
Также можно по смещению 0x205 обнаружить имя игрока.
В дальнейшем нам нужен этот класс, но, к сожалению, Cheat Engine не позволяет экспортировать структуру, а делать это вручную — значит подвергать себя мучениям. Это не нужно, поскольку существует готовый инструмент —
После присоединения к процессу создается класс по базовому адресу 400000, он называется N0000004E. Дважды кликаем по адресу и выставляем нужный нам: 0066ED48. Аналогично изменяем имя класса на Player и добавляем для отображения еще 4096 байт.
В данном контексте отображаемое количество байтов СE воспринимает как класс, который мы экспортируем. Если размер нашего класса больше, чем стандартное количество отображаемых байтов, нужно вручную увеличить размер.
Переходим к смещению 0xEC, к показателю здоровья. Меняем значение типа поля на DWORD (мы выставим значения типов для смещений, что позволит нам в дальнейшем экспортировать класс с нужными полями).
Тем, кто не знает или забыл, как работает трехмерная система координат в компьютерной графике, рекомендую статью «
Для реализации aimbot нам понадобятся значения тангажа (pitch), рысканья (yaw) и крена (roll). Не знаешь, что это? Давай покажу на примере игрового движка
Запись координат может разниться в зависимости от движка. Часто различается направление осей и то, какая из них считается высотой.
Для демонстрации возьмем с GitHub
Тангаж — движение персонажа относительно оси X. Нижняя стрелка — фактическое движение персонажа в данный момент, верхняя — другой вариант движения.
Рысканье — движение персонажа относительно оси Y.
Крен — движение персонажа относительно оси Z.
Можно заметить, что два набора из трех повторяются по X и Y, а вот координата Z у них разная. Отсюда мы можем сделать вывод, что один набор — это координаты игрока, а второй — головы. Так как в OpenGL в качестве высоты используется координата Z, мы попытаемся поменять высоту для каждого набора. Начнем с первого, но после попытки поменять значение c -0.5 на другое оно снова станет прежним. Значит, это координаты головы, второй набор — координаты игрока, а третий — движение относительно осей. Но мы в этом еще должны убедиться.
Теперь давай попробуем сделать для второго набора то, что мы делали для первого.
Для наглядной демонстрации встанем на ящик, а в Cheat Engine будем смотреть на значение по смещению 0x30.
Попробуем изменить это значение и увидим, как наш персонаж провалился сквозь ящик.
Это значит, что наши предположения верны.
Вернемся в окно ReClass.NET и выберем типы для этих смещений.
Вернемся в окно ReClass.NET и выберем типы для этих смещений.
Вот так теперь выглядит наш класс. Но что за VTable? Это поле не нужно, я добавил его для красоты. Подробнее о том, что это и зачем, можешь прочитать в статье
Теперь экспортируем наш класс (нажми на него правой кнопкой мыши и выбери соответствующий пункт меню). Можешь видеть, что есть поля, которые мы с тобой не искали: armor, team и прочие. Думаю, ты при желании сможешь найти их сам.
С наскока ничего найти не удается, но наметанный глаз заметит, что доступ к показателям здоровья нашего игрока идет через какой‑то статический адрес. Оказывается, мы выбрали неправильный! Правильный адрес такой: "ac_client.exe"+0018AC00.
Попробуем теперь поискать через имя игрока, которое находится по смещению 0x205.
И получим список инструкций, которые обращаются к имени.
Перебираем варианты и находим такой, где доступ происходит в цикле.
Те, кто знаком с ассемблером, понимают, что в регистре ebx находится адрес первого элемента, esi — это индекс, а умножение индекса говорит нам о типе данных, в данном случае это 4, DWORD.
Таким образом, статический адрес списка сущностей будет таким:
"ac_client.exe"+0018AC04
Убедиться в этом можно, проверив по смещению 0x205 имя игрока.
Также нам понадобится общее количество игроков. Полистав вывод дизассемблера, в конце цикла увидим проверку. В регистре edi хранится текущее количество игроков.
Немного подебажив, замечаем, что выше цикла находится статический адрес количества игроков: "ac_client.exe"+0018AC0C.
Перезапустим и проверим, что адреса правильные.
Теперь смотрим прямо вниз и выставляем новые параметры поиска. И чередуем, пока не найдем приемлемое количество результатов. В данном случае понадобилось не так много повторений, чтобы отыскать два статических адреса. Их‑то мы и будем проверять.
Откроем в дампе первый адрес и для удобства изменим тип отображаемых данных с байтов на float.
Вот что мы имеем.
Теперь нужно поднимать и опускать оружие и смотреть, как меняются значения. Мы видим, что в области, обозначенной красным, что‑то изменилось. Причем 1.0 и -1.0 будет только в выделенной области, а перед ней, если судить по размерам матриц, будут, соответственно, матрица мира и матрица перемещения. Таким образом, статический адрес матрицы вида получается "ac_client.exe"+17DFD0.
Не забываем перезапустить игру и проверить правильность наших находок.
Для активации ESP-хака жмем F1, для деактивации — F2.
Для активации aimbot жмем F2 и, чтобы он заработал, зажимаем правую кнопку мыши. Для деактивации хака жмем F2
Автор реально постарался всё расписать, респект:
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
Сегодня мы с тобой напишем чит для сетевого шутера. Мы реализуем хаки типа extrasensory perception (ESP) и aimbot. ESP отображает информацию об игроках над их головами. Здесь может быть здоровье игрока, имя или текущее используемое оружие. Aimbot автоматически нацеливается на других игроков.
В предыдущей статье «Вы должны зарегистрироваться, чтобы увидеть внешние ссылки» я заложил базу для будущих читов и объяснил основные понятия. Рекомендую ознакомиться с ней, чтобы лучше понимать, что мы будем сегодня делать.
ВЫБОР ИГРЫ
Мой выбор пал на
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
— бесплатный многопользовательский шутер от первого лица, основанный на движке CUBE. Используется графическая библиотека OpenGL.Использование читов нарушает пользовательское соглашение игры и может повлечь юридическое преследование. Мы обсуждаем здесь создание чита исключительно в целях обучения. Автор и редакция не несут ответственности за возможные последствия применения и распространения такого ПО.
ПОИСК ЗНАЧЕНИЙ
Для начала запустим игру и в настройках выберем оконный режим, ведь нам нужно, чтобы на экране помещалось еще что‑то, кроме игры.Также создадим локальный сервер. Нужно будет настроить его, а именно задать время игры, чтобы таймер не торопил нас. Конфиг лежит по следующему пути:
C:\Program Files (x86)\AssaultCube 1.3.0.2\config\maprot.cfg
Поменяем время на максимальное — 100 минут.
А после уже запускаем сам сервер.
И подключаемся к нему.
Запускаем
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и подключаемся к процессу игры.Поиск показателя здоровья
Для тестирования нам понадобится второй игрок. Можешь подключиться со второго устройства или, как сделал я, из виртуальной машины. Для поиска показателя здоровья выставляем параметры сканирования в Cheat Engine и вторым игроком наносим урон первому. После этого ищем здоровье в Cheat Engine.Будем наносить урон до тех пор, пока не найдем адрес, по которому хранится показатель здоровья нашего игрока.
На этом все знакомые по прошлой статье действия в Cheat Engine заканчиваются и начинаются новые. Наша цель — реализовать extrasensory perception и aimbot. Для этого нам нужно узнать класс игрока и его статический адрес. Чтобы найти класс, кликаем правой кнопкой мыши по нашему адресу и выбираем Find out what writes to this address (можно просто нажать F6).
Появится новое окно, где будут отображаться инструкции, которые производят запись по нашему адресу. Чтобы они появились, снова наносим урон вторым игроком первому.
Показатель здоровья записывает всего одна инструкция, она расположена по смещению 0xEC. Значит, значение, хранящееся в регистре edx, — это адрес класса игрока (но адрес в куче, а не статический). Поэтому добавим его вручную.
Поиск статического адреса объекта игрока
В прошлый раз я использовал отладчик, чтобы наглядно показать, что собой представляет статический адрес. В этот раз для поиска статического адреса мы будем использовать Cheat Engine. Жмем на ранее добавленный адрес 0x6AED20 правой кнопкой мыши и выбираем Pointer scan for this address.Как видишь, у нас есть множество параметров для поиска указателя, но нас интересует Max level. Это значение отвечает за то, сколько раз будет разыменован наш указатель (статический адрес). Оно‑то и поможет нам получить искомый адрес.
Исполняемый файл игры занимает мало, и кода в нем тоже негусто. Это наводит на мысль, что классов и структур в игре не так много и не будет большого количества смещений. Поэтому глубину поиска мы установим равной единице.
Мы видим следующий результат. Об адресах вида "ac_client.exe"+YYYYYYYY мы уже писали в прошлой статье. Именно они нам и понадобятся. А вот адреса формата "THREADSTACKX"-YYYYYYYY говорят нам о том, что искомый нами адрес был найден в стеке.
Добавим найденные пять адресов в список.
И выставим для каждого адреса смещение до показателей здоровья.
Далее перезапускаем игру, и Cheat Engine предложит сохранить наши адреса. Мы их сохраняем, чтобы загрузить при повторном подключении.
После переподключения к игре в списке адресов Cheat Engine видим, что только два адреса указывают на показатель здоровья: "ac_client.exe"+0017E0A8 и "ac_client.exe"+0018AC00.
Попробуем повторно получить урон и посмотреть, что будет с другими адресами. Как видим, еще в двух адресах появился наш показатель здоровья. Значит, в списке мы оставляем только два упомянутых ранее адреса.
Добавим статический адрес "ac_client.exe"+0017E0A8 под именем Player_ptr_1.
Добавим статический адрес "ac_client.exe"+0018AC00 под именем Player_ptr_2.
Класс игрока
Предположим, что статический адрес Player_ptr_1 — тот, что мы ищем (что на самом деле не так, правильным статическим адресом будет Player_ptr_2, но в этом мы убедимся позже). Чтобы просмотреть класс игрока в памяти, нажимаем правой кнопкой мыши на адрес и выбираем Browse this memory region (или жмем Ctrl-B).К счастью, у Cheat Engine есть инструмент, который позволяет нам лучше визуализировать структуры памяти, а не просматривать байты в дампе. Просто жми правой кнопкой мыши по выделенному байту и выбирай Open in dissect data/structure.
Откроется новое окно с адресом выбранного байта. Нажми Structures, затем Define new structure (или Ctrl-N).
Назовем структуру Player. Остальные настройки оставим по умолчанию: предположительный тип поля и размер структуры (4096).
Поставив галочку Guess field type, мы попросили Cheat Engine угадать тип поля. И он неплохо с этим справился.
Перейдем по смещению 0xEC, чтобы убедиться, что там находится показатель здоровья нашего игрока.
Также можно по смещению 0x205 обнаружить имя игрока.
В дальнейшем нам нужен этот класс, но, к сожалению, Cheat Engine не позволяет экспортировать структуру, а делать это вручную — значит подвергать себя мучениям. Это не нужно, поскольку существует готовый инструмент —
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
. Он дает возможность напрямую выгрузить структуру в виде кода на C++. Скачиваем, устанавливаем и подключаемся к процессу игры.После присоединения к процессу создается класс по базовому адресу 400000, он называется N0000004E. Дважды кликаем по адресу и выставляем нужный нам: 0066ED48. Аналогично изменяем имя класса на Player и добавляем для отображения еще 4096 байт.
В данном контексте отображаемое количество байтов СE воспринимает как класс, который мы экспортируем. Если размер нашего класса больше, чем стандартное количество отображаемых байтов, нужно вручную увеличить размер.
Переходим к смещению 0xEC, к показателю здоровья. Меняем значение типа поля на DWORD (мы выставим значения типов для смещений, что позволит нам в дальнейшем экспортировать класс с нужными полями).
Поиск координат
Начиная с этого места, мы будем искать значения, нужные непосредственно для реализации ESP и aimbot. Для ESP нам понадобятся координаты самого игрока и его головы в трехмерном пространстве.Тем, кто не знает или забыл, как работает трехмерная система координат в компьютерной графике, рекомендую статью «
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
» на «Хабрахабре».Для реализации aimbot нам понадобятся значения тангажа (pitch), рысканья (yaw) и крена (roll). Не знаешь, что это? Давай покажу на примере игрового движка
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.Запись координат может разниться в зависимости от движка. Часто различается направление осей и то, какая из них считается высотой.
Для демонстрации возьмем с GitHub
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
и посмотрим, как он будет двигаться, если менять координаты.Тангаж — движение персонажа относительно оси X. Нижняя стрелка — фактическое движение персонажа в данный момент, верхняя — другой вариант движения.
Рысканье — движение персонажа относительно оси Y.
Крен — движение персонажа относительно оси Z.
Поиск координат игрока
С новыми знаниями возвращаемся к игре и окну Cheat Engine. Начинаем перемещаться по карте и вертеть мышкой из стороны в сторону, а также вверх и вниз. В окне Cheat Engine можем видеть три последовательности из значений с плавающей запятой, которые менялись при наших действиях. Значит, это координаты игрока, координаты головы игрока и его поворот относительно осей.Можно заметить, что два набора из трех повторяются по X и Y, а вот координата Z у них разная. Отсюда мы можем сделать вывод, что один набор — это координаты игрока, а второй — головы. Так как в OpenGL в качестве высоты используется координата Z, мы попытаемся поменять высоту для каждого набора. Начнем с первого, но после попытки поменять значение c -0.5 на другое оно снова станет прежним. Значит, это координаты головы, второй набор — координаты игрока, а третий — движение относительно осей. Но мы в этом еще должны убедиться.
Теперь давай попробуем сделать для второго набора то, что мы делали для первого.
Для наглядной демонстрации встанем на ящик, а в Cheat Engine будем смотреть на значение по смещению 0x30.
Попробуем изменить это значение и увидим, как наш персонаж провалился сквозь ящик.
Это значит, что наши предположения верны.
Вернемся в окно ReClass.NET и выберем типы для этих смещений.
Поиск pitch, yaw, roll
Сделаем такую же проверку. Веди мышью из стороны в сторону, а потом вверх и вниз. Попробуем поменять значения, и оказывается, что наши предположения были верны.Вернемся в окно ReClass.NET и выберем типы для этих смещений.
Вот так теперь выглядит наш класс. Но что за VTable? Это поле не нужно, я добавил его для красоты. Подробнее о том, что это и зачем, можешь прочитать в статье
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
.Теперь экспортируем наш класс (нажми на него правой кнопкой мыши и выбери соответствующий пункт меню). Можешь видеть, что есть поля, которые мы с тобой не искали: armor, team и прочие. Думаю, ты при желании сможешь найти их сам.
Entity List
С нашим игроком мы разобрались, а как быть с другими игроками? Обычно где‑то есть огромный список всех игровых сущностей, в который входят и персонажи игроков. Можно предположить, что где‑то в коде должен существовать и цикл, который перебирает игроков, и дальше с их данными как‑то взаимодействует логика игры. Попробуем оттолкнуться от показателя здоровья и поискать, какие инструкции его запрашивают.С наскока ничего найти не удается, но наметанный глаз заметит, что доступ к показателям здоровья нашего игрока идет через какой‑то статический адрес. Оказывается, мы выбрали неправильный! Правильный адрес такой: "ac_client.exe"+0018AC00.
Попробуем теперь поискать через имя игрока, которое находится по смещению 0x205.
И получим список инструкций, которые обращаются к имени.
Перебираем варианты и находим такой, где доступ происходит в цикле.
Те, кто знаком с ассемблером, понимают, что в регистре ebx находится адрес первого элемента, esi — это индекс, а умножение индекса говорит нам о типе данных, в данном случае это 4, DWORD.
Таким образом, статический адрес списка сущностей будет таким:
"ac_client.exe"+0018AC04
Убедиться в этом можно, проверив по смещению 0x205 имя игрока.
Также нам понадобится общее количество игроков. Полистав вывод дизассемблера, в конце цикла увидим проверку. В регистре edi хранится текущее количество игроков.
Немного подебажив, замечаем, что выше цикла находится статический адрес количества игроков: "ac_client.exe"+0018AC0C.
Перезапустим и проверим, что адреса правильные.
Поиск View Matrix
Матрица View (вид) нужна для корректной работы ESP, более подробно об этом можешь почитать в
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки
на «Хабрахабре».- Локальные координаты — это координаты объекта, измеряемые относительно той точки, в которой находится объект.
- На следующем шаге локальные координаты преобразуются в координаты в пространстве мира. Они отложены от какой‑то точки, единой для всех других объектов, расположенных в мировом пространстве.
- Дальше мы трансформируем мировые координаты в координаты пространства вида таким образом, чтобы каждая вершина стала видна, как если бы на нее смотрели из камеры или с точки зрения наблюдателя.
- После того как координаты будут преобразованы в пространство вида, мы спроецируем их в координаты отсечения. Координаты отсечения задаются в диапазоне от -1.0 до 1.0 и определяют, какие вершины появятся на экране.
- И наконец, в процессе преобразования, который мы назовем трансформацией области просмотра, мы преобразуем координаты отсечения от -1.0 до 1.0 в область экранных координат, заданную функцией glViewport.
Теперь смотрим прямо вниз и выставляем новые параметры поиска. И чередуем, пока не найдем приемлемое количество результатов. В данном случае понадобилось не так много повторений, чтобы отыскать два статических адреса. Их‑то мы и будем проверять.
Откроем в дампе первый адрес и для удобства изменим тип отображаемых данных с байтов на float.
Вот что мы имеем.
Теперь нужно поднимать и опускать оружие и смотреть, как меняются значения. Мы видим, что в области, обозначенной красным, что‑то изменилось. Причем 1.0 и -1.0 будет только в выделенной области, а перед ней, если судить по размерам матриц, будут, соответственно, матрица мира и матрица перемещения. Таким образом, статический адрес матрицы вида получается "ac_client.exe"+17DFD0.
Не забываем перезапустить игру и проверить правильность наших находок.
НАПИСАНИЕ ЧИТА
Как и в предыдущей статье, мы будем писать внутренний чит, поэтому нам понадобится не только сама библиотека, но и инжектор, который внедрит нашу библиотеку в процесс игры. Инжектор получит список процессов, найдет процесс игры и выделит в ней память, в которую запишет наш внутренний чит. А потом создаст удаленный поток внутри игры для выполнения кода.Injector
Код инжектора выглядит следующим образом.
C++:
#include <windows.h>
#include <tlhelp32.h>
// Имя внедряемой DLL
const char* dll_path = "internal_cheat_ac.dll";
int main(void) {
HANDLE process;
void* alloc_base_addr;
HMODULE kernel32_base;
LPTHREAD_START_ROUTINE LoadLibraryA_addr;
HANDLE thread;
HANDLE snapshot = 0;
PROCESSENTRY32 pe32 = { 0 };
DWORD exitCode = 0;
pe32.dwSize = sizeof(PROCESSENTRY32);
// Получение снапшота текущих процессов
snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
Process32First(snapshot, &pe32);
do {
// Мы хотим работать только с процессом AC
if (wcscmp(pe32.szExeFile, L"ac_client.exe") == 0) {
// Во-первых, нам нужно получить дескриптор процесса, чтобы использовать его для следующих вызовов
process = OpenProcess(PROCESS_ALL_ACCESS, true, pe32.th32ProcessID);
// Чтобы не повредить память, выделим дополнительную память для хранения нашего пути к DLL
alloc_base_addr = VirtualAllocEx(process, NULL, strlen(dll_path) + 1, MEM_COMMIT, PAGE_READWRITE);
// Записываем путь к нашей DLL в память, которую мы только что выделили внутри игры
WriteProcessMemory(process, alloc_base_addr, dll_path, strlen(dll_path) + 1, NULL);
// Создаем удаленный поток внутри игры, который будет выполнять LoadLibraryA
// Этому вызову LoadLibraryA мы передадим полный путь к нашей DLL, которую мы прописали в игру
kernel32_base = GetModuleHandle(L"kernel32.dll");
LoadLibraryA_addr = (LPTHREAD_START_ROUTINE)GetProcAddress(kernel32_base, "LoadLibraryA");
thread = CreateRemoteThread(process, NULL, 0, LoadLibraryA_addr, alloc_base_addr, 0, NULL);
// Чтобы убедиться, что наша DLL внедрена, мы можем использовать следующие два вызова для синхронизации
WaitForSingleObject(thread, INFINITE);
GetExitCodeThread(thread, &exitCode);
// Наконец, освобождаем память и очищаем дескрипторы процесса
VirtualFreeEx(process, alloc_base_addr, 0, MEM_RELEASE);
CloseHandle(thread);
CloseHandle(process);
break;
}
// Перебор процессов из снапшота
} while (Process32Next(snapshot, &pe32));
return 0;
}
DLL
Наша библиотека будет состоять из следующих модулей:- главный модуль — dllmain.cpp;
- модуль смещений в игре и игровых структур — structures.h;
- модуль хуков — hook.cpp и hook.h;
- модуль рисования — gl_draw.cpp и gl_draw.h.
Главный модуль
При загрузке нашей библиотеки через функцию LoadLibraryA создается поток, в котором будет работать основная логика нашего чита. А именно:- получение указателей на нужные нам поля;
- установка хука на функцию отрисовки сцен;
- цикл, в котором реализовано включение и отключение хаков, завершение работы чита;
- функция, рисующая меню;
- хаки ESP и aimbot.
C++:
#include <iostream>
#include <string>
#include <tchar.h>
#include <thread>
#include <mutex>
#define _USE_MATH_DEFINES
#include <math.h>
#include "hook.h"
#include "structures.h"
#include "gl_draw.h"
// Переменная игрока
Player* player;
// Переменная синхронизации
std::mutex hook_mutex;
// Typedef для функции wglSwapBuffers
typedef BOOL(__stdcall* twglSwapBuffers) (HDC hDc);
Tramp_Hook* esp_hook;
Player** player_list = nullptr;
int* player_list_size = nullptr;
static int list_size = 0;
static Vector3 screen_position;
static Vector3 world_position;
static Vector3 player_position;
static float* view_matrix;
bool aimbot_enabled = false;
// Меню
void draw_menu(bool flag_esp) {
std::string esp = "ESP is ";
std::string aimbot = "Aimbot is ";
if (flag_esp) {
esp += "ON press F1 to OFF";
}
else {
esp += "OFF press F1 to ON";
}
if (aimbot_enabled) {
aimbot += "ON press F2 to OFF";
}
else {
aimbot += "OFF press F2 to ON";
}
GL::print_gl(50, 1200, rgb::gray, esp.c_str());
GL::print_gl(50, 1300, rgb::gray, aimbot.c_str());
GL::print_gl(50, 1400, rgb::gray, "Exit cheat press F3");
}
// Наша функция, которую вызывает установленный хук
BOOL _stdcall hooked_wglSwapBuffers(HDC hDc) {
hook_mutex.lock();
draw_menu(esp_hook->is_enabled());
if (esp_hook->is_enabled()) {
// Настроить орфографический режим
GL::setup_orthographic();
if (*player_list_size == list_size) {
for (int i = 0; i < list_size; ++i) {
// Проверить, действителен ли адрес игрока
if (!player_list[i]) {
continue;
}
if (player_list[i]->hp > 0 && player_list[i]->hp < 200) {
// Сохранить позицию других игроков в Vector3
world_position.x = player_list[i]->x_y_z_player.x;
world_position.y = player_list[i]->x_y_z_player.y;
world_position.z = player_list[i]->x_y_z_player.z;
// Сохранить позицию головы других игроков в Vector3
player_position.x = player->x_y_z_head.x;
player_position.y = player->x_y_z_head.y;
player_position.z = player->x_y_z_head.z;
// Рассчитать расстояние до другого игрока
float distance = sqrtf((player_position.x - world_position.x) * (player_position.x - world_position.x) + (player_position.y - world_position.y) * (player_position.y - world_position.y) + (player_position.z - world_position.z) * (player_position.z - world_position.z));
// Проверить, находится ли игрок в зоне обзора
if (distance > 5.0f && GL::world_to_screen(world_position, screen_position, view_matrix)) {
// Проверить, находится ли другой игрок в той же команде, что и мы
if (player_list[i]->team != player->team) {
// Команда врага
GL::draw_esp_box(screen_position.x, screen_position.y, distance, rgb::red, player_list[i]->name, player_list[i]->hp, player_list[i]->armor);
}
else {
// Наша команда
GL::draw_esp_box(screen_position.x, screen_position.y, distance, rgb::green, player_list[i]->name, player_list[i]->hp, player_list[i]->armor);
}
}
}
}
}
else {
list_size = *player_list_size;
}
GL::restore_gl(); // Восстановить исходный режим
}
if (aimbot_enabled && GetAsyncKeyState(VK_RBUTTON)) {
if (*player_list_size == list_size) {
// Эти переменные будут использоваться для удержания ближайшего к нам врага
float closest_player = -1.0f;
float closest_yaw = 0.0f;
float closest_pitch = 0.0f;
// Выбор ближайшего игрока в качестве цели
for (int i = 0; i < list_size; ++i) {
// Проверить, действителен ли адрес игрока, при этом пропускаем игроков из нашей команды и проверяем показатель здоровья
if (!player_list[i] || (player_list[i]->team == player->team) || (player_list[i]->hp < 0)) {
continue;
}
// Расчет абсолютного положения врага вдали от нас. Позволяет убедиться, что наши будущие расчеты верны и основаны на исходной точке
float abspos_x = player_list[i]->x_y_z_player.x - player->x_y_z_player.x;
float abspos_y = player_list[i]->x_y_z_player.y - player->x_y_z_player.y;
float abspos_z = player_list[i]->x_y_z_player.z - player->x_y_z_player.z;
// Расчет дистанции до врага
float temp_distance = sqrtf((abspos_x * abspos_x) + (abspos_y * abspos_y));
// Если это ближайший враг, рассчитываем рыскание и тангаж, чтобы нацелиться на него
if (closest_player == -1.0f || temp_distance < closest_player) {
closest_player = temp_distance;
// Рассчитываем рыскание
float azimuth_xy = atan2f(abspos_y, abspos_x);
// Переводим в градусы
float yaw = (float)(azimuth_xy * (180.0 / M_PI));
// Добавляем 90, так как игра предполагает, что прямой север равен 90 градусам
closest_yaw = yaw + 90;
// Рассчитываем тангаж
// Поскольку значения Z настолько ограничены, выбирай большее значение между X и Y, чтобы убедиться, что мы не смотрим прямо в небо, когда находимся рядом с врагом
if (abspos_y < 0) {
abspos_y *= -1;
}
if (abspos_y < 5) {
if (abspos_x < 0) {
abspos_x *= -1;
}
abspos_y = abspos_x;
}
float azimuth_z = atan2f(abspos_z, abspos_y);
// Преобразовываем значение в градусы
closest_pitch = (float)(azimuth_z * (180.0 / M_PI));
}
}
// Когда наш цикл завершится, устанавливаем для рысканья и тангажа самые близкие значения
player->yaw_pitch_roll.x = closest_yaw;
player->yaw_pitch_roll.y = closest_pitch;
}
else {
list_size = *player_list_size;
}
}
// Вызов перехваченной функции
BOOL ret_value = ((twglSwapBuffers)esp_hook->get_gateway())(hDc);
hook_mutex.unlock();
return ret_value;
}
DWORD WINAPI injected_thread(HMODULE hMod) {
// Получить адрес, по которому .exe был загружен внутри нашего игрового процесса
uintptr_t moduleBase = (uintptr_t)GetModuleHandle(0);
// Где находится указатель на нашего игрока
player = *reinterpret_cast<Player**>(moduleBase + player_offset);
// Указатель на список игроков
player_list = *reinterpret_cast<Player***>(moduleBase + entity_offset);
// Указатель на размер списка игроков
player_list_size = reinterpret_cast<int*>(moduleBase + entity_count_offset);
// Указатель на матрицу вида
view_matrix = reinterpret_cast<float*>(moduleBase + view_matrix_offset);
// Получить дескриптор модуля OpenGL
HMODULE open_gl = GetModuleHandleA("opengl32.dll");
if (!open_gl) {
return -1; // OpenGL не загружен
}
// Получить адрес wglSwapBuffers
void* orig_wglSwapBuffers = GetProcAddress(open_gl, "wglSwapBuffers");
// Установка хука на wglSwapBuffers
esp_hook = new Tramp_Hook(orig_wglSwapBuffers, hooked_wglSwapBuffers, 5);
while (!GetAsyncKeyState(VK_F3)) {
if (GetAsyncKeyState(VK_F1) & 1) { // Включить ESP
esp_hook->is_enabled() ? esp_hook->disable() : esp_hook->enable();
}
if (GetAsyncKeyState(VK_F2) & 1) { // Включить aimbot
aimbot_enabled = !aimbot_enabled;
}
Sleep(50);
}
hook_mutex.lock();
// Удаление хука
delete esp_hook;
hook_mutex.unlock();
// Извлечение нашей библиотеки
FreeLibraryAndExitThread(hMod, 0);
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)injected_thread, hModule, 0, 0);
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Модуль смещений в игре и игровых структур
Этот модуль содержит все нужные нам поля игрока, а также смещения.
C++:
#pragma once
#include "gl_draw.h"
class Player {
public:
DWORD* vftable; // 0x00
Vector3 x_y_z_head; // 0x04
BYTE pad_0010[24]; // 0x10
Vector3 x_y_z_player; // 0x28
Vector3 yaw_pitch_roll; // 0x34
BYTE pad_0040[172]; // 0x40
DWORD hp; // 0xEC
DWORD armor; // 0xF0
BYTE pad_00F4[273]; // 0xF4
BYTE name[16]; // 0x205
BYTE pad_0215[247]; // 0x215
BYTE team; // 0x30C
};
DWORD player_offset = 0x018AC00;
DWORD entity_offset = 0x018AC04;
DWORD entity_count_offset = 0x018AC0C;
DWORD view_matrix_offset = 0x17DFD0;
Модуль хуков
Это модуль, в котором будет перехватываться функция wglSwapBuffers, отвечающая за отрисовку сцен. Это необходимо для того, чтобы рисовать наше меню и боксы ESP.hook.h
C++:
#pragma once
#include <windows.h>
#include <memory>
// Класс перехватчика
class Hook {
// Указатель на перехватчик
void* this_to_hook;
// Сохраненные старые опкоды
std::unique_ptr<char[]> old_opcodes;
// Длина перезаписанных инструкций
int this_len;
// Включен ли перехватчик
bool enabled;
public:
// Конструктор для перехватчика
Hook(void* to_hook, void* our_func, int len);
// Деструктор для восстановления исходного кода
~Hook();
// Включить перехватчик
void enable();
// Отключить перехватчик
void disable();
// Включен ли перехватчик
bool is_enabled();
};
// Класс для реализации инлайн-перехватчика
class Tramp_Hook {
void* gateway;
Hook* managed_hook;
public:
Tramp_Hook(void* to_hook, void* our_func, int len);
// Восстанавливает исходный код
~Tramp_Hook();
void enable();
void disable();
bool is_enabled();
void* get_gateway();
};
hook.cpp
C++:
#include "hook.h"
Hook::Hook(void* to_hook, void* our_func, int len) : this_to_hook{to_hook }, old_opcodes{ nullptr }, this_len{ len }, enabled{ false } {
// Инструкция jmp имеет размер 5 байт. Место, которое мы перезаписываем, должно быть как минимум такого размера
if (len < 5) {
return;
}
DWORD curr_protection;
// Сделать доступной для записи память с кодом, который мы хотим перезаписать
VirtualProtect(to_hook, len, PAGE_EXECUTE_READWRITE, &curr_protection);
// Сохранить текущие байты в массив символов
old_opcodes = std::make_unique<char[]>(len);
if (old_opcodes != nullptr) {
for (int i = 0; i < len; ++i) {
old_opcodes[i] = ((char*)to_hook)[i];
}
}
// Перезапишем место, которое хотим перехватить, инструкциями nop
memset(to_hook, 0x90, len);
// Вычислить относительный адрес для перехода
DWORD rva_addr = ((DWORD)our_func - (DWORD)to_hook) - 5;
// Поместить опкод для инструкции jmp
*(BYTE*)to_hook = 0xE9;
// Поместить адрес для перехода
*(DWORD*)((DWORD)to_hook + 1) = rva_addr;
// Восстановить старую защиту кода
VirtualProtect(to_hook, len, curr_protection, &curr_protection);
}
Hook::~Hook() {
if (old_opcodes != nullptr) {
DWORD curr_protection;
// Сделать память доступной для записи
VirtualProtect(this_to_hook, this_len, PAGE_EXECUTE_READWRITE, &curr_protection);
// Записать старые опкоды обратно в перехваченное место
for (int i = 0; i < this_len; ++i) {
((char*)this_to_hook)[i] = Hook::old_opcodes[i];
}
// Восстановить старую защиту памяти
VirtualProtect(this_to_hook, this_len, curr_protection, &curr_protection);
}
}
void Hook::enable() {
this->enabled = true;
}
void Hook::disable() {
this->enabled = false;
}
bool Hook::is_enabled() {
return enabled;
}
Tramp_Hook::Tramp_Hook(void* to_hook, void* our_func, int len) : gateway{ nullptr }, managed_hook{ nullptr } {
// jmp имеет размер 5 байт
if (len < 5) {
return;
}
// Выделяем память для нашего трамплина
gateway = VirtualAlloc(0, len + 5, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Сохранение байтов, которые мы будем перезаписывать в трамплин
memcpy_s(gateway, len, to_hook, len);
// Получить адрес возврата
uintptr_t ret_addr = (BYTE*)to_hook - (BYTE*)gateway - 5;
// Разместить опкод jmp в конец трамплина
*(BYTE*)((uintptr_t)gateway + len) = 0xE9;
// Разместить адрес возврата после jmp
*(uintptr_t*)((uintptr_t)gateway + len + 1) = ret_addr;
// Создание перехватчика
managed_hook = new Hook(to_hook, our_func, len);
}
Tramp_Hook::~Tramp_Hook() {
managed_hook->disable();
delete managed_hook;
VirtualFree(gateway, 0, MEM_RELEASE);
}
void Tramp_Hook::enable() {
managed_hook->enable();
}
void Tramp_Hook::disable() {
managed_hook->disable();
}
bool Tramp_Hook::is_enabled() {
return managed_hook->is_enabled();
}
void* Tramp_Hook::get_gateway() {
return gateway;
}
Модуль рисования
Здесь у нас функции для непосредственной отрисовки меню и боксов ESP.gl_draw.h
C++:
#pragma once
#pragma comment(lib, "OpenGL32.lib")
#include <windows.h>
#include<gl/GL.h>
struct Vector3 {
float x, y, z;
};
struct Vector4 {
float x, y, z, w;
};
// Пространство имен цветов для рисования
namespace rgb {
const GLubyte red[3] = { 255,0,0 };
const GLubyte green[3] = { 0,255,0 };
const GLubyte blue[3] = { 0,0,255 };
const GLubyte gray[3] = { 55,55,55 };
const GLubyte light_gray[3] = { 192,192,192 };
const GLubyte yellow[3] = { 255, 255, 0 };
const GLubyte black[3] = { 0,0,0 };
}
// Пространство имен функций для отрисовки меню и ESP-хака
namespace GL {
void setup_orthographic();
void restore_gl();
void build_font();
void draw_filled_rectangle(float x, float y, float width, float height, const GLubyte color[3]);
void draw_out_line(float x, float y, float width, float height, float line_width, const GLubyte color[3]);
void draw_line(float fromX, float fromY, float toX, float toY, float line_width, const GLubyte color[3]);
void draw_esp_box(float pos_x, float pos_y, float distance, const GLubyte color[3], const BYTE* text, const int health_percent = -1, const int armor_percent = -1);
void print_gl(float x, float y, const GLubyte color[3], const char* fmt, ...);
bool world_to_screen(Vector3 pos, Vector3& screen, float matrix[16]);
}
gl_draw.cpp
C++:
#include "gl_draw.h"
#include <corecrt_math.h>
#include <stdio.h>
HDC h_DC;
HFONT h_old_font;
HFONT h_font;
UINT font_base;
bool b_font_build = 0;
void GL::setup_orthographic() {
// Cохранение атрибутов
glPushAttrib(GL_ALL_ATTRIB_BITS);
// Сохранение матрицы вида
glPushMatrix();
// Размеры экрана
GLint view_port[4];
// Получение размеров экрана
glGetIntegerv(GL_VIEWPORT, view_port);
// Установка размера экрана
glViewport(0, 0, view_port[2], view_port[3]);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
// Настройка орфографического режима
glOrtho(0, view_port[2], view_port[3], 0, -1, 1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
// Отключение проверки глубины
glDisable(GL_DEPTH_TEST);
}
void GL::restore_gl() {
// Восстановление матрицы вида
glPopMatrix();
// Восстановление всех атрибутов
glPopAttrib();
}
void GL::draw_filled_rectangle(float x, float y, float width, float height, const GLubyte color[3]) {
glColor3ub(color[0], color[1], color[2]);
glBegin(GL_QUADS);
glVertex2f(x, y);
glVertex2f(x + width, y);
glVertex2f(x + width, y + height);
glVertex2f(x, y + height);
glEnd();
}
void GL::draw_out_line(float x, float y, float width, float height, float line_width, const GLubyte color[3]) {
glLineWidth(line_width);
glBegin(GL_LINE_STRIP);
glColor3ub(color[0], color[1], color[2]);
glVertex2f(x - 0.5f, y - 0.5f);
glVertex2f(x + width + 0.5f, y - 0.5f);
glVertex2f(x + width + 0.5f, y + height + 0.5f);
glVertex2f(x - 0.5f, y + height + 0.5f);
glVertex2f(x - 0.5f, y - 0.5f);
glEnd();
}
void GL::draw_line(float fromX, float fromY, float toX, float toY, float line_width, const GLubyte color[3]) {
glLineWidth(line_width);
glBegin(GL_LINES);
glColor3ub(color[0], color[1], color[2]);
glVertex2f(fromX, fromY);
glVertex2f(toX, toY);
glEnd();
}
void GL::build_font()
{
h_DC = wglGetCurrentDC();
font_base = glGenLists(96);
h_font = CreateFont(-12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, FF_DONTCARE | DEFAULT_PITCH, L"Courier");
h_old_font = (HFONT)SelectObject(h_DC, h_font);
wglUseFontBitmaps(h_DC, 32, 96, font_base);
SelectObject(h_DC, h_old_font);
DeleteObject(h_font);
b_font_build = true;
}
void GL::print_gl(float x, float y, const GLubyte color[3], const char* fmt, ...)
{
if (!b_font_build) {
GL::build_font();
}
if (fmt == NULL) {
return;
}
glColor3f(color[0], color[1], color[2]);
glRasterPos2i(x, y);
char text[256];
va_list ap;
va_start(ap, fmt);
vsprintf(text, fmt, ap);
va_end(ap);
glPushAttrib(GL_LIST_BIT);
glListBase(font_base - 32);
glCallLists(strlen(text), GL_UNSIGNED_BYTE, text);
glPopAttrib();
}
void GL::draw_esp_box(float pos_x, float pos_y, float distance, const GLubyte color[3], const BYTE* text, const int health, const int armor) {
float line_width = 0.5f; // Толщина линии
GLint view_port[4];
glGetIntegerv(GL_VIEWPORT, view_port);
float height = (view_port[3] / distance) * 3; // Высота бокса
float width = (view_port[2] / distance); // Ширина бокса
// Snap lines
GL::draw_line(view_port[2] / 2.0f, (float)view_port[3], pos_x, pos_y, line_width + 2.0f, rgb::black);
GL::draw_line(view_port[2] / 2.0f, (float)view_port[3], pos_x, pos_y, line_width, color);
// Очертания
GL::draw_out_line(pos_x - (width / 2), pos_y - height, width, height, line_width + 2.0f, rgb::black);
GL::draw_out_line(pos_x - (width / 2), pos_y - height, width, height, line_width, color);
// Здоровье
if (health != -1) {
float perc = (width / 100);
float curr = perc * health;
GL::draw_filled_rectangle(pos_x - (width / 2) - 1, ((pos_y - (height / 10)) - 1) - height, width + 2, (height / 15) + 2, rgb::black);
GL::draw_filled_rectangle(pos_x - (width / 2), (pos_y - (height / 10)) - height, width, height / 15, rgb::light_gray);
GLubyte Hcolor[3]{ static_cast<GLubyte>(255 - (2.5f * health)), static_cast<GLubyte>(health * 2.5f), 0 };
GL::draw_filled_rectangle(pos_x - (width / 2), (pos_y - (height / 10)) - height, curr, height / 15, Hcolor);
}
// Броня
if (armor != -1) {
float perc = (width / 100);
float curr = perc * armor;
GL::draw_filled_rectangle(pos_x - (width / 2) - 1, ((pos_y - (height / 5)) - 1) - height, width + 2, (height / 15) + 2, rgb::black);
GL::draw_filled_rectangle(pos_x - (width / 2), (pos_y - (height / 5)) - height, width, height / 15, rgb::light_gray);
GL::draw_filled_rectangle(pos_x - (width / 2), (pos_y - (height / 5)) - height, curr, height / 15, rgb::blue);
}
// Имя
GL::print_gl(pos_x - (width / 2), (pos_y - (height / 4)) - height, rgb::yellow,(char *)text);
}
bool GL::world_to_screen(Vector3 pos, Vector3& screen, float matrix[16]) {
// Получение ширины и высоты экрана
GLint view_port[4];
glGetIntegerv(GL_VIEWPORT, view_port);
int window_width = view_port[2];
int window_height = view_port[3];
// Матрично-векторный результат, умножающий мировые (глазные) координаты на проекционную матрицу (clip_coords)
Vector4 clip_coords;
clip_coords.x = pos.x * matrix[0] + pos.y * matrix[4] + pos.z * matrix[8] + matrix[12];
clip_coords.y = pos.x * matrix[1] + pos.y * matrix[5] + pos.z * matrix[9] + matrix[13];
clip_coords.z = pos.x * matrix[2] + pos.y * matrix[6] + pos.z * matrix[10] + matrix[14];
clip_coords.w = pos.x * matrix[3] + pos.y * matrix[7] + pos.z * matrix[11] + matrix[15];
// Если координаты не на экране
if (clip_coords.w < 0.1f) {
return false;
}
// Перспективное деление, деление на clip.W, то есть нормализованные координаты устройства
Vector3 NDC;
NDC.x = clip_coords.x / clip_coords.w;
NDC.y = clip_coords.y / clip_coords.w;
NDC.z = clip_coords.z / clip_coords.w;
// Преобразование в координаты экрана
screen.x = (window_width / 2 * NDC.x) + (NDC.x + window_width / 2);
screen.y = -(window_height / 2 * NDC.y) + (NDC.y + window_height / 2);
return true;
}
ПРОВЕРКА РАБОТОСПОСОБНОСТИ
Для начала запустим наш чит.Для активации ESP-хака жмем F1, для деактивации — F2.
Для активации aimbot жмем F2 и, чтобы он заработал, зажимаем правую кнопку мыши. Для деактивации хака жмем F2