W3WProtect: Writing a minifilter

In my previous blog post, I covered what W3WProtect is and eluded to going into further detail. This week I’m going to cover minifilters, and how I use them to control what files w3wp.exe is able to write to.

But what is a minifilter?

Minifilters allow our drivers to essentially inject ourselves into all I/O operations on the Operating Systems. We’re registering a filter that allows us to respond to all messages going through. Whether it’s creating, writing or deleting something from disk, we can intercept the request, modify it, deny it or just about do whatever we want with it.

Microsoft uses a really great diagram explaining essentially where minifilters sit in the scheme of thing.

ref: https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/images/filter-manager-architecture-1.gif

Using this diagram, when an I/O request is made:

  1. A user mode interaction will generate an I/O request.

  2. The request is sent to the filter manager.

  3. The request is sent of to all the relevant minifilters:

    • The filter manager will prioritize minifilters based on altitude. The higher the altitude, the earlier you get called.

  4. If all minifilters approve the operation, it’ll continue on to the file system driver.

Altitude can play a huge role within the cyber security space. If a rootkit is operating at a higher altitude than the Anti-Virus, it can obfuscate malicious I/O requests before the AV receives it.

Minifilters can register Pre and Post filter, which essentially mean before and after the request. So this routine is followed again after the I/O operation for all minifilters with a post filters.

I’m going to try and keep this blog shorter than the last one, so I’m going to get straight into the code. If you want a more in-depth look into minifilters, let me know!


Getting to the code!

One of the big things I was conscious of with W3WProtect is speed. We could make the most secure driver in the world, but no one is going to use it if it’s slow!

Because of that, I only registered filters that I actually cared about. For the case of W3WProtect, that means I only registered a file creation and a file write minifilter.

This is provided within an Array of FLT_OPERATION_REGISTRATION. (FLT being the prefix of most of the minifilter functions)

Each Operation Registration takes:

  • The major function you want to inject yourself into.

  • Any additional flags.

  • The call-back function to call before the operation.

  • The call-back function to call after the operation.

In this case, I only inject into the create and write requests, but Major Functions can include:

  • Create

  • Read

  • Write

  • CreatePipe

  • QueryFileInformation

  • SetFileInformation

  • DirectoryControl

  • NetworkQueryOpen

  • WMI

CONST
FLT_OPERATION_REGISTRATION
Callbacks[] =
{
	{
		IRP_MJ_CREATE,	// Major Functions
		0,		// Flags
		PtPreCreateOp,	// Pre Operation
		NULL		// Post Operation
	},
	{
		IRP_MJ_WRITE,	// Major Function
		0,		// Flags
		PtPreWriteOp,	// Pre Operation
		NULL		// Post Operation
	},
	{IRP_MJ_OPERATION_END}
};

Once we have our Operation Registration, we can throw it into a FLT_REGISTRATION.

After that, we register our FLT during our DriverEntry and let the magic happen!

const FLT_REGISTRATION FilterRegistration = {
	sizeof(FLT_REGISTRATION),// Size
	FLT_REGISTRATION_VERSION,// Version
	0,		// Flags
	NULL,		// Context Registration,
	Callbacks,	// Operation Callbacks
	PtMFUnload,	// MiniFilterUnload
	NULL,		// InstanceSetup
	NULL,		// InstanceQueryTeardown
	NULL,		// InstanceTeardownStart
	NULL,		// InstanceTeardownComplete

	NULL,		// GenerateFileName
	NULL,		// NormalizeNameComponentCallback
	NULL,		// NormalizeContextCleanupCallback
	NULL,		// TransactionNotificationCallback
	NULL,		// NormalizeNameComponentExCallback
	NULL		// SectionNotificationCallback
};
//
// Setup the mini-filter.
//
status = FltRegisterFilter(
	DriverObject,
	&FilterRegistration,
	&Globals.Filter
);
if (!NT_SUCCESS(status))
	return status;

status = FltStartFiltering(Globals.Filter);
if (!NT_SUCCESS(status))
	return status;

Dealing with w3wp.exe

So, we’ve got our call-back functions and they’re getting called back…. a lot…

