Гайд Понимании концепта сканирования паттернов ( Understanding the pattern scanning concept)

Разработчик
Статус
Оффлайн
Регистрация
1 Сен 2018
Сообщения
1,602
Реакции[?]
882
Поинты[?]
117K
Всем привет,сделал перевод довольно полезной статьи для новичков.
Оригинальный автор статьи: @
Пожалуйста, авторизуйтесь для просмотра ссылки.

Если нашли ошибки - отпишите в теме)

Введение:
Поскольку у многих людей возникают проблемы с пониманием концепта сканирования паттернов,и того как реализовать их в собственных читах,поэтому я хотел быстро охватить концепцию сканирование паттернов и способы её реализации. Моя цель этой статьи - научить вас этой концепции,чтобы вы могли реализовать её в собственном коде,и применять в любой игре.

Что такое сканирование паттернов?:
Когда вы пишете вашу программу на C++,код компилируется в машинный код.
Компилятор Visual C++ непосредственно компилирует ваш код в бинарный,но вы можете использовать OllyDbg,x64dbg или любой другой отладчик,что-бы показать его в ассемблере. В создании читов мы используем так называемые оффсеты для доступа к переменным / адресам. Эти оффсеты в большинстве случаев используются в памяти для доступа к определенным данным, как описано ниже.
Чтобы избежать трудностей с обновлением вашего чита при каждом обновлении,либо захватывая оффсеты вручную или используя дампер оффсетов,вы можете реализовать сканирование паттернов
В зависимости от вашей реализации функции сканирования,вы можете предоставить конкретный паттерн,который должен находить внутри диапазона байтов.
Он просканирует байты и найдет адрес,по которому используется желаемый оффсет,что упростит его копирование.
Таким образом,вам не нужно обновлять свой чит всякий раз,когда изменяются оффсеты,вам нужно изменить только паттерн,если меняется код в котором он используется,что происходит не так часто.

Что такое оффсеты,и как они генерируются:
Оффсеты это адреса, часто используемые с другими адресами, например с адресом игрока.
Чтобы понять оффсеты,вам нужно знать размер типов данных. На 32-битной системе целое число,если не указано иное,имеет длину в 4 байта.
Тип bool имеет длину в 1 байт. Эти переменные позже будут размещены в памяти по определенным адресам. Я покажу вам пример написав небольшой класс игрока.

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;
    };
Если мы создадим экземпляр объекта игрока, и дадим ему дурацкие значения,и посмотрим область памяти в Cheat Engine,мы сможем перестроить структуру данных.

Здесь мы можем увидеть наш класс расположенный в памяти, вместе с его адресами. Адрес 18FDAC это наш базовый адрес, адрес нашего класса игрока. Если игра хочет проверить,действителен ли игрок,она может проверить bool m_bIsValid внутри этого класса. В большинстве случаев адрес игрока сохраняется в один из регистров.
В игре, скорее всего, будет такая инструкция,как
code_language.asm6502:
mov eax, [ecx + 00000018]
0x18 называется оффсетом. Для доступа к boolean m_bIsValid,мы используем адрес игрока + 0x18. Тот же принцип может быть применен к любому указателю. Статические указатели,указывающие,как пример на наш класс игрока,можно получить доступ, использую базовый адрес определенной динамической библиотеки линковки(компоновки) + оффсет. Если мы сможем найти инструкцию по которой осуществляется доступ к адресу,мы сможем прочитать наш оффсет. Поскольку инструкция находится памяти,мы можем просто прочитать .text секцию. Там мы сможем найти наш оффсет. Чтобы найти адрес,в котором находится оффсет, вы можете использовать сканирование паттернов.

Как я смогу реализовать сканирование паттернов?:

Давайте посмотрим на игру,я буду использовать Counter-Strike: Global Offensive,так как это одна из самых документированных игр.
Во-первых вам нужно знать текущий оффсет. Получите его с помощью поиска в Cheat Engine,используя оффсет дампера или посетив тему с оффсетами. Мы подключаем Cheat Engine к нашей игре,и нажимаем - "Add address Manually".

В этом примере я буду использовать оффсет здоровья для нашего локального игрока(LocalPlayer). Я сначала добавлю указатель на LocalPlayer,чтобы мы знали адрес нашего LocalPlayer.

Нажмите на правую кнопку мыши, нажмите - "show as decimal",и вы получите адрес LocalPlayer. Убедитесь,что вы играете с ботом. Теперь мы можем взять адрес,и добавить к нему 000000FC,чтобы получить доступ к здоровью.

Вы так-же можете использовать checkbox "Pointer",однако я его использую только для многоуровневых указателей.
Сейчас щелкаем на правую кнопку мыши,и нажимаем на "Find out what accesses this address". Он присоединит отладчик к процессу,и покажет все инструкции обращающиеся к адресу.

