
In the
first part of the article, you got a brief idea about drivers in a privileged mode. It's time to delve into our sandbox.
Sandbox example: driver assembly and installation
The core of our sandbox is a mini filter driver. You can find its source code in src \ FSSDK \ Kernel \ minilt. I assume that you are using the WDK 7.x kit to build the driver. To do this, you must run the appropriate environment, say, Win 7 x86 checked, and go to the directory with the source code. Just type “build / c” on the command line when running in the development environment and you will get the built drivers. To install the driver, just copy the * .inf file to the folder containing the * .sys file, navigate to this directory using Explorer and use the context menu on the * .inf file, where you select the "Install" option, and the driver will be installed. I recommend that you conduct all the experiments inside the virtual machine, VMware will be a good choice. Please note that 64-bit versions of Windows will not load unsigned drivers. To be able to run the driver in VMware, you must enable the privileged mode debugger in the guest OS. This can be done by executing the following cmd commands running as administrator:
1. bcdedit / debug on
2. bcdedit / bootdebug on
')
Now you must assign the named pipe as a serial port for VMware and setup using WinDBG installed on your machine. Then you can connect to VMware with a debugger and debug your drivers.
You can find detailed information on configuring VMware for debugging drivers in
this article .
Sandbox Example: Architecture Overview
Our simple sandbox consists of three modules:
• a privileged mode driver that implements virtualization primitives;
• a user mode service that receives messages from the driver and can change the behavior of the file system by changing the parameters of the received notifications from the driver;
• the fsproxy intermediate library that helps the service communicate with the driver.
Let's start the consideration of our simplest sandbox driver in privileged mode.
Example sandbox: write driver
While normal applications, as a rule, begin their execution in WinMain (), drivers do this with the DriverEntry () function. Let's start learning the driver with this feature.
NTSTATUS DriverEntry ( __in PDRIVER_OBJECT DriverObject, __in PUNICODE_STRING RegistryPath ) { OBJECT_ATTRIBUTES oa; UNICODE_STRING uniString; PSECURITY_DESCRIPTOR sd; NTSTATUS status; UNREFERENCED_PARAMETER( RegistryPath ); ProcessNameOffset = GetProcessNameOffset(); DbgPrint("Loading driver");
DriverEntry has several key features. First, this function registers the driver as a minifler with the FltRegisterFilter () function:
status = FltRegisterFilter( DriverObject, &FilterRegistration, &MfltData.Filter );
It is an array of pointers for handlers of certain operations that it wants to intercept in FilterRegistration, and receives an instance of the filter in MfltData.Filter in case of successful registration. FilterRegistration is declared as follows:
const FLT_REGISTRATION FilterRegistration = { sizeof( FLT_REGISTRATION ),
As you can see, the structure contains a pointer to an array of event handlers (Callbacks). This is an analogue of dispatching procedures in “outdated” drivers. In addition, this structure contains pointers to some other auxiliary functions — we will describe them later. Now we’ll focus on the handlers described in the Callbacks array. They are defined as follows:
const FLT_OPERATION_REGISTRATION Callbacks[] = { { IRP_MJ_CREATE, 0, FSPreCreate, NULL }, { IRP_MJ_CLEANUP, 0, FSPreCleanup, NULL}, { IRP_MJ_OPERATION_END} };
You can see a detailed description of the FLT_OPERATION_REGISTRATION structure in MSDN. Our driver registers only two handlers - FSPreCreate, which will be called each time the IRP_MJ_CREATE request is received, and FSPreCleanup, which, in turn, will be called each time the IRP_MJ_CLEANUP is received. This request will arrive when the last file handle is closed. We can (and will) change the input parameters and send the modified request down the stack, so that the lower filters and the file system driver will receive the modified request. We could register the so-called post-notifications coming at the end of the operation. For this, the null pointer that follows the pointer to FSPreCreate can be replaced by a pointer to the corresponding post-processor. We need to terminate our array with IRP_MJ_OPERATION_END. This is a “fake” operation, which marks the end of an array of event handlers. Note that we do not have to provide a handler for each IRP_MJ_XXX operation, as we would have to do for “traditional” filter drivers.
The second important thing that our DriverEntry () performs is the creation of a mini filter port. It is used to send notifications from the user-level service and receives responses from it. This is done using the FltCreateCommunicationPort () operation:
status = FltCreateCommunicationPort( MfltData.Filter, &MfltData.ServerPort, &oa, NULL, FSPortConnect, FSPortDisconnect, NULL, 1 );
Pointers to the FSPortConnect () and FSPortDisconnect () functions occur respectively when the user mode service is connected and disconnected from the driver.
And the last thing to do is to start filtering:
status = FltStartFiltering( MfltData.Filter );
Note that a pointer to the filter instance returned by FltRegisterFilter () is passed to this procedure. From this point on, we will start receiving notifications about IRP_MJ_CREATE & IRP_MJ_CLEANUP requests. Along with notifications about file filtering, we also ask the OS to inform us when a new process is loaded and unloaded using this function:
PsSetCreateProcessNotifyRoutine(CreateProcessNotify,FALSE);
CreateProcessNotify is our notification handler for process creation and termination.
Sandbox Example: FSPreCreate Handler
Real magic is born here. The essence of this function is to tell which file is open and which process initiated the opening. This data is sent to the user mode service. The service (service) provides a response in the form of a command either to deny access to the file, or to redirect the request to another file (this is exactly how the sandbox works), or to allow the operation to be performed. The first thing that happens in this case is to check the connection with the user mode service through the communication port (communication port) that we created in DriverEntry (), and, in the case of no connection, no further action will occur. We also check whether the service (service) is the source (initiator) of the request - we do this by checking the UserProcess field of the globally allocated MfltData structure. This field is populated in the PortConnect () subroutine, which is called when the user mode service (service) connects to the port. Also, we don’t want to deal with paging requests. In all these cases, we return the return code FLT_PREOP_SUCCESS_NO_CALLBACK, meaning that we have completed the processing of the request and we do not have a postoperative handler. Otherwise, we will return FLT_PREOP_SUCCESS_WITH_CALLBACK. If this were a “traditional” filter driver, then we would have to deal with stack frames that I mentioned earlier, the IoCallDriver procedure, etc. In the case of mini-filters, the transfer request is quite simple.
If we want to process the request, the first thing we need to do is to fill in the structure that we want to pass to the user mode - MINFILTER_NOTIFICATION. It is completely customizable. We pass the type of operation (CREATE), the file name on which the request was made, the identification number (PID) of the process and the name of the source process. It is worth paying attention to how we figure out the name of the process. In fact, this is an undocumented way to get the name of a process that is not recommended for use in commercial software. Moreover, it does not work with x64-versions of Windows. In commercial software, you will transfer only the process ID (identification number) of the process to user mode, and if you need an executable name, you can get it using the user mode API. For example, you can use the OpenProcess () API to open a process by its ID and then call the GetProcessImageFileName () API to get the name of the executable file. But to simplify our sandbox, we get the process name from the undocumented field of the PEPROCESS structure. To find out the name offset (relative address), we consider that the system has a process called "SYSTEM". We scan the process containing the given name in the PEPROCESS structure, then use the detected name offset when analyzing some other process. For more information, see the SetProcessName () function.
We obtain the file name from the “target” file for which a request was received (for example, a request to open a file) using two functions - FltGetFileNameInformation () and FltParseFileNameInformation ().
After filling in the MINFILTER_NOTIFICATION structure, we send it to user mode:
Status = FltSendMessage( MfltData.Filter, &MfltData.ClientPort, notification, sizeof(MINFILTER_NOTIFICATION), &reply, &replyLength, NULL );
And we get the answer in the reply variable. If we are asked to cancel the operation, then the action is simple:
if (!reply.bAllow) { Data->IoStatus.Status = STATUS_ACCESS_DENIED; Data->IoStatus.Information = 0; return FLT_PREOP_COMPLETE; }
The key points here are the following: first, we change the return code by returning FLT_PREOP_COMPLETE. This means that we will not send the request down the driver stack, as, for example, we would do when calling IoCompleteRequest () from the “traditional” driver without calling IoCallDriver (). Second, we fill in the IoStatus field in the request structure. Set the error code STATUS_ACCESS_DENIED and the Information field "to zero". As a rule, the number of bytes sent during the operation is recorded in the Information field, for example, the number of bytes copied is recorded during the copy operation.
If we want to redirect the operation, it looks different:
if (reply.bSupersedeFile) {
The key here is the call to IoReplaceFileObjectName
Status = IoReplaceFileObjectName(Data->Iopb->TargetFileObject, reply.wsFileName, wcslen(reply.wsFileName)*sizeof(wchar_t));
This function changes the file name of the transferred file object (FILE_OBJECT) —the I / O manager object, which is an open file. The manual name is replaced as follows: after freeing up the memory with the field containing the name, we allocate a buffer and copy the new name there. But since the appearance of the IoReplaceFileObjectName function in Windows 7, it is strongly recommended to be used instead of a buffer. In the author's personal project (Cybergenic Shade Sandbox product), which is compatible with all operating systems - from XP to Windows 10, I’m just manually managing buffers if the driver works on outdated OSs (up to Win 7). After the file name is changed, we fill in the data with the special status STATUS_REPARSE, and fill the Information field with the value IO_REPARSE. Next, we return the status FLT_PREOP_COMPLETE. REPARSE means that we want the I / O manager to restart the original request (with new parameters), as in the case when the application (the initiator of the request) initially asked to open a file with a new name. We also need to call FltSetCallbackDataDirty () - this API function is needed every time we change the data structure, except when we also change IoStatus. In fact, we are really changing IoStatus here, so we call this function to make sure that the I / O manager has been notified of these changes.
Sandbox Example: Name Providers
As we change the file names, our driver must contain the implementation of the name provider handlers that are called when the file name is requested or when the file name is "normalized". These handlers are FSGenerateFileNameCallback and FSNormalizeNameComponentCallback (Ex).
Our virtualization method is based on the “restarting” of the IRP_MJ_CREATE request (we pretend that the virtualized names are REPARSE_POINTS), and the implementation of these handlers is a fairly simple matter, which is described in detail
here .
User Mode Service
User mode is in the filewall project (see the source code for the article) and communicates with the driver. Key functionality is represented by the following function:
bool CService::FS_Emulate( MINFILTER_NOTIFICATION* pNotification, MINFILTER_REPLY* pReply, const CRule& rule) { using namespace std;
It is called when the driver decides to redirect the file name. The algorithm here is very simple: if the file placed in the sandbox already exists, the request is simply redirected by filling the variable pReply with the new file name — the name in the sandbox folder. If such a file does not exist, the original file is copied and only after that the original request is changed to point to the new copied file. How does the service know that the request needs to be redirected for a specific process? This is done using rules — see the implementation of the CRule class. Rules (usually the only rule in our demo service) are loaded with the LoadRules () function.
bool CService::LoadRules() { CRule rule; ZeroMemory(&rule, sizeof(rule)); rule.dwAction = emulate; wcscpy(rule.ImageName,L"cmd.exe"); rule.GenericNotification.iComponent = COM_FILE; rule.GenericNotification.Operation = CREATE; wcscpy(rule.GenericNotification.wsFileName,L"\\Device\\Harddisk*\\*.txt"); wcscpy(rule.SandBoxRoot,L"C:\\Sandbox"); GetRuleManager()->AddRule(rule); return true; }
This function creates a rule for the process (or processes) called “cmd.exe” and redirects all operations with * .txt files to the sandbox. If you run cmd.exe on the PC where our service is running, it isolates your operations in the sandbox. For example, you can create a txt file from cmd.exe, say, by running the “dir> files.txt” command, files.txt will be created in C: /sandbox//files.txt, where is the current directory for cmd.exe. If you add an existing file from cmd.exe, you will get two copies of it - the unchanged version in the original file system and modified in C: / Sandbox.
Conclusion
In this article we covered the main aspects of creating a sandbox. However, some of the details and problems remained unaffected.
For example, you cannot control the rules from user mode, as this significantly slows down the PC. This approach is quite simple in terms of implementation and is possible for use in educational purposes, but in no case should not be used in commercial software.
Another limitation is the structure of notifications / responses with predefined buffers for file names. These buffers have two drawbacks: first, their size is limited and some files deep in the file system will be processed incorrectly. Secondly, a substantial part of the kernel-mode memory allocated for the file name is not used in most cases. Thus, a more reasonable strategy for memory allocation in commercial software should be applied.
Another disadvantage is the widespread use of the FltSendMessage () function, which is rather slow. It should be used only when the user-mode application must show the user a request, and the user must allow or reject the operation. In this case, this function can be used, since interaction with a person is much slower than the execution of any code. But if the program responds automatically, you should avoid excessive interaction with the user mode code.
References to sources
»
Original article»
Source code of the sandbox in question