Механизм WFP, появившийся в Windows Vista, уже активно используется разработчиками защит (различных файерволов и антивирусов). Действительно, предоставляемый набор ядерных функций успешно справляется со своими обязанностями по упрощению жизни девелоперам. Но сегодня мы не будем говорить о разработке ядерных фильтров трафика при помощи WFP, а обсудим вопрос обхода этих фильтров.

 

Методика исследования

Сначала немного определимся с определениями. Callout в терминологии WFP — это набор функций для фильтрации трафика. У нас есть три функции, составляющие callout: classifyFn, notifyFn, flowDeleteFn (см. структуру FWPS_CALLOUT). Интереснее всего classifyFn (классифицирующая функция): она, собственно, и решает, что делать с соединением/пакетом — разрешить или запретить. notifyFn просто уведомляет (как можно понять из названия) о создании и удалении фильтра. Фильтр — это набор условий фильтрации. Когда мы вызываем функцию FwpsCalloutRegister, и нам возвращают 32-битный идентификатор коллаута.

Далее я не буду останавливаться на определениях, элементарные вещи ты сможешь вычитать в доках, ссылки на которые найдешь на боковых сносках. Еще одно замечание — для удаленной отладки используем WinDbg, гостевая система — Windows Vista x64, дизассемблер — Ida Pro, символы для дебаггера корректны. Перезагрузить их (символы) можно командой .reload /s /n. Для того, чтобы успешно исследовать внутреннее устройство WFP, нужно выработать определенный подход. В нашем случае довольно удобно написать свой callout (а точнее, драйвер, содержащий callout). Затем расставить брейки на функции нашего коллаута и посмотреть в окно Call Stack (в WinDbg вызывается <Alt+6> или одноименным пунктом меню), дабы выяснить, откуда происходит вызов наших функций. И вот, останавливаемся в первый раз на нотифицирующей функции. Call Stack.

ipsblock!FlNotify ;-> наша нотифицирующая функция
NETIO!FeNotifyFilter+0x3a
NETIO!HandleFilterFree+0x1f
NETIO!DeleteFilterFromIndex+0x22b
NETIO! ?? ::FNODOBFM::`string'+0x6f03
NETIO!IoctlKfdCommitTransaction+0x39

Ага, понятно. Непосредственный вызов происходит из функции FeNotifyFilter. Теперь посмотрим, что там:

fffffa6000bac100 sub rsp,20h
fffffa60
00bac104 mov rbx,rdx
fffffa6000bac107 mov rdi,rcx
fffffa60
00bac10a lea rdx,[rsp+38h]
fffffa6000bac10f mov ecx,dword ptr [rbx+2Ch]
fffffa60
00bac112 mov esi,r8d
fffffa6000bac115 call
NETIO!FeGetRefCallout (fffffa60
00ba3060)
fffffa6000bac11a mov r8,rbx
fffffa60
00bac11d mov rbx,qword ptr [rsp+38h]
; предположительно адрес объектаколлаута
fffffa6000bac122 mov rdx,rdi
fffffa60
00bac125 mov ecx,esi
fffffa6000bac127 call qword ptr [rbx+10h]
; вызов ipsblock!FlNotify
fffffa60
00bac12a test eax,eax

Продолжим наши исследования. Брякаемся на нашей классифицирующей функции (FlClassify). Колстек при вызове:

ipsblock!FlClassify ;-> наша функция
NETIO!ArbitrateAndEnforce+0x3b0
NETIO!KfdClassify+0x8f1
tcpip!WfpAleClassify+0x47
tcpip! ?? ::FNODOBFM::'string'+0x178d3
tcpip!WfpAleAuthorizeConnect+0x2ef
tcpip!TcpCreateAndConnectTcbWorkQueueRoutin e+0x4a2
tcpip!TcpCreateAndConnectTcb+0x48a
tdx!TdxConnectConnection+0x4e6
tdx!TdxTdiDispatchInternalDeviceControl+0x158

Посмотрим код NETIO!ArbitrateAndEnforce (в листинге оставлено только самое важное). Двойной щелчок по строке в окне вызовов переносит нас в недра дизассемблерного кода.

fffffa6000b9e9ef add rbp,rbp
fffffa60
00b9e9f2 lock xadd dword ptr
[r12+rbp*8+80h],eax
fffffa6000b9e9fc cmp qword ptr [r12+80h],r9
fffffa60
00b9ea04 jne NETIO! ??
::FNODOBFM::'string'+0x6719
fffffa6000b9ea0a mov rax,qword ptr [NETIO!gWfpGlobal]
fffffa60
00b9ea11 cmp ebx,dword ptr [rax+970h] ; max count
fffffa6000b9ea17 jae NETIO! ??
::FNODOBFM::'string'+0x6785
fffffa60
00b9ea1d imul rbx,rbx,38h
fffffa6000b9ea21 add rbx,qword ptr [rax+978h]
// add callout base
fffffa60
00b9ea28 cmp dword ptr [rbx],0
// ненулевой callout?
fffffa6000b9ea2b je NETIO! ??
::FNODOBFM::'string'+0x6785
...
fffffa60
00b9eacb lea rax,[rsp+78h]
fffffa6000b9ead0 mov rdx,r15
fffffa60
00b9ead3 mov qword ptr [rsp+28h], rax
fffffa6000b9ead8 mov qword ptr [rsp+20h], r12
fffffa60
00b9eadd call qword ptr [rbx+8]

Попробуем собрать воедино то, что видим в этом и предыдущем листинге. Очевидно, что значительную роль в получении адреса нужного callout играет переменная NETIO!gWfpGlobal — это основа для получения всех остальных нужных данных. Сначала значение в ebx сравнивается с некоторым значением.

Чтобы узнать, что лежит в ebx на момент вызова, я поставил брейк на сравнение по адресу fffffa60`00b9ea11. И как ты думаешь, что я увидел? Помнишь, я упоминал про то, что при регистрации callout нам возвращается его 32-битный идентификатор? Так вот, в момент сравнения как раз он и находится в ebx. Ага, теперь-то ясно, что в dword ptr [rax+970h] — это максимальное число коллаутов в системе (в моей — 0x11e). В ebx у нас индекс коллаута, потом выполняется определение адреса структуры коллаута, размер которой (судя по инструкции imul rbx,rbx,38h) равен 0x38 байт. По адресу qword ptr [rax+978h] находится база всех коллаутов (то есть адрес начала буфера для объекто-коллаутов).