Как видно на скриншоте, игра обращается к адресу по-разному,в разных местах. Для нас интересны инструкции,которые обращаются к адресу используя оффсет,который мы хотим получить. Первая инструкция обращается к нашему адресу,для перемещения значения ecx + 000000FC в eax регистер.
Эта инструкция находится где-то в .text разделе. Мы хотим ёе найти,и прочитать оффсет.
Если вы нажмете дважды на первую инструкцию,вы сможете увидеть дополнительную информацию включая состояние регистров,после того как инструкция была создана,и окружающими инструкциями.
Эта информация полезна для нас. Мы видим,что регистр ecx содержит адрес нашего LocalPlayer, и он получает доступ к состояние, копируя exc + 000000FC. Но есть одна проблема.
Окружающие ассемблированные инструкции скорее всего вызовут проблемы,если перестановка байтом используется в нескольких местах.
Он может найти не правильные инструкции,и следовательно привести к не правильным результатам.
Мы хотим найти паттерн,который приведет к единственному результату,а это означает перестановка байтов присутствует только один раз во всем блоке байтов,в котором мы ищем наш паттерн.
Попробуем четвертую инструкцию в списке. Это намного лучше,потому что в ней нету кучи int3 инструкций,самой инструкции,и инструкции возрата.
Сейчас нажимаем правой кнопкой мыши,копируем информацию,и вставляем её в текстовый редактор.

Закрываем Cheat Engine и открываем OllyDbg. Убедитесь,что у вас плагин SigMaker установлен. Подключаем OllyDbg к игре,и ждем пока загрузится.

После завершения загрузки щелкните правой кнопкой мыши, и нажмите "Go to" и кликнете на выражение.




Теперь вам нужно скопировать адрес,который содержит адрес инструкции,и вставить его в диалог "Go to expression".
Важно,чтобы вы не закрывали игру между копированием адресов с Cheat Engine и переходом по адресу в OllyDbg,иначе он будет иметь другой адрес.

После этого вы можете немного прокрутить вверх, чтобы увидеть окружающие байты инструкции в этой инструкции.
Инструкции можно найти по середине,а hexadecimal представление слева от инструкций.

Поскольку hexadecimal имеет основание 16,каждый байт должен иметь 2 символа, представляющих их.

Теперь нам нужно сделать паттерн самостоятельно. Как упоминалось выше, мы хотим создать паттерн, который можно найти только один раз в копируемом блоке памяти.
Если бы мы не щелкнули правой кнопкой мыши, на "Make Sig", "Test Sig" и нажали на Scan, мы бы получили 6 результатов.
Мы хотим увеличить размер сигнатуры,чтобы гарантировать получение одного результата.
Мы можем начать further above , further down, пока мы еще можем подсчитывать разницу в байтах.
Однако нам не нужна слишком большая сигнатура, иначе гораздо более вероятно, что нам придется обновить наш паттерн в будущем.
Инструкции могут измениться, поэтому старайтесь, чтобы паттерн был как можно меньше, пока вы получаете только один результат. Давайте начнем на 1 строку выше и разметим до строки под нашей инструкцией.

Возможно, вы уже заметили, что в hexadecimal хранится наш оффсет. Он хранится в обратном порядке, хотя, если мы прочитаем его позже, мы получим правильный адрес.

Опять же, функция сканирования паттерна выполняет итерацию по байтам, пока не найдет сигнатуру, соответствующую паттерну.

Как только он найдет подходящее место, он вернет нам, в зависимости от вашей реализации, адрес или индекс. Моя функция сканирования паттернов вернет вам индекс, чтобы вы могли скопировать смещение непосредственно из заполненной выделенной памяти.

Но подождите, допустим, оффсет изменился. Теперь это 0x100, паттерн на правой стороне не будет совпадать с байтами на левой стороне, так как FC000000 теперь будет 00010000.
Вот для чего нужны так называемые «подстановочные знаки». Подстановочные знаки будут пропускать байты, которые могут отличаться, например оффсет, но также и другие адреса, которые могут меняться каждый раз.
Обычно плагин SigMaker перехватывает эти адреса и заменяет их знаками подстановки во всплывающем окне, однако он не делал этого для этого оффсет, вероятно, потому что это очень маленький адрес.
Обычно он заменяет те, которые могут изменить адреса на \x00 в паттерн(также называемом сигнатурой), и заменяет соответствующий 'x' в маске на '?'.
На этот раз нам придется сделать это вручную. Поскольку длина адреса четыре байта, мы хотим обнулить четыре байта, в которых хранится адрес.
В зависимости от вашей реализации вам может также понадобиться маска, поэтому не забудьте также изменить ее.
Убедитесь, что вы получите только один результат, щелкнув "Scan", прежде чем использовать паттерн. Он нашел паттерн только один раз, что означает, что мы можем использовать этот паттерн.
Поскольку я не использую маску для своей реализации, мне нужно только заменить \xFC на \x00.



