Вступление
Доброго времени суток всем. Не так давно я решил заняться собственным сервером Team Fortress 2, и для этого мне понадобилось написание плагинов с помощью SourcePawn. Я начал изучать этот язык относительно недавно и в целом он мне нравится, за исключением некоторых моментов, о которых я бы хотел поговорить сегодня, а также обсудить решения для них.
Если вы не хотите читать философские рассуждения на тему ограниченности языка, то я рекомендую пропустить данный раздел и сразу перейти к следующему.
Один из этих моментов - ограничение в использовании адресного пространства процесса srcds. Увы, но мы можем работать только с библиотеками server и engine. Ну, этого, по идее, должно хватить, так как основная работа движка происходит именно в этих модулях. Однако, не поймите меня неправильно, но я не люблю чувствовать себя ограниченным при работе с памятью, хех. Тем не менее, с этим всё же можно смириться, думал я первое время.
Однако то, что мне действительно чертовски не понравилось, так это отсутствие возможности "смешивания" памяти плагина и сервера. То есть если я захочу внедрить указатель на какой-либо участок памяти своего плагина в любую библиотеку процесса или сам процесс srcds - я буду вынужден воздержаться, поскольку во-первых, получать нативные адреса собственных переменных или функций плагина невозможно, а во-вторых, работать я могу только с, опять же, server или engine. Даже если бы у меня была возможность получения адресов с помощью "#emit", то получал бы я отнюдь не адреса относительно начала памяти процесса (нуля), а относительно специальной памяти плагина. Более того, в самом плагине нельзя выделить память процесса.
Опять же, не поймите меня неправильно! Я не критикую язык SourcePawn с намеком на то, что язык нуждается в доработке. SourcePawn - не тот язык, который предназначен для сложных манипуляций с памятью. Он предназначен для написания обычных плагинов, которые будут менять существующий функционал на сервере или добавлять новый. В общем, неплохой язык для написания сервер-сайд модификаций. Однако те моменты, которые я перечислил выше, лично мне очень не нравятся, и поэтому здесь я бы хотел предложить варианты решения нескольких проблем для серверов на платформе Windows, с которыми я столкнулся при разработке собственных плагинов.
Я практически уверен в том, что кто-то скажет, что для реализации подобного функционала можно воспользоваться native функциями, и я буду вынужден согласиться. С помощью native можно реализовать всё, чем я был недоволен до этого, однако, я предпочитаю не таскать за своими проектами огромное количество модульных зависимостей, а native функции заставят вас это делать. Поэтому все решения проблем я буду воспроизводить с использованием чистого SourcePawn кода, который можно будет аккуратно завернуть в include файл.
Начнем же.
Что мы имеем?
Чтобы найти какой-то паттерн кода с целью произвести его патчинг, нам дают возможность использовать специальную вещь - gamedata. Это файл, в котором вы указываете паттерн для поиска, модуль, операционную систему и оффсет. Достаточно полезная вещь.
Для работы с чистой памятью процесса (не плагина) мы имеем две основные функций - StoreToAddress() и LoadFromAddress(). Данные функции дают возможность обращаться к памяти по указанному адресу, но так как полноценной информации о адресах у нас нет (мы можем получать адреса только поиском с помощью gamedata), то мы остаемся слепы в этом вопросе.
Еще одна самая главная особенность чистого языка - функция SDKCall(), которая была разработана для вызовов функций классов, как ни странно.
Вот и всё. Эти вещи позволят нам сделать SourcePawn намного мощнее в плане работы с памятью.
Получение версии Windows
Для начала начнем с самого простого - попробуем получить версию нашей операционной системы. Как это сделать, не прибегая к native? Очень просто! Следите за руками.
В ОС Windows есть особое место в памяти, адрес которого статичен еще со времен Windows 2000. Это место представляет собой структуру KUSER_SHARED_DATA, которая как раз-таки и содержит необходимую для нас информацию. Адрес данной структуры - 0x7FFE0000. Давайте на основе имеющейся информации напишем функцию для получения версии ОС.
Как видите - ничего сложного, за исключением не самой лучшей читабельности кода, но это можно исправить с помощью "#define".
Получение PEB
Process Environment Block, пожалуй, одна из самых полезных и нужных для нас вещей. С его помощью можно получить доступ к списку загруженных модулей, а также адрес самого процесса srcds. Есть только одно "но" - его невозможно получить с помощью LoadFromAddress(), так как его адрес меняется от запуска к запуску сервера. Более того, получить его можно только прибегнув к x86 ассемблеру. Вот как это выглядит на языке C++:
Выглядит просто, не так ли? Увы, но выполнить подобный трюк на SourcePawn невозможно. Ну а если сильно захотеть? Давайте попробуем.
Прежде всего необходимо создать файл в папке gamedata, назовем его "any.tutor.memory":
Увы, но решение без использования данного файла я не нашел. Зачем это необходимо я разъясню позднее.
Далее реализуем необходимый функционал. Для начала напишем вот такой код:
Данные дефайны можно опустить, но дабы увеличить читабельность кода я буду их использовать.
Затем напишем функции чтения данных по адресу:
Мы написали обертки для LoadFromAddress(), которые позволят читать типы данных, которые нам необходимы. Без этих функций наш код стал бы достаточно большим и неудобным в чтении.
После этих функций создадим еще две:
Функция Transpose() нужна для изменения значения указателя, не прибегая к приведению типов. Функция Dereference() аналогична Transpose(), но после изменения адреса она еще и произведет разыменования указателя. Опять же - всё для повышенной читабельности кода.
А теперь реализуем действительно необходимый функционал:
WriteData() нужна для записи массива байт по указанному адресу. Не обращайте внимание на то, что передается "int[]", функция работает с этим аргументом, как с "byte[]". GetModuleSize() функция получает виртуальный размер модуля, загруженного в адресное пространство. В аргумент pAddr нужно передавать результат GetModuleHandle()/LoadLibrary(), но об этом позднее. FindPattern() нужна для поиска паттерна в памяти без использования gamedata.
Теперь напишем достаточно специфичную функцию:
Пока что я не буду заострять на этой функции внимание, просто скажу, что без ее помощи у нас ничего не получится.
А теперь напишем кое-что действительно впечатляющее:
Впечатляет, не правда ли? Возможно, кто-то даже назовет всё это извращением и безумием, но тем не менее давайте разберем всё по порядку.
Сначала функция проверяет, был ли уже найдена PEB, и возвращает его адрес, если была. Затем происходит получение адреса модуля server. Мы используем здесь небольшой трюк, основываясь на архитектуре PE файлов, а именно правилу, что первые два байта exe или dll равны константе "MZ". При старте поиска ядро SourceMod сразу же натыкается на эти два байта и возвращает адрес начала модуля. После всего этого вызывается функция CreateMemoryForSDKCall(). Ее цель - создание уникального паттерна в неиспользуемой области адресного пространства модуля server, который будет найден функцией PrepSDKCall_SetSignature() и FindPattern(). Затем мы строим SDK вызов, который на самом деле является не SDK, а нашей будущей функцией, написанной на x86 ассемблере. PrepSDKCall_SetSignature() просто находит наш уникальный паттерн, в который мы в будущем сможем записать любой код. Затем мы с помощью FindPattern() находим это же самое место и изменяем его, внедряя туда наш ассемблерный код. После всего этого мы производим вызов SDKCall() функции и получаем наш PEB адрес. Хитро, не правда ли?
Теперь, имея на руках PEB, можно делать очень много интересных вещей. Давайте попробуем получить адрес srcds, например.
Переменная ImageBaseAddress, которая входит в состав структуры PEB и располагается по смещению 8, содержит адрес процесса, который загрузил библиотеку. Функция просто получает это число, ничего сложного. Точно так же работает функция GetModuleHandle(), если в ее аргументе указать 0.
Заключение
Туториал получился достаточно большой, как по мне, поэтому я считаю, что пока что достаточно. Мы рассмотрели реализацию получения версии Windows и возможность выполнения собственного x86 ассемблерного кода. Как видите, это возможно. Если пользователям понравится подобная тематика, то я продолжу написание туториалов, в которых рассмотрю еще более интересные вещи. Благодарю за чтение.
Доброго времени суток всем. Не так давно я решил заняться собственным сервером Team Fortress 2, и для этого мне понадобилось написание плагинов с помощью SourcePawn. Я начал изучать этот язык относительно недавно и в целом он мне нравится, за исключением некоторых моментов, о которых я бы хотел поговорить сегодня, а также обсудить решения для них.
Если вы не хотите читать философские рассуждения на тему ограниченности языка, то я рекомендую пропустить данный раздел и сразу перейти к следующему.
Один из этих моментов - ограничение в использовании адресного пространства процесса srcds. Увы, но мы можем работать только с библиотеками server и engine. Ну, этого, по идее, должно хватить, так как основная работа движка происходит именно в этих модулях. Однако, не поймите меня неправильно, но я не люблю чувствовать себя ограниченным при работе с памятью, хех. Тем не менее, с этим всё же можно смириться, думал я первое время.
Однако то, что мне действительно чертовски не понравилось, так это отсутствие возможности "смешивания" памяти плагина и сервера. То есть если я захочу внедрить указатель на какой-либо участок памяти своего плагина в любую библиотеку процесса или сам процесс srcds - я буду вынужден воздержаться, поскольку во-первых, получать нативные адреса собственных переменных или функций плагина невозможно, а во-вторых, работать я могу только с, опять же, server или engine. Даже если бы у меня была возможность получения адресов с помощью "#emit", то получал бы я отнюдь не адреса относительно начала памяти процесса (нуля), а относительно специальной памяти плагина. Более того, в самом плагине нельзя выделить память процесса.
Опять же, не поймите меня неправильно! Я не критикую язык SourcePawn с намеком на то, что язык нуждается в доработке. SourcePawn - не тот язык, который предназначен для сложных манипуляций с памятью. Он предназначен для написания обычных плагинов, которые будут менять существующий функционал на сервере или добавлять новый. В общем, неплохой язык для написания сервер-сайд модификаций. Однако те моменты, которые я перечислил выше, лично мне очень не нравятся, и поэтому здесь я бы хотел предложить варианты решения нескольких проблем для серверов на платформе Windows, с которыми я столкнулся при разработке собственных плагинов.
Я практически уверен в том, что кто-то скажет, что для реализации подобного функционала можно воспользоваться native функциями, и я буду вынужден согласиться. С помощью native можно реализовать всё, чем я был недоволен до этого, однако, я предпочитаю не таскать за своими проектами огромное количество модульных зависимостей, а native функции заставят вас это делать. Поэтому все решения проблем я буду воспроизводить с использованием чистого SourcePawn кода, который можно будет аккуратно завернуть в include файл.
Начнем же.
Что мы имеем?
Чтобы найти какой-то паттерн кода с целью произвести его патчинг, нам дают возможность использовать специальную вещь - gamedata. Это файл, в котором вы указываете паттерн для поиска, модуль, операционную систему и оффсет. Достаточно полезная вещь.
Для работы с чистой памятью процесса (не плагина) мы имеем две основные функций - StoreToAddress() и LoadFromAddress(). Данные функции дают возможность обращаться к памяти по указанному адресу, но так как полноценной информации о адресах у нас нет (мы можем получать адреса только поиском с помощью gamedata), то мы остаемся слепы в этом вопросе.
Еще одна самая главная особенность чистого языка - функция SDKCall(), которая была разработана для вызовов функций классов, как ни странно.
Вот и всё. Эти вещи позволят нам сделать SourcePawn намного мощнее в плане работы с памятью.
Получение версии Windows
Для начала начнем с самого простого - попробуем получить версию нашей операционной системы. Как это сделать, не прибегая к native? Очень просто! Следите за руками.
В ОС Windows есть особое место в памяти, адрес которого статичен еще со времен Windows 2000. Это место представляет собой структуру KUSER_SHARED_DATA, которая как раз-таки и содержит необходимую для нас информацию. Адрес данной структуры - 0x7FFE0000. Давайте на основе имеющейся информации напишем функцию для получения версии ОС.
stock void GetWindowsVersion(int& iMajorVer, int& iMinorVer)
{
Address pUserSharedData = view_as<Address>(0x7FFE0000);
/* ((KUSER_SHARED_DATA*)0x7FFE0000)->NtMajorVersion */
iMajorVer = LoadFromAddress(view_as<Address>(view_as<int>(pUserSharedData) + 0x26C), NumberType_Int32);
/* ((KUSER_SHARED_DATA*)0x7FFE0000)->NtMinorVersion */
iMinorVer = LoadFromAddress(view_as<Address>(view_as<int>(pUserSharedData) + 0x270), NumberType_Int32);
}
// ...
public void OnPluginStart()
{
int iMajor, iMinor;
GetWindowsVersion(iMajor, iMinor);
PrintToServer("Windows Version : Major=%d, Minor=%d", iMajor, iMinor);
}
Как видите - ничего сложного, за исключением не самой лучшей читабельности кода, но это можно исправить с помощью "#define".
Получение PEB
Process Environment Block, пожалуй, одна из самых полезных и нужных для нас вещей. С его помощью можно получить доступ к списку загруженных модулей, а также адрес самого процесса srcds. Есть только одно "но" - его невозможно получить с помощью LoadFromAddress(), так как его адрес меняется от запуска к запуску сервера. Более того, получить его можно только прибегнув к x86 ассемблеру. Вот как это выглядит на языке C++:
void* GetPEB()
{
__asm
{
mov eax, dword ptr fs:[30];
}
}
Выглядит просто, не так ли? Увы, но выполнить подобный трюк на SourcePawn невозможно. Ну а если сильно захотеть? Давайте попробуем.
Прежде всего необходимо создать файл в папке gamedata, назовем его "any.tutor.memory":
"Games"
{
"#default"
{
"Addresses"
{
"server"
{
"windows"
{
"signature" "Find_Server"
}
}
}
"Signatures"
{
"Find_Server"
{
"library" "server"
"windows" "\x4D\x5A"
}
}
}
}
Увы, но решение без использования данного файла я не нашел. Зачем это необходимо я разъясню позднее.
Далее реализуем необходимый функционал. Для начала напишем вот такой код:
#define Pointer Address #define nullptr Address_Null #define int(%1) view_as<int>(%1) #define ptr(%1) view_as<Pointer>(%1)
Данные дефайны можно опустить, но дабы увеличить читабельность кода я буду их использовать.
Затем напишем функции чтения данных по адресу:
stock int ReadByte(Pointer pAddr)
{
if(pAddr == nullptr)
{
return -1;
}
return LoadFromAddress(pAddr, NumberType_Int8);
}
stock int ReadWord(Pointer pAddr)
{
if(pAddr == nullptr)
{
return -1;
}
return LoadFromAddress(pAddr, NumberType_Int16);
}
stock int ReadInt(Pointer pAddr)
{
if(pAddr == nullptr)
{
return -1;
}
return LoadFromAddress(pAddr, NumberType_Int32);
}
Мы написали обертки для LoadFromAddress(), которые позволят читать типы данных, которые нам необходимы. Без этих функций наш код стал бы достаточно большим и неудобным в чтении.
После этих функций создадим еще две:
stock Pointer Transpose(Pointer pAddr, int iOffset)
{
return ptr(int(pAddr) + iOffset);
}
stock int Dereference(Pointer pAddr, int iOffset = 0)
{
if(pAddr == nullptr)
{
return -1;
}
return ReadInt(Transpose(pAddr, iOffset));
}
Функция Transpose() нужна для изменения значения указателя, не прибегая к приведению типов. Функция Dereference() аналогична Transpose(), но после изменения адреса она еще и произведет разыменования указателя. Опять же - всё для повышенной читабельности кода.
А теперь реализуем действительно необходимый функционал:
stock Pointer WriteData(Pointer pAddr, int[] data, int iSize)
{
if(pAddr == nullptr)
{
return nullptr;
}
for(int i = 0; i < iSize; i++)
{
StoreToAddress(pAddr, data[i], NumberType_Int8);
pAddr++;
}
pAddr++;
return pAddr;
}
stock Pointer FindPattern(Pointer pStart, int iSize, int[] pattern, int iPatternSize, int iOffset)
{
for(int i = 0; i < iSize; i++)
{
if(ReadByte(pStart) == pattern[0])
{
bool bFound = true;
for(int j = 1; j < iPatternSize; j++)
{
int iDestByte = ReadByte(ptr(int(pStart) + j));
int iSrcByte = pattern[j];
if(iSrcByte != 0xFF)
{
if(iDestByte != iSrcByte)
{
bFound = false;
break;
}
}
}
if(bFound)
{
return ptr(int(pStart) + iOffset);
}
}
pStart++;
}
return nullptr;
}
stock int GetModuleSize(Pointer pAddr)
{
if(pAddr == nullptr)
{
return 0;
}
if(ReadWord(pAddr) == 0x5A4D) // MZ
{
int iOffset = Dereference(pAddr, 0x3C); // NT headers offset
int iRes = Dereference(pAddr, iOffset + 0x50); // nt->OptionalHeader.SizeOfImage
return iRes;
}
else
{
return -1;
}
}
stock int GetModuleSize(Pointer pAddr)
{
if(pAddr == nullptr)
{
return 0;
}
if(ReadWord(pAddr) == 0x5A4D) // MZ
{
int iOffset = Dereference(pAddr, 0x3C); // NT headers offset
int iRes = Dereference(pAddr, iOffset + 0x50); // nt->OptionalHeader.SizeOfImage
return iRes;
}
else
{
return -1;
}
}
WriteData() нужна для записи массива байт по указанному адресу. Не обращайте внимание на то, что передается "int[]", функция работает с этим аргументом, как с "byte[]". GetModuleSize() функция получает виртуальный размер модуля, загруженного в адресное пространство. В аргумент pAddr нужно передавать результат GetModuleHandle()/LoadLibrary(), но об этом позднее. FindPattern() нужна для поиска паттерна в памяти без использования gamedata.
Теперь напишем достаточно специфичную функцию:
bool CreateMemoryForSDKCall(Pointer pAddr)
{
if(pAddr == nullptr)
{
return false;
}
static Pointer pZeroMem = nullptr;
if(pZeroMem != nullptr)
{
return true;
}
pAddr = ptr(int(pAddr) + GetModuleSize(pAddr) - 1);
/* I could use "true", but compiler blames this line with "redundant test" */
while(pAddr)
{
int b = ReadByte(pAddr);
if(b != 0x00)
{
break;
}
pAddr--;
}
/* Align for safe code injection */
pZeroMem = ptr(int(pAddr) + 0x10 & 0xFFFFFFF0);
/* Add unique signature for PrepSDKCall_SetSignature() call */
Addr = pZeroMem;
for(int i = 0; i < 4; i++)
{
StoreToAddress(pAddr, 0xDEADBEEF, NumberType_Int32);
pAddr = Transpose(pAddr, 4);
}
return true;
}
Пока что я не буду заострять на этой функции внимание, просто скажу, что без ее помощи у нас ничего не получится.
А теперь напишем кое-что действительно впечатляющее:
Pointer g_pPEB = nullptr;
int g_GetPEBAsmCode[] =
{
0x64, 0xA1, 0x30, 0x00, 0x00, 0x00, // mov eax, dword ptr fs:[30]
0xC3 // ret
};
int g_DeadBeef[] =
{
0xEF, 0xBE, 0xAD, 0xDE,
0xEF, 0xBE, 0xAD, 0xDE,
0xEF, 0xBE, 0xAD, 0xDE,
0xEF, 0xBE, 0xAD, 0xDE
};
/* Get Process Environment Block pointer */
stock Pointer NtCurrentPeb()
{
/* Don't do these crazy stuffs again */
if(g_pPEB != nullptr)
{
return g_pPEB;
}
Handle h = LoadGameConfigFile("any.tutor.memory");
if(h == null)
{
return nullptr;
}
Pointer pServerBase = GameConfGetAddress(h, "server");
delete h;
if(pServerBase == nullptr)
{
return nullptr;
}
if(CreateMemoryForSDKCall(pServerBase) == false)
{
return nullptr;
}
/* Init handle variable for call */
Handle hGetPEB = INVALID_HANDLE;
/* SDKCall_Static means don't use "this" argument */
StartPrepSDKCall(SDKCall_Static);
/* Create SDK call by finding our memory pointer from CreateMemoryForSDKCall() */
PrepSDKCall_SetSignature(SDKLibrary_Server, "\xEF\xBE\xAD\xDE\xEF\xBE\xAD\xDE\xEF\xBE\xAD\xDE\xEF\xBE\xAD\xDE", 16);
PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain);
hGetPEB = EndPrepSDKCall();
/* Failed to create SDKCall handle. Probably special signature wasn't found */
if(hGetPEB == INVALID_HANDLE)
{
return nullptr;
}
int iServerSize = GetModuleSize(pServerBase);
Pointer p = FindPattern(pServerBase, iServerSize, g_DeadBeef, 16, 0);
if(p == nullptr)
{
return nullptr;
}
/* Create little function that will help us to get process' PEB */
WriteData(p, g_GetPEBAsmCode, 7);
g_pPEB = SDKCall(hGetPEB);
/* We don't need this SDK call anymore */
delete hGetPEB;
return g_pPEB;
}
Впечатляет, не правда ли? Возможно, кто-то даже назовет всё это извращением и безумием, но тем не менее давайте разберем всё по порядку.
Сначала функция проверяет, был ли уже найдена PEB, и возвращает его адрес, если была. Затем происходит получение адреса модуля server. Мы используем здесь небольшой трюк, основываясь на архитектуре PE файлов, а именно правилу, что первые два байта exe или dll равны константе "MZ". При старте поиска ядро SourceMod сразу же натыкается на эти два байта и возвращает адрес начала модуля. После всего этого вызывается функция CreateMemoryForSDKCall(). Ее цель - создание уникального паттерна в неиспользуемой области адресного пространства модуля server, который будет найден функцией PrepSDKCall_SetSignature() и FindPattern(). Затем мы строим SDK вызов, который на самом деле является не SDK, а нашей будущей функцией, написанной на x86 ассемблере. PrepSDKCall_SetSignature() просто находит наш уникальный паттерн, в который мы в будущем сможем записать любой код. Затем мы с помощью FindPattern() находим это же самое место и изменяем его, внедряя туда наш ассемблерный код. После всего этого мы производим вызов SDKCall() функции и получаем наш PEB адрес. Хитро, не правда ли?
Теперь, имея на руках PEB, можно делать очень много интересных вещей. Давайте попробуем получить адрес srcds, например.
stock Pointer GetSRCDSPtr()
{
/* Impossible to get module handle without PEB */
if(NtCurrentPeb() == nullptr)
{
return nullptr;
}
return ptr(Dereference(NtCurrentPeb(), 8)); // g_pPEB->ImageBaseAddress
}
// ...
public void OnPluginStart()
{
PrintToServer("srcds.exe : Addr=0x%X", GetSRCDSPtr());
}
Переменная ImageBaseAddress, которая входит в состав структуры PEB и располагается по смещению 8, содержит адрес процесса, который загрузил библиотеку. Функция просто получает это число, ничего сложного. Точно так же работает функция GetModuleHandle(), если в ее аргументе указать 0.
Заключение
Туториал получился достаточно большой, как по мне, поэтому я считаю, что пока что достаточно. Мы рассмотрели реализацию получения версии Windows и возможность выполнения собственного x86 ассемблерного кода. Как видите, это возможно. Если пользователям понравится подобная тематика, то я продолжу написание туториалов, в которых рассмотрю еще более интересные вещи. Благодарю за чтение.
Серьёзно. Даже более чем.
ОтветитьУдалитьУ меня есть кое-какие вопросы, если еще не отошел от этого языка я бы хотел связаться с тобой.
Познавательно!
ОтветитьУдалитьSeminole Hard Rock Hotel Casino - MapyRO
ОтветитьУдалитьFind Seminole Hard Rock Hotel Casino, Fort 상주 출장샵 Lauderdale in picturesque Florida. 청주 출장안마 Seminole Hard Rock Hotel & 포천 출장마사지 Casino, Fort Lauderdale, is 남원 출장샵 a hotel on the 충청북도 출장마사지 lake