Содержание статьи
Механизм 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. Теперь посмотрим, что там:
fffffa60
00bac100 sub rsp,20h
00bac104 mov rbx,rdx
fffffa60
fffffa6000bac107 mov rdi,rcx
00bac10a lea rdx,[rsp+38h]
fffffa60
fffffa6000bac10f mov ecx,dword ptr [rbx+2Ch]
00bac112 mov esi,r8d
fffffa60
fffffa6000bac115 call
00ba3060)
NETIO!FeGetRefCallout (fffffa60
fffffa6000bac11a mov r8,rbx
00bac11d mov rbx,qword ptr [rsp+38h]
fffffa60
; предположительно адрес объектаколлаута
fffffa6000bac122 mov rdx,rdi
00bac125 mov ecx,esi
fffffa60
fffffa6000bac127 call qword ptr [rbx+10h]
00bac12a test eax,eax
; вызов ipsblock!FlNotify
fffffa60
Продолжим наши исследования. Брякаемся на нашей классифицирующей функции (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 (в листинге оставлено только самое важное). Двойной щелчок по строке в окне вызовов переносит нас в недра дизассемблерного кода.
fffffa60
00b9e9ef add rbp,rbp
00b9e9f2 lock xadd dword ptr
fffffa60
[r12+rbp*8+80h],eax
fffffa6000b9e9fc cmp qword ptr [r12+80h],r9
00b9ea04 jne NETIO! ??
fffffa60
::FNODOBFM::'string'+0x6719
fffffa6000b9ea0a mov rax,qword ptr [NETIO!gWfpGlobal]
00b9ea11 cmp ebx,dword ptr [rax+970h] ; max count
fffffa60
fffffa6000b9ea17 jae NETIO! ??
00b9ea1d imul rbx,rbx,38h
::FNODOBFM::'string'+0x6785
fffffa60
fffffa6000b9ea21 add rbx,qword ptr [rax+978h]
00b9ea28 cmp dword ptr [rbx],0
// add callout base
fffffa60
// ненулевой callout?
fffffa6000b9ea2b je NETIO! ??
00b9eacb lea rax,[rsp+78h]
::FNODOBFM::'string'+0x6785
...
fffffa60
fffffa6000b9ead0 mov rdx,r15
00b9ead3 mov qword ptr [rsp+28h], rax
fffffa60
fffffa6000b9ead8 mov qword ptr [rsp+20h], r12
00b9eadd call qword ptr [rbx+8]
fffffa60
Попробуем собрать воедино то, что видим в этом и предыдущем листинге. Очевидно, что значительную роль в получении адреса нужного 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
00e35070 488bc4 mov rax,rsp
kd> u 0xFFFFFA6000E35070 ;-> мы действительно имеем дело с классифицирующей функцией драйвера tcpip.sys?
tcpip!IPSecInboundTransportFilterCalloutClassifyV4:
fffffa60
Теперь можно с уверенностью сказать, что все верно.
От 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