When a file is deleted by any means possible, you want to ensure a backup of the file before its get permanently deleted. This post describes a windows minifilter and a user-space app working in tandem with minifilter to implement a working solution.
It is not a tutorial. You can browse the code and check if the patterns I’ve used make sense for your project. Additionally, you may like to have a look at the build system, packaging (nsis based installer) and application code.
Later I rewrote the application in Rust but that is not part of this project’s GitHub Repository.
Architectural summary
- The minifilter monitors certain events in filesystem that says ‘delete this file’ (on close!).
- Instead of letting kernel delete the file, the minifilter ask the kernel to rename the file (move it to a temporary space) and hide it. For example, if file
D:\MyDocuments\important.pdfis being deleted, it will be moved toC:\ProgramData\Minifilter\D:^^MyDocument^^__??important.pdf??__and then marked hidden. Minifilter also ensures that new file is not renamed by anyone. - Original filepath is converted to a special valid filename (filename!) e.g.
D:\MyDocuments\important.pdftoD__^^MyDocument^^??important.pdf??. As long as you can recover the original filepath from new filename unambigously, we are good to go. You can also use base64 encoded paths as filename. I used a simpler scheme since decoding base64 encoded path inside a kernel minifilter was quite a lot of work. - The new path is sent to user-space application which backs it in a bucket and then send a message a kernel minifilter to delete the file.
- When delete this file request come from the user-space application, the minifilter doesn’t modify the delete-file event and let is go down the kernel stack. The kernel delete the file for sure this time.
Minifilter
This kernel minifilter is a cmake based project that also create a nsis based installer and sign it using your personal key. Note that you need to get the minifilter signed by MS or MS approved vendors before you can publish it. To load the minifilter into your machine, you should follow the official guide https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/development-and-testing-tools
- The minifilter captures required filesystem events:
CONST FLT_OPERATION_REGISTRATION Callbacks[] = {
{ IRP_MJ_CREATE, 0, BackupOnDeleteShield, NULL },
// IRP_MJ_SET_INFORMATION is sent whenever file is marked for deletion.
{ IRP_MJ_SET_INFORMATION,
FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO,
BackupOnDeleteShield,
NULL },
{ IRP_MJ_OPERATION_END }
}; - See https://github.com/dilawar/minifilter-backup-before-delete/blob/e61e9c5fbfd9df51749d0de98acfbad7b7a6290b/minifilter/src/main.cpp#L396 for the definition of
BackupOnDeleteShieldfunction. - If the delete request has come from kenel mode application, let is pass. And if the user-space app is not connected with minifilter (more on it later), don’t do anything.
if (Data->RequestorMode == KernelMode) {
// if the requestor is kernel mode, pass the request on uninterrupted
ret = FLT_PREOP_SUCCESS_NO_CALLBACK;
goto CleanUp;
}//
// If no client is connected to create backups then there is no point using
// the shield.
//
if (!IsUserAppConnected()) {
DFLOG(ERROR, "Shield>" __FUNCTION__ ": No backup client connected.\n");
ret = FLT_PREOP_SUCCESS_NO_CALLBACK;
goto CleanUp;
} For an user-space app to talk to minifilter, the minifilter must create a channel to listen to. The port is opened by minifilter ‣. The channel is created using https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/fltkernel/nf-fltkernel-fltcreatecommunicationport
PORT_STATE
InitializeServerPort(IN PUNICODE_STRING CommunicationPortName,
IN PFLT_FILTER Filter)
{
NTSTATUS status = STATUS_SUCCESS;
gServerPortState = PORT_STATE::UnInitialized;
PSECURITY_DESCRIPTOR SecurityDescriptor;
OBJECT_ATTRIBUTES ObjectAttributes;
PAGED_CODE();
DFLOG(ERROR,
"Shield>" __FUNCTION__ ": Trying opening port: '%wZ'.\n",
CommunicationPortName);
//
// Create communication descriptor.
//
status = FltBuildDefaultSecurityDescriptor(&SecurityDescriptor,
FLT_PORT_ALL_ACCESS);
if (!NT_SUCCESS(status)) {
DFLOG(
ERROR,
"Shield>" __FUNCTION__ " : Port is not initialized. Error "
"FltBuildDefaultSecurityDescriptor - %X.\n",
status);
gServerPortState = PORT_STATE::UnInitialized;
goto CleanUp;
}
InitializeObjectAttributes(&ObjectAttributes,
CommunicationPortName,
OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE,
NULL,
SecurityDescriptor);
status = FltCreateCommunicationPort(
Filter,
&gServerPort,
&ObjectAttributes,
NULL,
ShieldConnect, // Connect notify callback
ShieldDisconnect, // Disconnect notify callback.
ShieldMessage, // message notify callback.
MAX_CLIENTS);
if (!NT_SUCCESS(status)) {
DFLOG(ERROR,
"Shield>" __FUNCTION__ " : Port is not initialized. Error "
"FltCreateCommunicationPort - %X.\n",
status);
gServerPortState = PORT_STATE::UnInitialized;
goto CleanUp;
}
DFLOG(ERROR,
"Shield>" __FUNCTION__ ": opened server port handle 0x%p.\n",
gServerPort);
gServerPortState = PORT_STATE::Initialized;
CleanUp:
if (SecurityDescriptor)
FltFreeSecurityDescriptor(SecurityDescriptor);
return gServerPortState;
}
User-space app
How to connect a user-space app to communicate with minifilter? We opened a port with a name. Use the same name to connect to the port (minifilter). See the app directory in the repository.
voidShieldClient::connect(){ port_ = INVALID_HANDLE_VALUE; HRESULT hr = S_OK;# COMMUNICATION_IN_SYNC_MODE // // port in sync mode. We don't have to use Overlapped structure here. // TODO: Not sure about the performance. // PLOGI << "Connecting to " << portname_ << " in SYNC mode."; hr = FilterConnectCommunicationPort( portname_.c_str(), FLT_PORT_FLAG_SYNC_HANDLE, NULL, 0, NULL, &port_);# // port in async mode. This is the preferred way. Use with completion port. PLOGI << "Connecting to " << portname_ << " in ASYNC mode."; hr = FilterConnectCommunicationPort( portname_.c_str(), 0, NULL, 0, NULL, &port_);# if (FAILED(hr)) { PLOGW << "Failed to connect to Shield: Error: 0x" << std::hex << hr; connected_ = false; goto MAIN_EXIT; } completion_port_ = CreateIoCompletionPort(port_, NULL, 0, 2 /* 2 user threads */); if (NULL == completion_port_) { hr = HRESULT_FROM_WIN32(GetLastError()); PLOGW << "Error in creating completion port. Code: 0x" << std::hex << hr; goto MAIN_EXIT; } PLOGI << " ... Successfully connected."; connected_ = true;MAIN_EXIT: if (!connected_ && INVALID_HANDLE_VALUE != port_) { CloseHandle(port_); }}
Messaging
To keep it simple,
- Each messages has first two bytes of it designated as message code.
- Next 4 bytes are the size of the message, and rest is the message.