Теперь для вывода инфы об установленных в системе коллаутах можно написать свою функцию. В качестве аргумента она принимает значение переменной gWfpGlobal (узнается адрес командой dq poi(netio!gWfpGlobal) в WinDbg).

typedef struct _FW_CALLOUT_OBJECT
{
ULONG64 uFlag;
ULONG64 uClassifyFunction;
ULONG64 uNotifyFunction;
ULONG64 uFlowDeleteFunction;
//ULONG64 uReserved[3];
}FW_CALLOUT_OBJECT,*PFW_CALLOUT_OBJECT;
#define CALLOUT_OBJECT_SIZE 0x38
....
VOID PrintCallouts6(ULONG64 gWfpGlobal)
{
ULONG uMaxCount;
ULONG64 uCalloutBase;
PFW_CALLOUT_OBJECT pCurrentCallout;
// база коллаутов
uCalloutBase = *(PULONG64)(gWfpGlobal+0x978);
// получаем максимальное число коллаутов
uMaxCount = *(PULONG)(gWfpGlobal + 0x970);
CHAR ModuleName[10]={0};
FLOUT(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "Max id count %dn", uMaxCount);
for(int i=0;i<uMaxCount;i++)
{
pCurrentCallout = (PFW_CALLOUT_OBJECT)
(uCalloutBase + i*CALLOUT_OBJECT_SIZE);
if(pCurrentCallout->uFlag)
{
FLOUT(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "Current callout 0x%I64Xn", pCurrentCallout);
FLOUT(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, " Notify routine 0x%I64Xn Classify routine 0x%I64Xn Flow delete function 0x%I64Xn", pCurrentCallout->uNotifyFunction, pCurrentCallout->uClassifyFunction, pCurrentCallout->uFlowDeleteFunction);
// получаем имя модуля, которому принадлежит нотифицирующая функция
GetModuleName(ModuleName, 8, pCurrentCallout->uClassifyFunction);
FLOUT(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, " Module name = %sn", ModuleName);
RtlZeroMemory(ModuleName,sizeof(ModuleName));
}
}
}

Код достаточно прокомментирован, да и не сложный вовсе. Для удобства чтения кода я объявил структуру FW_CALLOUT_ OBJECT. GetModuleName — это моя функция, ищущая имя модуля по адресу внутри него. Использует пресловутую ZwQuerySystemInformation(… SystemModuleInformation…) и, думаю, большинство читателей так или иначе представляет, как энумить модули таким способом :).

Имея такую важную инфу, как адреса классифицирующих функций, можно делать все, что угодно. Например, перехватить их. Кстати, проверить, что мы все сделали правильно, можно, «не отходя от кассы», прямо тут же, в WinDbg, командой u адрес.

