Logo
blank Skip to main content

Development of a Virtual Disk for Windows: Approach, Tips, Code Samples

This article would be useful for Windows developers, as it explains how to create a virtual disk for the Windows system.

Windows virtual disks are developed using kernel mode drivers. The Windows and disks section will provide you necessary information about Windows disk drivers.

The widely known open-source project named FileDisk will also provide you the information about ways for Windows virtual disk implementation.

As Apriorit frequently works on the advanced driver development projects, virtual disk technologies are an important part of our profile. The main differentiator of the Virtual Disk Development Kit, provided in this article, is the fact that it serves requests to the virtual disk in the user mode in contrast to FileDisk, which processes them in the kernel mode.

Advantages of processing requests in the user mode

Processing requests in the user mode is rather useful if we’ve already developed user mode code that gives us access to the data source (like disk image in memory, remote disk, cash register), and it’s difficult to port this code to the kernel mode; or we have no source code, for example during network access or if we use a specific encryption library.

We used SparseFile (file-container with data; in the case of data accumulation, it’ll size up to the maximum size) as an example of such user mode library.

Used technologies

  • C++. The whole project is developed in C++ with the utilizing of the exceptions (both kernel mode and user mode parts). There are conflicting opinions concerning the use of C++ for driver development. You can learn more details on this subject here
  • CppLib, provides the possibility to develop drivers in C++
  • STLPort, which is a standard library
  • BOOST

Project structure

There are several projects in the solution:

  • CoreMntTest (user mode, executable) – makes and mounts a disk image using the CoreMnt_user code.
  • CoreMnt_user (user mode, library) – gets requests to virtual disks from the CoreMnt driver and processes them.
  • UsrUtils (user mode, library) – uses DeviceIoControl to contain auxiliary code of interaction with drivers.
  • CoreMnt (kernel mode, executable) – implements OS requirements for the disks; transforms requests; sends them to CoreMnt_user.
  • drvUtils (kernel mode, headers only library) – auxiliary kernel mode code, for example, synchronization tools.

The following scheme shows the projects’ relations.

Projects relation structure

Structure of the project directory

  .bin – folder with binary files

  .lib – folder with library files

  .obj – folder with object files

  .src – folder with source files

     |

     |-> .CoreMnt       – Kernel mode driver.

     |-> .CoreMnt_user  – User mode mount library.

     |-> .CoreMntTest   – User mode mount test.

     |-> .drvCppLib     – Kernel Library to develop driver in C++.

     |-> .drvUtils      – Kernel Library with utils for kernel mode projects.

     |-> .mnt_cmn       – Files that are shared between projects.

     |-> .STLPort       – Directory with STLPort 4.6 ported for

            utilizing in windows drivers.

     |-> .usrUtils      – Win32 Library with utils for user mode projects.

Read also:
Virtualization in Software Testing: Advantages and Disadvantages

Windows and disks

You can simply skip this section if you have any experience in Windows driver development. We’re happy to appease the rest – this process is very simple. Windows sends write or read request to the disk. The disk returns read data or error code. That’s all.

Of course, there are a few nuances.

We’ll discuss a simple scheme of processing the requests to the disk. So, what happens after an application calls, for example, the ReadFile function?

First, a file system driver (for example ntfs.sys) receives the read file request. The scheme below illustrates this process:

Read file request

File system driver defines the exact location of the requested file (the offset) and creates the read disk request. The file can be divided into several parts, located at different places on the disk. Thus, a few requests will be created. Our virtual disk driver will receive requests like that from the file system driver. Virtual disk can also be implemented at the file system level. You can learn more about this process from windows file system driver tutorial.

Let’s consider the technical terms we use:

  • IRP (I/O Request Packet) is a Windows kernel structure that stores parameters of the requests. In order to read data from a device, we should indicate the request type, the buffer to read data to, size and offset. We can assume that IRP is a request to some device. Talking about IRP in this article, we always mean request. You can learn more about it in Microsoft documentation.
  • STATUS_PENDING is a special return code that alerts the request initiator that IRP can’t be served now and will be served later on. There’s a termination event for this case, the device will set it when it completes the request serving. Below we’ll describe the usage of this return code.
  • Device is a Windows kernel object that represents any device. It contains information about this device, for example, its name. It also contains DeviceExtension.
  • DeviceExtension is a Device structure field that can be used by the device creator in his own way. We’ll consider the usage of DeviceExtension below.

Related services

C/C++/C#/Obj-C Programming

How to implement Windows virtual disk

The solution consists of a driver (CoreMnt.sys) and an application (CoreMntTest.exe). The general scheme is shown below:

Windows virtual disk scheme

Driver helps to mount disks. Application creates a data source and mounts it as a disk via the driver service. After receiving IRP, driver processes them in the user mode and returns the result. Below you can find the general scheme of driver’s work:

Driver work scheme

CoreMntTest.exe processes requests from the operating system to the virtual disks. Its structural scheme is shown below:

CoreMntTest.exe application structure

Now let’s consider the source code stage by stage.

1. Initialization

Now we’re going to start the CoreMnt driver using the following command:

ShellScript
c:\>sc start CoreMnt

In DriverEntry, we should create the management device as an access point for CoreMntTest from the user mode:

C
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
                     IN PUNICODE_STRING RegistryPath)
{
...
    NTSTATUS status;
    status = IoCreateDevice(DriverObject,     // pointer on DriverObject
                            0,                // additional size of memory
                            &gDeviceName,     // pointer to UNICODE_STRING
                            FILE_DEVICE_NULL, // Device type
                            0,                // Device characteristic
                            FALSE,            // "Exclusive" device
                            &gDeviceObject);  // pointer do device object
    if (status != STATUS_SUCCESS)
        return STATUS_FAILED_DRIVER_ENTRY;
    status = IoCreateSymbolicLink(&gSymbolicLinkName,&gDeviceName);
    if (status != STATUS_SUCCESS)
        return STATUS_FAILED_DRIVER_ENTRY;

Then we should register the driver request handler. We’re going to use one handler for all types of requests:

C
for (size_t i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; ++i) 
    DriverObject->MajorFunction[i] = IrpHandler;

Finally, we’re going to create MountManager:

C
    gMountManager = new MountManager(DriverObject);
    return STATUS_SUCCESS;
}

2. Virtual disk mounting

This is the stage where we run the CoreMntTest.exe application. It sends the CORE_MNT_MOUNT_IOCTL management message to the driver:

C
CORE_MNT_MOUNT_REQUEST request;
request.totalLength = totalSize;
request.mountPojnt = mountPoint;
DWORD bytesWritten = 0;
CORE_MNT_MOUNT_RESPONSE response;
if(!m_coreControl.DeviceIoGet(CORE_MNT_MOUNT_IOCTL, 
                              &request, 
                              sizeof(request),
                              &response,
                              sizeof(response),
                              &bytesWritten))
{
    throw std::exception(__FUNCTION__" DeviceIoGet failed.&);
}

The DispatchMount function deserializes request parameters and calls MountManager::Mount:

C
      if(inputBufferLength < sizeof(CORE_MNT_MOUNT_REQUEST) || 
          outputBufferLength < sizeof(CORE_MNT_MOUNT_RESPONSE) )
      {
          throw std::exception(__FUNCTION__" buffer size mismatch");
      }
      DISK_PROPERTIES diskProperties;
      CORE_MNT_MOUNT_REQUEST * request = (CORE_MNT_MOUNT_REQUEST *)buffer;
      diskProperties.totalLength.QuadPart = request->totalLength;
      CORE_MNT_MOUNT_RESPONSE * response = 
          (CORE_MNT_MOUNT_RESPONSE *)buffer;
response->deviceId = gMountManager->Mount(&diskProperties);

In MountManager::Mount, we make and save the MountedDisk object. MountedDisk includes LogicIrpDispatcher, whose constructor creates a disk device. The operating system will send requests to this device:

C
LogicIrpDispatcher::LogicIrpDispatcher(PDISK_PROPERTIES diskProperties, 
                                       PDRIVER_OBJECT   DriverObject,
                                       MountManager* mountManager)
{
...
    //create device
    status = IoCreateDevice(DriverObject,sizeof(InThreadDeviceExtension),
        &deviceName,FILE_DEVICE_DISK,
        0,
        FALSE,&deviceObject_);
    if (!NT_SUCCESS(status))
        throw std::exception(__FUNCTION__" can't create device.");

After creating a device, we should initialize DeviceExtension. We’ll use it for storing device identifier. Therefore, after getting IRP, we’ll simply find the corresponding MountedDisk instance:

C
InThreadDeviceExtension* devExt = 
    (InThreadDeviceExtension*)deviceObject_->DeviceExtension;
memset(devExt, 0, sizeof(InThreadDeviceExtension));
devExt->mountManager = mountManager;
devExt->deviceId = diskProperties->deviceId;

Therefore, MountManager has created the MountedDisk instance and saved it to the container. Then weєre finishing the initialization stage in the user mode. For each disk, we create a thread where all disk requests will be served. A thread sends IOCTL RequestExchange to the driver and proceeds to the request awaiting mode:

C
while(true)
{
    int type = 0;
    int size = 0;
    __int64 offset = 0;
    drvCtrl->RequestExchange(deviceId, lastType, lastStatus, lastSize, &dataBuf[0], dataBuf.size(),
                             &type, &size, &offset);
    //do requested operation
    DispatchImageOperation(image, type, size, offset, &dataBuf[0], dataBuf.size(), &lastStatus);
    lastType = type;
    lastSize = size;
}

Performance note: surely, processing requests in one thread is the «bottle neck». A thread pool should certainly be there in the real project.

3. Requests processing

Now our virtual disk is able to serve requests. We’ll describe the complete chain of request processing. Everything starts from the IrpHandler function registered by our driver as the IRP processing procedure. Now we receive the device identifier from DeviceExtension (we saved it there during the initialization stage) and transmit IRP to MountManager:

C
NTSTATUS IrpHandler( IN PDEVICE_OBJECT fdo, IN PIRP pIrp )
{
...
        InThreadDeviceExtension* devExt = 
            (InThreadDeviceExtension*)fdo->DeviceExtension;
        return gMountManager->DispatchIrp(devExt->deviceId, pIrp);

MountManager receives IRP, finds the corresponding MountedDisk by the device identifier, and redirects IRP to it. The code below makes decision whether it’s possible to process this request at once or it should be processed in the user mode:

C
NTSTATUS MountedDisk::DispatchIrp(PIRP irp)
{
    IrpParam irpParam(0,0,0,0);
    irpDispatcher_.getIrpParam(irp, &irpParam);
    if(irpParam.type == directOperationEmpty)
    {
...
            irpDispatcher_.dispatch(irp);
...
        NTSTATUS status = irp->IoStatus.Status;
        IoCompleteRequest(irp, IO_NO_INCREMENT);
        return status;
    }
    IoMarkIrpPending( irp );
    irpQueue_.push(irp);
    return STATUS_PENDING;
}

In the case of IRP_MJ_READ or IRP_MJ_WRITE, it should be processed in the user mode. All other requests can be processed by the driver itself. For example, in the case of IOCTL_DISK_GET_LENGTH_INFO, driver knows the disk size as well as that this size can’t be changed. In LogicIrpDispatcher::dispatchIoctl, you can find the complete list of requests, which Windows can send to the disk.

The thread, which serves this disk, chooses requests from the list:

C
void MountedDisk::RequestExchange(UINT32 lastType, UINT32 lastStatus, UINT32 lastSize, char* buf, UINT32 bufSize,
                     UINT32 * type, UINT32 * length, UINT64 * offset)
{
...
    NTSTATUS status = KeWaitForMultipleObjects(sizeof(eventsArray)/sizeof(PVOID), eventsArray, WaitAny, 
        Executive, KernelMode, FALSE, NULL, 0);
...
            IrpParam irpParam(0,0,0,0);
            irpDispatcher_.getIrpParam(lastIrp_, &irpParam);
            *type = irpParam.type;
            *length = irpParam.size;
            *offset = irpParam.offset;

If it’s IRP_MJ_WRITE, we should copy data to write to the buffer. After that, the solution passes this buffer to the user mode code:

C
if(*type != directOperationEmpty 
    && opType2DirType(directOperationTypes(*type)) == directOperationWrite)
{
    IrpParam irpParam(0,0,0,0);
    irpDispatcher_.getIrpParam(lastIrp_, &irpParam);
    if(irpParam.buffer)
        memcpy(buf, irpParam.buffer, *length);

After exiting the RequestExchange function, we get to the DispatchImage (cycle of requests processing) again:

C
while(true)
{
    int type = 0;
    int size = 0;
    __int64 offset = 0;
    drvCtrl->RequestExchange(deviceId, lastType, lastStatus, lastSize, &dataBuf[0], dataBuf.size(),
                             &type, &size, &offset);
    //do requested operation
    DispatchImageOperation(image, type, size, offset, &dataBuf[0], dataBuf.size(), &lastStatus);
    lastType = type;
    lastSize = size;
}

The type, size, and offset variables now contain the new request for processing. The DispatchImageOperation function will manage it:

C
void DispatchImageOperation(IImage * image, 
                            int type, int size, __int64 in_offset, char* buf,
                            int bufsize,
                            int* status)
{
    switch(type)
    {
...
    case directOperationRead:
        {
           image->Read((char*)buf, in_offset, size);
           *status = 0;
            break;
        }
    case directOperationWrite:
        {    
            image->Write((const char*)buf, in_offset, size);
            *status = 0;
            break;
        }

After serving the request, the RequestExchange function will be called again and the thread will proceed to the mode of waiting for a new request.

4. Virtual disk unmounting

It starts in the user mode from the UnmountImage function calling. Below you can find the code that checks whether the disk is being used now or not:

C
void UnmountImage(int devId, wchar_t mountPoint, DriverControl * drvCtrl)
{
...
        if (!DeviceIoControl(hVolume,FSCTL_LOCK_VOLUME,NULL,
                             0,NULL,0,&BytesReturned,NULL))
        {
            throw std::exception("Unable to lock logical drive");
        }
        else if (!DeviceIoControl(hVolume,FSCTL_DISMOUNT_VOLUME,
                                  NULL,0,NULL,0,&BytesReturned,NULL))
        {
            throw std::exception("Unable to dismount logical drive");
        }
        else if (!DeviceIoControl(hVolume,FSCTL_UNLOCK_VOLUME,NULL,
                                  0,NULL,0,&BytesReturned,NULL))
        {
            throw std::exception("Unable to unlock logical drive");
        }

Then we eliminate the connection between the mounting point and our device:

C
if (UndefineLogicDrive(mountPoint))
    throw std::exception("Unable to undefine logical drive");

After that, we send a message to all components that store the list of disks in the system (for example, explorer.exe or any other file manager):

C
 SHChangeNotify(SHCNE_DRIVEREMOVED, SHCNF_PATH, root, NULL);

Finally, we notify our driver that it’s time to delete the device:

C
 drvCtrl->Unmount(devId);
}

MountManager::Unmount simply removes the corresponding MountedDisk from the container. It causes the calling of its destructor:

C
MountedDisk::~MountedDisk()
{

We set the stop event for the thread of requests processing for this disk:

C
 stopEvent_.set();

Then we terminate all IRPs that were not processed and now are in a queue:

C
    if(lastIrp_)
        CompleteLastIrp(STATUS_DEVICE_NOT_READY, 0);
    while(irpQueue_.pop(lastIrp_))
        CompleteLastIrp(STATUS_DEVICE_NOT_READY, 0);
}

The thread of requests serving, which has been awaiting in MountedDisk::RequestExchange, reacts on stopEvent_ set and throws an exception:

C
NTSTATUS status = KeWaitForMultipleObjects(sizeof(eventsArray)/sizeof(PVOID), eventsArray, WaitAny, 
    Executive, KernelMode, FALSE, NULL, 0);
if(status != STATUS_SUCCESS)
{
    throw std::exception("MountedDisk::RequestExchange - mount stop.");
}

We are catching the thrown exception in the DispatchException function catch block, and returning STATUS_UNSUCCESSFUL to the user mode:

C
NTSTATUS DispatchExchange(PVOID buffer, ULONG inputBufferLength, ULONG outputBufferLength)
{
    try
    {
...
        gMountManager->RequestExchange(request->deviceId, 
                                       request->lastType, 
                                       request->lastStatus, 
                                       request->lastSize, 
                                       request->data, 
                                       request->dataSize, 
                                       &response.type,
                                       &response.size, 
                                       &response.offset);
...
    }
    catch(const std::exception & ex)
    {
        KdPrint((__FUNCTION__" %s\n", ex.what()));
        return STATUS_UNSUCCESSFUL;
    }
}

The returned error state is processed in the DriverControl::RequestExchange function by the user mode code and throws an exception:

C
void DriverControl::RequestExchange(int deviceId, 
                                    int lastType, 
                                    int lastStatus, 
                                    int lastSize, 
                                    char * data, 
                                    int dataSize,
                                    int *type, 
                                    int *size, 
                                    __int64 * offset)
{
...
    if(!m_coreControl.DeviceIoGet(CORE_MNT_EXCHANGE_IOCTL, 
                                  &request, 
                                  sizeof(request), 
                                  &response, 
                                  sizeof(response), 
                                  &bytesWritten))
    {
        throw std::exception(__FUNCTION__" DeviceIoGet failed.");
    }
...
}

This exception is caught in the SyncMountmanager::mountDispatchThread catch block:

C
void SyncMountManager::mountDispatchThread(void* pContext)
{
...
    try
    {
        DispatchImage(dispContext->devId, 
                      image->GetMountPoint(),
                      dispContext->image, 
                      dispContext->mountManager->GetDriverControl());
    }
    catch(const std::exception& ex)
    {
        dispContext->mountManager->OnUnmount(dispContext->image, ex.what());
    }
...
}

Therefore, it’ll lead to requests processing thread termination and the Image destructor calling.

Related services

Kernel and Driver Development

Steps to build a Windows virtual disk Solution

1. Windows virtual disk building

  1. Install Windows Driver Developer Kit 2003
  2. Set global environment variable “BASEDIR” to the path to the installed DDK.

Computer -> Properties -> Advanced -> Environment variables ->System Variables -> New
Like this: BASEDIR -> c:winddk3790

  1. Download and install boost (tested with 1.40 version)
  2. Set global environment variable “BOOST” to the path to the installed boost. Restart your computer after this.
  3. Build the solution by means of Visual Studio 2008.

2. Steps to test the Solution

  1. Build the solution by means of described instructions.
  2. Copy CoreMnt.sys to %windir%system32drivers.
CoreMnt.sys
  1. Register the driver in the system:
sc create CoreMnt type=  kernel binPath= system32driversCoreMnt.sys
Driver registration
  1. Start the driver using the command:
sc start CoreMnt
  1. Start CoreMntTest.exe.

If everything was OK, CoreMntTest.exe displays the message:

Starting CoreMntTest.exe
  Image was mounted. Press  any key for unmount.

Disk Z appears in the system.

Windows virtual disk

Now we’re able to format it.

Windows virtual disk

The tst_img file appears in the directory along with CoreMntTest.exe.

3. Supported Windows versions

  • Windows XP SP2

Read also:
How to Use a Virtual CPU to Emulate and Analyze Suspicious Code Execution at Runtime

Conclusion

In this article, we described just one of the approaches for creating a Windows virtual disk. We can call it logical virtual disk. You can also consider the following approaches:

  • File system virtual disk. Driver receives the CreateFile, ReadFile, WriteFile requests and so on.
  • Physical virtual disk. The approach of physical virtual disk is semantically similar to the logical one but it has some differences on the driver level: there’s another format of requests to the virtual disk as well as PNP disk device.

Download sample sources

Tell us about your project

Send us a request for proposal! We’ll get back to you with details and estimations.

By clicking Send you give consent to processing your data

Book an Exploratory Call

Do not have any specific task for us in mind but our skills seem interesting?

Get a quick Apriorit intro to better understand our team capabilities.

Book time slot

Contact us