Вступление
Доброго времени суток всем. Не так давно я решил заняться собственным сервером 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