Теперь мы можем написать функцию сканирование паттернов. Я буду использовать свой класс 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 );
        }
    };
Давайте посмотрим на это поближе. После создания объекта я создаю unique_ptr,который выделяет место в куче. Как только место выделено,я копирую байты динамической библиотеки линковки в выделенное пространство кучи.
Я использую вектор для моего паттерна с нулями(0x00) в качестве подстановочные знаков . Как только паттерн найден,я могу вернуть индекс,по которому он был найден.
Я также мог бы вернуть адрес по которому паттерн расположен в памяти. чтобы прочитать оффсет с исходного процесса,но поскольку я уже скопировал всю динамическую библиотеку линковки,я также могу копировать оффсет с выделенной памяти.

Но подождите,мы что-то пропустили? Почему это не дает нам правильного оффсета? Что-ж давай еще раз взглянем.
Функция возращает адрес,по котрому найдет паттерн,это означает,что она вернет нам эту позицию(
оносительно индекса в моем случае, она также может вернуть вам адрес, который вы можете прочитать через ReadProcessMemory)

Если вы прочитаете следующие четыре байта по адресу, возвращенному функцией сканирования паттернов, вы, по сути, прочитаете следующее:

Очевидно, это не тот результат, который вам нужен, так как это не тот оффсет, которой вам нужен.
При этом вам нужно добавить собственный оффсет поверх возвращенного адреса / индекса. Поскольку мы знаем,что hexadecimal имеет основание 16,и следовательно для отображение одного байта нужно 2 символа,выделенное поле имеет длину в 4 байта.
Сразу после этих четырех байтов находится желаемый оффсет. Это означает, что вам нужно прочитать / скопировать адрес, который вернула функция + 4 байта. Это будет адрес / индекс, где хранится оффсет.
Предположим вы хотите прочитать байты 00 00 7F 2D.Что-бы получить правильный адрес,вам нужно посчитать байты перед байтами,которые вы хотите прочитать.

В привиденом выше случае будет 7 байтов. Возьмем адрес/индекс возращаемый функцией сканирование паттернов,и добавем к нему 7 байтов,и мы должны получить правильный оффсет.

Давайте возьмем другой пример,на этот раз со статическим указателем на LocalPlayer.
Если мы выполним сканирование указателя в Cheat Engine,желательно несколько раз,чтобы убедить что наш результат всегда указывает на LocalPlayer, мы можем увидеть несколько указателей которые указывают на LocalPlayer.
Давайте попробуем создать паттерн для указателя LocalPlayer, который мы использовали ранее, чтобы сделать себя паттерном оффсета здововья.

Мы сканируем несколько раз, повторно просматривая предыдущие результаты после перезапуска игры. Это устранит временные указатели и покажет нам указатели которые, кажется, всегда указывают на LocalPlayer.

Первый результат это дейсвительные указатели на нашего LocalPlayer. Поскольку почти каждый кто использует указатель LocalPlayer - использут его,мы бы могли создать сигнатуру используя этот указатель.
Если мы дважды нажмем на мышку,указатель будет добавлен в наш список адресов. Нажав на правую кнопку мыши и кликнув на "Find out what accesses this address" откроется другое диалоговое окно. Мы можем либо узнать, что обращается к самому указателю (который мы хотим захватить), либо узнать, что обращается к адресу, на который указывает этот указатель.Мы хотим захватить сам указатель, поэтому нажимаем на "Find out what accesses this pointer" .

Но подождите, есть только одна инструкция, которая обращается к этому указателю в данный момент времени, и она не содержит фактического оффсета в своей инструкции. Как нам его взять?


Мы могли бы искать окружающие инструкции, щелкнув на инструкцию, а затем "Show Dissasembly" и посмотреть, как формируются инструкции и к ним осуществляется доступ, но сейчас мы не хотим об этом беспокоиться, у нас есть много других указателей, которые указывают на наш LocalPlayer. , поэтому мы просто посмотрим на другой.

