Kernel Mode Rootkits: File Deletion Protection

rootkit
kernel
windows

#1

Hey guys, back with just another casual article from me this time around since one of my projects failed miserably and I don’t really have the time for another serious one. Also I’ve getting into something new, as you may have already guessed, kernel-mode development! Yeah, pretty exciting stuff and I’m here to share a little something I’ve learned that might be interesting to you all. Brace yourselves!

Disclaimer:

The following article documents what I’ve learned and may or may not be completely accurate because I am very new to this. Of course I would never intentionally provide misinformation to this community, but please approach it relatively lightly. If there are any mistakes, please do not hesitate to inform me so I can fix them. I would also like to apologise for any garbage code, I can’t help it. :’)


Windows Kernel Mode Drivers and I/O Request Packets

So just really briefly, since this is not an article about kernel mode or drivers in general, I will describe some basic concepts that will aid in the understanding of the content I will present. The first thing is what’s called an “I/O Request Packet” (IRP for short) which represent the majority of what is delivered to the operating system and drivers. This can be things like a file request or a keyboard key press. What happens with IRPs and drivers is that the IRP is sent down a “stack” of drivers that are registered (with the I/O manager) to process them. It looks something like this:

Obtained from http://www.windowsbugcheck.com/2017/05/device-objects-and-driver-stack-part-2.html

The IRP falls down the stack until it reaches the device or driver that is capable of handling the specified request and then it will get sent back upwards once it is fulfilled. Note that the IRP does not necessarily have to go all the way down to the bottom to be completed.


File Deletion Protection

Here I will present the high-level conceptual overview on how it is possible to protect a file from being deleted. The condition which I have selected in order for this mechanism to prevent a file from deletion is that the file must have the .PROTECTED extension (case-insensitive). Previously, I have described that IRPs must be sent down the driver stack until the bottom or until it can be completed. If a special driver can be slotted into a position in the driver stack before whatever fulfills the targeted IRPs, it has the power to filter the request and interrupt or modify it if desired. This notion serves as the core to the file deletion protection mechanism.

In order to detect if IRPs should be interrupted from deletion, it simply needs to extract the file extension and compare it to whatever is disallowed for deletion. If the extensions match, the driver will prevent the IRP from any further processing by completing the request and sending it back up the driver stack with an error. It may look a little something like this:

                            ^  +------------+  |
                            +> |  Driver 1  | <+   IRP being sent down
                            |  +------------+  |   the driver stack.
                            |        ...       |
 (If file should be         |  +------------+  |   (If the file should not
  protected, send it ---->  +- | Our driver | <+    be protected, continue
  back up prematurely.)        +------------+       sending it down.)
                                     ...       
                               +------------+     
                               |  Driver n  |
                               +------------+
                                     ...
                               +------------+
                               |   Device   |
                               +------------+ 

Anti Delete

So time for some juicy code to put theory to practice. The following code is example code of a “minifilter” driver which mainly handles file system requests.

// The callbacks array defines what IRPs we want to process.
CONST FLT_OPERATION_REGISTRATION Callbacks[] = {
	{ IRP_MJ_CREATE, 0, PreAntiDelete, NULL },				// DELETE_ON_CLOSE creation flag.
	{ IRP_MJ_SET_INFORMATION, 0, PreAntiDelete, NULL },		// FileInformationClass == FileDispositionInformation(Ex).
	{ IRP_MJ_OPERATION_END }
};

CONST FLT_REGISTRATION FilterRegistration = {
	sizeof(FLT_REGISTRATION),				// Size
	FLT_REGISTRATION_VERSION,				// Version
	0,										// Flags
	NULL,									// ContextRegistration
	Callbacks,								// OperationRegistration
	Unload,									// FilterUnloadCallback
	NULL,									// InstanceSetupCallback
	NULL,									// InstanceQueryTeardownCallback
	NULL,									// InstanceTeardownStartCallback
	NULL,									// InstanceTeardownCompleteCallback
	NULL,									// GenerateFileNameCallback
	NULL,									// NormalizeNameComponentCallback
	NULL									// NormalizeContextCleanupCallback
};

PFLT_FILTER Filter;
static UNICODE_STRING ProtectedExtention = RTL_CONSTANT_STRING(L"PROTECTED");

NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
	// We can use this to load some configuration settings.
	UNREFERENCED_PARAMETER(RegistryPath);

	DBG_PRINT("DriverEntry called.\n");

	// Register the minifilter with the filter manager.
	NTSTATUS status = FltRegisterFilter(DriverObject, &FilterRegistration, &Filter);
	if (!NT_SUCCESS(status)) {
		DBG_PRINT("Failed to register filter: <0x%08x>.\n", status);
		return status;
	}
	
	// Start filtering I/O.
	status = FltStartFiltering(Filter);
	if (!NT_SUCCESS(status)) {
		DBG_PRINT("Failed to start filter: <0x%08x>.\n", status);
		// If we fail, we need to unregister the minifilter.
		FltUnregisterFilter(Filter);
	}

	return status;
}

