< BACK TO TERMINAL

Nothing Up My Sleeve: Filesystem Trickery with BindLink

Introduction

As Microsoft publishes new updates, we get new tools to explore. This time, Microsoft introduced new functionality that can be used to virtualize parts of the filesystem.

This article explains how BindLink works, how it was introduced, and why it matters from a security perspective.

The feature

The BindLink feature lets administrators map a filesystem "backing path" to a local "virtual path" through the Bind Filter driver (bindflt.sys), transparently redirecting I/O.

Examples:

C:\App\Config  ➤  C:\Windows\System32\cfg

BindLink also supports remote backing paths:

C:\VirtualDir  ➤  \server\share\realdir

Note: Bind links are not persistent across reboots and must be recreated after a restart.

BindFltApi's Origin

The user-mode library used to manage bind links is BindFltApi.dll (C:\Windows\System32\bindfltapi.dll).

Checking the file’s ProductVersion indicates the release build that introduced it: 10.0.26100.3194.

Searching for this build leads to the KB5051987 update: https://support.microsoft.com/en-us/topic/february-11-2025-kb5051987-os-build-26100-3194-63fb007d-3f52-4b47-85ea-28414a24be2d

Per Microsoft's practice, we can verify the inclusion of BindFltApi.dll via the CSV linked in the KB article: https://go.microsoft.com/fwlink/?linkid=2302954

File name File version Date Time File size
bindfltapi.dll 10.0.26100.3194 2025-02-08 13:42 229376

We can confirm that BindFltApi.dll has been available since Windows 11 build 26100.3194 (2025‑02‑11).

Use cases

This functionality can be used to, but is not limited to:

The implementation

#include <Windows.h>
#include <stdio.h>

// Define the mask bits for the Bind Link flags.
typedef enum CREATE_BIND_LINK_FLAGS
{
	CREATE_BIND_LINK_FLAG_NONE = 0x00000000,
	CREATE_BIND_LINK_FLAG_READ_ONLY = 0x00000001,
	CREATE_BIND_LINK_FLAG_MERGED = 0x00000002,
} CREATE_BIND_LINK_FLAGS;

// Allow bitwise operations on the CREATE_BIND_LINK_FLAGS enum.
DEFINE_ENUM_FLAG_OPERATORS(CREATE_BIND_LINK_FLAGS);

// Define the type for the `BfSetupFilter` function.
typedef HRESULT(__stdcall* PtrCreateBindLink)(
	PVOID jobHandle,
	CREATE_BIND_LINK_FLAGS createBindLinkFlags,
	PCWSTR virtualPath,
	PCWSTR backingPath,
	UINT32 exceptionCount,
	PCWSTR* const exceptionPaths);

// Define the type for the `BfRemoveMapping` function.
typedef HRESULT(__stdcall* PtrRemoveBindLink)(
	PVOID reserved,
	PCWSTR backingPath);

// Function to remove an existing bind link.
int RemoveBindLink(PCWSTR from) {
	HMODULE hBindflt = LoadLibraryW(L"bindfltapi.dll");
	if (!hBindflt) {
		wprintf(L"Failed to load bindfltapi.dll.\n");
		return 1;
	}

	PtrRemoveBindLink RemoveBindLink = (PtrRemoveBindLink)GetProcAddress(hBindflt, "BfRemoveMapping");
	if (!RemoveBindLink) {
		wprintf(L"Failed to retrieve BfRemoveMapping function.\n");
		FreeLibrary(hBindflt);
		return 1;
	}

	HRESULT hrr = RemoveBindLink(
		0,     // Reserved parameter (set to 0).
		from   // Path to remove the bind link.
	);

	if (FAILED(hrr)) {
		if (hrr != 0x80070490) {
			wprintf(L"RemoveBindLink failed with error code: 0x%08X\n", hrr);
			FreeLibrary(hBindflt);
			return 1;
		}
	}

	wprintf(L"RemoveBindLink succeeded.\n");
	FreeLibrary(hBindflt);
	return 0;
}

// Function to create a new bind link.
int CreateBindLink(PCWSTR from, PCWSTR to) {
	HMODULE hBindflt = LoadLibraryW(L"bindfltapi.dll");
	if (!hBindflt) {
		wprintf(L"Failed to load bindfltapi.dll.\n");
		return 1;
	}

	PtrCreateBindLink CreateBindLink = (PtrCreateBindLink)GetProcAddress(hBindflt, "BfSetupFilter");
	if (!CreateBindLink) {
		wprintf(L"Failed to retrieve BfSetupFilter function.\n");
		FreeLibrary(hBindflt);
		return 1;
	}

	CREATE_BIND_LINK_FLAGS flags = CREATE_BIND_LINK_FLAG_NONE;

	HRESULT hrc = CreateBindLink(
		NULL,  // No specific job handle.
		flags, // Flags for the bind link.
		from,  // Source folder (virtual path).
		to,    // Destination folder (backing path).
		0,     // No exception paths.
		NULL   // No exception paths provided.
	);

	if (FAILED(hrc)) {
		wprintf(L"CreateBindLink failed with error code: 0x%08X\n", hrc);
		FreeLibrary(hBindflt);
		return 1;
	}

	wprintf(L"CreateBindLink succeeded.\n");
	FreeLibrary(hBindflt);
	return 0;
}

int printusage(const wchar_t* path) {
	wprintf(L"Usage:\n");
	wprintf(L"  %s enable <from> <to>\n", path);
	wprintf(L"  %s disable <from>\n", path);
	return 1;
}

int wmain(int argc, wchar_t** argv) {
	// Check parameters
	if (argc < 2) {
		return printusage(argv[0]);
	}

	if (_wcsicmp(argv[1], L"enable") == 0) {
		if (argc == 4) {
			// Remove any existing bind link before creating a new one.
			RemoveBindLink(argv[2]);
			CreateBindLink(argv[2], argv[3]);
		}
		else {
			return printusage(argv[0]);
		}
	}
	else if (_wcsicmp(argv[1], L"disable") == 0) {
		if (argc == 3) {
			RemoveBindLink(argv[2]);
		}
		else {
			return printusage(argv[0]);
		}
	}
	else {
		return printusage(argv[0]);
	}
	return 0;
}

Detection

Use this Sigma rule to detect processes that load bindfltapi.dll.

title: Potential Directory Tampering via Bindfilter Redirection
id: 269e6be0-8a4c-46be-ae86-818b39655eb8
status: stable
description: Adversaries may abuse Windows BindFilter to overlay a process’s files or directories with benign ones, hiding its real metadata or executables from system tools. This redirection can also disrupt normal application behavior, such as preventing programs from finding required DLLs.
author: serrac
date: 01-12-2025
tags:
  - attack.t1098
logsource:
  product: windows
  category: library_event
detection:
  selection:
    - ImageLoaded|endswith: '\bindfltapi.dll'
  condition: selection
falsepositives:
  - unknown
level: high

Credits

Credits to research and inspiration from:

Next steps

See Owning Memory: Memory Layout Basics

Next article →