Статья Виртуальные таблицы объектов(классов) и получение из них методов(функций)

  • 681
  • 315
Эта статья не совсем для новичков, вы уже должны знать, что такое указатели, как работают указатели, как работает память компьютера, что такое смещения, что такое классы, и как присваивать адреса классам. Данную информацию вы можете изучить на GuidedHacking, и о адресах в моей теме: Статья - Память и адреса компьютера [#1]

Сегодня я объясню, как работает получение функций из виртуальных таблиц классов.

VTable или виртуальная таблица - массив по которому находятся указатели на объявленные методы производного класса.
Виртуальные таблицы находятся по первому смещению в объектах(классах).

Когда вызывается метод(функция), мы ищем виртуальную таблицу объекта, и вызываем соответствующий метод производного класса.
Методы в виртуальной таблице находятся по каждому 4 байту в 32-х битном приложении, и каждому 8 байту в 64-х. То есть, в 32 битном приложении первая функция в таблице будет по адресу 0x0, вторая по 0x4, третья по 0x8, и так далее.

Методы из VTable имеют сооглашения о вызовах __thiscall*, потому-что им требуется, чтобы указатель this(объекта) передавался в ECX.

1641014998018.png

1641015610322.png

1641013587885.png
1641013663465.png
Теперь мы можем выстроить иерархию до метода из виртуальной таблицы какого-либо объекта(если вы все поняли, конечно же).
Базовый адрес модуля объекта -> Адрес объекта -> смещение до VTable(всегда 0x0) -> смещение до метода

Но нам вовсе не нужно добавлять смещение до метода, потому-что виртуальная таблица это массив, поэтому нам нужно это реализовать индексированием, потому-что каждый индекс также занимает 4 байта в 32-х битном приложении, и 8 байтов в 64-х битном.
То есть, схема теперь будет выглядеть так:
Базовый адрес модуля объекта -> Адрес объекта -> смещение до VTable[ индекс метода ]

mp4.gif

Для гораздо более удобных манипуляций с свойствами и методами классов игр мы создаём собственный класс, которому присваиваем адрес класса игры, таким образом "реконструируя" класс игры.

mp4 (3).gif


Найти адрес класса игры можно разными способами, но в некоторых игровых движках существует у модулей экспортируемая функция "CreateInterface", которая как раз и вернёт адрес интересующего нам класса игры. Я не буду рассказывать, как это сделать, за меня это давно сделали, оставляю ссылки:

https://guidedhacking.com/threads/csgo-createinterface-tutorial-how-to-get-interfaces.14701/
Если вы уже знаете о получении интерфейсах, и вызове методов из него, то для вас это интересующий вопрос, ведь вы и так можете легко получить интересующий вас метод просто объявив виртуальную функцию по нужному индексу.

В этом и проблема, что вам нужно объявлять функцию по конкретному индексу, иначе у вас ничего не получится.
В некоторых классах игр могут быть абсолютно не нужные для вас методы, как например здесь:
cstrike15_src/cdll_int.h at f82112a2388b841d72cb62ca48ab1846dfcc11c8 · perilouswithadollarsign/cstrike15_src

Поэтому в конце концов мы сталкиваемся с тем, что у вас появляется нужда в получении метода по вашему указанному индексу.
Вы бы могли конечно создавать кучу методов-пустышек, как на примере ниже, но зачем? Это занимает место, и выглядит плохо. Поэтому я подробно расскажу, как получать метод по вашему указанному индексу.
1641031996766.png

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

Для примера я покажу получение метода GetHighestEntityIndex из объекта IClientEntityList игры CS:GO
1641018223476.png


Первым делом нам требуется шаблон функции, который мы можем сделать с помощью объявления using либо typedef. Шаблон функции будет представлять возвращаемое значение функции, сооглашение о вызове, и аргументы функции. Объявлять шаблон функции с using либо typedef решать вам, разницы нет.

C++:
// Шаг 1: Объявляем using либо typedef,
// после using сразу пишем название шаблона функции
using GetHighestEntityIndexFn =
typedef
// Шаг 2: Теперь нам нужно указать возвращаемое значение метода,
// написать сооглашение о вызове __thiscall*,
// и только после этого название шаблона функции в случае с typedef
using GetHighestEntityIndexFn = int( __thiscall* )
typedef int( __thiscall* GetHighestEntityIndexFn )
// Шаг 3: Теперь в скобках пишем аргументы функции,
// первый аргумент всегда void* в который помещается наш объект(this),
// который мы получили с помощью CreateInterface(https://guidedhacking.com/threads/csgo-createinterface-tutorial-how-to-get-interfaces.14701/)
using GetHighestEntityIndexFn = int( __thiscall* )( void* );
typedef int( __thiscall* GetHighestEntityIndexFn )( void* );
Теперь требуется получить адрес массива VTable из объекта, тут не трудно, просто преобразуем полученный интерфейс класса(либо адрес класса) 2 раза в указатель, и разыменовываем, сначала, чтобы получить указатель на объект, потом, чтобы получить указатель первого смещения, по которому всегда находится VTable.
C++:
// C
uintptr_t* virtual_table = *( uintptr_t** )g_interfaces.m_pClientEntityList;
// C++
uintptr_t* virtual_table = *reinterpret_cast< uintptr_t** >( g_interfaces.m_pClientEntityList );
Тут ещё проще, объявляем переменную, и присваиваем ей адрес метода, который мы получаем из массива виртуальной таблицы. В моем случае, индекс функции равен 6, учитывайте, что индексы функций начинаются с нуля, а не с единицы!
C++:
uintptr_t get_highest_entity_index_address = virtual_table[ 6 ];

Последнее действие - преобразование адреса метода в наш шаблон функции. Также делаем это объявлением переменной, которую мы будем уже использовать, как вызов метода.
C++:
// C
GetHighestEntityIndexFn get_highest_entity_index_fn = ( GetHighestEntityIndexFn )get_highest_entity_index_address;
// C++
GetHighestEntityIndexFn get_highest_entity_index_fn = reinterpret_cast< GetHighestEntityIndexFn >( get_highest_entity_index_address );
1641020013656.png

Как можете видеть на скриншоте, все отлично работает, нам выводит наибольший индекс существующей сущности. И нет, на сервере не 227 игроков, просто к сущностям относятся не только игроки. Проверить работоспособность этого можно вызвав и другой метод, но я вас уверяю, вас бы просто крашнуло, если что-то было не так.

mp4 (1).gif

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

Для начала избавимся от адреса метода, и получим следующий результат:
C++:
// C
GetHighestEntityIndexFn get_highest_entity_index_fn = ( GetHighestEntityIndexFn )( virtual_table[ 6 ] );
// C++
GetHighestEntityIndexFn get_highest_entity_index_fn = reinterpret_cast< GetHighestEntityIndexFn >( virtual_table[ 6 ] );
C++:
using GetHighestEntityIndexFn = int( __thiscall* )( void* );
uintptr_t* virtual_table = *reinterpret_cast< uintptr_t** >( g_interfaces.m_pClientEntityList );
GetHighestEntityIndexFn get_highest_entity_index_fn = reinterpret_cast< GetHighestEntityIndexFn >( virtual_table[ 6 ] );

Пора избавиться от переменной с виртуальной таблицей! Для этого мы также должны получить виртуальную таблицу, но теперь преобразуем её не в uintptr_t(unsigned int), а в шаблон функции, и после этого оборачиваем все это дело в скобки, и указываем индекс метода. Оборачивать в скобки нужно обязательно, иначе мы попытаемся взять данные не из массива виртуальной таблицы(и вообще у вас по идеи не получится, вы должны получить ошибку от компилятора).
C++:
// C
GetHighestEntityIndexFn get_highest_entity_index_fn = ( *( GetHighestEntityIndexFn** )( g_interfaces.m_pClientEntityList ) )[ 6 ];
// C++
GetHighestEntityIndexFn get_highest_entity_index_fn = ( *reinterpret_cast< GetHighestEntityIndexFn** >( g_interfaces.m_pClientEntityList ) )[ 6 ];
C++:
using GetHighestEntityIndexFn = int( __thiscall* )( void* );
GetHighestEntityIndexFn get_highest_entity_index_fn = ( *reinterpret_cast< GetHighestEntityIndexFn** >( g_interfaces.m_pClientEntityList ) )[ 6 ];

Последнее действие - избавиться от шаблона. Для этого мы просто помещаем шаблон без using/typedef и названия в преобразование адреса метода, и добавляем два уровня указателя, после __thiscall*. У нас получится аж 3 уровня указателя, но почему? Потому-что сооглашение о вызове __thiscall* всегда идёт с указателем, и так как нам нужно метод получить из виртуальной таблицы, то мы добавляем ещё два уровня указателя. Но не забываем про тип переменной, вместо типа шаблона функции мы также просто пишем сам шаблон функции, и после __thiscall* пишем название переменной.
C++:
// C
int( __thiscall* get_highest_entity_index_fn )( void* ) = ( *( int( __thiscall*** )( void* ) )( g_interfaces.m_pClientEntityList ) )[ 6 ];
// C++
int( __thiscall* get_highest_entity_index_fn )( void* ) = ( *reinterpret_cast< int( __thiscall*** )( void* ) >( g_interfaces.m_pClientEntityList ) )[ 6 ];
Конечно же мы не будем в каждой функции нашего чита создавать переменную-метод. Для упрощения нашей жизни мы создадим функцию в интерфейс-классе, который мы и создали для этого. Туда мы поместим наш код, но без переменной, сразу возвращая вызов метода, не забывайте, что первый аргумент всегда объект, так как наша функция вызывается из этого же объекта, то просто пишем this. Выглядеть это будет примерно так:
C++:
class IClientEntityList {
    enum Indicies : size_t {
        GETHIGHESTENTITYINDEX = 6
    };
public:
    int GetHighestEntityIndex( ) {                                                                       //Индекс метода        //Вызываем метод
        return ( *reinterpret_cast< int( __thiscall*** )( void* ) >( g_interfaces.m_pClientEntityList ) )[ GETHIGHESTENTITYINDEX ]( this );
    }
};

Конечно же каждый раз писать столько кода не очень читабельно, и удобно, учитывая, что в одном классе игры может быть более 10 методов. Поэтому мы можем создать дефайн для всего этого дела. Я поделюсь с вами своими дефайнами. Я не буду их разбирать, тут должно быть все понятно, только оставлю ссылку на ресурс, где вы можете узнать о вариативных макросах.

C++:
#define GET_VIRTUAL_METHOD( return_type, method_index, ... ) ( *reinterpret_cast< return_type ( __thiscall*** )( void*, __VA_ARGS__ ) >( this ) )[ method_index ]
#define VIRTUAL_METHOD( return_type, name, method_index, call_args, ... ) \
return_type name( __VA_ARGS__ ) noexcept { \
    return GET_VIRTUAL_METHOD( return_type, method_index, __VA_ARGS__ )call_args;  \
}
Variadic macros

Пример использования
1641024545397.png

Написал на английском потому-что на русском не уместится.
Получать функции игр по индексу из виртуальной таблицы объектов очень удобно, и экономит много времени.
Теперь вы знаете, что такое виртуальная таблица, как из неё получать методы(функции), и использовать.
Надеюсь я достаточно хорошо разобрал получение методов из виртуальных таблиц, и вы все поняли, но если вам что-то не понятно, напишите в тему, я постараюсь помочь.

Всех с новым годом!
mp4 (2).gif
 
Последнее редактирование:
Сверху Снизу