First of all, the IRPs that should be processed by the driver are IRP_MJ_CREATE and IRP_MJ_SET_INFORMATION which are requests made when a file (or directory) is created and when metadata is being set, respectively. Both of these IRPs have the ability to delete a file which will be detailed later. The Callbacks array is defined with the respective IRP to be processed and the pre-operation and post-operation callback functions. The pre-operation defines the function that is called when the IRP goes down the stack while the post-operation is the function that is called when the IRP goes back up after it has been completed. Note that the post-operation is NULL as this scenario does not require one; the interception of file deletion is only handled in the pre-operation.

DriverEntry is the driver’s main function where the registration with the filter manager is performed using FltRegisterFilter. Once that is successful, to start filtering IRPs, it must call the FltStartFiltering function with the filter handle. Also note that we have defined the extension to protect as .PROTECTED as aforementioned.

It is also good practice to define an unload function so that if the driver has been requested to stop, it can perform an necessary cleanups. Its reference in this article is purely for completeness and does not serve any purpose for the main content.

/*
 * This is the driver unload routine used by the filter manager.
 * When the driver is requested to unload, it will call this function
 * and perform the necessary cleanups.
 */
NTSTATUS Unload(_In_ FLT_FILTER_UNLOAD_FLAGS Flags) {
	UNREFERENCED_PARAMETER(Flags);

	DBG_PRINT("Unload called.\n");

	// Unregister the minifilter.
	FltUnregisterFilter(Filter);

	return STATUS_SUCCESS;
}

The final function in this code is the PreAntiDelete pre-operation callback which handles the IRP_MJ_CREATE and IRP_MJ_SET_INFORMATION IRPs. IRP_MJ_CREATE includes functions that request a “file handle or file object or device object”[1] to be opened such as ZwCreateFile. IRP_MJ_SET_INFORMATION includes functions that request to set “metadata about a file or a file handle”[2] such as ZwSetInformationFile.

/*
 * This routine is called every time I/O is requested for:
 * - file creates (IRP_MJ_CREATE) such as ZwCreateFile and 
 * - file metadata sets on files or file handles 
 *   (IRP_MJ_SET_INFORMATION) such as ZwSetInformation.
 *
 * This is a pre-operation callback routine which means that the
 * IRP passes through this function on the way down the driver stack
 * to the respective device or driver to be handled.
 */
FLT_PREOP_CALLBACK_STATUS PreAntiDelete(_Inout_ PFLT_CALLBACK_DATA Data, _In_ PCFLT_RELATED_OBJECTS FltObjects, _Flt_CompletionContext_Outptr_ PVOID *CompletionContext) {
	UNREFERENCED_PARAMETER(CompletionContext);

	/* 
	 * This pre-operation callback code should be running at 
	 * IRQL <= APC_LEVEL as stated in the docs:
	 * https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/writing-preoperation-callback-routines
	 * and both ZwCreateFile and ZwSetInformaitonFile are also run at 
	 * IRQL == PASSIVE_LEVEL:
	 * - ZwCreateFile: https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-ntcreatefile#requirements
	 * - ZwSetInformationFile: https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-ntsetinformationfile#requirements
	 */
	PAGED_CODE();

	/*
	 * By default, we don't want to call the post-operation routine
	 * because there's no need to further process it and also
	 * because there is none.
	 */
	FLT_PREOP_CALLBACK_STATUS ret = FLT_PREOP_SUCCESS_NO_CALLBACK;

	// We don't care about directories.
	BOOLEAN IsDirectory;
	NTSTATUS status = FltIsDirectory(FltObjects->FileObject, FltObjects->Instance, &IsDirectory);
	if (NT_SUCCESS(status)) {
		if (IsDirectory == TRUE) {
			return ret;
		}
	}

	/*
	 * We don't want anything that doesn't have the DELETE_ON_CLOSE 
	 * flag.
	 */
	if (Data->Iopb->MajorFunction == IRP_MJ_CREATE) {
		if (!FlagOn(Data->Iopb->Parameters.Create.Options, FILE_DELETE_ON_CLOSE)) {
			return ret;
		}
	}

	/*
	 * We don't want anything that doesn't have either 
	 * FileDispositionInformation or FileDispositionInformationEx or 
	 * file renames (which can just simply rename the extension).
	 */
	if (Data->Iopb->MajorFunction == IRP_MJ_SET_INFORMATION) {
		switch (Data->Iopb->Parameters.SetFileInformation.FileInformationClass) {
			case FileRenameInformation:
			case FileRenameInformationEx:
			case FileDispositionInformation:
			case FileDispositionInformationEx:
			case FileRenameInformationBypassAccessCheck:
			case FileRenameInformationExBypassAccessCheck:
			case FileShortNameInformation:
				break;
			default:
				return ret;
		}
	}

	/*
	 * Here we can check if we want to allow a specific process to fall 
	 * through the checks, e.g. our own application.
	 * Since this is a PASSIVE_LEVEL operation, we can assume(?) that 
	 * the thread context is the thread that requested the I/O. We can  
	 * check the current thread and compare the EPROCESS of the 
	 * authenticated application like so:
	 *
	 * if (IoThreadToProcess(Data->Thread) == UserProcess) {
	 *     return FLT_PREOP_SUCCESS_NO_CALLBACK;
	 * }
	 *
	 * Of course, we would need to find and save the EPROCESS of the 
	 * application somewhere first. Something like a communication port 
	 * could work.
	 */

	PFLT_FILE_NAME_INFORMATION FileNameInfo = NULL;
	// Make sure the file object exists.
	if (FltObjects->FileObject != NULL) {
		// Get the file name information with the normalized name.
		status = FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, &FileNameInfo);
		if (NT_SUCCESS(status)) {
			// Now we want to parse the file name information to get the extension.
			FltParseFileNameInformation(FileNameInfo);

			// Compare the file extension (case-insensitive) and check if it is protected.
			if (RtlCompareUnicodeString(&FileNameInfo->Extension, &ProtectedExtention, TRUE) == 0) {
				DBG_PRINT("Protecting file deletion/rename!");
				// Strings match, deny access!
				Data->IoStatus.Status = STATUS_ACCESS_DENIED;
				Data->IoStatus.Information = 0;
				// Complete the I/O request and send it back up.
				ret = FLT_PREOP_COMPLETE;
			}

			// Clean up file name information.
			FltReleaseFileNameInformation(FileNameInfo);
		}
	}

	return ret;
}

