Kernel Mode Rootkits: File Deletion Protection

The following article documents what we’ve learned and may or may not be completely accurate because we are very new to this. Of course we 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.

Windows Kernel Mode Drivers and I/O Request Packets

We 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

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 we 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, we 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.
	{ IRP_MJ_CREATE, 0, PreAntiDelete, NULL },				// DELETE_ON_CLOSE creation flag.
	{ IRP_MJ_SET_INFORMATION, 0, PreAntiDelete, NULL },		// FileInformationClass == FileDispositionInformation(Ex).

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


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

	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.

	return status;

First of all, the IRPs that should be processed by the driver are IRP_MJ_CREATE & 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.

	DBG_PRINT("Unload called.\n");

	// Unregister the minifilter.


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) {

	 * This pre-operation callback code should be running at 
	 * IRQL <= APC_LEVEL as stated in the docs:
	 * and both ZwCreateFile and ZwSetInformaitonFile are also run at 
	 * - ZwCreateFile:
	 * - ZwSetInformationFile:

	 * 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.

	// 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:
				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) {
	 * }
	 * Of course, we would need to find and save the EPROCESS of the 
	 * application somewhere first. Something like a communication port 
	 * could work.

	// 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.

			// 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.

			// Clean up file name information.

	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 unauthorized file deletion, the status of the operation is set to STATUS_ACCESS_DENIED and the pre-operation function completes the IRP.


Attempt to delete the file:

Attempt to rename the file:


As usual, you can find the code on my GitHub. Thanks for reading!



Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.