Guest Article: Kernel Mode DLLs

By Charles Saperstein of Hauppauge Computer Works

Ó 1997 OSR Open Systems Resources, Inc.

 

Recently, I was working on an NT kernel driver project where it would have been advantageous to partition the work into parts like dynamic link libraries (DLLs). Fortunately, this can be done, but has not been documented until now. The sample console application in this article sends an IOCTL to a kernel driver called master.sys. The kernel driver calls a function in a kernel mode DLL called slave.sys. The function processes the IRP and puts a value into the I/O buffer. The application tests the returned value to see if it is correct.

Building the Library

The DLL has to be built before the driver because the driver is linked to the import library of the DLL. If you look in makefile.def, you will find a TARGETTYPE called EXPORT_DRIVER. This is the key to making a kernel mode DLL and should be included in your sources file (See Figure 1). The sources file is used by the build utility to make the DLL. The sources file will put the libraries in the DDK tree. For our purposes here, the DLL was built in a directory called slave and the driver in directory called master. (Note: Be sure to include the path to the driver in the sources file if using a common header file to define the IOCTL as I have below).

# SOURCES FILE FOR SLAVE.SYS

TARGETNAME=SLAVE
TARGETPATH=$(BASEDIR)\lib
TARGETTYPE=EXPORT_DRIVER

INCLUDES=$(BASEDIR)\inc;..\MASTER

SOURCES=SLAVE.C

C_DEFINES=-DUNICODE -DSTRICT

Figure 1 -- sources

The next thing you have to do is to create a DEF file (See Figure 2). Do not put the names of your functions under the EXPORTS section.

;DEF FILE FOR SLAVE.SYS

NAME SLAVE.SYS

DESCRIPTION 'SLAVE.SYS'

EXPORTS

Figure 2 – slave.def

Finally, you must create a function. The DLL needs a DriverEntry(…) function which can be as simple as just returning STATUS_SUCCESS. (Note: During my testing, I used a DriverEntry(…) function that created a device object and an UnLoad(…) function so that I could load and unload the driver using the Devices applet in Control Panel. All functions that are to be called from the driver have to be declared _declspec( dllexport )).

 

#include "ntddk.h"
#include "MYIOCTL.H"

NTSTATUS
DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath )
{
return STATUS_SUCCESS;
}

NTSTATUS
_declspec( dllexport )DoSomethingMeaningless(IN PIRP pIrp,
    IN PIO_STACK_LOCATION
    pIrpStack)
{
USHORT *pw;

/* check to see if this is our ioctl. */

if (pIrpStack->Parameters.DeviceIoControl.IoControlCode != IOCTL_CALL_KERNEL_MODE_DLL)
    return STATUS_INVALID_PARAMETER;

/* check to see if output buffer size is big enough. */

if (pIrpStack->Parameters.DeviceIoControl.OutputBufferLength < sizeof(USHORT)) { 
    pIrp->IoStatus.Status = STATUS_BUFFER_TOO_SMALL;
    pIrp->IoStatus.Information = 0;
    return STATUS_BUFFER_TOO_SMALL;
}

/* return a value back to the user. */

pIrp->IoStatus.Information = sizeof( USHORT );
pw = (USHORT *)pIrp->AssociatedIrp.SystemBuffer;
*pw = 0XAA55;
pIrp->IoStatus.Status = STATUS_SUCCESS;

return STATUS_SUCCESS;

}

Figure 3 – slave.c

Building the Driver

In the sources file, the driver has to linked with the DLL import library (See Figure 4).

# SOURCES FILE FOR MASTER.SYS

TARGETNAME=MASTER
TARGETPATH=$(BASEDIR)\lib
TARGETTYPE=DRIVER
TARGETLIBS=$(BASEDIR)\lib\*\$(DDKBUILDENV)\slave.lib

INCLUDES=$(BASEDIR)\inc;.

SOURCES=MASTER.C

C_DEFINES=-DUNICODE -DSTRICT

Figure 4 – sources

I put my custom IOCTL’s in a header file so that it could be shared between projects (See Figure 5).

//CTL_CODE macro defined in devioctl.h in DDK\inc.
//custom device types (non Microsoft) in range 32768 to 65535
//custom user defined function codes in range 2048 to 4095
#define FILE_DEVICE_FOO 65534
#define IOCTL_CALL_KERNEL_MODE_DLL CTL_CODE(FILE_DEVICE_FOO, 2049, METHOD_BUFFERED, FILE_ANY_ACCESS)

Figure 5 – myioctl.h

Figure 6 contains the code for the kernel driver that processes only one custom IOCTL and calls a function in the DLL. For my sample, I passed the function a pointer to the IRP and to the current stack location of the IRP. The function in the DLL then processes the IRP. For testing purposes, I had an UnLoad(…) function and a Dispatch function to handle IRP_MJ_CLOSE and IRP_MJ_CREATE.

#include "ntddk.h"
#include "myioctl.h"

typedef struct _FOO_DEVICE_EXTENSION {
ULONG Information;

} FOO_DEVICE_EXTENSION, *PFOO_DEVICE_EXTENSION;

// Function declarations

NTSTATUS
_declspec( dllimport )DoSomethingMeaningless(IN PIRP pIrp, IN PIO_STACK_LOCATION pIrpStack );

