64-bit Device Driver Development

This article is intended to be a “quickstart” guide for building, loading, and debugging a simple device driver for a 64-bit Windows 7 system. Basic familiarity with device driver development and kernel debugging is assumed. For more of an introduction I recommend Toby Opferman’s articles at CodeProject which are still mostly relevant despite being several years old. Also, as a security professional my interest and expertise with device drivers is limited to the use of kernel-mode code to modify operating system behavior, rather than interfacing with actual hardware devices.

Instructions are provided for using WinDbg for remote kernel debugging to a VirtualBox virtual machine. The procedure is similar for using VMware and other virtualization software. Note that running a 64-bit guest operating system in a virtual machine requires a CPU which supports hardware-assisted virtualization (Intel-VT or AMD-V). I ended up having to upgrade the processor in one of my systems after finding out that it lacked Intel-VT support. It also may be necessary to go into the BIOS setup and ensure that the hardware virtualization is enabled.

Differences

There are several differences from earlier versions of Windows affecting device driver developers:

  • Driver Signing. 64-bit Windows systems will not allow drivers to be loaded unless they have a valid digital signature, which requires a signing key to be purchased from Microsoft. This is only made available to approved vendors and is not really an option for the hobbyist or researcher. For development and testing there is an option that can be selected from the boot menu to disable driver signing requirements. This cannot be made the default and must be manually selected at every boot. This is a nuisance for driver developers but is generally a good thing as it makes it harder for malware writers to install kernel rootkits.
  • Debugging Message Filtering. Beginning with Vista (including 32-bit versions), all debugging messages printed by drivers are not necessarily displayed in the kernel debugger. Rather, the system must be configured to display messages matching certain criteria.
  • Kernel Patch Protection (PatchGuard). Modifying certain kernel data structures is no longer allowed, again in an effort to crack down on kernel rootkits. Drivers that rely on certain techniques for hooking system calls may need to use an alternate approach. For details see Microsoft’s Patching Policy for x64-Based Systems.

Sample Code

In addition to the minimal DriverEntry and DriverUnload routines, this sample driver also implements a Device I/O Control (IOCTL) interface for communicating from user to kernel mode.

//testdrv.c

#include <wdm.h>

#define DEVICE_NAME L"\\Device\\Testdrv"
#define DOS_DEVICE_NAME L"\\DosDevices\\Testdrv"

//numeric value = 0x22a001
#define IOCTL_TESTDRV CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_IN_DIRECT, FILE_WRITE_DATA)

//macros for OACR
DRIVER_INITIALIZE DriverEntry;
DRIVER_UNLOAD TestdrvUnload;
__drv_dispatchType(IRP_MJ_CREATE)
__drv_dispatchType(IRP_MJ_CLOSE)
__drv_dispatchType(IRP_MJ_DEVICE_CONTROL)
DRIVER_DISPATCH TestdrvDispatch;

#pragma alloc_text(INIT, DriverEntry)
#pragma alloc_text(PAGE, TestdrvUnload)
#pragma alloc_text(PAGE, TestdrvDispatch)

//log message with filter mask "IHVDRIVER" and severity level "INFO"
void DebugInfo(char *str) {
    DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "testdrv: %s\n", str);
}

NTSTATUS TestdrvDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
	PIO_STACK_LOCATION iostack;
	NTSTATUS status = STATUS_NOT_SUPPORTED;
	PCHAR buf;
    ULONG len;
    
    PAGED_CODE();

	iostack = IoGetCurrentIrpStackLocation(Irp);

	switch(iostack->MajorFunction) {
	    case IRP_MJ_CREATE:
	    case IRP_MJ_CLOSE:
	        status = STATUS_SUCCESS;
	        break;
	    case IRP_MJ_DEVICE_CONTROL:
		    if (iostack->Parameters.DeviceIoControl.IoControlCode == IOCTL_TESTDRV) {

			    len = iostack->Parameters.DeviceIoControl.InputBufferLength;
			    buf = (PCHAR)Irp->AssociatedIrp.SystemBuffer;

                //verify null-terminated and print string to debugger
                if (buf[len-1] == '\0') {
                    DebugInfo(buf);
                }

			    status = STATUS_SUCCESS;
    		} else {
    		    status = STATUS_INVALID_PARAMETER;
    		}
    		break;
    	default:
    	    status = STATUS_INVALID_DEVICE_REQUEST;
    }

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

	IoCompleteRequest(Irp, IO_NO_INCREMENT);

	return status;
}

VOID TestdrvUnload(PDRIVER_OBJECT DriverObject)
{
	UNICODE_STRING dosdev;
	
	PAGED_CODE();

    DebugInfo("driver unloading");

	RtlInitUnicodeString(&dosdev, DOS_DEVICE_NAME);
	IoDeleteSymbolicLink(&dosdev);

	IoDeleteDevice(DriverObject->DeviceObject);
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject,
					 PUNICODE_STRING RegistryPath)
{
    UNICODE_STRING devname, dosname;
	PDEVICE_OBJECT devobj;
	NTSTATUS status;

    DebugInfo("driver initializing");

	DriverObject->MajorFunction[IRP_MJ_CREATE] = TestdrvDispatch;
	DriverObject->MajorFunction[IRP_MJ_CLOSE] = TestdrvDispatch;
	DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = TestdrvDispatch;
    DriverObject->DriverUnload = TestdrvUnload;

	RtlInitUnicodeString(&devname, DEVICE_NAME);
	RtlInitUnicodeString(&dosname, DOS_DEVICE_NAME);

	status = IoCreateDevice(DriverObject, 0, &devname, FILE_DEVICE_UNKNOWN, 
		FILE_DEVICE_SECURE_OPEN, FALSE, &devobj);

	if (!NT_SUCCESS(status)) {
		DebugInfo("error creating device");
		return status;
	}

	status = IoCreateSymbolicLink(&dosname, &devname);
	
	if (!NT_SUCCESS(status)) {
		DebugInfo("error creating symbolic link");
		return status;
	}
	    
    return STATUS_SUCCESS;
}

Building

There are two additional files required to build a device driver: sources and makefile (the file names do not have extensions). sources should contain the following lines:

TARGETNAME=testdrv
TARGETTYPE=DRIVER
SOURCES=testdrv.c

makefile only needs to contain this one line:

!INCLUDE $(NTMAKEENV)\makefile.def

Build the driver as follows:

  • Of course the Windows Driver Kit (WDK) must be installed. The WDK also includes the Debugging Tools for Windows (WinDbg).
  • From the Windows Driver Kit start menu group, launch the command-line build environment for the desired target platform and architecture, e.g. “Windows 7 x64 Checked Build Environment”.
  • Change to the directory containing¬† testdrv.c, sources, and makefile, and enter the command build.
  • If all goes well the driver file testdrv.sys will be built in a subdirectory named according the selected platform and architecture, e.g. objchk_win7_amd64.
  • By default the WDK will start a program called OACR (Auto Code Review) that will display an icon in the taskbar and automatically detect when a driver is being built and analyze the code for common programming errors. There are some declarations in the testdrv.c source code for preventing OACR warning messages. For details see Microsoft Auto Code Review.

Debugging Message Filtering

Note in the sample source code that the DbgPrintEx() function is used with additional parameters, as opposed to the standard DbgPrint(). The additional parameters are the Component ID and Severity Level. Only the component IDs defined in dpfilter.h that start with DPFLTR_IHV_ should be used by driver developers. IHV stands for Independent Hardware Vendor, i.e. any third-party non-Microsoft driver.

This example uses the generic DPFLTR_IHVDRIVER but other more specific IDs are available for video/audio/network/etc. drivers. The severity level can technically be any 32-bit number but standard constants exist for ERROR, WARNING, TRACE, and INFO levels (corresponding to the numeric values 0, 1, 2, and 3, respectively). ERROR should only be used to indicate a truly serious error, but otherwise it is a matter of preference. This example logs everything at INFO level.

For each Component ID, the system maintains a kernel variable named Kd_<Component ID>_Mask which is a 32-bit number representing a bit mask for the severity levels of debugging messages that should be displayed. For example, if bit #3 in this variable is set to 1, then INFO level messages will come through. The full list of these variables can be seen in WinDbg with this command:

kd> x nt!Kd_*_Mask
fffff800`02e03198 nt!Kd_LDR_Mask = <no type information>
fffff800`02e031ec nt!Kd_WDI_Mask = <no type information>
fffff800`02e0304c nt!Kd_SETUP_Mask = <no type information>
fffff800`02e03150 nt!Kd_DMIO_Mask = <no type information>
[...]

The current value of the IHVDRIVER mask can be displayed as follows (it should be zero by default):