Notify routine 0xFFFFFA6000E113B0
Classify routine 0xFFFFFA6000E35070
Flow delete function 0x0
Module name = tcpip.sys

...
kd> u 0xFFFFFA6000E113B0 ;-> мы действительно имеем дело с нотифицирующей функцией драйвера tcpip.sys?
tcpip!IPSecAleConnectCalloutNotify:
fffffa6000e113b0 33c0 xor eax,eax
kd&gt; u 0xFFFFFA6000E35070 ;-&gt; мы действительно имеем дело с классифицирующей функцией драйвера tcpip.sys?
tcpip!IPSecInboundTransportFilterCalloutClassifyV4:
fffffa60
00e35070 488bc4 mov rax,rsp

Теперь можно с уверенностью сказать, что все верно.

 

От Vista к Windows 7

Что касается Windows 7, которая до сего времени совсем не упоминалась, то здесь все проще (но по-другому!). И вообще, WFP был несколько изменен и дополнен. Там есть внутренняя функция netio!ProcessCallout:

.text:000000000001C680 ProcessCallout proc near
; CODE XREF: ArbitrateAndEnforce+2A457
...
.text:000000000001C71D mov rax, cs:gWfpGlobal
.text:000000000001C724 cmp ebx, [rax+548h]
// max count
.text:000000000001C72A jnb loc_2D270
.text:000000000001C730 mov rdi, rbx
.text:000000000001C733 shl rdi, 6 // callout size
.text:000000000001C737 add rdi, [rax+550h]
// callout base
.text:000000000001C73E cmp [rdi+4], esi
.text:000000000001C741 jz loc_2D270
...
.text:000000000001C7E6 mov r10, [rdi+10h]
.text:000000000001C7EA mov rbx, qword ptr
[rsp+118h+arg_30.LockState]
.text:000000000001C7F2 mov r8, [rsp+118h+arg_18]
.text:000000000001C7FA mov rcx, [rsp+118h+arg_8]
.text:000000000001C802 mov rdx, rbp
.text:000000000001C805 cmp [rdi], esi
.text:000000000001C807 jz loc_2D56F
.text:000000000001C80D mov [rsp+118h+var_E8], rbx
.text:000000000001C812 mov r9, r13
.text:000000000001C815 mov [rsp+118h+var_F0], r14
.text:000000000001C81A mov [rsp+118h+var_F8], rax
.text:000000000001C81F call r10 // Classifyfn

Как видно из листинга выше, gWfpGlobal играет такую же важную роль в функционировании WFP Win7, как и в Vista. Однако структуры теперь 0x40 (shl rdi, 6, что эквивалентно * 2^6), изменилось смещение callout base ([rax+550h]) и других полей. Так что enum случая Win7 я оставляю for fun увлеченному читателю :). А также в netio есть функция GetCalloutEntry, которая подтверждает сказанное выше:

.text:000000000001CE30 GetCalloutEntry proc near
; CODE XREF: FeGetRefCallout+2057
; FeGetCalloutFlowDelete+28 ...
.text:000000000001CE30
.text:000000000001CE30 ; FUNCTION CHUNK AT
.text:0000000000028954 SIZE 0000000B BYTES
.text:000000000001CE30
.text:000000000001CE30 mov rax, cs:gWfpGlobal
.text:000000000001CE37 cmp ecx, [rax+548h]
// max count
.text:000000000001CE3D jnb loc_28954
.text:000000000001CE43 mov rax, [rax+550h]
// callout base
.text:000000000001CE4A mov ecx, ecx
.text:000000000001CE4C shl rcx, 6
// callout object size
.text:000000000001CE50 add rcx, rax
.text:000000000001CE53 mov [rdx], rcx
.text:000000000001CE56 cmp dword ptr [rcx+4], 0
.text:000000000001CE5A jz loc_28954
.text:000000000001CE60 rep retn
.text:000000000001CE60 GetCalloutEntry endp

 

Заключение

Конечно, такой монстр, как WFP, требует гораздо более глубокого исследования. Я рассмотрел только некоторые интересные моменты функционирования WFP, но это далеко не все. Освоиться с программированием WFP тебе поможет WDK, а с его внутренним устройством — стандартный инструментарий реверсера.

 

Links

Документация по Windows Filtering Platform на сайте MS:msdn.microsoft.com/en-us/library/aa366510(VS.85).aspx

Оставить мнение

Check Also

Цифровой паноптикон. Настоящее и будущее тотальной слежки за пользователями

Даже если ты тщательно заботишься о защите своих данных, это не даст тебе желаемой приватн…