NTSTATUS
DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath )
{
        UNICODE_STRING nameString, linkString;
        PDEVICE_OBJECT deviceObject;
        NTSTATUS status;

        //slave.sys needs to start before master.sys
        // Create the device object.

        RtlInitUnicodeString( &nameString, L "\\Device\\MASTER");
        status = IoCreateDevice( DriverObject,
                sizeof(FOO_DEVICE_EXTENSION),
                 &nameString,
                FILE_DEVICE_UNKNOWN,
                0,
                FALSE,
                &deviceObject );

        if (!NT_SUCCESS( status ))
                return status;

        // Create the symbolic link.

        RtlInitUnicodeString( &linkString, L"\\DosDevices\\MASTER") );
        status = IoCreateSymbolicLink (&linkString, &nameString);

        if (!NT_SUCCESS( status )) {

        IoDeleteDevice (DriverObject->DeviceObject);
        return status;
        }

        // Initialize the driver object with this device driver's entry points.

        DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MasterDispatchIoctl;
        return STATUS_SUCCESS;

}

NTSTATUS
MasterDispatchIoctl( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp )
{
        NTSTATUS                                  status;
        PIO_STACK_LOCATION          irpSp;

        // Init to default settings- we only expect 1 type of
        // IOCTL to roll through here, all others an error.

        Irp->IoStatus.Status = STATUS_SUCCESS;
        Irp->IoStatus.Information = 0;

        // Get a pointer to the current location in the Irp. This is where
        // the function codes and parameters are located.

        irpSp = IoGetCurrentIrpStackLocation( Irp );

        switch (irpSp->Parameters.DeviceIoControl.IoControlCode) {

                case IOCTL_CALL_KERNEL_MODE_DLL:

                        if ( NT_SUCCESS( DoSomethingMeaningless( Irp , irpSp) ) ) {
                                status = STATUS_SUCCESS;
                        }
                        else
                                status = STATUS_INVALID_PARAMETER;
                    break;

                default:
                                Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;

                    break;
        }

        // DON'T get cute and try to use the status field of
        // the irp in the return status. That IRP IS GONE as
        // soon as you call IoCompleteRequest.

        IoCompleteRequest(Irp, IO_NO_INCREMENT);

        // We never have pending operation so always return the status code.

        return status;

}

Figure 6 – master.c

Installing the Driver

Both the driver and DLL are copied into the \%SystemRoot%\system32\drivers directory. I created a file called test.ini (Figure 7) and used the regini.exe utility in the DDK to update the registry. I was advised that the DLL should be loaded before the driver. Therefore, I set the Tag value accordingly so that the DLL would start before the driver when the group started.

During my testing, I found that if I unloaded the DLL using the devices applet (or set the startup to Manual), my test application still worked. I then removed the entire registry entry for the DLL and was still able to load the driver and run my test application. However, if the slave.sys was not in the \%SystemRoot%\system32\drivers directory, then the driver failed to load.

\registry\machine\system\currentcontrolset\services\MASTER
            Type = REG_DWORD 0x00000001
            Start = REG_DWORD 0x00000002
            Group = Extended base
            ErrorControl = REG_DWORD 0x00000001
            Tag = REG_DWORD 0x00000002

\registry\machine\system\currentcontrolset\services\SLAVE
            Type = REG_DWORD 0x00000001
            Start = REG_DWORD 0x00000002
            Group = Extended base
            ErrorControl = REG_DWORD 0x00000001
            Tag = REG_DWORD 0x00000001

Figure 7 – test.ini

Testing the Driver and DLL

To test the driver and DLL, I used a simple console application (Figure 8) that is run from the command prompt. The application gets a handle to the driver, sends it the IOCTL, tests the returned value, and displays the result. The application was successfully tested on NT Workstation 4.0.

#include "windows.h"
#include "winioctl.h"
#include "stdio.h"
#include "stdlib.h"
#include "myioctl.h"

int main ( void )
{
    HANDLE hDriver;
    DWORD cbReturned = 0x0;
    WORD iobuf = 0x0;

    // Try to open the device

    if ((hDriver = CreateFile("\\\.\\MASTER",
                        GENERIC_READ | GENERIC_WRITE,
                        0,
                        NULL,
                        OPEN_EXISTING,
                        FILE_ATTRIBUTE_NORMAL,
                        NULL )) != INVALID_HANDLE_VALUE )

            printf ("\nRetrieved valid handle for MASTER driver\n");

        else
    {
        printf ("Can't get a handle to MASTER driver\n");
        return 0;
    }

    // iobuf initialized to zero, so we better get something different after call.

    if ( DeviceIoControl (hDriver,
                        (DWORD) IOCTL_CALL_KERNEL_MODE_DLL,
                        &iobuf,
                        (DWORD)(2*(sizeof (WORD))),
                        &iobuf,
                        (DWORD)(2*(sizeof (WORD))),
                        &cbReturned,
                        (LPVOID)NULL) )
    {

        if (iobuf == 0xAA55) {
                        printf("DeviceIoControl worked and returned the correct value.\n");
        } else {
                        printf("DeviceIoControl worked but reurned the wrong value %x\n", iobuf);
        }
    } else {
                    printf("DeviceIoControl Failed\n");
    }

    CloseHandle(hDriver);

    return 1;
}

Figure 8 – test.c

Conclusion

Kernel mode DLL’s can be a useful tool in developing and testing kernel drivers. I found that they are easy to build and simple to use, once you know how.

NT Insider 1997 On-line Articles