We need to be able to filter out the requests, as we only care about w3wp. Thankfully when a minifilter is triggered, it’s triggered under the context of the process that triggered the I/O request. This means that we just need to get the process name of our current process, if it’s not w3wp then we leave it alone!

NTSTATUS WINAPI ZwQueryInformationProcess(
  _In_      HANDLE           ProcessHandle,
  _In_      PROCESSINFOCLASS ProcessInformationClass,
  _Out_     PVOID            ProcessInformation,
  _In_      ULONG            ProcessInformationLength,
  _Out_opt_ PULONG           ReturnLength
);

We can use ZwQueryInformationProcess() to get the process name based on a process handle. We just need to provide the ProcessInformationClass ProcessImageFileName (0x27).

Note: In Kernel Space, the ProcessHandle is actually its PID represented as a handle.

status = ZwQueryInformationProcess(
	NtCurrentProcess(),		// Process Handle
	ProcessImageFileName,		// PROCESSINFOCLASS
	processName,			// Buffer
	PTDEF_MF_LENGTH_PROCESS_NAME,	// SizeOfBuffer
	&retSize			// Return size. 
);
if (!NT_SUCCESS(status) ||
	retSize == 0 ||
	processName->Length == 0
)
	goto Cleanup;

Note: We actually check to see if processName->Length is zero, because the System process does not return its name if that’s the process context that we’re operating under. I was getting random blue screens for a day or so before I worked that one out.

We also get rid of any requests that are from the Kernel. w3wp should never be executing from within the kernel, so we don’t want to waste their time.

// 
// Instantly dismiss Kernel Code, preventing any holdups. 
//
if (Data->RequestorMode == KernelMode)
	return returnStatus;

Once we know that we have the right process, we can use the call-back data parameter with FltGetFileNameInformation(). The information we receive back is standard file infromation, such as name, parent directory and size. This lets us check if the parent directory is with our whitelisted directories, if it is we let it continue on.

//
// Get the filename. 
//
PFLT_FILE_NAME_INFORMATION nameInfo;

status = FltGetFileNameInformation(
	Data,
	FLT_FILE_NAME_OPENED | FLT_FILE_NAME_QUERY_ALWAYS_ALLOW_CACHE_LOOKUP,
	&nameInfo
);
if (!NT_SUCCESS(status))
	goto Cleanup;

status = FltParseFileNameInformation(nameInfo);
if (!NT_SUCCESS(status))
	goto Cleanup;

if (nameInfo->ParentDir.Length == 0)
	goto Cleanup;

for (ULONG i = 0; i < Globals.ConfigDirSize; i++)
{
	// if item in whitelist, go to cleanup.
	if (wcsstr(nameInfo->ParentDir.Buffer, Globals.ConfigWhiteListedDirectory[i]->Buffer) == NULL)
		goto Cleanup;
}

If the operation has made it this far, it is not long for this world. All that’s left to do is grab the data we need for our ETW event, then depending on if enforced mode is on we can block the operation with an ACCESS_DENIED.

if (!Globals.Enforced)
{
	EventWriteFileCreateBlock_Passive(
		NULL,
		HandleToULong(NtCurrentProcess()),
		FileName,
		ParentDir
	);
	goto Cleanup;
}

EventWriteFileCreateBlock_Enforced(
	NULL,
	HandleToULong(NtCurrentProcess()),
	FileName,
	ParentDir
);

Data->IoStatus.Status = STATUS_ACCESS_DENIED;
returnStatus = FLT_PREOP_COMPLETE;

All that’s left to do now is cleanup after ourselves, otherwise we’ll cause a kernel leak. Thankfully we don’t end up allocated that much within this function, but you may have noticed the goto cleanup, which I use to make sure that I don’t leave any allocated pools behind.


Wrap up

Overall, the concept of minifilters is pretty straight forward and using them within W3WProtect is not complex. But the security that this provides us is a huge benefit when we look at prevent web shells from being written.

Over the coming weeks, I look to cover how we do process and registry handling as well. I might also include a couple of other areas like ETW events from kernel. If you have any questions or want a deeper dive into a particular area, let me know!

Previous
Previous

Debugging Kernel Dumps: Episode 1

Next
Next

Over Engineering a Cookie: Part 1