For IRP_MJ_CREATE, we want to check for the FILE_DELETE_ON_CLOSE create option which is described as "Delete the file when the last handle to it is passed to NtClose. If this flag is set, the DELETE flag must be set in the DesiredAccess parameter."[3] If this flag is not present, we do not care about it so it is passed down the stack for further processing as represented by the FLT_PREOP_SUCCESS_NO_CALLBACK return value. Note that the NO_CALLBACK means that the post-operation routine should not be called when the IRP is completed and passed back up the stack which is what should always be returned by this function as there is no post-operation.

For IRP_MJ_SET_INFORMATION, the FileInformationClass parameter should be checked. The FileDispositionInformation value is described as "Usually, sets the DeleteFile member of a FILE_DISPOSITION_INFORMATION to TRUE, so the file can be deleted when NtClose is called to release the last open handle to the file object. The caller must have opened the file with the DELETE flag set in the DesiredAccess parameter."[4]. To prevent the file from simply being renamed such that the protected extension no longer exists, the FileRenameInformation and FileShortNameInformation values must also be checked.

If the driver receives an IRP request that is selected for file deletion, it must parse the file name information to extract the extension by using the FltGetFileNameInformation and FltParseFileNameInformation functions. Then it is a simple string comparison between the requested file for deletion’s extension and the protected extension to determine whether the delete operation should be allowed or disallowed. In the case of an unauthorised file deletion, the status of the operation is set to STATUS_ACCESS_DENIED and the pre-operation function completes the IRP.


Demonstration

Attempt to delete the file:

Attempt to rename the file:


FIN

Hope that was educational and somewhat interesting or motivational. As usual, you can find the code on my GitHub. Thanks for reading!


(meee) #2

Nice write-up man. Got a few noob questions. I’ve never really delved into kernel-mode dev before. How would this be inserted? I’m assuming it’s just along the lines of the normal process of installing a regular driver? If so, does it need to be done with some sort of driver signing bypassing technique? Or is it more along the lines of patching?


#3

Yep, if you look in the demonstration section, I’ve used OSR Loader to install the driver as a service with the type Minifilter (you can’t see it in the screen capture). Currently the driver is installed under a “test mode” environment which allows test-signed drivers to be loaded but for real-world situations, you may need some kind of legitimate signature.


#4

I’m also pretty new to working with the kernel. I fell into many traps while working (mostly my lack of understanding) How did you approach learning the material? I’ve been mostly a book reader – most books on windows drivers are like 10+ years now and pretty bland and lacking of examples. Any tips/resources to learn this type of information?


#5

Yeah I understand that problem. I still read books dating back to 2007 for the core concepts on the Windows kernel. As for actually developing drivers, I reference the Windows driver samples on GitHub, the online documentation and various forums like OSR very, very, VERY heavily just constantly switching back and forth learning what each line of code/function does. It’s pretty much the same thing when I first tried to learn the WinAPI. I then set up projects for myself to do to try and get familiar with how everything ties together.


#6

I see. Thanks for the guidance and awesome writeup! :slight_smile:


#7

This topic was automatically closed after 30 days. New replies are no longer allowed.