kd> dd nt!Kd_IHVDRIVER_Mask L1
fffff800`02e03178  00000000

This value can be modified directly in the debugger and will take effect immediately and persist until the system is rebooted. For example, to set the lowest 4 bits (and therefore enable debugging messages for levels ERROR thru INFO), set the value to hex 0x0000000f:

kd> ed nt!Kd_IHVDRIVER_Mask f

To make this change permanent, create a registry value IHVDRIVER (must be uppercase) in HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter. The value should be a DWORD corresponding to the desired mask, e.g. 0x0000000f. The change will not take effect until the next reboot.

Refer to Reading and Filtering Debugging Messages for more information.

Note that debug messages will not display in the kernel debugger during local kernel debugging no matter what you do. The only options for seeing messages in local kernel debugging are to execute the !dbgprint command periodically or use the DebugView tool from SysInternals.

Test Program

The following user-mode application can be used to invoke the DeviceIoControl (IOCTL) interface and pass data into the driver.

//ioctltest.c

#define UNICODE
#define _UNICODE

#include <windows.h>

#define DEVICE_NAME L"\\\\.\\Testdrv"
#define IOCTL_CODE 0x22a001

const char message[] = "Greetings from user mode";

void debug(char *text) {
	LPTSTR msgbuf;

    //get error message string for last error
	FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
               NULL,  GetLastError(), LANG_USER_DEFAULT,
               (LPTSTR) &msgbuf, 0, NULL);
               
	printf("%s: %S\n", text, msgbuf);
}

int wmain(int argc, wchar_t* argv[])
{
	HANDLE hDevice;
	char outBuf[16];
	DWORD returned;

	hDevice = CreateFile(DEVICE_NAME, GENERIC_READ | GENERIC_WRITE, 
	            FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
	            FILE_ATTRIBUTE_NORMAL, NULL);

	if (hDevice == INVALID_HANDLE_VALUE) {
	    debug("error opening device");
	} else {
		if (0 == DeviceIoControl(hDevice, IOCTL_CODE, message, sizeof(message),
		            outBuf, sizeof(outBuf), &returned, NULL)) {
		    debug("ioctl error");
		}

		CloseHandle(hDevice);
	}

	return 0;
}

Build with:

cl ioctltest.c

Virtual Machine Configuration

This section assumes that Windows 7 64-bit has been installed in a VirtualBox virtual machine. The following steps will prepare the virtual system for driver loading and kernel debugging. First, with the virtual machine powered off, configure the virtual serial port to point to a named pipe for the kernel debugger to connect to:

virtual serial port configuration

Next, enable kernel debugging in the Boot Configuration Database using the built-in bcdedit command line tool, and also configure the boot menu to always display at boot. (There is also a nice free GUI tool EasyBCD available for modifying these settings). Start a command prompt with administrator privileges and run the following commands:

bcdedit /set {current} debug yes

bcdedit /set {bootmgr} displaybootmenu yes

Run bcdedit with no parameters to confirm the current configuration.

Finally, reboot the virtual machine. When the boot menu is displayed, press F8 and then select “Disable Driver Signature Enforcement” from the menu.

Attaching Debugger

This section assumes that debugging symbols have already been configured in WinDbg on the host system. For more information see Debugging Tools and Symbols on MSDN. Launch WinDbg on the host system, select File->Kernel Debug and specify the following settings:

kernel debugger connection

Press Ctrl-Break (or select Debug->Break from the menu) and wait patiently. Kernel debugging over the virtual serial port is SLOW – it may take several seconds before any feedback is received at all from the Break request, and may take 30 seconds or more for the prompt to display:

*******************************************************************************
*                                                                             *
*   You are seeing this message because you pressed either                    *
*       CTRL+C (if you run kd.exe) or,                                        *
*       CTRL+BREAK (if you run WinDBG),                                       *
*   on your debugger machine's keyboard.                                      *
*                                                                             *
*                   THIS IS NOT A BUG OR A SYSTEM CRASH                       *
*                                                                             *
* If you did not intend to break into the debugger, press the "g" key, then   *
* press the "Enter" key now.  This message might immediately reappear.  If it *
* does, press "g" and "Enter" again.                                          *
*                                                                             *
*******************************************************************************
nt!DbgBreakPointWithStatus:
fffff800`026c87a0 cc              int     3

This confirms that kernel debugging is working. If nothing happens, double-check the virtual serial port settings, debug boot configuration, and WinDbg connection settings.

Configure the debugging message filter mask as previously discussed:

kd> ed nt!Kd_IHVDRIVER_Mask f

Finally, enter g at the prompt to resume execution of the virtual machine.

Testing driver

Copy the testdrv.sys driver to the virtual machine using the drag-and-drop or shared folder support. Also copy over the test program ioctltest.exe.

The driver can be installed using the built-in Windows command-line tool sc.exe. This can also be done using the GUI tool OSRLoader available from OSR Online (free registration required). To use sc.exe, start a command prompt with administrative privileges and run the following commands (replacing c:\testdrv.sys with the path where testdrv.sys is located on the virtual machine):

sc create testdrv binPath= c:\testdrv.sys type= kernel

Note that the spaces after the equals signs are required. A pop-up warning may be received about the driver not having a valid digital signature, which can be ignored. The sc create command only needs to be run once, and from then on the driver can be just be started and stopped as desired. All the sc create command does is create the necessary registry entries under HKLM\System\CurrentControlSet\Services\testdrv. The registry entries can be removed if needed with sc delete testdrv.

To load the driver:

sc start testdrv

If the driver starts successfully, the following message should be seen in the kernel debugger (if the message is not seen, double-check the filter mask settings):

testdrv: driver initializing

Next, run ioctltest.exe and the following output should be seen in the debugger:

testdrv: Greetings from user mode

The driver can be unloaded with sc stop and the following message should be seen:

testdrv: driver unloading

Comments are closed.