Creating and Using IOCTLs

June 7th, 2008 - Fernando Roberto

Last week I had received the following question from a reader:

“Is it possible for my application to send a customized IOCTL (made by myself) to a driver, and that driver being able to recognize it with no problems?”

The short-term answer for this question is: “Yes, and good luck!”. However, what is the  fun a blog has  if we can not talk more about this subject and even give a simple example? You’re assumed by this you’re already aware of some basic concepts, such as compiling, installing and testing drivers; but, if you have not known this yet, do not worry, just read this post.

The driver that used to calculate

Let’s create a sample driver that uses the same idea my friend Heldai had already used to illustrate IOCTLs use when I was still learning how to make blue screens. At today’s example, we are making a driver that adds two numbers contained into a structure that will be received via an IOCTL.

With no more yada yada, I think we can start by defining the structure. Like the kindergarten teacher had taught us, we are just creating a header file that is included by both, the application project and the driver project. This header file should not include any other specific one from User Mode nor Kernel Mode. That means, it cannot include files like Windows.h nor Ntddk.h. All this is aleady done, tested and able to be built in a project available for downloading at the end of the post.

typedef struct _KERNEL_MATH_REQUEST
{
    //-f--> I can define this structure in any way I
    //      want to.
    //      These are the two numbers to be added to it.
    ULONG   x;
    ULONG   y;
 
} KERNEL_MATH_REQUEST, *PKERNEL_MATH_REQUEST;
 
 
typedef struct _KERNEL_MATH_RESPONSE
{
    //-f--> To the newbies: I know it's possible to be done it using
    //      just one structure. But I'm only doing in that
    //      way for a better illustration.
    ULONG   r;
 
} KERNEL_MATH_RESPONSE, *PKERNEL_MATH_RESPONSE;

Creating the IOCTL

IOCTL is more than just a number to identify the desired operation. It consists of a bit mask interpreted by Windows. This mask is defined as it is shown below. You can get more details at this macro on this link.

To define an IOCTL, we use the CTL_CODE macro which has its parameters shown below:

#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)

Let’s define our IOCTL, as it is shown below:

//-f--> Here, we define IOCTL for our driver.
#define IOCTL_ADD_THESE_TWO_NUMBERS \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
 
//-f--> Although this driver haa the word "Sum" as part of its name,
//      I've decided to put an IOCTL for subtraction, just as an example.
#define IOCTL_SUBTRACT_THESE_TWO_NUMBERS \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)

Now, let’s take a look at every used parameter and talk a litle about it.

FILE_DEVICE_UNKNOWN – If you take a look at the DriverEntry routine, you can notice this is the same device type used at the call for the IoCreateDevice() routine.

/****
***     DriverEntry
**
**      That's the driver entry point.
**      "A knife among the teeth and some blood in the eyes".
*/
extern "C" NTSTATUS
DriverEntry(__in PDRIVER_OBJECT     pDriverObj,
            __in PUNICODE_STRING    pusRegistryPath)
{
    UNICODE_STRING  usDeviceName = RTL_CONSTANT_STRING(L"\\Device\\KernelSum");
    UNICODE_STRING  usSymbolicLink = RTL_CONSTANT_STRING(L"\\DosDevices\\KernelSum");
    NTSTATUS        nts;
    PDEVICE_OBJECT  pDeviceObj;
 
    //-f--> Setting the unload routine to allow the 
    //      driver to be dynamincaly unloaded.
    pDriverObj->DriverUnload = OnDriverUnload;
 
    //-f--> The Open, Cleanup e Close routines behave in the same way.
    //      So, through the minimum effort law use, they will be the same one.
    //      Besides this one, we must handle the DeviceControl which is
    //      responsible for dealing with the received IOCTLs.
    pDriverObj->MajorFunction[IRP_MJ_CREATE] =
    pDriverObj->MajorFunction[IRP_MJ_CLEANUP] =
    pDriverObj->MajorFunction[IRP_MJ_CLOSE] = OnCreateCleanupClose;
    pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = OnDeviceIoControl;
 
    //-f--> Here we are creating the driver control device. Notice that we used
    //      FILE_DEVICE_UNKNOWN as the device type. This same one is used
    //      at the CTL_CODE macro. See: IoCtl.h
    nts = IoCreateDevice(pDriverObj,
                         0,
                         &usDeviceName,
                         FILE_DEVICE_UNKNOWN,
                         0,
                         FALSE,
                         &pDeviceObj);
 
    //-f--> Is everything fine so far? OK...
    if (!NT_SUCCESS(nts))
    {
        ASSERT(FALSE);
        return nts;
    }
 
    //-f--> Here we are creating a symbolic link so that, the application
    //      can get a handle for the control device.
    nts = IoCreateSymbolicLink(&usSymbolicLink,
                               &usDeviceName);
 
    //-f--> Is it all right?
    if (!NT_SUCCESS(nts))
    {
        //-f--> Oops... I think my pant is sort of wet...
        IoDeleteDevice(pDeviceObj);
 
        ASSERT(FALSE);
        return nts;
    }
 
    //-f--> Great, that is it!
    return STATUS_SUCCESS;
}

The secong CTL_CODE parameter Function, receives a 0x800 number, since numbers below this one are reserved for Microsoft.

METHOD_BUFFERED – indicates to the I/O Manager the driver will receive an input buffer copy passed to DeviceIoControl function. I/O Manager allocates a system buffer, which means in kernel space, that it is big enough to fit both the input and output data. When the DeviceIoControl function is called, the I/O Manager allocates the system buffer and copies the input buffer inside it. The driver receives the IRP, gets the input parameters, processes them and writes the output data at the same system buffer. When the IRP is completed, the I/O Manager copies the output data from the system buffer to the output buffer provided by the application.

Besides the Buffered method, there are still the Direct I/O and Neither methods. For our currently example, the Buffered method is great. I am owing a post to you that talks about how to use every one of the other methods.

FILE_ANY_ACCESS – That indicates the handle doesn’t need to be opened with any special kind of access to allow the IOCTL to be excetuted.

From the application to the driver

To send an IOCTL to the driver, you need to use the DeviceIoControl function as it is shown at the example below. This is a very simple program that shows its use.

/****
***     main
**
**      I hope all of you have already known that this is the entry
**      point of the application. Otherwise you might be
**      precipitated on reading a Windows driver blog.
**
*/
int __cdecl main(int argc, CHAR* argv[])
{
    HANDLE                  hDevice;
    DWORD                   dwError = ERROR_SUCCESS,
                            dwBytes;
    KERNEL_MATH_REQUEST     Request;
    KERNEL_MATH_RESPONSE    Response;
 
    printf("Opening \\\\.\\KernelSum device...\n");
 
    //-f--> Here, we are opening a handle for our device, which
    //      has been created by the driver. Remember that our
    //      sample driver must be installed and started
    //      so that the call below can work properly.
    hDevice = CreateFile("\\\\.\\KernelSum",
                         GENERIC_ALL,
                         0,
                         NULL,
                         OPEN_EXISTING,
                         0,
                         NULL);
 
    //-f--> It Verifies whether the open was opened.
    if (hDevice == INVALID_HANDLE_VALUE)
    {
        printf("Error #%d opening device...\n",
               (dwError = GetLastError()));
        return dwError;
    }
 
    //-f--> I got lazy and I put the values fixed at the sample code.
    Request.x = 3;
    Request.y = 2;
 
    printf("Calling DeviceIoControl...\n");
 
    //-f--> It sends the IOCTL
    if (!DeviceIoControl(hDevice,
                         IOCTL_SOMA_QUE_EU_TO_MANDANDO,
                         &Request,
                         sizeof(KERNEL_MATH_REQUEST),
                         &Response,
                         sizeof(KERNEL_MATH_RESPONSE),
                         &dwBytes,
                         NULL))
    {
        //-f--> Oops...
        printf("Error #%d calling DeviceIoControl...\n",
               (dwError = GetLastError()));
 
        CloseHandle(hDevice);
        return dwError;
    }
 
    //-f--> Show the results.
    printf("%d + %d = %d\n",
           Request.x,
           Request.y,
           Response.r);
 
    printf("Closing device...\n");
    //-f--> Close the handle.
    CloseHandle(hDevice);
    return 0;
}

Note that there is no strong relation between the IOCTL and the used input or output buffers, but we must be attentive to the buffer sizes when the driver makes use of them. Nobody wants to corrupt the kernel heap allocations or throwing off exceptions in kernel mode, do they?

You can see how data are processed by the driver when receiving the IRP_MJ_DEVICE_CONTROL. Reading the comments is a good idea; they are a part of the explanation. Wow, that sentence representing my laziness got nice!! Basically, it is the same to say: “Oh my! Seriously, besides preparing this example, you still want me to duplicate all the comment information? I don’t think so!”

/****
***     OnDeviceIoControl
**
**      This routine is called when an application
**      sends an IOCTL to our driver via DeviceIoControl.
*/
NTSTATUS
OnDeviceIoControl(__in PDEVICE_OBJECT   pDeviceObj,
                  __in PIRP             pIrp)
{
    PIO_STACK_LOCATION      pStack;
    NTSTATUS                nts;
    PKERNEL_MATH_REQUEST    pRequest;
    PKERNEL_MATH_RESPONSE   pResponse;
 
    //-f--> We get the parameters referring to our driver.
    pStack = IoGetCurrentIrpStackLocation(pIrp);
 
    switch(pStack->Parameters.DeviceIoControl.IoControlCode)
    {
    case IOCTL_SUM_THIS_TWO_NUMBERS:
    case IOCTL_SUBTRACT_THIS_TWO_NUMBERS:
 
        //-f--> Let's do the checking about the input
        //      and output buffer sizes.
        if (pStack->Parameters.DeviceIoControl.InputBufferLength !=
            sizeof(KERNEL_MATH_REQUEST) ||
            pStack->Parameters.DeviceIoControl.OutputBufferLength !=
            sizeof(KERNEL_MATH_RESPONSE))
        {
            nts = STATUS_INVALID_BUFFER_SIZE;
            pIrp->IoStatus.Information = 0;
            break;
        }
 
        //-f--> The safe then sorry.
        ASSERT(pIrp->AssociatedIrp.SystemBuffer != NULL);
 
        //-f--> Making use of METHOD_BUFFERED, the system allocates only one
        //      buffer to transport either the input and output data. The buffer
        //      size is the same as the biggest one of them. Thus, be careful having read
        //      all the input data before starting to write the output to the buffer.
        pRequest = (PKERNEL_MATH_REQUEST)pIrp->AssociatedIrp.SystemBuffer;
        pResponse = (PKERNEL_MATH_RESPONSE)pIrp->AssociatedIrp.SystemBuffer;
 
        //-f--> It does the math and indicates its success.
        if (pStack->Parameters.DeviceIoControl.IoControlCode == IOCTL_SOMA_QUE_EU_TO_MANDANDO)
            pResponse->r = pRequest->x + pRequest->y;
        else
            pResponse->r = pRequest->x - pRequest->y;
 
        nts = STATUS_SUCCESS;
 
        //-f--> It informs the I/O Manager how many bytes have to be transferred 
        //      back to the application.
        pIrp->IoStatus.Information = sizeof(KERNEL_MATH_RESPONSE);
        break;
 
    default:
        //-f--> Oops... We have received an unknown IOCTL.
        nts = STATUS_INVALID_DEVICE_REQUEST;
        pIrp->IoStatus.Information = 0;
    }
 
    //-f--> It copies the final IRP status and completes it.
    pIrp->IoStatus.Status = nts;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
 
    //-f--> NOTE: Remember that we cannot touch the IRP after completing it;
    //      so, don't be the "smart ass" changing the line below to return the
    //      IRP status taking the value from the IRP. (return pIrp->IoStatus.Status;)
    return nts;
}

Building in this or in that way

The sample project available for downloading at the end of this post can be compiled in two ways. One way is using the standard way to compile drivers offered by WDK. Briefly you go through “Start->All Programs->Windows Driver Kits->WDK 6000->Build Environments->Windows XP->Windows XP x86 Checked Build Environment”. This should open a console prompt, as it is shown below. Then, just go to the directory where you download these Internet sources and to the project root directory. There you need to call Build.exe and it is done.

The other way to compile the entire project allows you to build it using Visual Studio 2008, which has been the environment I have used to compose this project. However, to build the project by using the IDE you have to use DDKBUILD, as it is shown in this post referring to  it.

While I was out, I had received another reader’s question who wanted to know how to read or write into the registry using a driver. In the next post, I’ll talk about that and other details regarding to the registry from a driver’s point of view.

CYA!

KernelSum.zip

Leave a Reply