Author: Unseen
Источник: xss.pro
Предыдущая часть: https://xss.pro/threads/104114/
Вступление...
Доброго времени суток, дорогие Форумчане! Не получилось выпустить эту часть пораньше в связи с тем, что винт решил перед НГ катапультироваться и унести всю информацию в том числе и эту статью в мир иной. НО! Праздничное настроение спасли бэкапы =)
Слова благодарности...
Прежде всего хочу поблагодарить admin этого форума, за его поддержку и отзывчивость.
Благодарю snoww за его материальную поддержку в виде 300$.
И всех, кто принимает активное участие в развитии статей своими идеями и советами.
Предыстория…
Как вы помните в прошлых наших частях 1 и 2, мы разобрались, что из себя представляет память, байт коды, типы данных, и разобрались каких видов(External, Internal) бывает софт и написали свою первую программу(проще говоря Memory Manager) для работы с памятью другой программы(в данном случае игры). Если какие-то моменты подзабыли – рекомендую быстро пройтись по предыдущим частям.
Новые проблемы…
Конечно все бы ничего, если адреса к которым мы обращаемся не менялись… Наверное, вы замечали, что после нахождения адреса какого-то значения в игре, после перезапуска уровня или игры этот адрес в 90% случаев уже был не рабочим. Это происходит, потому что как мы обсуждали про ОЗУ в 1 части статьи, из-за того что ОС(*В данном случае Windows) после запуска программы на исполнение, копирует ее в выделенную под нее Виртуальную область памяти(Virtual Memory) из-за соображений безопасности и производительности. При чем при каждом перезапуске, модуль игры будет загружена в случайную область памяти.
Теория…
Иллюстрация загрузки игры в данном случае AssaultCube из предыдущей части в память:
Как видим на абстрактной картине, после запуска на исполнение, ОС выделила участок виртуальной памяти и скопировала туда модуль ac_client.exe.
После перезапуска наш исполняемый модуль(.exe) может быть скопирован в любую случайную область выделенной под нее памяти.
Иллюстрация Случайное расположение:
И поэтому, те адреса, которые мы нашли уже будут не рабочими. Если поглубже подойти к этому, то будет верно еще то, что когда вы запускаете уровень / миссию и т.д. игра будет подгружать некоторые объекты/структуры/модули динамически. По мере их необходимости.
Иллюстрация создания новых объектов/структур и размещение их в памяти:
Как выяснили ранее, порядок размещения модулей, объектов, структур в памяти может быть случайным.
Например: То есть синий участок(модуль игры) окажется в самом краю памяти(внизу), а объекты выделенные динамично в процессе взаимодействия пользователя с игрой выше.
Иллюстрация Случайного размещения:
Немного примеров из мира разработки игр…
При разработке игры, программисту нужно построить логику всей игры, чтобы она была производительной и не занимала много ОЗУ. Когда вы запускаете уровень в игре, игра динамически выделяет память под те или иные объекты и структуры. Например: создание игрока, ботов, предметов, квестов и т.д. на уровне. Ведь было бы глупо сразу статично загружать все уровни в память игры, даже те, которые недоступны игроку. Это бы нагружало нашу память. А она имеет свойство заканчиваться. То есть к чему я веду вас, если бы игра создавала эти объекты сразу после запуска ее на исполнение, то скорее у вас бы были жуткие лаги, просадка FPS - во точно, возможно игра крашилась и завершалась аварийно! А обычно, создание объектов осуществляется по мере необходимости. Например: из-за действий игрока/событий, вы открываете дверь и через некоторое время на вас нападают боты из-за угла. Вот тогда происходит создание новых объектов. Это называется динамическим выделением памяти. То есть под новые объекты будет выделено место из свободного пространства памяти процесса в этот момент времени. А ненужные объекты будут удаляться и происходить освобождение памяти.
Еще это называют аллокация и деаллокация.
Представим ситуацию, мы зашли в игру, нас встречает меню и как обычно нажимаем на «Новая игра», у нас происходит загрузка уровня: загружается карта, игрок, инвентарь игрока, предметы на карте, боты, рисуется интерфейс, еще могут быть подключены модули в виде dll(физика, particle системы и т.д.). Проще говоря, все нужные ресурсы для игры. Это все происходит динамично, по мере их необходимости. И под это все будет выделен новый участок памяти из свободного пространства.
Более подробно можете прочитать об Динамичном распределении памяти Здесь
То есть каждый объект/структура/класс будет подгружать нужные ему такие же структуры и объекты в процессе – динамично.
Как эта структура будет выглядеть графически:
Как видим на иллюстрации, после загрузки в память AssaultCube, мы начинаем игру и в этот момент произошли некоторые действия: Загрузилась локация/карта, она в свою очередь загрузила предметы(гранаты, патроны), ботов и наконец игрока, а игрок в свою очередь загрузил некий инвентарь, если это можно так назвать. И интерфейс, который отображает некоторые данные игрока(количество здоровья, брони, патронов), если заметили он тоже связан с нашим инвентарем игрока. Чтобы считывать количество наших предметов.
Вот поэтому, наши адреса постоянно распределяются в случайных позициях при перезапуске и перезагрузке уровня, каждый раз этот модуль игры, эти объекты меняют свое положение в адресном пространстве. Но как это можно решить? Может все же имеется какой-то способ узнать, в каких адресах окажутся эти модули/объекты? Ответа – да!
Размышления…0x…указатели…
Для начала давайте подумаем и найдем какую то логику в этом. У нас есть модуль игры(*Синий участок на картине), в нашем случае ac_client.exe , который копируется в память на исполнение. Возможно нам поможет снова ОС? Что если при помощи запросов WinApi к ОС(*Windows) спросить, в какой участок памяти был загружен этот модуль? Да, Вы будете абсолютно правы!
Хорошо, теоретически мы узнали адрес загруженного модуля в память.
НО! Почему бы нам не спросить ОС, где находятся адреса объектов? Это же очень легкий способ.
Я должен огорчить, что эти объекты, как мы узнали выше, будут выделяться в процессе игры, и из-за этого данный метод будет выведен на – нет! Но как вы заметили, на иллюстрациях выше я выделил красным стрелки от одного модуля/объекта к другому. Это называется указателем.
Иллюстрация:
Вот они то и придут нам в помощь.
Что такое указатель? Указатель (pointer) в программировании представляет собой переменную, которая содержит адрес в памяти другой переменной. Он "указывает" на местонахождение в памяти, где хранится значение определенной переменной или структуры. Указатели позволяют эффективно работать с памятью, обеспечивая доступ к данным по их адресам.
Указатели используются в различных контекстах, таких как динамическое выделение памяти, передача параметров в функции по ссылке, работа с массивами, структурами данных и многими другими случаями, где требуется эффективная работа с памятью.
Важно: Указатель являясь тоже переменной имеет свой адрес в памяти, а значением его будет адрес другой переменной. Размер типа указатель будет зависеть от разрядности системы. Для х86(32бит) это – 4 байта, а для х64бит это обычно 8 байт.
Более подробно про указатели можно почитать Здесь
Пример указателя:
Как видим на примере выше, есть некая переменная с адресом 00123ABC, а в его ячейки лежит другой адрес. Вот это и есть указатель! Мы можем обращаться к указателю и прямо получать доступ к ячейки переменной на который указывает этот сам УКАЗАТЕЛЬ!
Для наглядности, написал тестовую программу, которая отлично демонстрирует указатели:
Видим у нас есть переменная hp и ее значение 100, а есть еще указатель, который тоже имеет свой адрес и содержимое его ячейки будет адресом переменной hp, на которую он указывает.
Сам код:
Тот же пример с инвентарем игрока, где-то в коде игры может быть класс или структура UI интерфейс и в ее полях перемененная указатель pointerToInventoryPlayer, которая будет указывать на другую структуру или класс Player в котором есть возможно указатель на другую структуру или класс вида Inventory. Может и быть такое, что она прямо указывает на саму структуру Inventory.
Не открывай(Указатель):
Если бы мы хотели получить доступ к инвентарю нашего игрока в игре, у нас было бы 2 варианта решения этой задачи, а именно:
Пройтись через модуль игры + локация/Карта + Структура игрока + Инвентарь
Итого цель наша достигнута будет через 3 указателя. Точнее глубина указателей будет равна 3.
Пройтись через модуль игры + структура Интерфейс UI.
Глубина здесь 1.
P.S. Это всего лишь пример, возможно в игре, как увидим далее будет все реализовано по другому!
Еще более подробный пример:
Задача: Нам нужно получить доступ к адресу переменной здоровья игрока. В этом случае мы бы пошли через такой путь: модуль игры(*ac_client.exe) + локация/уровень(Level) + структура игрока(Player);
То есть через 2 указателя мы придем к объекту игрока и уже могли работать с его полями в структуре.
Важно отметить, что структуру можно представить как некую сущность, которая имеет свойства(поля), та же структура игрока(Player), может иметь такие поля как количество здоровья, брони, имя, и т.д.
Структура — это объединение нескольких объектов, возможно, различного типа под одним именем, которое является типом структуры. В качестве объектов могут выступать переменные, массивы, указатели и другие структуры. Структуры позволяют трактовать группу связанных между собой объектов не как множество отдельных элементов, а как единое целое. Структура представляет собой сложный тип данных, составленный из простых типов.
Структуры помогают улучшить организацию кода, разбив его на более логические и удобные части. Например, вы можете использовать структуры для представления объектов в вашей программе. Еще этот метод называют ООП. Мы сегодня базово узнаем, что оно из себя представляет.
Более подробно про ООП прочитать Здесь.
Предлагаю посмотреть это на уровне кода:
Как видно схеме, у нас есть несколько структур, на практике имеем очень много других.
Рассмотрим их для понимания сути и логики. Как мы узнали выше игра грузит нужные структуры и объекты – динамично. Допустим мы начали играть и у нас модуль игры(ac_client.exe) загружает выбранную карту пользователем – структуру Level, а в этой структуре содержатся такие данные: размеры карты – float Width и float Height. Указатель типа Player* player на структуру Player. P.S. указатель отмечается символом *(звездочка). К сведению, указатель будет указывать на адрес структуры Player, а именно на ее первый элемент массива char nickname[0]. Индекс массива в языке С++ будет начинаться с 0, а не как нам привычно 1. Значит можно сказать, что структура Level загружает структуру Player.
По условию задачи, нам нужно было получить доступ к адресу здоровья игрока. Для этого, нам нужно узнать расстояние(в байтах) относительно начала структуры Player до ее поля health. Обычно под «расстоянием» предполагается смещение(Offset - оффсет). Таким образом, смещение до поля health будет равно сумме размеров полей, предшествующих полю health:
Давайте как раз вычислим это смещение. Значит будем считать от начала структуры Player, первое ее поле это массив nickname из 32 элементов с типом char, как знаем из прошлых частей char имеет размер 1 байт:
Значит, чтобы вычислить адрес здоровья, мы должны взять адрес структуры, в данном случае Player и прибавить + 0x24. А адрес самой структуры Player мы можем вычислить через переменную указатель, который находится в структуре Level.
Выходит, чтобы добраться нам до адреса здоровья, нужно пройтись через все структуры, которые создаются по цепочке: сам модуль – ac_client.exe -> структура Level -> структура Player.
Более подробный план:
Следуя плану:
Но здесь есть одна ловушка, вы можете подумать, что сейчас адрес здоровья будет равен: 00223344 + 12 + 8 + 36 = 00223400. Но это на самом деле не так, по причине того, как сказал ранее, когда мы считали оффсеты для указателей, то мы находили не сам адрес структуры, а адрес УКАЗАТЕЛЯ, в котором расположен адрес начала структуры. Значит мы должны будем где то в коде своей программы, найдя указатель, должны считать его содержимое,
а именно адрес другой структуры и уже к нем прибавить оффсет, чтобы найти другой такой же указатель на другую структуру или переменную.
Еще иллюстрация:
Как видим, если мы прибавим к базовому адресу оффсет для указателя level, то получим адрес самого указателя и в ячейки по этому адресу будет лежать адрес начала структуры Level.
Для полноты картины:
То есть мы поэтапно идем от модуля игры к адресу указателя и в этой ячейки адреса уже находим адрес другой структуры и так дальше пока не дойдем до нужной нам структуры. Чаще всего путей добраться до нужной структуры может быть много, еще это называют глубиной указателей. Допустим мы могли добраться до структуры игрока через структуру интерфейса, ведь он тоже чаще всего связан с игроком. Даже могло выйти так, что он окажется короче и будет надежнее держаться после обновления игры. Ведь разработчик может добавить новое поле в структуру Level и уже те оффсеты, которые мы посчитали будут недействительный и приводить вообще непонятно к чему. Еще нужно учесть, что оффсеты не долго не живут, обычно при среднем обновлении игры, их приходится заново вычислять сканером памяти. Конечно есть более другие методы поиска этих оффсетов, называется поиск по сигнатурам – они более надежны, но пока эту тему отложим для будущих статей.
По началу это будет воспринять сложно, но это нормально. Если чувствуете себя неуверенно, перечитайте через час – полтора. Мозгу надо время на переваривание информации!
Практическая часть… Наконец-то!
P.S. Нашим подопытным будет тот же пациент Assault Cube!
Теперь, когда мы базово разобрались что же такое на самом деле виртуальная память, структуры данных, объекты, смещения(оффсеты), указатели. Думаю можем приступить к поиску этих самых оффсетов и указателей. Для начала полностью проведем рефакторинг нашего проекта из прошой части, если не сделаем этого сейчас, то в будущем будет сложно поддерживать наш проект. Напишем функцию, которая будет делать запрос при помощи WinApi к ОС, чтобы узнать базовый адрес модуля игры. И под конец все это испытаем и убедимся, что найденные нами оффсеты работают даже после перезапуска игры!
Надежный план, как швейцарские часы!
Итак весь наш план будет таков:
Ну что же, как всегда запускаем наш сканер Cheat Engine и подключаемся к процессу игры Assault Cube.
Теперь так же, как с прошлой части, найдем нужные нам адреса в памяти. Пусть это будет количество патронов и уровень здоровья.
Для начала найдем адрес здоровья:
Теперь, чтобы вычислить, те самые оффсеты, указатели нам нужно кликнуть ПКМ на этот адрес и выбираем “Pointer scan for this address” или же “Сканирование указателя по этому адресу”. Далее у нас появляется окошко на нем нажимаем “ОК”. Пока про тонкую настройку поиска указателей не будем говорить.
Далее нас просят ввести имя для файла скана. Даем что нибудь логичное, я назвал healtScanPtr. Дальше у нас происходит сканирование памяти и поиск в нем указателей. Следующее, что нас встречает это окошко с результатами:
Как выделено на картине мы имеем 3 цвета, это:
Иногда одного сканирования недостаточно, ведь здесь аж целых 26790 указателей. Многие из них возможно не будут рабочими после перезапуска игры. Для этого нужно проделать действия выше 2-3 раза перезапустив игру.
Покажу это на видео для экономии времени:
Как увидели, после 3 сканирований наших указателей осталось очень мало. Для надежности два раза кликнете по самым разным из них, как показано на видео. Эти указатели перенесутся в таблицу адресов в подменю сканера.
Желтыми отмечены указатели, которые остались работоспособными после перезапуска игры.
Значит они более надежны! Красные как видим поменяли значение на рандомный мусор.
Два раза кликнем теперь по любому из них(желтые):
Как видим внутри расположены зеленым наши те самые оффсеты, которые приведут к нужному адресу в структуре!
Теперь тоже самое проделайте и с адресом патронов и сохраните конечный результат в таблице!
Теперь осталось переписать наш проект!
Следуя плану составленному выше:
Вот полный исходный код нашего проекта:
Разбор кода...
Подключаем нужные библиотеки и заголовочные файлы:
Далее объявляем функцию GetProcessID, которая принимает в качестве параметра строку targetNameProcess, представляющую имя целевого процесса, и возвращает идентификатор (ID) процесса (process ID), если процесс с таким именем был найден. В противном случае возвращается 0.
Если перейдете в диспетчер задач и во вкладке “Подробности” , увидите те самые запущенные процессы в системе. Этот код делает “Скриншот” на данный момент запущенных процессов и ищет в них искомый нами процесс.
К примеру процесс ac_client.exe имеет у меня ID 9952.
Дальше объявим функцию GetModuleBaseAddress, которая принимает идентификатор процесса (procID) и имя модуля (moduleName) и возвращает базовый адрес загруженного модуля в указанном процессе. Если модуль с указанным именем не найден, возвращается 0.
Теперь функция FindTargetAddress, которая принимает дескриптор процесса (handleProc), базовый адрес (baseAddr) и вектор смещений (offsets) или же проще сказать массив/список смещений. Функция пытается найти целевой адрес, выполняя последовательные чтения памяти с использованием указанных смещений.То есть эта функция будет идти от начала адреса модуля до искомого нами адреса в структуре.
Теперь немного изменений в коде функций шаблонов для работы с памятью.
Функция memWrite, которая служит для записи значения в память процесса. Функция использует функцию WriteProcessMemory, которая позволяет записывать данные в виртуальную память другого процесса.
функцию memRead, которая служит для чтения данных из виртуальной памяти процесса. Эта функция также является шаблонной и использует ReadProcessMemory для выполнения операции чтения.
Когда с функциями закончили, осталась главная функция main:
Разрешим для начала кириллицу в консоли для удобства.
Дальше вызовем функцию GetProccesID, и передадим ему название модуля в данном случае ac_client.exe. Результат сохраним в переменную processID. И выведем этот айди в консоли.
Теперь вызовем функцию GetModuleBaseAddress для получения базового адреса модуля, результат запишем в переменную baseAddress. Дальше объявим переменную DynamicPtrAddress. Значением будет сам базовый адрес и смещение для этого модуля.
Дальше объявим беззнаковый вектор offsets. В нем будем хранить массив оффсетов.
вам нужно подставить свои оффсеты, которые нашли по видео.
Теперь выведем сам адрес относительно базового модуля. Для информативности.
Получаем хэндл и права на этот процесс.
Давайте вызовем функцию FindTargetAddres и передаем в параметры сам хэндл процесса, адрес относительно базового модуля, и сам массив оффсетов. Результатом будет конечный адрес того значения, что искали. Допустим адрес патронов.
Сделаем вывод этого адреса в hex формате для информативности.
Теперь как всегда сначала прочитаем значение по найденному значению при помощи функции memRead. И выведем результат.
далее вызываем функцию записи в память memWrite и передаем ему хэндл, сам адрес по которому нужно записать, и значение для записи.
Теперь снова прочитаем новое значение при помощи memRead.
Поздравляю! Вы очень молодцы! Все получилось, все прошло по плану! Попробуйте несколько раз перезапустить игру и снова проверить работоспособность ваших оффсетов!
Соглашусь статья очень получилась объемной, даже сам не представлял. Она отняла очень много сил и времени. Последние строчки еле дописывал… Я попытался все передать подробно, показать что и как. Если у вас остались какие нибудь вопросы по данной статье – смело задавайте в обсуждении. Все разберем и решим!
Всех благодарю за чтение! Если я пропустил ошибку или что то не разобрал по данной теме, прощу дополнить в комментариях.
Если хотите отблагодарить меня чашечкой кофе – реквизиты в подписи.
Всех с наступающим Новым Годом! Пожелаю, чтобы ваши профиты выросли в разы, а то и сотни иксов. А у новичков появились первые бабки $. До следующей части!
Ваш Unseen!
Источник: xss.pro
Предыдущая часть: https://xss.pro/threads/104114/
Вступление...
Доброго времени суток, дорогие Форумчане! Не получилось выпустить эту часть пораньше в связи с тем, что винт решил перед НГ катапультироваться и унести всю информацию в том числе и эту статью в мир иной. НО! Праздничное настроение спасли бэкапы =)
Слова благодарности...
Прежде всего хочу поблагодарить admin этого форума, за его поддержку и отзывчивость.
Благодарю snoww за его материальную поддержку в виде 300$.
И всех, кто принимает активное участие в развитии статей своими идеями и советами.
Предыстория…
Как вы помните в прошлых наших частях 1 и 2, мы разобрались, что из себя представляет память, байт коды, типы данных, и разобрались каких видов(External, Internal) бывает софт и написали свою первую программу(проще говоря Memory Manager) для работы с памятью другой программы(в данном случае игры). Если какие-то моменты подзабыли – рекомендую быстро пройтись по предыдущим частям.
Новые проблемы…
Конечно все бы ничего, если адреса к которым мы обращаемся не менялись… Наверное, вы замечали, что после нахождения адреса какого-то значения в игре, после перезапуска уровня или игры этот адрес в 90% случаев уже был не рабочим. Это происходит, потому что как мы обсуждали про ОЗУ в 1 части статьи, из-за того что ОС(*В данном случае Windows) после запуска программы на исполнение, копирует ее в выделенную под нее Виртуальную область памяти(Virtual Memory) из-за соображений безопасности и производительности. При чем при каждом перезапуске, модуль игры будет загружена в случайную область памяти.
Теория…
Иллюстрация загрузки игры в данном случае AssaultCube из предыдущей части в память:
После перезапуска наш исполняемый модуль(.exe) может быть скопирован в любую случайную область выделенной под нее памяти.
Иллюстрация Случайное расположение:
Иллюстрация создания новых объектов/структур и размещение их в памяти:
Например: То есть синий участок(модуль игры) окажется в самом краю памяти(внизу), а объекты выделенные динамично в процессе взаимодействия пользователя с игрой выше.
Иллюстрация Случайного размещения:
Немного примеров из мира разработки игр…
При разработке игры, программисту нужно построить логику всей игры, чтобы она была производительной и не занимала много ОЗУ. Когда вы запускаете уровень в игре, игра динамически выделяет память под те или иные объекты и структуры. Например: создание игрока, ботов, предметов, квестов и т.д. на уровне. Ведь было бы глупо сразу статично загружать все уровни в память игры, даже те, которые недоступны игроку. Это бы нагружало нашу память. А она имеет свойство заканчиваться. То есть к чему я веду вас, если бы игра создавала эти объекты сразу после запуска ее на исполнение, то скорее у вас бы были жуткие лаги, просадка FPS - во точно, возможно игра крашилась и завершалась аварийно! А обычно, создание объектов осуществляется по мере необходимости. Например: из-за действий игрока/событий, вы открываете дверь и через некоторое время на вас нападают боты из-за угла. Вот тогда происходит создание новых объектов. Это называется динамическим выделением памяти. То есть под новые объекты будет выделено место из свободного пространства памяти процесса в этот момент времени. А ненужные объекты будут удаляться и происходить освобождение памяти.
Еще это называют аллокация и деаллокация.
Представим ситуацию, мы зашли в игру, нас встречает меню и как обычно нажимаем на «Новая игра», у нас происходит загрузка уровня: загружается карта, игрок, инвентарь игрока, предметы на карте, боты, рисуется интерфейс, еще могут быть подключены модули в виде dll(физика, particle системы и т.д.). Проще говоря, все нужные ресурсы для игры. Это все происходит динамично, по мере их необходимости. И под это все будет выделен новый участок памяти из свободного пространства.
Более подробно можете прочитать об Динамичном распределении памяти Здесь
То есть каждый объект/структура/класс будет подгружать нужные ему такие же структуры и объекты в процессе – динамично.
Как эта структура будет выглядеть графически:
Как видим на иллюстрации, после загрузки в память AssaultCube, мы начинаем игру и в этот момент произошли некоторые действия: Загрузилась локация/карта, она в свою очередь загрузила предметы(гранаты, патроны), ботов и наконец игрока, а игрок в свою очередь загрузил некий инвентарь, если это можно так назвать. И интерфейс, который отображает некоторые данные игрока(количество здоровья, брони, патронов), если заметили он тоже связан с нашим инвентарем игрока. Чтобы считывать количество наших предметов.
Вот поэтому, наши адреса постоянно распределяются в случайных позициях при перезапуске и перезагрузке уровня, каждый раз этот модуль игры, эти объекты меняют свое положение в адресном пространстве. Но как это можно решить? Может все же имеется какой-то способ узнать, в каких адресах окажутся эти модули/объекты? Ответа – да!
Размышления…0x…указатели…
Для начала давайте подумаем и найдем какую то логику в этом. У нас есть модуль игры(*Синий участок на картине), в нашем случае ac_client.exe , который копируется в память на исполнение. Возможно нам поможет снова ОС? Что если при помощи запросов WinApi к ОС(*Windows) спросить, в какой участок памяти был загружен этот модуль? Да, Вы будете абсолютно правы!
Хорошо, теоретически мы узнали адрес загруженного модуля в память.
НО! Почему бы нам не спросить ОС, где находятся адреса объектов? Это же очень легкий способ.
Я должен огорчить, что эти объекты, как мы узнали выше, будут выделяться в процессе игры, и из-за этого данный метод будет выведен на – нет! Но как вы заметили, на иллюстрациях выше я выделил красным стрелки от одного модуля/объекта к другому. Это называется указателем.
Иллюстрация:
Вот они то и придут нам в помощь.
Что такое указатель? Указатель (pointer) в программировании представляет собой переменную, которая содержит адрес в памяти другой переменной. Он "указывает" на местонахождение в памяти, где хранится значение определенной переменной или структуры. Указатели позволяют эффективно работать с памятью, обеспечивая доступ к данным по их адресам.
Указатели используются в различных контекстах, таких как динамическое выделение памяти, передача параметров в функции по ссылке, работа с массивами, структурами данных и многими другими случаями, где требуется эффективная работа с памятью.
Важно: Указатель являясь тоже переменной имеет свой адрес в памяти, а значением его будет адрес другой переменной. Размер типа указатель будет зависеть от разрядности системы. Для х86(32бит) это – 4 байта, а для х64бит это обычно 8 байт.
Более подробно про указатели можно почитать Здесь
Пример указателя:
Как видим на примере выше, есть некая переменная с адресом 00123ABC, а в его ячейки лежит другой адрес. Вот это и есть указатель! Мы можем обращаться к указателю и прямо получать доступ к ячейки переменной на который указывает этот сам УКАЗАТЕЛЬ!
Для наглядности, написал тестовую программу, которая отлично демонстрирует указатели:
Сам код:
C++:
#include <iostream>
using namespace std;
int main
{
cout << "\t\t\tСпециально для форума: xss.pro || Author: Unseen"<<endl;
cout << "\t\t\tОбычные переменные и переменные-УКАЗАТЕЛИ!<" << endl;
int hp = 100;
int* ptrHP = &hp;
cout << "\n\tАдрес переменной hp = " << &hp << " || Значение переменной hp в ячейки: " << hp << endl;;
cout << "\n\tАдрес переменной-Указатель ptrHP = " << &ptrHP << " || Значение переменной-указатель в ячейки: " << ptrHP<<endl;
cin.get();
return 0;
}
Тот же пример с инвентарем игрока, где-то в коде игры может быть класс или структура UI интерфейс и в ее полях перемененная указатель pointerToInventoryPlayer, которая будет указывать на другую структуру или класс Player в котором есть возможно указатель на другую структуру или класс вида Inventory. Может и быть такое, что она прямо указывает на саму структуру Inventory.
Не открывай(Указатель):
Если бы мы хотели получить доступ к инвентарю нашего игрока в игре, у нас было бы 2 варианта решения этой задачи, а именно:
Пройтись через модуль игры + локация/Карта + Структура игрока + Инвентарь
Итого цель наша достигнута будет через 3 указателя. Точнее глубина указателей будет равна 3.
Пройтись через модуль игры + структура Интерфейс UI.
Глубина здесь 1.
P.S. Это всего лишь пример, возможно в игре, как увидим далее будет все реализовано по другому!
Еще более подробный пример:
Задача: Нам нужно получить доступ к адресу переменной здоровья игрока. В этом случае мы бы пошли через такой путь: модуль игры(*ac_client.exe) + локация/уровень(Level) + структура игрока(Player);
То есть через 2 указателя мы придем к объекту игрока и уже могли работать с его полями в структуре.
Важно отметить, что структуру можно представить как некую сущность, которая имеет свойства(поля), та же структура игрока(Player), может иметь такие поля как количество здоровья, брони, имя, и т.д.
Структура — это объединение нескольких объектов, возможно, различного типа под одним именем, которое является типом структуры. В качестве объектов могут выступать переменные, массивы, указатели и другие структуры. Структуры позволяют трактовать группу связанных между собой объектов не как множество отдельных элементов, а как единое целое. Структура представляет собой сложный тип данных, составленный из простых типов.
Структуры помогают улучшить организацию кода, разбив его на более логические и удобные части. Например, вы можете использовать структуры для представления объектов в вашей программе. Еще этот метод называют ООП. Мы сегодня базово узнаем, что оно из себя представляет.
Более подробно про ООП прочитать Здесь.
Предлагаю посмотреть это на уровне кода:
Как видно схеме, у нас есть несколько структур, на практике имеем очень много других.
Рассмотрим их для понимания сути и логики. Как мы узнали выше игра грузит нужные структуры и объекты – динамично. Допустим мы начали играть и у нас модуль игры(ac_client.exe) загружает выбранную карту пользователем – структуру Level, а в этой структуре содержатся такие данные: размеры карты – float Width и float Height. Указатель типа Player* player на структуру Player. P.S. указатель отмечается символом *(звездочка). К сведению, указатель будет указывать на адрес структуры Player, а именно на ее первый элемент массива char nickname[0]. Индекс массива в языке С++ будет начинаться с 0, а не как нам привычно 1. Значит можно сказать, что структура Level загружает структуру Player.
По условию задачи, нам нужно было получить доступ к адресу здоровья игрока. Для этого, нам нужно узнать расстояние(в байтах) относительно начала структуры Player до ее поля health. Обычно под «расстоянием» предполагается смещение(Offset - оффсет). Таким образом, смещение до поля health будет равно сумме размеров полей, предшествующих полю health:
Давайте как раз вычислим это смещение. Значит будем считать от начала структуры Player, первое ее поле это массив nickname из 32 элементов с типом char, как знаем из прошлых частей char имеет размер 1 байт:
- char nickname[32] = 32 байта
- int armor = 4 байта
Значит, чтобы вычислить адрес здоровья, мы должны взять адрес структуры, в данном случае Player и прибавить + 0x24. А адрес самой структуры Player мы можем вычислить через переменную указатель, который находится в структуре Level.
Выходит, чтобы добраться нам до адреса здоровья, нужно пройтись через все структуры, которые создаются по цепочке: сам модуль – ac_client.exe -> структура Level -> структура Player.
Более подробный план:
- Попросить ОС при помощи WinApi предоставить нам базовый адрес модуля игры - ac_client.exe
- Вычислить смещение в модуле игры для указателя Level* level, который хранит адрес новой структуры Level.
- Вычислить смещение в структуре Level для указателя Player* player, который хранит адрес новой структуры Player.
- Вычислить смещение в структуре Player до ее поля int health, который и является искомым адресом.
Следуя плану:
- Базовый адрес модуля 00223344
- Далее считаем смещение указателя level.
P.S. Предполагается, что система 32битная. Размер указателя = 4 байт. - Interface* interface = 4
- SoundSystem* sound = 4
- PhysicsSystem* physics = 4
- Считаем смещение для указателя player.
- float Width = 4
- float Height = 4
- Считаем смещение до health.
- char nickname[32] = 32
- int armor = 4
Но здесь есть одна ловушка, вы можете подумать, что сейчас адрес здоровья будет равен: 00223344 + 12 + 8 + 36 = 00223400. Но это на самом деле не так, по причине того, как сказал ранее, когда мы считали оффсеты для указателей, то мы находили не сам адрес структуры, а адрес УКАЗАТЕЛЯ, в котором расположен адрес начала структуры. Значит мы должны будем где то в коде своей программы, найдя указатель, должны считать его содержимое,
Еще иллюстрация:
Для полноты картины:
По началу это будет воспринять сложно, но это нормально. Если чувствуете себя неуверенно, перечитайте через час – полтора. Мозгу надо время на переваривание информации!
Практическая часть… Наконец-то!
P.S. Нашим подопытным будет тот же пациент Assault Cube!
Теперь, когда мы базово разобрались что же такое на самом деле виртуальная память, структуры данных, объекты, смещения(оффсеты), указатели. Думаю можем приступить к поиску этих самых оффсетов и указателей. Для начала полностью проведем рефакторинг нашего проекта из прошой части, если не сделаем этого сейчас, то в будущем будет сложно поддерживать наш проект. Напишем функцию, которая будет делать запрос при помощи WinApi к ОС, чтобы узнать базовый адрес модуля игры. И под конец все это испытаем и убедимся, что найденные нами оффсеты работают даже после перезапуска игры!
Надежный план, как швейцарские часы!
Итак весь наш план будет таков:
- Найдем при помощи сканера Cheat Engine, те самые оффсеты.
- Объявим функцию GetProcessID, которая будет получать ID запущенного процесса в системе. Только в этот раз будем находить ID не по названию окна игры, а по названию исполняемого файла / модуля.
- Объявим функцию GetModuleBaseAddress, которая будет делать запрос при помощи WinApi к ОС на получение базового адреса запущенного модуля в памяти.
- Объявим функцию FindTargetAddress, которая будет вычислять при помощи базового адреса и оффсетов искомый адрес чего-то, например патронов, жизней и т.д.
- Внесем изменения в шаблоны функций memWrite и memRead, для корректной их работы уже в новом нашем проекте.
- Испытаем на практике написанную нами программу.
Ну что же, как всегда запускаем наш сканер Cheat Engine и подключаемся к процессу игры Assault Cube.
Теперь так же, как с прошлой части, найдем нужные нам адреса в памяти. Пусть это будет количество патронов и уровень здоровья.
Для начала найдем адрес здоровья:
Теперь, чтобы вычислить, те самые оффсеты, указатели нам нужно кликнуть ПКМ на этот адрес и выбираем “Pointer scan for this address” или же “Сканирование указателя по этому адресу”. Далее у нас появляется окошко на нем нажимаем “ОК”. Пока про тонкую настройку поиска указателей не будем говорить.
Далее нас просят ввести имя для файла скана. Даем что нибудь логичное, я назвал healtScanPtr. Дальше у нас происходит сканирование памяти и поиск в нем указателей. Следующее, что нас встречает это окошко с результатами:
Как выделено на картине мы имеем 3 цвета, это:
- Красный это название модуля(ac_client.exe) + некий оффсет, как говорили в теории мы нашли оффсет в самом модуле и если прибавим его к адресу модуля, то вероятно придем к первой некой структуре, возможно это будет наш Level. Можете посмотреть это визуально на картинке под названием спойлера “Полная структура”.
- Зеленый это глубина указателей при сканировании.
- Желтый это те самые оффсеты(смещения). Их видим для примера 3 начиная от 0. Счет в программировании в большинстве случаев начинается с 0.
Иногда одного сканирования недостаточно, ведь здесь аж целых 26790 указателей. Многие из них возможно не будут рабочими после перезапуска игры. Для этого нужно проделать действия выше 2-3 раза перезапустив игру.
Покажу это на видео для экономии времени:
Как увидели, после 3 сканирований наших указателей осталось очень мало. Для надежности два раза кликнете по самым разным из них, как показано на видео. Эти указатели перенесутся в таблицу адресов в подменю сканера.
Желтыми отмечены указатели, которые остались работоспособными после перезапуска игры.
Значит они более надежны! Красные как видим поменяли значение на рандомный мусор.
Два раза кликнем теперь по любому из них(желтые):
Как видим внутри расположены зеленым наши те самые оффсеты, которые приведут к нужному адресу в структуре!
Теперь тоже самое проделайте и с адресом патронов и сохраните конечный результат в таблице!
Теперь осталось переписать наш проект!
Следуя плану составленному выше:
Вот полный исходный код нашего проекта:
C++:
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>
using namespace std;
DWORD GetProcessID(const wchar_t* targetNameProcess)
{
DWORD procID = 0;
HANDLE handleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (handleSnap != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32 procEntry;
procEntry.dwSize = sizeof(procEntry);
if (Process32First(handleSnap, &procEntry))
{
do
{
if (!_wcsicmp(procEntry.szExeFile, targetNameProcess))
{
procID = procEntry.th32ProcessID;
cout << "break" << endl;
break;
}
} while (Process32Next(handleSnap, &procEntry));
}
}
CloseHandle(handleSnap);
return procID;
}
uintptr_t GetModuleBaseAddress(DWORD procID, const wchar_t* moduleName)
{
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, procID);
uintptr_t baseAddr = 0;
if (hSnap != INVALID_HANDLE_VALUE)
{
MODULEENTRY32 modEntry;
modEntry.dwSize = sizeof(modEntry);
if (Module32First(hSnap, &modEntry))
{
do
{
if (!_wcsicmp(modEntry.szModule, moduleName))
{
baseAddr = (uintptr_t)modEntry.modBaseAddr;
break;
}
} while (Module32Next(hSnap, &modEntry));
}
}
CloseHandle(hSnap);
return baseAddr;
}
uintptr_t FindTargetAddress(HANDLE handleProc, uintptr_t baseAddr, vector<unsigned int>offsets)
{
uintptr_t address = baseAddr;
for (unsigned int i = 0; i < offsets.size(); ++i)
{
ReadProcessMemory(handleProc, (BYTE*)address, &address, sizeof(address), 0);
address += offsets[i];
}
return address;
}
template<class dataType>
void memWrite(HANDLE targetProc, DWORD address, dataType value)
{
WriteProcessMemory(targetProc, (PVOID)address, &value, sizeof(dataType), 0);
}
template <class dataType>
dataType memRead(HANDLE targetProc, DWORD address)
{
dataType buffer;
ReadProcessMemory(targetProc, (PVOID)address, &buffer, sizeof(dataType), 0);
return buffer;
}
int main() {
setlocale(LC_ALL, "Russian");
DWORD processID = GetProcessID(L"ac_client.exe");
cout << "ProccesID: " << processID << endl;
uintptr_t baseAddress = GetModuleBaseAddress(processID,L"ac_client.exe");
uintptr_t DynamicPtrAddress = baseAddress + 0x00183828;
vector<unsigned int> offsets_ammo = { 0x8, 0xB74, 0x30, 0x744 };
cout << "Dynamic ptr address:" << hex << DynamicPtrAddress << endl;
HANDLE handleProcess = OpenProcess(PROCESS_ALL_ACCESS, false, processID);
uintptr_t addresses_ammo = FindTargetAddress(handleProcess, DynamicPtrAddress, offsets_ammo);
cout << "addresses_ammo: " << hex << addresses_ammo << endl;
cout <<"Патронов было: "<< dec << memRead<int>(handleProcess, addresses_ammo)<<endl;
memWrite<int>(handleProcess, addresses_ammo, 1000);
cout << "Патронов стало: " << dec << memRead<int>(handleProcess, addresses_ammo)<<endl;
CloseHandle(handleProcess);
cin.get();
return 0;
}
Разбор кода...
Подключаем нужные библиотеки и заголовочные файлы:
C++:
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
#include <vector>
Далее объявляем функцию GetProcessID, которая принимает в качестве параметра строку targetNameProcess, представляющую имя целевого процесса, и возвращает идентификатор (ID) процесса (process ID), если процесс с таким именем был найден. В противном случае возвращается 0.
C++:
DWORD GetProcessID(const wchar_t* targetNameProcess)
{
// Идентификатор процесса, который будет возвращен
DWORD procID = 0;
// Создаем снимок текущих процессов в системе
HANDLE handleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
// Проверяем, удалось ли создать снимок
if (handleSnap != INVALID_HANDLE_VALUE)
{
// Инициализируем структуру для хранения информации о процессе
PROCESSENTRY32 procEntry;
procEntry.dwSize = sizeof(procEntry);
// Начинаем перебор процессов в снимке
if (Process32First(handleSnap, &procEntry))
{
do
{
// Сравниваем имя текущего процесса с целевым именем
if (!_wcsicmp(procEntry.szExeFile, targetNameProcess))
{
// Если найдено совпадение, сохраняем ID процесса и выходим из цикла
procID = procEntry.th32ProcessID;
cout << "break" << endl;
break;
}
} while (Process32Next(handleSnap, &procEntry));
}
}
// Закрываем дескриптор снимка процессов
CloseHandle(handleSnap);
// Возвращаем ID найденного процесса (или 0, если не найден)
return procID;
}
Если перейдете в диспетчер задач и во вкладке “Подробности” , увидите те самые запущенные процессы в системе. Этот код делает “Скриншот” на данный момент запущенных процессов и ищет в них искомый нами процесс.
К примеру процесс ac_client.exe имеет у меня ID 9952.
Дальше объявим функцию GetModuleBaseAddress, которая принимает идентификатор процесса (procID) и имя модуля (moduleName) и возвращает базовый адрес загруженного модуля в указанном процессе. Если модуль с указанным именем не найден, возвращается 0.
C++:
uintptr_t GetModuleBaseAddress(DWORD procID, const wchar_t* moduleName)
{
// Дескриптор снимка модулей процесса
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, procID);
// Инициализация базового адреса модуля
uintptr_t baseAddr = 0;
// Проверка, удалось ли создать снимок модулей
if (hSnap != INVALID_HANDLE_VALUE)
{
// Структура для хранения информации о модуле
MODULEENTRY32 modEntry;
modEntry.dwSize = sizeof(modEntry);
// Начинаем перебор модулей в снимке
if (Module32First(hSnap, &modEntry))
{
do
{
// Сравниваем имя текущего модуля с целевым именем
if (!_wcsicmp(modEntry.szModule, moduleName))
{
// Если найдено совпадение, сохраняем базовый адрес модуля и выходим из цикла
baseAddr = (uintptr_t)modEntry.modBaseAddr;
break;
}
} while (Module32Next(hSnap, &modEntry));
}
}
// Закрываем дескриптор снимка модулей
CloseHandle(hSnap);
// Возвращаем базовый адрес найденного модуля (или 0, если модуль не найден)
return baseAddr;
}
Теперь функция FindTargetAddress, которая принимает дескриптор процесса (handleProc), базовый адрес (baseAddr) и вектор смещений (offsets) или же проще сказать массив/список смещений. Функция пытается найти целевой адрес, выполняя последовательные чтения памяти с использованием указанных смещений.То есть эта функция будет идти от начала адреса модуля до искомого нами адреса в структуре.
C++:
uintptr_t FindTargetAddress(HANDLE handleProc, uintptr_t baseAddr, vector<unsigned int> offsets)
{
// Инициализация адреса с базовым адресом
uintptr_t address = baseAddr;
// Итерация по вектору смещений
for (unsigned int i = 0; i < offsets.size(); ++i)
{
// Чтение значения по текущему адресу
ReadProcessMemory(handleProc, (BYTE*)address, &address, sizeof(address), 0);
// Добавление текущего смещения к адресу
address += offsets[i];
}
// Возвращение найденного адреса
return address;
}
Теперь немного изменений в коде функций шаблонов для работы с памятью.
Функция memWrite, которая служит для записи значения в память процесса. Функция использует функцию WriteProcessMemory, которая позволяет записывать данные в виртуальную память другого процесса.
C++:
template<class dataType>
void memWrite(HANDLE targetProc, DWORD address, dataType value)
{
WriteProcessMemory(targetProc, (PVOID)address, &value, sizeof(dataType), 0);
}
функцию memRead, которая служит для чтения данных из виртуальной памяти процесса. Эта функция также является шаблонной и использует ReadProcessMemory для выполнения операции чтения.
C++:
template <class dataType>
dataType memRead(HANDLE targetProc, DWORD address)
{
dataType buffer;
ReadProcessMemory(targetProc, (PVOID)address, &buffer, sizeof(dataType), 0);
return buffer;
}
Когда с функциями закончили, осталась главная функция main:
Разрешим для начала кириллицу в консоли для удобства.
C++:
setlocale(LC_ALL, "Russian");
Дальше вызовем функцию GetProccesID, и передадим ему название модуля в данном случае ac_client.exe. Результат сохраним в переменную processID. И выведем этот айди в консоли.
C++:
DWORD processID = GetProcessID(L"ac_client.exe");
cout << "ProccesID: " << processID << endl;
Теперь вызовем функцию GetModuleBaseAddress для получения базового адреса модуля, результат запишем в переменную baseAddress. Дальше объявим переменную DynamicPtrAddress. Значением будет сам базовый адрес и смещение для этого модуля.
C++:
uintptr_t DynamicPtrAddress = baseAddress + 0x00183828;
Дальше объявим беззнаковый вектор offsets. В нем будем хранить массив оффсетов.
C++:
vector<unsigned int> offsets_ammo = { 0x8, 0xB74, 0x30, 0x744 };
Теперь выведем сам адрес относительно базового модуля. Для информативности.
C++:
cout << "Dynamic ptr address:" << hex << DynamicPtrAddress << endl;
Получаем хэндл и права на этот процесс.
C++:
HANDLE handleProcess = OpenProcess(PROCESS_ALL_ACCESS, false, processID);
Давайте вызовем функцию FindTargetAddres и передаем в параметры сам хэндл процесса, адрес относительно базового модуля, и сам массив оффсетов. Результатом будет конечный адрес того значения, что искали. Допустим адрес патронов.
C++:
uintptr_t addresses_ammo = FindTargetAddress(handleProcess, DynamicPtrAddress, offsets);
Сделаем вывод этого адреса в hex формате для информативности.
C++:
cout << "addresses_ammo: " << hex << addresses_ammo << endl;
Теперь как всегда сначала прочитаем значение по найденному значению при помощи функции memRead. И выведем результат.
C++:
cout <<"Патронов было: "<< dec << memRead<int>(handleProcess, addresses_ammo)<<endl;
далее вызываем функцию записи в память memWrite и передаем ему хэндл, сам адрес по которому нужно записать, и значение для записи.
C++:
memWrite<int>(handleProcess, addresses_ammo, 1000);
Теперь снова прочитаем новое значение при помощи memRead.
C++:
cout << "Патронов стало: " << dec << memRead<int>(handleProcess, addresses_ammo)<<endl;
Поздравляю! Вы очень молодцы! Все получилось, все прошло по плану! Попробуйте несколько раз перезапустить игру и снова проверить работоспособность ваших оффсетов!
Соглашусь статья очень получилась объемной, даже сам не представлял. Она отняла очень много сил и времени. Последние строчки еле дописывал… Я попытался все передать подробно, показать что и как. Если у вас остались какие нибудь вопросы по данной статье – смело задавайте в обсуждении. Все разберем и решим!
Всех благодарю за чтение! Если я пропустил ошибку или что то не разобрал по данной теме, прощу дополнить в комментариях.
Если хотите отблагодарить меня чашечкой кофе – реквизиты в подписи.
Всех с наступающим Новым Годом! Пожелаю, чтобы ваши профиты выросли в разы, а то и сотни иксов. А у новичков появились первые бабки $. До следующей части!
Ваш Unseen!
Последнее редактирование:
