Windows Filtering Platform Callout Driver – Traffic Redirector POC

Motivation

Cisco Talos published a report on a browser hijacker driver named “RedDriver”, sharing the link here, it mentions that the malicious driver abuse Windows Filtering Platform/WFP to control the network traffic. I managed to get the sample from VT and loaded it in IDA just to take a look at the code implementation. Even though I am familiar with the WFP,shout out to Pavel Yosifovich’s Windows Kernel Programming Second Edition, but seeing five functions listed in driver’s imports poked my curiosity. Its my first time seeing these routines.

Also you can see a list of web browsers driver targets as shown in the image below.

To my surprise, another interesting piece of data I found in another sub routine in the driver is the type FWPS_CALLOUT1 callout as shown below. You can see in the subsequent lines of code, the driver initializes two members classifyFn and routineFn with addresses of functions.

Further checking the classFn we can clearly see, driver calls the functions seen in the imports here.

I had no idea what I was looking at the time of reading the disassembly. Hence this post! I googled a lot and found mentions of the functions seen in imports of the RedDriver in “redirector/proxy code samples” in stackoverflow and CodeProject. Following this blog the reader will gain basic understanding of WFP and kernel level redirection we can employ on a target application running on Windows.

Disclaimer: This not going to be a “Hello World” for Windows Driver development, rather I expect reader to have basic understanding of driver development on Windows.

Kernel Mode WFP Basic Architecture

  • Despite of the direction of the network flow, the traffic passes through network interface card and TCP/IP stack implemented in the OS. On Windows tcpip.sys driver implements the whole low level networking. The kernel Filter Engine aka Windows Filtering Platform/WFP has direct access to traffic that pass through the network stack.
  • WFP kernel client implemented in fwpkclnt.sys exposes APIs which we can use to tap into the filter engine and manipulate the whole flow.
  • To manage the traffic in a better way, “layers” are made available to the filter engine, so that engine can perform filtering in a more efficient way. We will discuss this aspect in more details in the ALE section.
  • We can set filters at one or more layers which will make the engine to filter the network traffic, that means we will get to manipulate the flow or even the contents of the packet. Once a filter is active, engine can execute a special driver code called callouts on the filtered traffic.
  • A callout provides functionality that extends the capabilities of the Windows Filtering Platform.A callout consists of a set of callout functions and a GUID key that uniquely identifies the callout. There are several built-in callouts that are included with the Windows Filtering Platform. Additional callouts can be added by using callout drivers.
  • A callout function is a function that is implemented by a callout driver that is one of the functions that defines a callout. A callout consists of the following list of callout functions
    • A notifyFn function to process notifications.
    • A classifyFn function to process classifications.
    • A flowDeleteFn function to process flow deletions (optional).
  • Built in callouts are shown below.

  • An action is the result of filtering. The callout code needs to take an action for example we can block an application from reaching out to internet by dropping the packets in the network flow or we can permit it, this decision is purely based on what we want to achieve with our callout driver.

Application Layer Enforcement (ALE) Layers

  • ALE is a set of Windows Filtering Platform (WFP) kernel-mode layers that are used for stateful filtering.
  • A filtering layer is a point in the TCP/IP network stack where network data is passed to the filter engine for matching against the current set of filters. Each filtering layer in the network stack is identified by a unique filtering layer identifier, for example FWPM_LAYER_ALE_CONNECT_REDIRECT_V4 is identified by GUID value c6e63c8c-b784-4562-aa7d-0a67cfcaf9a3 (taken from fwpmu.h)
  • When a filter is added to the filter engine, it is added at a designated filtering layer where it will filter the network data.
  • Specific data fields are made available at each filtering layer for processing by the filters that have been added to the filter engine at that layer.
  • If the filter engine passes the network data to a callout for additional processing, it includes these data fields and any metadata that is available at that filtering layer
  • Above points are taken from the official WFP documentation, you can refer fwpsk.h header to see all the built in layers as shown below. The field information can be also found in the header for respective layers.

There is a better way to see everything related to WFP, say all the filters and callouts active on the system by using Pavel Yosifovich’s WFP explorer.

As you can see from below image, we can select a layer, in our case “ALE Connect Redirect v4 Layer” and see all the fields available to that particular layer.

WFP IS A MESS!

Naming convention

  • The WFP APIs and types vary with Windows versions hence we use a versioning scheme.
  • For example a callout is represented in a program by type FWPM_FILTERX.
  • The X denotes the version
    • FWPM_FILTER0 for Win vista+
    • FWPM_FILTER1 for Win7+
    • FWPM_FILTER2 for Win8+
    • FWPM_FILTER3 for Win10+
  • It is not a good idea to directly go and use one these versions. Instead use version independent identifiers like FWPM_FILTER in your code, the header will do the work for us by finding the right version to use based on system settings.
  • One way to confirm this is to go to VS IDE and right click on a type and select “Go to Definition” and see header performing checks, using the NTDDI_VERSION it picks the right version for us as shown in the image below

WFP Management Vs Runtime APIs

  • APIs and types that start with FWPM_XXX come under management, ie the user space applications can access these through base filtering engine of WFP.
  • APIs and types that start with FWPS_XXX come under runtime, ie it is accessible to kernel. Only driver code can use these APIs and types. FWPM_XXX are also accessible to the kernel.

Library References

If you get stuck or not satisfied with information shared in this post and you want to explore more, feel free to check these header files.

  • fwpmtypes.h
  • fwpmu.h
  • fwptypes.h
  • fwpvi.h
  • fwpsk.h

The Game Plan!

  • In this post we will be writing Windows Filtering Platform/WFP callout driver and its client component to redirect the network communication of a target application running in user space to another application which we call a proxy service.
  • The user mode component “callout client” will add filters to monitor network traffic that pass through ALE layers and send the process id of the target application that we want to monitor. The filters added by the client application will have a logical association with the callout driver. Once the filter gets added by client, the kernel filtering engine will start filtering the network traffic that pass through corresponding ALE layers mentioned in our filter.
  • The callout client needs to pass following data to the callout driver to perform the filtering:
    • The PID of target application.
    • The PID of our proxy service application
    • The IP address and port number where the proxy service is listening on.
  • Following the execution of the client application by passing above data, the filter engine will invoke our callout driver which will start getting the traffic from our target application, now we have power to manipulate the traffic even modify the packet data using our callout driver code.
  • Our objective is to divert the traffic from the application to our proxy service application. The callout driver will do it for us.
  • Following a successful redirection, our proxy service will simply prints out the origin and destination addresses.
  • Our target application will be a simple Http client that connects to a remote server.
  • The proxy application will be a simple socket that listens on 127.0.0.1:5150 for incoming connection. Once connected it will display actual destination ip address and port information used by the traffic of target application before redirection.

The driver code template used in this post is from Pavel Yosifovich’s Windows Kernel Programming Second Edition (Chapter 13 Windows Filtering Platform). You can find the code here

Building Blocks

Callout Driver

  • Callout Driver needs to register callout via FwpsCalloutRegister routine. This needs to done from kernel space.
  • As part of the callout registration process, the callout driver must implement classification function (classifyFn) and notification function (notifyFn).
  • The driver will get user data (PID of the target application) from the client and use it in the classification routine to start filtering the traffic based on PID of the target application. The classification routine will take care of the redirection.

Callout Client

Client will be responsible for:

  • Getting the PID of the target application whose traffic we want to redirect, the PID of our proxy service, IP address and port number of the proxy service.
  • Adding Provider and ProviderContext, Filters and Callouts.
  • Passing the PID of the target application to driver via DeviceIoControl, IO Method is BUFFERED. The PID, IP address and port number are all part of the provider context data, kernel filter engine will pass this data to classification function implemented in our callout driver when the filtering starts.

Callout Driver

  • As far as the WFP is concerned, the callout driver should register a callout by calling FwpsCalloutRegister function. The registration involves initialisation of two callback function pointers classifyFn and notifyFn.
  • Lets take a look at definition of the “callout”. A callout is of the type FWPS_CALLOUT , definition can be found in the fwpsk.h header.

We are only interested in following members :

  • calloutkey – This is a GUID value used to uniquely identify our callout.
  • classifyFn – Filter engine invokes the function whose address is stored in classifyFn member when the network traffic goes through our filters. Our classification function will hold the traffic processing logic.
  • notifyFn – Filter engine will let us know about the events associated with the callout through notifyFn. This is a pointer to our notification function. We are not going to use this feature in this post. We will be performing an “in-line” redirection where the actual logic that does redirection will be present in our classification hence in-inline , rather than offloading this process to some other elements, relieving the driver from the task of processing the incoming traffic. In such offloading scenarios, we make use of notifyFn.

The Classification Function

  • The classification function is of type FWPS_CALLOUT_CLASSIFY_FN , its type definition is shown in the image above.
  • I am not going to cover all seven parameters passed to this function, only relevant ones that we are going to use in our code.
  • Remember classification function is invoked by the filter engine and not by us, therefore all these parameters will be carrying a wealth of data that we can use in classifying the traffic.
  • We will discuss significance of following parameters in depth:
    • FWPS_INCOMING_METADATA_VALUES* inMetaValues
    • FWPS_FILTER* filter
    • FWPS_CLASSIFY_OUT* classifyOut

FWPS_INCOMING_METADATA_VALUES* inMetaValues

  • As the name FWPS_INCOMING_METADATA_VALUES suggests, the filter engine fills up this structure with meta data of the filtered traffic.
  • This is quite a big structure that holds very interesting information like the process id responsible for the traffic. This way we can filter traffic based on the running applications on the system. When the filter engine starts executing our callout’s classification for every traffic generated on the system, we can parse the processId member of this structure and filter the traffic if it matches our value.

