WFP изнутри: Исследуем внутренности Windows Filtering Platform
Механизм 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
fffffa60 00bac104 mov rbx,rdx
fffffa60 00bac107 mov rdi,rcx
fffffa60 00bac10a lea rdx,[rsp+38h]
fffffa60 00bac10f mov ecx,dword ptr [rbx+2Ch]
fffffa60 00bac112 mov esi,r8d
fffffa60 00bac115 call
NETIO!FeGetRefCallout (fffffa60 00ba3060)
fffffa60 00bac11a mov r8,rbx
fffffa60 00bac11d mov rbx,qword ptr [rsp+38h]
; предположительно адрес объектаколлаута
fffffa60 00bac122 mov rdx,rdi
fffffa60 00bac125 mov ecx,esi
fffffa60 00bac127 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 (в листинге оставлено только самое важное). Двойной щелчок по строке в окне вызовов переносит нас в недра дизассемблерного кода.
fffffa60 00b9e9ef add rbp,rbp
fffffa60 00b9e9f2 lock xadd dword ptr
[r12+rbp*8+80h],eax
fffffa60 00b9e9fc cmp qword ptr [r12+80h],r9
fffffa60 00b9ea04 jne NETIO! ??
::FNODOBFM::’string’+0x6719
fffffa60 00b9ea0a mov rax,qword ptr [NETIO!gWfpGlobal]
fffffa60 00b9ea11 cmp ebx,dword ptr [rax+970h] ; max count
fffffa60 00b9ea17 jae NETIO! ??
::FNODOBFM::’string’+0x6785
fffffa60 00b9ea1d imul rbx,rbx,38h
fffffa60 00b9ea21 add rbx,qword ptr [rax+978h]
// add callout base
fffffa60 00b9ea28 cmp dword ptr [rbx],0
// ненулевой callout?
fffffa60 00b9ea2b je NETIO! ??
::FNODOBFM::’string’+0x6785
.
fffffa60 00b9eacb lea rax,[rsp+78h]
fffffa60 00b9ead0 mov rdx,r15
fffffa60 00b9ead3 mov qword ptr [rsp+28h], rax
fffffa60 00b9ead8 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:
fffffa60 00e113b0 33c0 xor eax,eax
kd> u 0xFFFFFA6000E35070 ;-> мы действительно имеем дело с классифицирующей функцией драйвера 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, а с его внутренним устройством — стандартный инструментарий реверсера.
ВСЕМИРНАЯ ПРОДОВОЛЬСТВЕННАЯ ПРОГРАММА
ВСЕМИ́РНАЯ ПРОДОВО́ЛЬСТВЕННАЯ ПРОГРА́ММА (ВПП; англ. World Food Programme, WFP), международная организация, специализированное учреждение ООН в сфере продовольствия. Создана по инициативе Экономического и социального совета ООН (ЭКОСОС) и Продовольственной и сельскохозяйственной организации ООН (ФАО) 24.11.1961 как временная (действует с 1963, на постоянной основе с 20.12.1965) организация по борьбе с голодом и недоеданием в мире. ВПП обеспечивает продовольственную помощь населению государств, пострадавших от стихийных бедствий, вооружённых конфликтов, испытывающих хронический дефицит продовольствия. Особое внимание уделяется обеспечению продовольствием беременных женщин, кормящих матерей и детей. Впервые обеспечила продовольствием Иран, пострадавший от землетрясения в сентябре 1962. В 1994–2008 ВПП осуществляла поддержку населения Северного Кавказа, пострадавшего в результате 1-й и 2-й Чеченских кампаний. С 1997 Россия неоднократно входила в состав Исполнительного совета (в 2009 председатель), с 2003 спонсирует поставки продовольствия в страны СНГ, Африки, Сирию, с 2014 – в непризнанные Донецкую и Луганскую народные республики. ВПП объединяет все государства – члены ООН. Высший орган – Исполнительный совет (36 чел.; избирается ЭКОСОС и ФАО 1 раз в 3 года; собирается 3–4 раза в год), координирует деятельность 5 региональных групп (Африки, Азии и Тихоокеанского региона, Восточной и Центральной Европы, Западной Европы, Латинской Америки и Карибского бассейна). Секретариат и штаб-квартира – в г. Рим (Италия).
Миссия
Помогая 97 миллионам человек — наибольшее количество с 2012 года — в 88 странах мира, Всемирная продовольственная программа Организации Объединенных Наций (ВПП ООН) является ведущей гуманитарной организацией, борющейся с голодом во всем мире. С этой целью она доставляет продовольственную помощь в чрезвычайных ситуациях и работает с сообществами для улучшения питания и повышения устойчивости.
В то время как международное сообщество взяло на себя обязательства положить конец голоду, достичь продовольственной безопасности и улучшить питание к 2030 году, каждый девятый человек в мире все еще недоедает. Продовольственная помощь лежит в основе борьбы, которая ведется с целью разорвать порочный круг голода и бедности.
Ежедневно 5000 грузовиков, 20 океанских грузовых судов и 92 самолета находятся в пути, доставляя продовольствие и другую помощь ВПП ООН тем, кто больше всего в ней нуждается. Ежегодно мы распределяем приблизительно 12,6 миллиардов продовольственных рационов со средней стоимостью в 0,31 доллара США за рацион. Эти показатели лежат в основе беспрецедентной репутации ВПП ООН как организации быстрого реагирования на чрезвычайные ситуации, которая быстро и эффективно выполняет масштабную работу в самых сложных условиях.
Усилия ВПП ООН сосредоточены на оказании чрезвычайной помощи, содействии в восстановлении, проектах развития и проведении специальных операций. Две трети нашей работы проводится в зонах конфликта, где люди в три раза чаще страдают от недоедания, чем жители стран без конфликтов.
В чрезвычайных ситуациях представители ВПП ООН часто первыми прибывают на место и предоставляют продовольственную помощь жертвам войны, гражданских конфликтов, засухи, наводнений, землетрясений, ураганов, неурожаев и стихийных бедствий. По мере завершения чрезвычайной ситуации ВПП ООН помогает сообществам восстанавливать разрушенные жизни и источники средств к существованию. Мы также работаем над повышением устойчивости сообществ, подверженных длительным кризисам, учитывая аспекты развития в программах гуманитарной помощи.
В проектах развития ВПП ООН основное внимание уделяется питанию, особенно питанию матерей и детей, и решению проблемы недоедания с самого раннего возраста с помощью программ, ориентированных на первые 1000 дней — от зачатия до второго дня рождения ребенка, а затем с помощью программ школьного питания.
ВПП ООН — это крупнейшая гуманитарная организация, которая реализует программы школьного питания по всему миру на протяжении более 50 лет. Каждый год ВПП ООН предоставляет школьное питание 17,5 миллионам детей в 59 странах, часто в самых труднодоступных районах.
В 2019 году ВПП ООН предоставила более 4,2 миллионов метрических тонн продовольствия и 2,1 миллиарда долларов США наличными и ваучерами. По крайней мере три четверти этого продовольствия поступает из развивающихся стран. Приобретая продовольствие как можно ближе к месту потребности в нем, мы экономим время и сокращаем транспортные расходы, в то же время поддерживая местную экономику. Все чаще ВПП ООН удовлетворяет потребности людей в продовольствии при помощи денежных выплат. Они позволяют людям, которым мы служим, выбирать и приобретать необходимое продовольствие на месте.
ВПП ООН также оказывает различные услуги всему гуманитарному сообществу, в том числе осуществляет пассажирские воздушные перевозки с помощью Службы гуманитарных воздушных перевозок ООН, выполняющей полеты в более чем 250 пунктов назначения по всему миру.
ВПП ООН финансируется исключительно за счет добровольных пожертвований. В 2019 году организация привлекла рекордные 8 миллиарда долларов США. По всему миру в ВПП ООН работает более 14 000 сотрудников, из которых более 90 процентов находятся в странах, где агентство оказывает помощь.
Управление ВПП ООН осуществляет Исполнительный совет из 36 членов. ВПП ООН тесно сотрудничает с двумя родственными организациями, находящимися в Риме, а именно Продовольственной и сельскохозяйственной организацией Объединенных Наций и Международным фондом сельскохозяйственного развития. ВПП ООН поддерживает партнерские отношения с более чем 1000 национальными и международными НПО для оказания продовольственной помощи и устранения коренных причин голода.