Часть 2, Архитектура.
Введение.
Рассмотрим типичный жизненный цикл обмена информацией по протоколу TCP.
1. Приложение (клиент) вызывает функцию connect, указывая адрес и порт пункта
назначения, а также семейство и тип протокола. Или же (сервер) оно вызывает accept в
ожидании подключений.
2. Через некоторое время соединение установлено. Клиенту возвращается управление из
функции connect (ну или приходит соответствующий сигнал, как в случае с моделью
select или асинхронным I/O — это уже за рамками темы), серверу возвращается
новый сокет, связанный с клиентом.
3. Приложения обмениваются данными посредством функций send и recv.
4. Приложения закрывают свои концы соединений функцией shutdown.
Когда оба конца закрыты, связь завершается, соединение считается разомкнутым.
Представим, что мы пишем фильтр TCP-трафика, и нас на каждом шаге этой
последовательности интересуют определенные данные:
1. Адрес и порт пункта назначения, ID процесса, который выполнил connect/accept.
Имя и вообще контекст безопасности пользователя, которому принадлежит процесс.
2. Статус операции connect (успех или ошибка).
3. Буферы, передаваемые через send и recv.
Предположим, мы хотим блокировать нежелательные соединения (по набору запрещенных
IP-адресов и портов, а также по процессам или пользователям). Мы также хотим получать
уведомление о закрытии соединения (п.4), чтобы освободить все связанные с ним данные.
WFP для решения этой задачи предлагает несколько абстракций:
Layers — уровни фильтрации. Каждым уровнем обслуживается определенный этап обработки
соединения или передачи данных. Например, установка соединения обрабатывается на
уровне ALE (будет рассмотрен ниже), а работа с буферами send/recv — на уровне Stream.
У каждого уровня своя специфика и свой набор свойств, с которыми можно работать.
Часто ту информацию, что можно достать на одном уровне, уже нельзя увидеть на других.
Аналогично, не все функции, которые работают на одних уровнях, работают на других.
Sublayers — подуровни. Нужны для организации более гибкой стратегии фильтрации.
Например, можно зарегистрировать проверки на разных Sublayers и они будут выполняться
по очереди.
Filters — фильтры. Задают разные условия, при которых должна (или не должна) выполняться
фильтрация. Например, можно фильтровать только трафик, идущий на порт 80, а остальной
игнорировать. Фильтры определяют не только условия, но и действия, т.е. что делать с трафиком,
когда условие срабатывает. Основные операции: разрешить, заблокировать, задержать, либо
вызвать соответствующий Callout. Про Callout-ы будет написано ниже.
Conditions — условия, заданные в фильтре. В фильтре может быть несколько условий.
Shims — исполнительные объекты, расположенные во всех ключевых точках сетевого стека.
Если Filters и Conditions только задают правила, что делать с трафиком, то Shims отвечают за
само выполнение этих правил, т.е. блокировка, задержка трафика, вызов Callout-а и так далее.
Callout — блок функций, которые обрабатывают трафик и решают, что конкретно с ним делать.
Callout Driver — драйвер, реализующий один или несколько Callout-ов.
Provider — поставщик. Объединяет логически связанные Filters, Sublayers и Callouts,
принадлежащие одной программе, одной задаче/политике, или объединенные какими-то
другими общими признаками.
Filter Engine (Engine) — функциональное ядро WFP, которое управляет фильтрами, Shim-ами,
Callout-ами и остальными объектами технологии.
Base Filter Engine (BFE) — служба, отвечающая за координацию компонентов WFP, за
регистрацию новых фильтров, за хранение конфигурации, настройки безопасности и т.д.
Ссылки по теме:
WFP Architecture
http://msdn.microsoft.com/en-u… 85%29.aspx
Object Model
http://msdn.microsoft.com/en-u… 85%29.aspx
Как это работает.
Когда в сетевом стеке происходит какое-то событие, например установка или закрытие
соединения, приход дейтаграммы, получение буфера с данными и т.п., Filter Engine с
помощью Filters определяет, нужно ли как-то обрабатывать это событие. В терминах WFP
данный процесс называется классификацией (classify). Если хотя бы один Condition
срабатывает, вызывается Shim, который, в зависимости от того, что задано в фильтре,
выполняет нужное действие. В частности, Shim может вызвать ваш зарегистрированный
Callout, точнее, одну из его функций (их всего три).
Все остальное выполняет Callout, в соответствии с логикой фильтрации трафика.
Если, к примеру, Callout работает на уровне ALE, то в функцию ему будут переданы
адрес и порт пункта назначения и источника, тип протокола, ID приложения, полный
путь к exe и другая полезная информация. Если на уровне Stream, там будут другие
данные — направление трафика (входящий/исходящий), флаги и т.д. Далее Callout
может поступать с данными на свое усмотрение: блокировка, игнор, редирект, анализ,
задержка трафика и многое другое. Все ограничено лишь фантазией разработчика.
Выше была описана типичная последовательность операций при работе с TCP.
Давайте посмотрим, как она будет фильтроваться WFP.
1. Установка соединения (connect).
Здесь будут срабатывать фильтры на уровне FWPS_LAYER_ALE_AUTH_CONNECT_V4.
Здесь же можно заблокировать соединение.
2. Соединение установлено.
Сработают фильтры на уровне FWPS_LAYER_ALE_FLOW_ESTABLISHED_V4.
Добавлю, что на этом уровне блокировать коннект не рекомендуется, он создан
только для того, чтобы Callout получил уведомление о том, успешно или нет
создано соединение. Здесь же можно установить связь между коннектом и
потоком данных.
3. Передача данных.
Будут срабатывать фильтры на уровне FWPS_LAYER_STREAM_V4, причем как
для send (outgoing data), так и для recv (incoming data).
4. Закрытие соединения.
Если с потоком данных был ассоциирован контекст, будет вызвана одна из
функций Callout-а. Также на Windows 7 есть специальные уровни для
очистки ресурсов — FWPS_LAYER_ALE_RESOURCE_RELEASE_V4.
Еще определения.
Flow (поток данных). Каждое соединение — это отдельный двунаправленный поток данных.
Если, к примеру, программа создает два коннекта на один и тот же адрес, вы будете в
драйвере видеть два разных потока данных, каждый со своим состоянием.
Flow Context (контекст потока). 64-битное число, ассоциированное с определенным
потоком данных. Нужно, чтобы отличать потоки друг от друга.
Fixed Values. Фиксированный набор аргументов, приходящий в Callout.
Описан здесь:
Data Field Identifiers
http://msdn.microsoft.com/en-u… 85%29.aspx
Для каждого Layer-а свой набор аргументов.
Например, на уровне FWPS_LAYER_ALE_CONNECT_REDIRECT_V4 (установка соединения) через
Fixed Values можно получить имя пользователя, а на уровне FWPS_LAYER_STREAM_V4
его уже нет.
Meta Values. Аналогично Fixed Values.
Информация здесь:
Metadata Fields at Each Filtering Layer
http://msdn.microsoft.com/en-u… 85%29.aspx
Итак, Callout, заглядывая в Fixed Values и Meta Values, решает, что делать с
данным событием/трафиком и предпринимает определенные действия, после чего весь
цикл повторяется заново. Доступ к буферам с данными осуществляется через
структуру NET_BUFFER (будет описана в следующих частях).
13
Provide feedback
Saved searches
Use saved searches to filter your results more quickly
Sign up
Appearance settings
Introduction
Anyone working in security or networking will stumble upon the Windows Filtering Platform (WFP) at some point in their career. The WFP is utilized by a whole host of security apparatuses (the Windows firewall, Windows services, applications, and more), which each create their own customized network rules. This is all well and good, until something breaks. Then, debugging the WFP is no walk in the park.
This post will help you make sense of the WFP by exploring it hands on. To accomplish this, we will use a new open-source tool we released called WTF-WFP. WTF-WFP enables users to understand complex WFP issues in production environments via a simple command line interface.
After running WTF-WFP, the reader will finally understand WTF (’What The Filter’) is going on with WFP.
Windows Filtering Platform Basics
Before we provide a quick primer on the WFP, it is important to note that this is not a comprehensive analysis of the WFP. For more details on WFP, we suggest starting with James Forshaw’s excellent WFP architecture primer in this post about AppContainers, or dive into the official Microsoft documentation.
To put it simply, the Windows Filtering Platform is the underlying mechanism that enables various components to block, permit, audit and perform more complex operations on network traffic (such as deep packet inspection). The Windows Defender Firewall with Advanced Security—which is the built in firewall for modern Windows OS—is probably the most obvious example of an application that utilizes the WFP to enforce network restrictions. Additional applications, 3rd party firewalls, and various services can also interact with the WFP. Changes to the WFP are mediated between the user mode and kernel via the Base Filtering Engine service.
From WFP Architecture Overview
The most important elements of WFP to understand are the following:
- Filter: A filter is made up of conditions (IP address, port, application, protocol, user, etc.) and a decision: permit, block or callout (which also breaks down to terminating / unknown / inspection).
- Sublayer: A sublayer is a way to group filters together so their arbitration in a single sublayer can be predicted. For example, firewall rules all belong to the same sublayer, and Windows Service Hardening rules are in a separate sublayer.
- Layer: Most layers correlate to an event in the networking stack, such as listening on a socket, or various stages in the TCP handshake. There are usually two matching layers of the same network: one for IPv4 and one for IPv6.
Now that we have the basics covered, we can start exploring the WFP. But before we do, it will be helpful to visualize just how the WFP layers relate to actual TCP and UDP connections. Here’s a diagram that shows how different network operations translate to WFP layers.
Various Layers involved in a TCP connection
Various Layers involved in a UDP connection
Understanding these layers is helpful when debugging various WFP issues. The ALE layers (any layer with ALE in its name) are stateful, while the others are stateless. So an ALE layer already indicates at which state of the connection the filters apply.
Exploring with WTF-WFP
Now that we got those pesky definitions out of the way, we can really start exploring the WFP ourselves! We will start by installing the WTF-WFP. WTF-WFP depends on NtObjectManager to interact with the WFP API (thanks to James Farshow for the hard work he put there). Both are PowerShell modules that can be installed from the PowerShell Gallery. Note: MS Defender may alert on NtObjectManager, so you may need to turn off real time protection / set the PS Gallery as trusted / create exclusion for the module.
From an elevated PS:
Install-Module NtObjectManager
Install-Module wtf-wfp
Once done, you should have the Get-WFPInfo command available. To get more info about this command, please refer to the readme and consult the help:
Get-Help Get-WFPInfo
Now that installation is done, we can start by looking at the WFP status. Let’s start with some sheer numbers. We will run the Get-FwFilter command (from NtObjectManager) to see just how many filters we have on our host. In the next example, we ran the command twice: first when the firewall was on, and then when it was turned off:
As we can see, there are A LOT of filters. Even when the firewall is off, we still have 204(!) filters – showing us that the firewall is indeed not the only thing using the WFP.
Now, let’s start by trying to understand some of the filters (at this stage, the firewall is still turned off). We are interested in filters that are relevant for inbound traffic for our local IP address. Get-WFPInfo uses the connection direction and IP address to show only filters in layers that are relevant to the traffic direction, and with conditions that match the IP protocol we’re interested in.
Right off the bat, we understand that most filters are either in the FWPM_LAYER_ALE_AUTH_CONNECT or FWPM_LAYER_ALE_AUTH_ACCEPT layers. This shouldn’t be surprising, as it makes most sense to have stateful filters, right before an outbound connection is made, or right when an inbound connection should be accepted (or not).
Another thing to note are the sublayers. The majority of filters are in the MICROSOFT_DEFENDER_SUBLAYER_WSH sublayer , but there are a couple more for the firewall sublayer (which seems odd, as we turned the firewall off). These are actually two filters that do the exact same thing, which is to allow AppContainers with the same SID to communicate with one another.
Now, let’s turn on the firewall again, and rerun our Get-WFPInfo command. The filters that match our condition will be in under the WFP Relevant Filters section. This time, the output is much more verbose – so we will only add snippets here:
First thing that is important to understand, is that WTF-WFP shows the filters ordered by weight. This is the actual filter processing order (or arbitration if we use MS terminology) that takes place. Arbitration is a bit confusing at first, but looking at the output of Get-WFPInfo orders makes the arbitration easier to understand:
- Each sublayer is considered from top to bottom for the relevant layer matching the network traffic.
- If there is a match with the filter conditions, the processing for this sublayer is done, and WFP moves on to the next sublayer.
- Finally, the WFP takes the results from all the sublayers and decides whether to permit or block the connection. Usually a block overrides a permit (apart from more complex conditions of override policy, which we will not go into).
TIP: The filter conditions are shown in the strConditions part. Due to the abundance of information, it is recommended to output the command result into a csv, which then can be inspected more thoroughly. To do this use the -csvPath parameter.
Knowing how arbitration works, we can notice the following:
- At the top, the initial filters are the quarantine filters, which take precedence over all other filters. These are the filters that block network traffic while the network interface is undergoing changes which could affect the network profile (such as switching WiFi networks).
- In the middle, there are the Windows service hardening rules, which are there whether the firewall is on or off.
- Lastly, the firewall filters themselves are considered.
One of the big takeaways from this should be that there is a lot of filtering happening “outside” the firewall filters. Which could cause network traffic to be blocked or permitted, even if there is no firewall rule that matches this traffic, or even in-spite of a firewall rule that does the opposite. For example, if network traffic resulted in a block because of quarantine or WSH filters, it will override permitted traffic in the firewall.
Tracing Traffic
Even though we now understand layers, sublayers, filters and how the WFP processes them against a given network traffic, it could still be challenging to understand why specific traffic is getting blocked or allowed. Using the “old” ways, one would have to enable windows security events or other WFP traces, and export complex filter configurations while trying to understand why a certain connection is getting blocked. Luckily, Get-WFPInfo also has a tracing capability, that will show us the WFP decisions that match specific network traffic, along with the matching filters.
Let’s assume we want to understand why port 5375 is being blocked. Again, we will use the Get-WFPInfo command with the netTrace param:
Now we see the network events that were captured, and which layers and sublayers were responsible for dropping these packets. Finally, the details of each filter shown in the trace are printed for easier analysis. In this case, the “Query User” filter hit because there is no other more specific filter that allows this connection in the firewall sublayer. So, any network traffic that originated from a domain network is blocked.
Summary
WTF-WFP is obviously not the only method to explore WFP. There are additional tools that can help you explore the WFP configuration:
- The NtObjectManager exposes many WFP APIs, so you can do more than just explor WFP via PowerShell, you can even create filters or build your own analysis modules (like WTF-WFP).
- WFPExplorer is another excellent tool, that displays filters in a GUI.
However, even with such tools, debugging the WFP is no easy task. WTF-WFP is the first attempt at giving users the ability to quickly understand WFP issues, without familiarizing themselves with all of the WFP details, and without the need to familiarize themselves with the WFP API.
As with all our open-source tools, we are eager to hear feedback from the community. Please don’t hesitate to reach out via Zero Labs Slack for any suggestions or issues.
As part of the second edition of Windows Kernel Programming, I’m working on chapter 13 to describe the basics of the Windows Filtering Platform (WFP). The chapter will focus mostly on kernel-mode WFP Callout drivers (it is a kernel programming book after all), but I am also providing a brief introduction to WFP and its user-mode API.
This introduction (with some simplifications) is what this post is about. Enjoy!
The Windows Filtering Platform (WFP) provides flexible ways to control network filtering. It exposes user-mode and kernel-mode APIs, that interact with several layers of the networking stack. Some configuration and control is available directly from user-mode, without requiring any kernel-mode code (although it does require administrator-level access). WFP replaces older network filtering technologies, such as Transport Driver Interface (TDI) filters some types of NDIS filters.
If examining network packets (and even modification) is required, a kernel-mode Callout driver can be written, which is what we’ll be concerned with in this chapter. We’ll begin with an overview of the main pieces of WFP, look at some user-mode code examples for configuring filters before diving into building simple Callout drivers that allows fine-grained control over network packets.
WFP is comprised of user-mode and kernel-mode components. A very high-level architecture is shown here:
In user-mode, the WFP manager is the Base Filtering Engine (BFE), which is a service implemented by bfe.dll and hosted in a standard svchost.exe instance. It implements the WFP user-mode API, essentially managing the platform, talking to its kernel counterpart when needed. We’ll examine some of these APIs in the next section.
User-mode applications, services and other components can utilize this user-mode management API to examine WFP objects state, and make changes, such as adding or deleting filters. A classic example of such “user” is the Windows Firewall, which is normally controllable by leveraging the Microsoft Management Console (MMC) that is provided for this purpose, but using these APIs from other applications is just as effective.
The kernel-mode filter engine exposes various logical layers, where filters (and callouts) can be attached. Layers represent locations in the network processing of one or more packets. The TCP/IP driver makes calls to the WFP kernel engine so that it can decide which filters (if any) should be “invoked”.
For filters, this means checking the conditions set by the filter against the current request. If the conditions are satisfied, the filter’s action is applied. Common actions include blocking a request from being further processed, allowing the request to continue without further processing in this layer, continuing to the next filter in this layer (if any), and invoking a callout driver. Callouts can perform any kind of processing, such as examining and even modifying packet data.
The relationship between layers, filters, and callouts is shown here:
As you can see the diagram, each layer can have zero or more filters, and zero or more callouts. The number and meaning of the layers is fixed and provided out of the box by Windows. On most system, there are about 100 layers. Many of the layers are sets of pairs, where one is for IPv4 and the other (identical in purpose) is for IPv6.
The WFP Explorer tool I created provides some insight into what makes up WFP. Running the tool and selecting View/Layers from the menu (or clicking the Layers tool bar button) shows a view of all existing layers.
You can download the WFP Explorer tool from its Github repository
(https://github.com/zodiacon/WFPExplorer) or the AllTools repository
(https://github.com/zodiacon/AllTools).
Each layer is uniquely identified by a GUID. Its Layer ID is used internally by the kernel engine as an identifier rather than the GUID, as it’s smaller and so is faster (layer IDs are 16-bit only). Most layers have fields that can be used by filters to set conditions for invoking their actions. Double-clicking a layer shows its properties. The next figure shows the general properties of an example layer. Notice it has 382 filters and 2 callouts attached to it.
Clicking the Fields tab shows the fields available in this layer, that can be used by filters to set conditions.
The meaning of the various layers, and the meaning of the fields for the layers are all documented in the official WFP documentation.
The currently existing filters can be viewed in WFP Explorer by selecting Filters from the View menu. Layers cannot be added or removed, but filters can. Management code (user or kernel) can add and/or remove filters dynamically while the system is running. You can see that on the system the tool is running on there are currently 2978 filters.
Each filter is uniquely identified by a GUID, and just like layers has a “shorter” id (64-bit) that is used by the kernel engine to more quickly compare filter IDs when needed. Since multiple filters can be assigned to the same layer, some kind of ordering must be used when assessing filters. This is where the filter’s weight comes into play. A weight is a 64-bit value that is used to sort filters by priority. As you can see in figure 13-7, there are two weight properties – weight and effective weight. Weight is what is specified when adding the filter, but effective weight is the actual one used. There are three possible values to set for weight:
- A value between 0 and 15 is interpreted by WFP as a weight index, which simply means that the effective weight is going to start with 4 bits having the specified weight value and generate the other 60 bit. For example, if the weight is set to 5, then the effective weight is going to be between
0x5000000000000000
and0x5FFFFFFFFFFFFFFF
. - An empty value tells WFP to generate an effective weight somewhere in the 64-bit range.
- A value above 15 is taken as is to become the effective weight.
What is an “empty” value? The weight is not really a number, but a FWP_VALUE
type can hold all sorts of values, including holding no value at all (empty).
Double-clicking a filter in WFP Explorer shows its general properties:
The Conditions tab shows the conditions this filter is configured with. When all the conditions are met, the action of the filter is going to fire.
The list of fields used by a filter must be a subset of the fields exposed by the layer this filter is attached to. There are six conditions shown in figure 13-9 out of the possible 39 fields supported by this layer (“ALE Receive/Accept v4 Layer”). As you can see, there is a lot of flexibility in specifying conditions for fields – this is evident in the matching enumeration, FWPM_MATCH_TYPE
:
typedef enum FWP_MATCH_TYPE_ { FWP_MATCH_EQUAL = 0, FWP_MATCH_GREATER, FWP_MATCH_LESS, FWP_MATCH_GREATER_OR_EQUAL, FWP_MATCH_LESS_OR_EQUAL, FWP_MATCH_RANGE, FWP_MATCH_FLAGS_ALL_SET, FWP_MATCH_FLAGS_ANY_SET, FWP_MATCH_FLAGS_NONE_SET, FWP_MATCH_EQUAL_CASE_INSENSITIVE, FWP_MATCH_NOT_EQUAL, FWP_MATCH_PREFIX, FWP_MATCH_NOT_PREFIX, FWP_MATCH_TYPE_MAX } FWP_MATCH_TYPE;
The WFP API exposes its functionality for user-mode and kernel-mode callers. The header files used are different, to cater for differences in API expectations between user-mode and kernel-mode, but APIs in general are identical. For example, kernel APIs return NTSTATUS
, whereas user-mode APIs return a simple LONG
, that is the error value that is returned normally from GetLastError
. Some APIs are provided for kernel-mode only, as they don’t make sense for user mode.
W> The user-mode WFP APIs never set the last error, and always return the error value directly. Zero (ERROR_SUCCESS
) means success, while other (positive) values mean failure. Do not call GetLastError
when using WFP – just look at the returned value.
WFP functions and structures use a versioning scheme, where function and structure names end with a digit, indicating version. For example, FWPM_LAYER0
is the first version of a structure describing a layer. At the time of writing, this was the only structure for describing a layer. As a counter example, there are several versions of the function beginning with FwpmNetEventEnum
: FwpmNetEventEnum0
(for Vista+), FwpmNetEventEnum1
(Windows 7+), FwpmNetEventEnum2
(Windows 8+), FwpmNetEventEnum3
(Windows 10+), FwpmNetEventEnum4
(Windows 10 RS4+), and FwpmNetEventEnum5
(Windows 10 RS5+). This is an extreme example, but there are others with less “versions”. You can use any version that matches the target platform. To make it easier to work with these APIs and structures, a macro is defined with the base name that is expanded to the maximum supported version based on the target compilation platform. Here is part of the declarations for the macro FwpmNetEventEnum
:
DWORD FwpmNetEventEnum0( _In_ HANDLE engineHandle, _In_ HANDLE enumHandle, _In_ UINT32 numEntriesRequested, _Outptr_result_buffer_(*numEntriesReturned) FWPM_NET_EVENT0*** entries, _Out_ UINT32* numEntriesReturned); #if (NTDDI_VERSION >= NTDDI_WIN7) DWORD FwpmNetEventEnum1( _In_ HANDLE engineHandle, _In_ HANDLE enumHandle, _In_ UINT32 numEntriesRequested, _Outptr_result_buffer_(*numEntriesReturned) FWPM_NET_EVENT1*** entries, _Out_ UINT32* numEntriesReturned); #endif // (NTDDI_VERSION >= NTDDI_WIN7) #if (NTDDI_VERSION >= NTDDI_WIN8) DWORD FwpmNetEventEnum2( _In_ HANDLE engineHandle, _In_ HANDLE enumHandle, _In_ UINT32 numEntriesRequested, _Outptr_result_buffer_(*numEntriesReturned) FWPM_NET_EVENT2*** entries, _Out_ UINT32* numEntriesReturned); #endif // (NTDDI_VERSION >= NTDDI_WIN8)
You can see that the differences in the functions relate to the structures returned as part of these APIs (FWPM_NET_EVENTx
). It’s recommended you use the macros, and only turn to specific versions if there is a compelling reason to do so.
The WFP APIs adhere to strict naming conventions that make it easier to use. All management functions start with Fwpm
(Filtering Windows Platform Management), and all management structures start with FWPM
. The function names themselves use the pattern <prefix><object type><operation>, such as FwpmFilterAdd
and FwpmLayerGetByKey
.
It’s curious that the prefixes used for functions, structures, and enums start with FWP
rather than the (perhaps) expected WFP
. I couldn’t find a compelling reason for this.
WFP header files start with fwp
and end with u
for user-mode or k
for kernel-mode. For example, fwpmu.h
holds the management functions for user-mode callers, whereas fwpmk.h
is the header for kernel callers. Two common files, fwptypes.h
and fwpmtypes.h
are used by both user-mode and kernel-mode headers. They are included by the “main” header files.
User-Mode Examples
Before making any calls to specific APIs, a handle to the WFP engine must be opened with FwpmEngineOpen
:
DWORD FwpmEngineOpen0( _In_opt_ const wchar_t* serverName, // must be NULL _In_ UINT32 authnService, // RPC_C_AUTHN_DEFAULT _In_opt_ SEC_WINNT_AUTH_IDENTITY_W* authIdentity, _In_opt_ const FWPM_SESSION0* session, _Out_ HANDLE* engineHandle);
Most of the arguments have good defaults when NULL
is specified. The returned handle must be used with subsequent APIs. Once it’s no longer needed, it must be closed:
DWORD FwpmEngineClose0(_Inout_ HANDLE engineHandle);
Enumerating Objects
What can we do with an engine handle? One thing provided with the management API is enumeration. These are the APIs used by WFP Explorer to enumerate layers, filters, sessions, and other object types in WFP. The following example displays some details for all the filters in the system (error handling omitted for brevity, the project wfpfilters has the full source code):
#include <Windows.h> #include <fwpmu.h> #include <stdio.h> #include <string> #pragma comment(lib, "Fwpuclnt") std::wstring GuidToString(GUID const& guid) { WCHAR sguid[64]; return ::StringFromGUID2(guid, sguid, _countof(sguid)) ? sguid : L""; } const char* ActionToString(FWPM_ACTION const& action) { switch (action.type) { case FWP_ACTION_BLOCK: return "Block"; case FWP_ACTION_PERMIT: return "Permit"; case FWP_ACTION_CALLOUT_TERMINATING: return "Callout Terminating"; case FWP_ACTION_CALLOUT_INSPECTION: return "Callout Inspection"; case FWP_ACTION_CALLOUT_UNKNOWN: return "Callout Unknown"; case FWP_ACTION_CONTINUE: return "Continue"; case FWP_ACTION_NONE: return "None"; case FWP_ACTION_NONE_NO_MATCH: return "None (No Match)"; } return ""; } int main() { // // open a handle to the WFP engine // HANDLE hEngine; FwpmEngineOpen(nullptr, RPC_C_AUTHN_DEFAULT, nullptr, nullptr, &hEngine); // // create an enumeration handle // HANDLE hEnum; FwpmFilterCreateEnumHandle(hEngine, nullptr, &hEnum); UINT32 count; FWPM_FILTER** filters; // // enumerate filters // FwpmFilterEnum(hEngine, hEnum, 8192, // maximum entries, &filters, // returned result &count); // how many actually returned for (UINT32 i = 0; i < count; i++) { auto f = filters[i]; printf("%ws Name: %-40ws Id: 0x%016llX Conditions: %2u Action: %s\n", GuidToString(f->filterKey).c_str(), f->displayData.name, f->filterId, f->numFilterConditions, ActionToString(f->action)); } // // free memory allocated by FwpmFilterEnum // FwpmFreeMemory((void**)&filters); // // close enumeration handle // FwpmFilterDestroyEnumHandle(hEngine, hEnum); // // close engine handle // FwpmEngineClose(hEngine); return 0; }
The enumeration pattern repeat itself with all other WFP object types (layers, callouts, sessions, etc.).
Adding Filters
Let’s see if we can add a filter to perform some useful function. Suppose we want to prevent network access from some process. We can add a filter at an appropriate layer to make it happen. Adding a filter is a matter of calling FwpmFilterAdd
:
DWORD FwpmFilterAdd0( _In_ HANDLE engineHandle, _In_ const FWPM_FILTER0* filter, _In_opt_ PSECURITY_DESCRIPTOR sd, _Out_opt_ UINT64* id);
The main work is to fill a FWPM_FILTER
structure defined like so:
typedef struct FWPM_FILTER0_ { GUID filterKey; FWPM_DISPLAY_DATA0 displayData; UINT32 flags; /* [unique] */ GUID *providerKey; FWP_BYTE_BLOB providerData; GUID layerKey; GUID subLayerKey; FWP_VALUE0 weight; UINT32 numFilterConditions; /* [unique][size_is] */ FWPM_FILTER_CONDITION0 *filterCondition; FWPM_ACTION0 action; /* [switch_is] */ /* [switch_type] */ union { /* [case()] */ UINT64 rawContext; /* [case()] */ GUID providerContextKey; } ; /* [unique] */ GUID *reserved; UINT64 filterId; FWP_VALUE0 effectiveWeight; } FWPM_FILTER0;
The weird-looking comments are generated by the Microsoft Interface Definition Language (MIDL) compiler when generating the header file from an IDL file. Although IDL is most commonly used by Component Object Model (COM) to define interfaces and types, WFP uses IDL to define its APIs, even though no COM interfaces are used; just plain C functions. The original IDL files are provided with the SDK, and they are worth checking out, since they may contain developer comments that are not “transferred” to the resulting header files.
Some members in FWPM_FILTER
are necessary – layerKey
to indicate the layer to attach this filter, any conditions needed to trigger the filter (numFilterConditions
and the filterCondition
array), and the action to take if the filter is triggered (action
field).
Let’s create some code that prevents the Windows Calculator from accessing the network. You may be wondering why would calculator require network access? No, it’s not contacting Google to ask for the result of 2+2. It’s using the Internet for accessing current exchange rates.
Clicking the Update Rates button causes Calculator to consult the Internet for the updated exchange rate. We’ll add a filter that prevents this.
We’ll start as usual by opening handle to the WFP engine as was done in the previous example. Next, we need to fill the FWPM_FILTER
structure. First, a nice display name:
FWPM_FILTER filter{}; // zero out the structure WCHAR filterName[] = L"Prevent Calculator from accessing the web"; filter.displayData.name = filterName;
The name has no functional part – it just allows easy identification when enumerating filters. Now we need to select the layer. We’ll also specify the action:
filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4; filter.action.type = FWP_ACTION_BLOCK;
There are several layers that could be used for blocking access, with the above layer being good enough to get the job done. Full description of the provided layers, their purpose and when they are used is provided as part of the WFP documentation.
The last part to initialize is the conditions to use. Without conditions, the filter is always going to be invoked, which will block all network access (or just for some processes, based on its effective weight). In our case, we only care about the application – we don’t care about ports or protocols. The layer we selected has several fields, one of with is called ALE App ID (ALE stands for Application Layer Enforcement).
This field can be used to identify an executable. To get that ID, we can use FwpmGetAppIdFromFileName
. Here is the code for Calculator’s executable:
WCHAR filename[] = LR"(C:\Program Files\WindowsApps\Microsoft.WindowsCalculator_11.2210.0.0_x64__8wekyb3d8bbwe\CalculatorApp.exe)"; FWP_BYTE_BLOB* appId; FwpmGetAppIdFromFileName(filename, &appId);
The code uses the path to the Calculator executable on my system – you should change that as needed because Calculator’s version might be different. A quick way to get the executable path is to run Calculator, open Process Explorer, open the resulting process properties, and copy the path from the Image tab.
The R"(
and closing parenthesis in the above snippet disable the “escaping” property of backslashes, making it easier to write file paths (C++ 14 feature).
The return value from FwpmGetAppIdFromFileName
is a BLOB that needs to be freed eventually with FwpmFreeMemory
.
Now we’re ready to specify the one and only condition:
FWPM_FILTER_CONDITION cond; cond.fieldKey = FWPM_CONDITION_ALE_APP_ID; // field cond.matchType = FWP_MATCH_EQUAL; cond.conditionValue.type = FWP_BYTE_BLOB_TYPE; cond.conditionValue.byteBlob = appId; filter.filterCondition = &cond; filter.numFilterConditions = 1;
The conditionValue
member of FWPM_FILTER_CONDITION
is a FWP_VALUE
, which is a generic way to specify many types of values. It has a type
member that indicates the member in a big union that should be used. In our case, the type is a BLOB (FWP_BYTE_BLOB_TYPE
) and the actual value should be passed in the byteBlob
union member.
The last step is to add the filter, and repeat the exercise for IPv6, as we don’t know how Calculator connects to the currency exchange server (we can find out, but it would be simpler and more robust to just block IPv6 as well):
FwpmFilterAdd(hEngine, &filter, nullptr, nullptr); filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6; // IPv6 FwpmFilterAdd(hEngine, &filter, nullptr, nullptr);
We didn’t specify any GUID for the filter. This causes WFP to generate a GUID. We didn’t specify weight, either. WFP will generate them.
All that’s left now is some cleanup:
FwpmFreeMemory((void**)&appId); FwpmEngineClose(hEngine);
Running this code (elevated) should and trying to refresh the currency exchange rate with Calculator should fail. Note that there is no need to restart Calculator – the effect is immediate.
We can locate the filters added with WFP Explorer:
Double-clicking one of the filters and selecting the Conditions tab shows the only condition where the App ID is revealed to be the full path of the executable in device form. Of course, you should not take any dependency on this format, as it may change in the future.
You can right-click the filters and delete them using WFP Explorer. The FwpmFilterDeleteByKey
API is used behind the scenes. This will restore Calculator’s exchange rate update functionality.
Windows 7 / Networking
The Windows Filtering Platform (WFP) is an architectural feature of Windows Vista and
later versions that allows access to Transmission Control Protocol/Internet Protocol (TCP/
IP) packets as they are being processed by the TCP/IP networking stack. WFP is the engine
that implements packet-filtering logic, and it is accessible through a collection of public APIs
which provide hooks into the networking stack and the underlying filtering logic upon which
Windows Firewall is built. Independent Software Vendors (ISVs) can also use WFP to develop
third-party firewalls, network diagnostic software, antivirus software, and other types of
network applications. Using these APIs, a WFP-aware filtering application can access a packet
anywhere in the processing path to view or modify its contents. Third-party vendors and
network application developers should utilize the WFP APIs only for filtering applications or
security applications.
As shown in Figure below, the main features of the WFP are as follows:
- Base Filter Engine The Base Filter Engine (BFE) runs in user mode and receives filtering
requests made by Windows Firewall, third-party applications, and the legacy IPsec
policy service. The BFE then plumbs the filters created by these requests into the Kernel
Mode Generic Filter Engine. The BFE (Bfe.dll) runs within a generic SvcHost.exe process. - Generic Filter Engine The GFE receives the filters plumbed from the BFE and stores
them so that the different layers of the TCP/IP stack can access them. As the stack
processes a packet, each layer the packet encounters calls the GFE to determine whether
the packet should be passed or dropped. The GFE also calls the various callout modules
(defined next) to determine whether the packet should be passed or dropped. (Some
callouts may perform an identical function, especially if multiple third-party firewalls
are running concurrently.) The GFE (Wfp.lib) is part of the Kernel Mode Next Generation
TCP/IP Stack (NetioTcpip.sys) first introduced in Windows Vista. The GFE is actually the
Kernel Mode enforcement engine portion of the BFE and is not a separate feature. - Callout modules These features are used for performing deep inspection or data
modification of packets being processed by the pack. Callout modules store additional
filtering criteria that the GFE uses to determine whether a packet should be passed or dropped.
Note The BFE can support multiple clients simultaneously. This means that a third-party,
WFP-aware application can interact with and even override Windows Firewall with
Advanced Security if so designed.
The APIs of the BFE are all publicly documented so that ISVs can create applications that
hook into the advanced filtering capabilities of the Next Generation TCP/IP Stack in Windows
Vista and later versions. Some of the filtering features of the WFP are implemented using callouts,
but most filtering is performed using static filters created by the BFE as it interacts with
Windows Firewall. The Windows Firewall service monitors the system to make sure the filters
passed to BFE reflect the environment of the system at any given time. These public WFP APIs
are scriptable and expose the full configurability of Windows Firewall, but they have some
limitations, such as no support for IPsec integration.