FWPS_FILTER* filter

  • When filter engine invokes our classification routine, it passes all the information of the filter associated with callout.
  • As far as the traffic redirection is concerned, the most important member of the FWPS_FILTER structure is providerContext.
  • Provider context is an opaque data structure of type FWPM_PROVIDER_CONTEXT, the purpose of this structure is to pass context specific data to the callout driver. For example the context in our traffic redirection scenario will be information like :
    • IP address and port number of the proxy service
    • PID of the proxy service etc..
    • Above data form the context, hence it can vary depending on the application needs. In following sections, we will cover this in more details.

FWPS_CLASSIFY_OUT* classifyOut

This is classification output, simply put it is the final result of the classification routine, here we can inspect packets and even modify it and make a decision whether to block or allow the traffic. We will instruct the classification function to make a decision at the end of the processing. As shown above, the classifyOut is of the type FWPS_CLASSIFY_OUT where actionType (basically UINT32, see below definition) member will hold the final decision in the form of flags shown below. These flags start with “FWP_ACTION_XXXX”.

Lets rewind and summarise various concepts discussed so far:

  • Our classification routine will fetch the processId from FWPS_INCOMING_METADATA_VALUES inMetaValues parameter to perform application based filtering.
  • Following process classification, we can retrieve provider context from FWPS_FILTER *filter parameter that holds proxy service PID, IP address and port information to perform redirection.
  • Finally we can set action flag in the FWPS_CLASSIFY_OUT *classifyOut parameter to let the filter engine know about the decision whether to allow the traffic flow or drop it.

Writing Our Callout Classification Routine

Structure Definitions

Context Data

typedef struct PC_PROXY_DATA_
{	
    UINT32  targetProcessID;
    UINT16  proxyLocalPort
    union
    {
        BYTE pIPv4[4];                       /// Network Byte Order
        BYTE pIPv6[16];
        BYTE pBytes[16];
    }proxyLocalAddress;
}PC_PROXY_DATA, * PPC_PROXY_DATA;
  • The PC_PROXY_DATA will serve as provider context. The callout client program will first initialize this structure with user provided data and then the whole structure data is stored in an instance of FWPM_PROVIDER_CONTEXT .
  • When the filter engine invokes our classification function (pointer stored in classifyFn), we can retrieve the provider context data from FWPS_FILTER* pfilter parameter.
  • Then we can simply use the context data to replace the destination address of filtered traffic packets to address of the proxy service

Layer Data

  • The data present in the filtered layer is represented by the type FWPS_CONNECT_REQUEST as shown above.
  • The localAddressAndPort and remoteAddressAndPort are IP address of localhost and IP address of the remote host where traffic is headed respectively.
  • The localRedirectTargetPID is the PID of our proxy service.
  • To redirect the traffic we need to initialise localRedirectHandle member with redirect handle, we wil discuss this later in the following sections.
  • The localRedirect Context is an important member in the structure as we will be storing the original local and remote addresses obtained from the filtered layer in this member, so that after redirection when proxy accepts the redirected connection, it can restore the original addresses and let the traffic reach the intended host.

Packet Modification

To start modifying the packets we need to perform following steps:

  • Use FwpsAcquireClassifyHandle() to obtain a special handle called classifyhandle.
  • A redirect handle needs to be retrieved to perform traffic redirection by invoking FwpsRedirectHandleCreate().
  • Next we need to retrieve a pointer to “writable layer data” which is nothing but data coming from the filtered layers. As discussed before, this is of the type FWPS_CONNECT_REQUEST*
  • Now we have the layer data, we can modify member data present in FWPS_CONNECT_REQUEST structure.
  • To apply the modification, a simple call to FwpsApplyModifiedLayerData() will do the job.
  • Finally we need to do some clean by calling FwpsRedirectHandleDestroy() and FwpsReleaseClassifyHandle().

Now we are ready to write the code for our classification function.

DriverEntry

GUIDs & IOCTL Codes

  • The header file common.h contains definitions of the IOCTL codes and GUIDs. This is shared between driver and client.
  • GUIDs are generated using GUID creator in Visual Studio IDE.
  • Tools -> Create GUID -> DEFIEN_GUID(…)

Register Callouts

NTSTATUS Initialize::RegisterCallouts(PDEVICE_OBJECT devObj)
{
	const GUID* guids[] = {&GUID_PROXY_REDIRECTION_V4};
	NTSTATUS status = STATUS_SUCCESS;

	for (auto& guid : guids)
	{
		FWPS_CALLOUT callout{};
		callout.calloutKey = *guid;
		callout.notifyFn = OnCalloutNotify;
		callout.classifyFn = OnCalloutClassify;
		status |= FwpsCalloutRegister(devObj, &callout, nullptr);
	}
	return status;

}
  • Callouts need to be registered from within the kernel space. Remember that a callout is of type FWPS_CALLOUT. After normal driver entry initialisation task, we will call RegisterCallouts method to register our callout.
  • To register a callout we need following data:
    • A GUID to uniquely identify our callout.
    • Classification and notification routines, in our case its OncalloutClassify and OnCalloutNotify respectively.

OnCalloutClassify Function

void OnCalloutClassify(const FWPS_INCOMING_VALUES* inFixedValues, const FWPS_INCOMING_METADATA_VALUES* inMetaValues, void* layerData, const void* classifyContext, const FWPS_FILTER* filter, UINT64 flowContext, FWPS_CLASSIFY_OUT* classifyOut)
{
	Initialize::Get().DoCalloutClassify(inFixedValues, inMetaValues, layerData, classifyContext, filter, flowContext, classifyOut);

}

When filter engine calls OnCalloutClassify function, DoCalloutClassify routine is executed as shown above. We simply pass all the data OnCalloutClassify receives from the engine to DoCalloutClassify.

void Initialize::DoCalloutClassify(const FWPS_INCOMING_VALUES* inFixedValues, const FWPS_INCOMING_METADATA_VALUES* inMetaValues, void* layerData, const void* classifyContext, const FWPS_FILTER* filter, UINT64 flowContext, FWPS_CLASSIFY_OUT* classifyOut)
{
	

	if ((inMetaValues->currentMetadataValues & FWPS_METADATA_FIELD_PROCESS_ID) == 0)
	{
		return;
	}
	Locker locker(m_ProcessesLock);
	bool check = m_Processes.Contains((ULONG)inMetaValues->processId);


	if (check)
	{
		ProxyRedirect(inFixedValues, inMetaValues, layerData, classifyContext, filter, flowContext, classifyOut);
	}


}
  • We perform process level filtering in the DoCalloutClassify routine as shown above.
  • As we have discussed before, the layer specific data can be retrieved via FWPS_INCOMING_METADATA_VALUES* inMetaValues.
  • The currentMetadataValues member in inMetaValues contains a bitwise OR of a combination of metadata field identifiers that specify which metadata values are set in the structure. If FWPS_METADATA_FIELD_PROCESS_ID is set that means we can fetch the PID of the process responsible for the traffic.
  • Depending on the nature of the ALE layer that we are filtering, if process id field is not supported by the target layer then we simply return.
  • If PID is present then we simply check if the PID is in our monitoring list. The list is being accesses through proper synchronisation using spin locks via Locker class that we have implemented.
  • If the PID is found in the list then we call ProxyRedirect() function by passing all data that OnCalloutClassify() received from the filter engine.

ProxyRedirect Function

void ProxyRedirect(const FWPS_INCOMING_VALUES* pinFixedValues, const FWPS_INCOMING_METADATA_VALUES* pinMetaValues, void* playerData, const void* pclassifyContext, const FWPS_FILTER* pfilter, UINT64 pflowContext, FWPS_CLASSIFY_OUT* pclassifyOut)
{	
    UINT64 ClassifyHandle = 0;
    HANDLE redirecthandle = 0;
    PVOID writablelayerData = 0;
    PC_PROXY_DATA* proxydata = 0;
    FWPS_CONNECT_REQUEST* pConnectRequest = { 0 };
    SOCKADDR_STORAGE* pSockAddrStorage = 0;
    UINT32 actionType = FWP_ACTION_PERMIT;
    
 //redacted code - redirection logic   
}
  • Before we jump to the redirection logic, we need to declare few variables to hold different data we need to process incoming traffic.
  • ClassifyHandle and redirectHandle will be initialised with classification handle and redirect handle respectively.
  • The writablelayerData will hold the filtered traffic which will later get cast to FWPS_CONNECT_REQUEST*. This will be stored in the pConnectRequest variable.
  • pSockAddrStorage will be used to store original addresses in the filtered packet, which we call localRedirectContext in FWPS_CONNECT_REQUEST type
status = FwpsAcquireClassifyHandle((void*)pclassifyContext, 0, &ClassifyHandle);
	if (NT_SUCCESS(status))
	{
		status = FwpsRedirectHandleCreate(&CALLOUT_PROXY_PROVIDER, 0, &redirecthandle);
		if (NT_SUCCESS(status))
		{
			
			status = FwpsAcquireWritableLayerDataPointer(ClassifyHandle, pfilter->filterId, 0, &writablelayerData, pclassifyOut);
			if (NT_SUCCESS(status))
			{
				//redirection logic
			}
		}
	}
  • FwpsAcquireClassifyHandle is used to retrieve a classification handle to perform the classification, we need to pass this handle to fetch filtered layer data from target application.
  • Following the acquisition of the classification handle, we need to get the redirect handle via FwpsRedirectHandleCreate(), we need to store this handle in the localRedirectHandle member of pConnectRequest instance.
  • Finally we can call FwpsAcquireWritableLayerDataPointer() by passing classification handle, the pointer to filtered layer data will be available in writablelayerData variable.

Redirection Logic

proxydata = (PC_PROXY_DATA*)pfilter->providerContext->dataBuffer->data;
pConnectRequest = (FWPS_CONNECT_REQUEST*)writablelayerData;
pConnectRequest->localRedirectHandle = redirecthandle;
HLPR_NEW_ARRAY(pSockAddrStorage, SOCKADDR_STORAGE,2,PROXY_CALLOUT_DRIVER_TAG);

RtlCopyMemory(&(pSockAddrStorage[0]),&(pConnectRequest->remoteAddressAndPort),sizeof(SOCKADDR_STORAGE));
RtlCopyMemory(&(pSockAddrStorage[1]),&(pConnectRequest->localAddressAndPort),sizeof(SOCKADDR_STORAGE));

pConnectRequest->localRedirectContext = pSockAddrStorage;
pConnectRequest->localRedirectContextSize = sizeof(SOCKADDR_STORAGE) * 2;

INETADDR_SET_ADDRESS((PSOCKADDR) & (pConnectRequest->remoteAddressAndPort), proxydata->proxyLocalAddress.pBytes);
INETADDR_SET_PORT((PSOCKADDR) & (pConnectRequest->remoteAddressAndPort), proxydata->proxyLocalPort);


if (proxydata->targetProcessID)
{
    pConnectRequest->localRedirectTargetPID = proxydata->targetProcessID;
}
pclassifyOut->actionType = actionType;
  • The data we need to perform redirection, the provider context can be fetched from FWPS_FILTER* pfilter which is passed to the classification function by the filter engine. The provider context data contains the PID and address/port information of our proxy service. The data is cast to PC_PROXY_DATA and stored in the variable proxydata.
  • The filtered layer data we retrieved from earlier is cast to FWPS_CONNECT_REQUEST* and it is stored in pConnectRequest variable. From now on, the pConnectRequest will represent our filtered data, ie the actual traffic generated by our target application. We will be redirecting this data to our proxy.
  • The localRedirectHandle member of pConnectRequest is initialised with redirect handle.
  • We need to store the original local and remote address present in the filtered traffic. This needs to be stored in the lcoalRedirectContext member of pConnectRequest.
  • We will be creating an array of type SOCKADDR_STORAGE to hold both local and remote addresses using a helper function we created HLPR_NEW_ARRAY.
  • We then simply copy the original traffic addresses present in remoteAddressAndPort and localAddressAndPort members of pConnectRequest instance to our redirect context array pSockAddrStorage.
  • The above data is then stored in the localRedirectContext member of the pConnectRequest instance.
  • Now we are all set to perform redirection, lets change the remote address of the filtered traffic to IP address and port on which our proxy service is running, thats it.
FwpsApplyModifiedLayerData(ClassifyHandle, writablelayerData, FWPS_CLASSIFY_FLAG_REAUTHORIZE_IF_MODIFIED_BY_OTHERS);
if (ClassifyHandle)
{
    if (redirecthandle)
    {
        FwpsRedirectHandleDestroy(redirecthandle);

        redirecthandle = 0;
    }
    FwpsReleaseClassifyHandle(ClassifyHandle);
    ClassifyHandle = 0;
}
  • To apply changes made to the filtered traffic, we need to call FwpsApplyModifiedLayerData by passing the classifyHandle, writablelayer data and a special flag FWPS_CLASSIFY_FLAG_REAUTHORIZE_IF_MODIFIED_BY_OTHERS.
  • The purpose of the flag is to not let other callout drivers actively filtering the same layer modify our data.
  • FwpsRedirectHandleDestroy() and FwpsReleaseClassifyHandle() clean up all the allocated resources.

OnCalloutNotify Function

NTSTATUS OnCalloutNotify(FWPS_CALLOUT_NOTIFY_TYPE notifyType, const GUID* filterKey, FWPS_FILTER* filter)
{
	return Initialize::Get().DoCalloutNotify(notifyType, filterKey, filter);
}
  • The notifyFn is not much use to us in this POC, engine notifies our driver of various filtering events happening through notifyFn callback function. In our case, the engine will invoke OnCalloutNotify which will execute DoCalloutNotify. This is shown above
  • We simply prints the events to a kernel debugger based various notification type flags retrieved from the engine as shown in the code below.
NTSTATUS Initialize::DoCalloutNotify(FWPS_CALLOUT_NOTIFY_TYPE notifyType, const GUID* filterKey, FWPS_FILTER* filter)
{
	UNREFERENCED_PARAMETER(filter);

	UNICODE_STRING sguid = RTL_CONSTANT_STRING(L"<Noguid>");
	if (filterKey)
		RtlStringFromGUID(*filterKey, &sguid);

	if (notifyType == FWPS_CALLOUT_NOTIFY_ADD_FILTER) {
		KdPrint((DRIVER_PREFIX "Filter added: %wZ\n", sguid));
	}
	else if (notifyType == FWPS_CALLOUT_NOTIFY_DELETE_FILTER) {
		KdPrint((DRIVER_PREFIX "Filter deleted: %wZ\n", sguid));
	}
	if (filterKey)
		RtlFreeUnicodeString(&sguid);

	return STATUS_SUCCESS;
}

IOCTLS

  • The IO method used for IOCTLs is METHOD_BUFFERED, hence the user input is fetched from the SystemBuffer member in AssociatedIrp of IRP
  • Callout’s DeviceIoControl is very simple , it supports 3 operations:
    • IOCTL_PRXY_REDIR_PROCESS_ADD – Adds the user supplied target app’s pid to a vector allocated in kernel address space.
    • IOCTL_PRXY_REDIR_PROCESS_REM – Deletes a PID from the vector. (cannot be used with current program structure)
    • IOCTL_PRXY_REDIR_PROCESS_CLEANUP – Deallocates the vector that holds target app PIDs

Usermode Driver Client

  • Major responsibilities of the client code are as follows:
    • Parse the user supplied proxy data and initialize the provider context with proxy data.
    • Retrieve WFP engine handle and Initialize callout/filter instances
    • initiate tracnsaction
    • And finally add callout and filters to the WFP engine
    • Commit the transaction.
    • Above picture summarises whole process and shows apis used in the process.

Choosing ALE Layers

Our objective is to redirect the network traffic of a target application to our proxy service, there is one dedicated layer for this purpose – redirect layer.

  • You can search for the keyword “redirect” in the fwpsk.h and see four ALE layers present in the FWPS_BUILTIN_LAYERS_ enum as shown above.
  • We are interested in FWPS_LAYER_ALE_CONNECT_REDIRECT_V4 and V6 layers. The bind redirect layers are used to redirect traffic destined to a specific port listening on the system. Keep in mind the connect redirect layers cannot change the value of localAddressAndPort member of the FWPS_CONNECT_REQUEST instance (which represents the filtered traffic layer data fetched through FwpsAcquireWritableLayerDataPointer), only remoteAddressAndPort is allowed to change.
  • In case of bind redirect layers, we are allowed to change value of localAddressAndPort member
  • In our POC we will be filtering FWPS_LAYER_ALE_CONNECT_REDIRECT_V4 only

Local Variables

  • In client program we use following local variables to hold various information like our proxy data, WFP engine handles etc.
  • The provider context data will comprise of user provided information of the proxy service. We need a variable to pProxyData of type PC_PROXY_DATA to hold this information.
  • One of the members present in the FWPM_PROVIDER_CONTEXT providerContext will hold initialized instance of the pProxyData which will then be passed to our classification function by the filter engine at the time of filtering.
  • Instances of the type FWPM_CALLOUT and FWPM_FILTER (variables callout and filter shown above) will be initialized and later we use them when they are added to the engine.

Provider Context

  • PopulateProxyContextdata() will initialize the pProxyData instance with the data provided by the user:
    • PID of the running proxy service, its IP address and port number

Definition of PC_PROXY_DATA is shown below

typedef struct PC_PROXY_DATA_
{	
    UINT32  targetProcessID;
    UINT16  proxyLocalPort
    union
    {
        BYTE pIPv4[4];                       /// Network Byte Order
        BYTE pIPv6[16];
        BYTE pBytes[16];
    }proxyLocalAddress;
}PC_PROXY_DATA, * PPC_PROXY_DATA;

Once pProxyData is initialized, we can start working on the providerContext structure as shown below

  • The provider context is identified by a GUID value which is stored in the providerkey member.
  • The most important field is dataBuffer as this will hold our proxy data.

WFP Engine Initialization

  • From user space we will be using WFP management APIs that starts with Fwpm*.
  • First we need to retrieve WFP engine handle by calling FwpmEngineOpen. The function will initialize engineHandle variable with a handle.
  • Next we need to add a provider by calling FwpmProviderAdd function by passing an initialized instance of the type FWPM_PROVIDER. The provider is identified by a GUID.
  • For more information regarding flags and other members refer to documentation of FWPM_PROVIDER type
  • FwpmProviderGetByKey is used to check if a provider with a specific GUID is already added to WFP, if its present already then we dont have to add it again. FwpmProviderDeleteByKey deletes a provider.
  • The WFP API is transactional, and most function calls are made within the context of a transaction, hence while adding callouts and filters from the userspace we need to do it from within a transaction. This means before adding we need to initiate a transaction by calling FwpmTransactionBegin and after adding the callouts and filters we need to commit the transaction by calling FwpmTrasactionCommit. We can use FwpmTransactionAbort to abort a transaction. Learn more about WFP object management here
  • Transactions are either read-only or read/write and enforce rigorous Atomic Consistent Isolated Durable (ACID) semantics.
  • Each client session can have only one transaction in progress at a time. If the caller attempts to begin a second transaction before committing or aborting the first, Base Filtering Engine (engine accessible to user mode applications, counterpart of kernel filtering engine) returns an error.

Adding Callouts and Filters

  • Our driver registers a callout in kernel space. The same GUID used in driver for the callout should be used in client to add the callout to the filtering engine.
  • We need to specify the layer we are interested to filter by initializing the applicableLayer field of FWPS_CALLOUT callout instance.
  • The most important step is initialization of the flags, since we are using a provider context, we need to explicitly mention this to engine by setting the flag member to FWPM_CALLOUT_FLAG_USES_PROVIDER_CONTEXT.
  • FwpmCalloutAdd is used to add the callout to engine. We use FwpmCalloutGetByKey to check if callout with our GUID is already added to the engine, if it is then we clear the callout data by calling FwpmFreeMemory
  • There is no use to a callout if we are not going to use it in a filter. We need to associate our callout with a filter, then only the engine will invoke our callout driver at the time of filtering.
  • We need to initialize an instance of the FWPM_FILTER filter and pass it to FwpmFilterAdd api to add a filter to the engine.
  • Next we need to add a filter, here we will explicitly tell the filter of which callout should be invoked at the time of the filtering by setting the field calloutkey with GUID of our callout.

Committing Transaction

  • Finally we can commit the transaction by calling FwpmTransactionCommit as shown below.
  • If something goes wrong then we will call FwpmTransactionAbort to abort the transaction and FwpmEngineClose to free the engine.

DeviceIoControl

Now back to normal driver client technicalities, only data that we have to pass to the driver directly is the PID of the target application whose traffic we want to redirect. We achieve this by passing IOCTL_PRXY_REDIR_PROCESS_ADD to DeviceIoControl as shown below. This IOCTL signals the driver to add the PID to the vector allocated in the kernel address space

Finally just before the exitm client deletes callouts and filters we added in the prior steps and signal the driver to clear the vector that holds the PIDs of the target application by passing IOCTL_PRXY_REDIR_PROCESS_CLEANUP to DeviceIoControl.

Proxy Service

  • Our simple proxy uses WinSock, remember about our pretty redirect context stored in the localRedirectContext member of pConnectRequest? If not go back and check the code mentioned in the REDIRECTION LOGIC section. This context holds original destination address. The proxy needs to fetch this data to let the traffic reach the destined host.
  • WinSock IOCTLs will help us to retrieve this data for us as shown in the image above.
  • Invoke WSAIctl on proxy’s listening socket by passing the flag SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT to retrieve the context . The context data will be store in the variable redirectContext.
  • Now we need the traffic data, we can retrieve the same by passing the flag SIO_QUERY_WFP_CONNECTION_REDIRECT_RECORDS to WSAIoctl function. The redirectRecords variable will hold the traffic data.
  • Ideally a proxy should set the data for new connection from the redirect records and connect to the address stored in the remoteAddressAndPort in the redirect context.
  • But in our case we will simply prints the redirect context data.
  • The whole proxy code is shown below
#include <stdio.h>
#include<iostream>
#include <winsock2.h>
#include <Mstcpip.h>

#define CONTEXT_SIZE 2048
#define RECORDS_SIZE 4096
#pragma warning( disable : 4996)
#pragma comment(lib, "Ws2_32.lib")

int main()

{
    BYTE redirectContext[CONTEXT_SIZE];
    BYTE redirectRecords[RECORDS_SIZE];
    DWORD bytesReturned;


    DWORD PID = GetCurrentProcessId();
    std::cout << "PID : " << PID;
    // Initialize Winsock.

    WSADATA wsaData;

    int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);

    if (iResult != NO_ERROR)

        printf("Server: Error at WSAStartup().\n");

    else

        printf("Server: WSAStartup() is OK.\n");



    // Create a SOCKET for listening for

    // incoming connection requests.

    SOCKET ListenSocket;

    ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    if (ListenSocket == INVALID_SOCKET)

    {

        printf("Server: Error at socket(): %ld\n", WSAGetLastError());

        WSACleanup();

        return 0;

    }

    else

        printf("Server: socket() is OK.\n");



    // The sockaddr_in structure specifies the address family,

    // IP address, and port for the socket that is being bound.

    sockaddr_in service;

    service.sin_family = AF_INET;

    service.sin_addr.s_addr = inet_addr("127.0.0.1");

    service.sin_port = htons(5150);



    if (bind(ListenSocket, (SOCKADDR*)&service, sizeof(service)) == SOCKET_ERROR)

    {

        printf("Server: bind() failed.\n");

        closesocket(ListenSocket);

        return 0;

    }

    else

        printf("Server: bind() is OK.\n");



    // Listen for incoming connection requests on the created socket

    if (listen(ListenSocket, 10) == SOCKET_ERROR)

        printf("Server: Error listening on socket.\n");

    else

        printf("Server: listen() is OK.\n");



    // Create a SOCKET for accepting incoming requests.

    SOCKET AcceptSocket;

    printf("Server: Waiting for client to connect...\n");



    // Accept the connection if any...

    while (1)

    {

        AcceptSocket = SOCKET_ERROR;

        while (AcceptSocket == SOCKET_ERROR)

        {

            AcceptSocket = accept(ListenSocket, NULL, NULL);

        }

        printf("Server: accept() is OK.\n");

        printf("Server: Client connected...ready for communication.\n");

        ListenSocket = AcceptSocket;

        break;

    }

    WSAIoctl(ListenSocket, SIO_QUERY_WFP_CONNECTION_REDIRECT_CONTEXT, NULL, 0, redirectContext, CONTEXT_SIZE, &bytesReturned, NULL, NULL);

    WSAIoctl(ListenSocket, SIO_QUERY_WFP_CONNECTION_REDIRECT_RECORDS, NULL, 0, redirectRecords, RECORDS_SIZE, &bytesReturned, NULL, NULL);

    //remote address
    struct sockaddr_in* sin = (struct sockaddr_in*)&redirectContext[0];
    unsigned char* ip = (unsigned char*)&sin->sin_addr.s_addr;

    USHORT port = htons(sin->sin_port);
    printf("%d %d %d %d : %d\n", ip[0], ip[1], ip[2], ip[3], port);

    //local address
    sin = (struct sockaddr_in*)&redirectContext[1];
    ip = (unsigned char*)&sin->sin_addr.s_addr;
    port = htons(sin->sin_port);
    printf("%d %d %d %d : %d\n", ip[0], ip[1], ip[2], ip[3], port);



    WSACleanup();

    return 0;

}

Our Target Application

#include <iostream>
#include <string>

#include <stdlib.h>
#include <winsock.h>//dont forget to add wsock32.lib to linker dependencies
#pragma comment(lib, "Ws2_32.lib")

using namespace std;

#define BUFFERSIZE 1024
void die_with_error(char* errorMessage);
void die_with_wserror(char* errorMessage);

int main(int argc, char* argv[])
{
    DWORD PID = GetCurrentProcessId();
    std::cout << "\n PID : " << PID;
    getchar();
    string request;
    string response;
    int resp_leng;

    char buffer[BUFFERSIZE];
    struct sockaddr_in serveraddr;
    int sock;

    WSADATA wsaData;
    const char* ipaddress = "138.199.57.66";
    int port = 80;

    request += "GET / HTTP/1.1\r\n";
    request += "Host: www.http2demo.io\r\n"; 
    request += "\r\n";

    //init winsock
    if (WSAStartup(MAKEWORD(2, 0), &wsaData) != 0)
        die_with_wserror((char*)"WSAStartup() failed");

    //open socket
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        die_with_wserror((char*)"socket() failed");

    //connect
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(ipaddress);
    serveraddr.sin_port = htons((unsigned short)port);
    if (connect(sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0)
        die_with_wserror((char*)"connect() failed");

    //send request
    if (send(sock, request.c_str(), request.length(), 0) != request.length())
        die_with_wserror((char*)"send() sent a different number of bytes than expected");

    //get response
    response = "";
    resp_leng = BUFFERSIZE;
    while (resp_leng == BUFFERSIZE)
    {
        resp_leng = recv(sock, (char*)&buffer, BUFFERSIZE, 0);
        if (resp_leng > 0)
            response += string(buffer).substr(0, resp_leng);
        //note: download lag is not handled in this code
    }

    //display response
    cout << response << endl;

    //disconnect
    closesocket(sock);

    //cleanup
    WSACleanup();
    return 0;
}

void die_with_error(char* errorMessage)
{
    cerr << errorMessage << endl;
    exit(1);
}

void die_with_wserror(char* errorMessage)
{
    cerr << errorMessage << ": " << WSAGetLastError() << endl;
    exit(1);
}

Demo

  • Load the driver and execute client application.
  • ProxyClient.exe is our user mode component of callout driver. It takes following argument values:
    • PID of target application, in our case a simple http socket program WinSockHttp.exe
    • PID of our proxy application proxyserviceTestService.exe
    • IP address of proxy
    • Port number of proxy

Below image shows the output from our partial implementation of proxy, when redirected connection reaches our proxy, using WSA IOCTL we will fetch redirect context from the traffic data and simply prints the original destination address and local address (in this order)

How to advance further?

  • To get the basic idea refer chapter 13 of Pavel Yosifovich’s Windows Kernel Programming Second Edition.
  • To gain advance knowledge go through the official Windows driver code samples here

Leave a Reply

Your email address will not be published. Required fields are marked *