Арбитр
-
Автор темы
- #1
Снова решил перевести тему с зарубежного форума.
Автор данной темы @WasserEsser
Это просто перевод, пожалуйста не бейте меня)
Перевод получился не очень качественным, если нашли ошибки в тексте сообщите о них.
Сделал этот перевод только для тех кому он нужен.
Этот пост предназначен для людей, которые знают, как кодить, но не знают, что такое сканирование шаблонов. Это должна быть информативная тема.
В целом, что такое сканирование паттернов.Автор данной темы @WasserEsser
Это просто перевод, пожалуйста не бейте меня)
Перевод получился не очень качественным, если нашли ошибки в тексте сообщите о них.
Сделал этот перевод только для тех кому он нужен.
Этот пост предназначен для людей, которые знают, как кодить, но не знают, что такое сканирование шаблонов. Это должна быть информативная тема.
Когда вы пишете свою программу на C++, код компилируется в машинный код.
Компилятор Visual C++ компилирует код непосредственно в двоичный код, но мы можем использовать такие инструменты, как OllyDbg, x64dbg или любой другой отладчик, чтобы отобразить его в сборке.
В разработке читов мы используем так называемые смещения для доступа к определенным переменным / адресам. Эти смещения в большинстве случаев также используются в памяти для доступа к определенным данным, как описано ниже.
Чтобы избавиться от хлопот с обновлением вашего чита при каждом обновлении, либо захватывая смещения самостоятельно, либо используя дампер смещения, вы можете реализовать сканирование шаблонов.
В зависимости от вашей реализации вы можете предоставить своей функции сканирования шаблонов определенный шаблон, который должен быть найден внутри диапазона байтов.
Он будет сканировать байты и находить адрес, где используется желаемое смещение, что позволит вам легко скопировать его.
Таким образом, вам не нужно обновлять свой чит всякий раз, когда меняется смещение, вам нужно только изменить шаблон, если меняется код, в котором он используется, что происходит не так часто.
Что такое смещения и как они генерируются?
Смещения-это адреса, часто используемые с другим адресом, например адресом игрока.
Чтобы понять смещения, вам нужно знать размер типов данных. На 32-битной машине целое число, если не указано иное, имеет длину 4 байта.
Книга имеет длину в один байт. Эти переменные позже будут размещены в памяти по определенным адресам. Я покажу вам пример, кодируя небольшой класс игроков.
C++:
class CPlayer
{
public:
CPlayer( int Health, int Team, int Armor, CVector Pos, bool Valid, bool Alive )
: m_iHealth( Health ), m_iTeam( Team ), m_iArmor( Armor ), m_vecPosition( Pos ), m_bValidPlayer( Valid ), m_bIsAlive( Alive )
{
}
~CPlayer( )
{
}
int m_iHealth;
int m_iTeam;
int m_iArmor;
CVector m_vecPosition;
bool m_bValidPlayer;
bool m_bIsAlive;
};
https://imgur.com/a/kmzzS42 ( Тут приложил скриншот, он не загружается )
Здесь мы видим, как наш класс воспроизводится в памяти вместе с его адресами. Адрес 18FDAC - это наш базовый адрес, адрес нашего класса игроков. Если игра хочет проверить, действителен ли игрок, она может проверить, что bool m_bIsValid внутри этого класса. В большинстве случаев адрес игрока сохраняется в одном из регистров. В игре, скорее всего, будет такая инструкция, как
[LIST=1]
[*]mov eax, [ecx + 00000018]
[/LIST]
0x18 называется смещением. Чтобы получить доступ к логическому значению m_bIsValid , мы используем адрес игрока + 0x18. Тот же принцип может быть применен к любому указателю. Например, статические указатели, указывающие на наш класс плеера, могут быть доступны с помощью базового адреса определенной библиотеки динамических ссылок + смещение. Если мы сможем найти инструкцию, где адрес доступен, мы сможем прочитать наше смещение. Поскольку инструкция также хранится в памяти, мы можем просто прочитать раздел .text. Там мы можем найти наше смещение. Чтобы найти адрес, где хранится это смещение, мы можем использовать сканирование шаблонов.
Как я могу реализовать сканирование шаблонов?
Давайте объективно взглянем на игру. Я буду использовать Counter-Strike: Global Offensive здесь, поскольку это одна из самых документированных игр.
Во-первых, вам нужно знать текущее смещение. Получите его, либо ища его с помощью cheat engine, используя offset dumper, либо посещая offset thread. Мы подключаем Cheat Engine к нашей игре и нажимаем на кнопку "Добавить адрес вручную".
https://imgur.com/a/cxNRsxI ( Скриншот )
В этом примере я буду использовать смещение здоровья локального игрока. Сначала я добавляю указатель на локального плеера, чтобы мы знали адрес нашего локального плеера.
https://imgur.com/a/PSmngRt ( Скриншот )
Нажмите правой кнопкой мыши, покажет как десятичное число, и вы получите адрес локального игрока. Обязательно находитесь на карте с ботом. Теперь мы можем взять этот адрес и добавить к нему 0xFC, чтобы получить доступ к нашему здоровью.
https://imgur.com/a/HCDVfAU ( Скриншот )
Вы также можете использовать кнопку как "Указатель", однако я использую его только для многоуровневых указателей.
Теперь щелкните правой кнопкой мыши по нему и нажмите "Узнать, что обращается к этому адресу". Он прикрепит отладчик к процессу и перечислит вам все вещи, обращающиеся к этому адресу.
https://imgur.com/a/lFVfBd7 ( Скриншот )
Как мы видим на скриншоте, игра обращается к этому адресу по-разному в разных местах.
Для нас интересны инструкции, которые обращаются к нашему адресу с помощью смещения, которое мы хотим захватить. Первая инструкция обращается к нашему адресу, перемещая значение ecx + 000000FC в регистр eax.
Эта инструкция находится где-то в разделе .text. Мы хотим найти его и прочитать смещение.
Если вы дважды щелкните на первой инструкции, вы можете увидеть больше информации об этой инструкции вместе с состояниями регистра после того, как произошла инструкция и окружающие инструкции.
Эта информация полезна для нас. Мы видим, что регистр ecx содержит адрес нашего локального игрока, и он получает доступ к здоровью, копируя значение ecx + 000000FC. Но есть одна проблема.
Окружающие инструкции по сборке, скорее всего, вызовут проблемы, если перестановка байтов используется в нескольких местах.
Он может найти неправильные инструкции и, следовательно, может привести к неправильным результатам.
Мы хотим найти шаблон, который приведет к одному результату, а это означает, что перестановка байтов присутствует только один раз во всем массиве байтов, в котором мы ищем наш шаблон.
Давайте попробуем четвертую инструкцию в списке. Он выглядит намного лучше, так как в нем нет кучи инструкций int3, фактической инструкции и инструкции возврата.
Теперь щелкните правой кнопкой мыши, скопируйте информацию и вставьте ее в редактор.
Закройте Cheat Engine и откройте OllyDbg. Убедитесь, что у вас установлен плагин SigMaker. Прикрепите OllyDbg к игре и дождитесь ее загрузки.
https://imgur.com/a/jH6Ir2X ( Скриншот )
Как только он закончит загрузку, щелкните правой кнопкой мыши, нажмите "Перейти к" и нажмите "Выражение".
https://imgur.com/a/0hjrbXm ( Скриншот )
Теперь вам нужно скопировать адрес, содержащий адрес инструкции, и вставить его в диалоговое окно "Перейти к выражению".
Важно то, что вы не закрываете свою игру между копированием адреса из Cheat Engine и переходом на адрес в OllyDbg, иначе у него, скорее всего, будет другой адрес.
https://imgur.com/a/5CYTugO ( Скриншот )
После этого вы можете немного прокрутить вверх, чтобы увидеть окружающие байты инструкций в этой инструкции.
Инструкции можно найти в середине, в то время как шестнадцатеричное представление можно найти слева от инструкций.
https://imgur.com/a/rN8OJSt ( Скриншот )
Поскольку шестнадцатеричное число является базовым 16, каждый байт должен иметь 2 символа, представляющих их.
https://imgur.com/a/J2FKBTu ( Скриншот )
Теперь нужно сделать себе выкройку. Как уже упоминалось выше, мы хотим создать шаблон, который можно найти только один раз в блоке памяти, который мы копируем.
Если бы мы сейчас щелкнули правой кнопкой мыши, Сделали Sig, протестировали Sig и нажали на Scan, то получили бы 6 результатов.
Мы хотим увеличить размер подписи, чтобы гарантировать, что мы получим только один результат.
Мы можем листать дальше вверх, дальше вниз, пока мы все еще можем считать разницу в байтах.
Однако мы не хотим, чтобы подпись была слишком большой, иначе гораздо более вероятно, что нам придется обновить наш шаблон в будущем.
Инструкции могут измениться, поэтому старайтесь держать шаблон как можно меньше, получая только один результат. Давайте начнем с 1 строки выше и будем отмечать до тех пор, пока не появится строка под вашей инструкцией.
https://imgur.com/a/437p7hw ( Скриншот )
Возможно, вы уже заметили, что шестнадцатеричное представление имеет наше смещение, хранящееся внутри него. Оно хранится в обратном порядке, хотя если мы прочитаем его позже, то получим правильный адрес.
https://imgur.com/a/rHBjgLO ( Скриншот )
Опять же, функция сканирования шаблона перебирает байты, пока не найдет сигнатуру, соответствующую шаблону.
https://imgur.com/a/J3sxV1U ( Скриншот )
Как только он найдет место, где он соответствует, он вернет нам в зависимости от вашей реализации адрес или индекс. Моя функция сканирования шаблонов вернет вам индекс, чтобы вы могли скопировать смещение непосредственно из заполненной выделенной памяти.
https://imgur.com/a/JuImguF ( Скриншот )
Но подождите, давайте предположим, что смещение изменилось. Теперь это 0x100, шаблон на правой стороне не будет совпадать с байтами на левой стороне, так как FC000000 теперь будет 00010000.
Для этого и существуют так называемые "подстановочные знаки". Подстановочные знаки будут пропускать байты, которые могут отличаться, например смещение, но также и другие адреса, которые могут меняться каждый раз.
Обычно плагин SigMaker ловит эти адреса и заменяет их подстановочными знаками во всплывающем окне, однако он не сделал этого для этого смещения, вероятно, потому, что это очень маленький адрес.
Обычно он заменял бы те, которые могли бы изменить адреса, на \x00 в шаблоне (также называемом сигнатурой ) и заменял бы соответствующий "x" в маске на"?".
На этот раз мы должны сделать это вручную. Поскольку адрес имеет длину четыре байта, мы хотим обнулить четыре байта, в которых хранится адрес.
В зависимости от вашей реализации вам может понадобиться маска, поэтому обязательно измените ее.
Убедитесь, что вы получили только один результат, нажав на кнопку сканировать, прежде чем использовать шаблон. Он нашел шаблон только один раз, а это значит, что мы можем использовать этот шаблон.
Поскольку я не использую маску для своей реализации, мне нужно только заменить маску \xFC c \x00.
https://imgur.com/a/2IfWBOS ( Скриншот )
https://imgur.com/a/k59yEJE ( Скриншот )
Теперь мы можем закодировать себе функцию сканирования паттернов. Я буду использовать свой класс CPatternScan, который загрузит байты модуля в кучу и будет хранить их до тех пор, пока объект не будет уничтожен.
В конце этого урока вы должны были полностью понять концепцию сканирования шаблонов и, немного подумав, вы должны были создать свой собственный класс сканирования шаблонов в зависимости от ваших потребностей.
C++:
class CPatternScan
{
CPatternScan( )
{
Process = nullptr;
Data = nullptr;
Size = nullptr;
}
const CProcess* Process;
std::unique_ptr< BYTE[ ] > Data;
const DWORD* Size;
public:
CPatternScan( CProcess* Process, char* ModuleName )
{
if ( Process != nullptr && ModuleName != nullptr )
{
this->Process = Process;
Size = this->Process->GetModuleSize( ModuleName );
Data = std::make_unique< BYTE[ ] >( *Size );
auto BytesRead{ SIZE_T( ) };
if ( !ReadProcessMemory( *this->Process->GetHandle( ), reinterpret_cast< LPCVOID >( this->Process->GetModuleBaseAddress( ModuleName ) ), Data.get( ), *Size, &BytesRead ) || BytesRead != *Size )
{
memset( &Data, 0, *Size );
}
}
~CPatternScan( )
{
}
auto FindPattern( std::vector< BYTE > Pattern ) const -> DWORD
{
Pattern.shrink_to_fit( );
for ( DWORD i = 0; i < *Size; i++ )
{
auto DoesMatch{ true };
for ( DWORD j = 0; j < Pattern.size( ); j++ )
{
if ( Pattern[ j ] == 0 ) continue;
if ( Pattern[ j ] != Data[ i + j ] ) { DoesMatch = false; break; }
}
if ( DoesMatch ) return i;
}
return 0;
}
auto GetOffset( DWORD Offset ) const -> DWORD
{
auto Buffer{ DWORD( 0 ) };
memcpy( &Buffer, &Data[ Offset ], sizeof( DWORD ) );
return Buffer;
}
auto GetOffset( std::vector< BYTE > Pattern, DWORD Offset ) const -> DWORD
{
return GetOffset( FindPattern( Pattern ) + Offset );
}
};
Я использую вектор для своего шаблона с нулями ( 0x00 ) в качестве подстановочных знаков. Как только паттерн найден, я могу вернуть индекс, по которому он найден.
Я также мог бы вернуть адрес, по которому шаблон находится в памяти, чтобы прочитать смещение из исходного процесса, но поскольку я уже скопировал всю библиотеку динамических ссылок, я также могу скопировать смещение из выделенного пространства.
Но подождите, мы что-то пропустили? Почему это не дает мне правильного смещения? Что ж, давайте посмотрим еще раз.
Функция вернет нам адрес, по которому найден паттерн, это означает, что она вернет нам эту позицию ( относительный индекс в моем случае, она также может вернуть вам адрес, который вы можете прочитать через ReadProcessMemory ):
https://imgur.com/a/PiQHMCW ( Скриншот )
Если бы вы прочитали следующие четыре байта по адресу, возвращенному функцией сканирования шаблона, вы бы, по сути, получили следующее:
https://imgur.com/a/zt05scD ( Скриншот )
Это, очевидно, не тот результат, который вы хотите, так как это не то смещение, которое вы хотели.
При этом вам нужно добавить свое собственное смещение поверх возвращаемого адреса / индекса. Поскольку мы знаем, что шестнадцатеричное число является базовым 16 и поэтому для отображения одного байта требуется 2 символа, поле, которое мы отметили, имеет длину всего четыре байта.
Сразу после этих четырех байтов находится наше желаемое смещение. Это означает, что вам нужно прочитать / скопировать адрес, возвращенный функцией + 4 байта. Это будет адрес / индекс, где хранится смещение.
Предположим, вы хотите прочитать байты 00 00 7F 2D. Чтобы получить правильный адрес, вам нужно посчитать байты перед байтами, которые вы хотите прочитать.
В приведенном выше случае это будет 7 байт. Возьмите адрес / индекс, возвращенный функцией сканирования шаблона, и добавьте к нему 7 байт, и вы получите правильное смещение.
Давайте выберем другой пример, на этот раз со статическим указателем на наш LocalPlayer.
Если мы выполняем сканирование указателя в cheat engine, предпочтительно несколько раз, чтобы убедиться, что каждый результат всегда указывает на наш LocalPlayer, мы можем увидеть несколько указателей, которые указывают на адрес нашего LocalPlayer.
Давайте попробуем сделать шаблон для указателя LocalPlayer, который мы использовали ранее, чтобы сделать себе шаблон смещения здоровья.
Мы сканируем несколько раз, повторно просматривая предыдущие результаты после перезапуска игры. Это устранит временные указатели и покажет нам указатели, которые, кажется, всегда указывают на наш локальный плеер.
https://imgur.com/a/QnM7oLL ( Скриншот )
Первый результат-это действительный указатель на нашего локального игрока. Поскольку почти каждый, кто использует LocalPlayerpointer, использует его, поэтому мы могли бы попытаться создать подпись, используя этот указатель.
Если мы дважды щелкнем, указатель будет добавлен в наш список адресов. Щелчок правой кнопкой мыши и щелчок на "Узнайте, что обращается к этому адресу" теперь вызывает другой диалог.
Мы можем либо выяснить, что обращается к самому указателю ( который мы хотим захватить), либо выяснить, что обращается к адресу, на который указывает этот указатель. Мы хотим захватить сам указатель, поэтому нажимаем на кнопку "Узнать, что обращается к этому указателю".
Но подождите, есть только одна инструкция, которая обращается к этому указателю в данный момент времени, и она не содержит фактического смещения в своей инструкции. Как нам его получить?
https://imgur.com/a/RpjuFVq ( Скриншот )
Мы могли бы искать окружающие инструкции, нажав на инструкцию, а затем на "Show Dissasembly" и посмотреть, как эти инструкции формируются и доступны, но сейчас мы не хотим беспокоиться об этом, у нас есть много других указателей, которые указывают на наш локальный плеер, поэтому мы просто взглянем на другой.
Итак, давайте обратимся к следующему, client.dll + 4A98144. Дважды щелкните по нему, чтобы добавить его в наш список адресов и узнать, что обращается к этому указателю.
Мы можем видеть много инструкций, которые перемещают регистры, поэтому мы не можем считывать регистры без подключения функции и считывания регистров в этот момент времени.
Но у нас также есть инструкции, которые содержат абсолютный адрес. Нажав на него один раз, мы увидим, что этот абсолютный адрес состоит из client.dll + 4A98164, что означает, что это статический глобальный указатель, который всегда будет находиться по базовому адресу client.dll модуль + 4A98164.
Но если мы сравним это смещение с нашим смещением в списке указателей, то увидим, что оно отличается на 10 байт. Как же так?
Давайте добавим указатель в наш список и рассмотрим область памяти, на которую он указывает. Текущий указатель в списке указывает на адрес, на который указывает первая запись в классе плеера, поэтому мы не можем просматривать эту область памяти.
https://imgur.com/a/auCVxHo ( Скриншот )
https://imgur.com/a/2lPSMlQ ( Скриншот )
Мы можем видеть много адресов в этой области памяти, и при более внимательном рассмотрении мы можем обнаружить там наш адрес LocalPlayer, снова сохраненный в обратном порядке.
https://imgur.com/a/IkFulph ( Скриншот )
При дальнейшем исследовании это оказывается Список сущностей, который содержит указатели на каждую сущность в игре.
Client.dll + 4A98164 всегда указывает на мир, в то время как client.dll + 4A98174 указывает на первую сущность в мире, которая в локальной игре ботов является нашим собственным игроком, так как боты появляются, как только мы находимся на сервере и "запускаем" сервер.
Это отличается от онлайн-серверов, мы редко будем первой сущностью в списке.
Я не собираюсь вдаваться слишком подробно в исходный движок, поскольку эта статья посвящена сканированию шаблонов, однако мы видим, что это не очень хороший указатель на наш LocalPlayer, поскольку мы не всегда находимся в одной и той же позиции в этом списке.
Давайте возьмем следующий указатель в нашем списке указателей. Если мы узнаем, что обращается к этому адресу, мы снова увидим список с другим смещением, поэтому мы не можем прочитать или компенсировать эти инструкции.
https://imgur.com/a/BMPFuGF ( Скриншот )
На следующий указатель. На этот раз мы можем видеть гораздо больше инструкций, которые обращаются к этому указателю, вместе с тем же смещением каждый раз.
Это может означать, что это снова статический глобальный указатель, находящийся в одном и том же адресе при каждом запуске игры относительно базового адреса модуля.
Если мы нажмем на первую инструкцию и взглянем на окно внизу, то увидим, что абсолютный адрес в инструкции формируется из client.dll + 4F2B50C, именно то, что мы можем использовать, поэтому давайте дважды щелкнем эту инструкцию и скопируем информацию.
Но эй, у нас есть инструкция, которая не содержит много ценных инструкций вокруг нее, а это означает, что ее можно найти снова в нескольких местах, поэтому мы ищем другую инструкцию, которая могла бы иметь лучшие окружающие инструкции.
Двойной щелчок на следующем дает нам лучшие инструкции по окружению, поэтому давайте скопируем информацию и снова вставим ее в редактор.
https://imgur.com/a/SAA92Ld ( Скриншот )
Закройте Cheat Engine, откройте OllyDbg, присоединитесь к нашему процессу и перейдите к скопированному выражению. После маркировки различных частей байтов я пришел к следующему выводу, который даст вам один результат при сканировании.
https://imgur.com/a/3ird6Q5 ( Скриншот )
На этот раз плагин SigMaker сформировал правильную сигнатуру и маску из байтов в шестнадцатеричном представлении, так что нам не нужно исправлять шаблон.
https://imgur.com/a/e0QbLDA ( Скриншот )
Теперь нам нужно добавить наше смещение к возвращенному адресу, иначе мы бы прочитали E8FC4D89, которые являются первыми 4 байтами на возвращенном адресе, в обратном порядке.
https://imgur.com/a/fscfBL8 ( Скриншот )
Наш адрес, который мы хотим прочитать, находится в зеленом поле, что означает, что нам нужно добавить 10 байт к возвращенному адресу и прочитать новый адрес, чтобы получить нужный адрес. Помните, что 2 символа-это 1 байт.
Когда мы сканируем этот адрес, мы получаем абсолютный адрес, который является текущим адресом указателя на наш LocalPlayer, который мы можем разыменовать / ReadProcessMemory.
В большинстве случаев это будет не тот же адрес, если вы перезагрузите игру. Если вы хотите внедрить сканирование шаблонов в свой чит, вы можете разыменовать / ReadProcessMemory этот адрес непосредственно, чтобы получить адрес вашего LocalPlayer, но если вы хотите сделать смещенный дампер, вы можете вычесть его по базовому адресу модуля, который в данном случае является client.dll.
Это дало бы вам необработанное смещение.
Теперь, поскольку у нас есть указатель на наш LocalPlayer, мы можем разыменовать его / ReadProcessMemory it и захватить базовый адрес нашего LocalPlayer, добавить смещение работоспособности и разыменовать его снова / ReadProcessMemory it и прочитать наше текущее состояние.
Программы, которые на понадобятся:
https://www.cheatengine.org/ ( не реклама )
http://www.ollydbg.de/ ( не реклама )