Итак, возьмем следующий, client.dll + 4A98144.Дважды нажмите по нему, чтобы добавить его в наш список адресов и узнать, что обращается к этому указателю.
Мы можем видеть множество инструкций, которые перемещают регистры, поэтому мы не можем считывать регистры, не подключив функцию и не прочитав регистры в этот момент.
Но у нас также есть инструкции, содержащие абсолютный адрес. Нажав по нему один раз, мы увидим, что этот абсолютный адрес состоит из client.dll + 4A98164, что означает, что это статический глобальный указатель, который всегда будет по базовому адресу модуля client.dll module + 4A98164.
Но если мы сравним этот оффсет с нашим оффсетмо в списке указателей, мы увидим, что оно отличается на 10 байт. Почему?
Давайте добавим указатель в наш список и просмотрим область памяти, на которую он указывает. Текущий указатель в списке указывает на адрес, на который указывает первая запись в классе игрока, поэтому мы не можем просматривать эту область памяти.


Мы можем видеть множество адресов в этой области памяти, и при более внимательном рассмотрении мы можем обнаружить там наш адрес LocalPlayer, снова сохраненный в обратном порядке.

При дальнейшем иследовании,выясняется что это EntityList , содержащий указатели на каждую сущность в игре.
Client.dll + 4A98164 всегда указывает на мир, в то время как client.dll + 4A98174 указывает на первую сущность в мире, которая в локальной игре с ботами является нашим собственным игроком, поскольку боты появляются, когда мы находимся на сервере и "запускают" сервер.
На онлайн-серверах все по-другому, мы редко будем первыми в списке.
Я не собираюсь вдаваться в подробности об исходном коде движке, поскольку эта статья посвящена сканированию паттернов , однако мы видим, что это не лучший указатель на наш LocalPlayer, поскольку мы не всегда находимся в одной и той же позиции в этом списке.
Возьмем следующий указатель в нашем списке указателей. Если мы узнаем, что обращается к этому адресу, мы снова увидим список с другим оффсетом, поэтому мы не сможем прочитать наш оффсет из этих инструкций.

Идем на следующий указатель. На этот раз мы видим намного больше инструкций, обращающихся к этому указателю, вместе с тем же оффсетом каждый раз.
Это может означать, что это снова статический глобальный указатель, находящийся в одном и том же адресе при каждом запуске игры относительно базового адреса модуля.
Если мы нажмем по первой инструкции и посмотрим на окно внизу, мы увидим, что абсолютный адрес в инструкции сформирован из client.dll + 4F2B50C, именно то, что мы можем использовать, поэтому давайте дважды щелкнем на эту инструкцию и скопируем информацию.
Но эй, у нас есть инструкция, которая не содержит вокруг себя большого количества полезных инструкций, а это означает, что ее снова можно найти в нескольких местах, поэтому мы ищем другую инструкцию, которая могла бы иметь лучшие окружающие инструкции.
Двойное нажатие мыши по следующей дает нам более точные инструкции, поэтому давайте скопируем информацию и снова вставим ее в текстовый редактор.

Закройте Cheat Engine, откройте OllyDbg, подключитесь к нашему процессу и перейдите к скопированному выражению. После маркировки разных частей байтов я придумал следующее, которое даст вам один результат при сканировании.

На этот раз плагин SigMaker сформировал правильную сигнатуру и маску из байтов в hexadecimal представлении, поэтому нам не нужно исправлять паттерн.

Теперь нам нужно добавить наш оффсет,к возвращаемому адресу, иначе мы считали бы E8FC4D89, которые являются первыми 4 байтами по возвращаемому адресу, в обратном порядке.

Наш адрес, который мы хотим прочитать, находится в зеленом поле, что означает, что нам нужно добавить 10 байтов к возвращаемому адресу и прочитать новый адрес, чтобы получить нужный адрес. Помните, что 2 символа - это 1 байт.

Когда мы сканируем этот адрес, мы получаем абсолютный адрес, который является текущим адресом указателя на наш LocalPlayer, который мы можем dereference / ReadProcessMemory.

В большинстве случаев это будет другой адрес, если вы перезапустите игру. Если вы хотите внедрить сканирование паттернов в свой чит, вы можете dereference / ReadProcessMemory этот адрес напрямую, чтобы получить адрес вашего LocalPlayer, но если вы хотите сделать дампер оффсетов, вы можете вычесть его на базовый адрес модуля, который в данном случае это client.dll.
Это даст вам raw оффсет.

Теперь, когда у нас есть указатель на наш LocalPlayer, мы можем dereference его / ReadProcessMemory и получать базовый адрес нашего LocalPlayer, добавить оффсет здоровья и снова dereference его / ReadProcessMemory ,и прочитать наше текущее здоровья.

Sources / Tools:

Пожалуйста, авторизуйтесь для просмотра ссылки.

Пожалуйста, авторизуйтесь для просмотра ссылки.

Пожалуйста, авторизуйтесь для просмотра ссылки.

Пожалуйста, авторизуйтесь для просмотра ссылки.
 

Вложения

Последнее редактирование:
Сверху Снизу