This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


March 1998

Microsoft Systems Journal Homepage

Pop Open a Privileged Set of APIs with Windows NT Kernel Mode Drivers

Download driver.exe (57KB)

James Finnegan is a developer at Communica, Inc., a Massachusetts-based company specializing in system software design. He can be reached via his web page at http://www.windows.to or via email at jimf@nema.com.

Have you ever been intimidated by Windows NT¨ and its seemingly secure environment? Have you ever needed to do the "impossible" in your Win32¨ application, employing the far-reaching powers of a VxD under Windows¨ 95, only to assume that similar capabilities don't exist under Windows NT? If so, you need to understand the power provided by Windows NT drivers.
      Why am I writing about this nearly five years after the release of Windows NT? It's recently become apparent to me that Windows NT and Windows NT drivers, although tried and true, are still woefully misunderstood. It's time to demystify the role of drivers under Windows NT. I'll implement some practical examples that will show that Windows NT drivers are not just "drivers for devices"; rather, they are the means of doing things that were previously considered impossible under Windows NT.
      But what if you never intend to develop a Windows NT driver? Why should you care about this? Understanding these drivers not only provides valuable insight into the workings of Windows NT, it also exposes you to a handy set of privileged APIs that you can take advantage of without writing a stitch of driver code.
      If you read the Editor's Note from the February 1998 issue of MSJ, you were treated to an account of one of the inevitable hardware bugs that manage to manifest themselves: Joe Flanigen (your fearless Technical Editor) discussed the Intel Pentium F00F erratum that you've probably heard about.
      Of course, Microsoft would not stand by idly and permit your computing experience to be diminished by the likes of some hardware errata (look for the Windows NT fix at ftp://ftp.microsoft.com/
bussys/winnt/winnt-public/fixes/usa/NT40/hotfixes-postSP3/pent-fix/
). Implementation of the fix, of course, requires modification of some privileged-mode data structures. These structures are generally under the tight control of the operating system, and are off limits to the prying eyes of lowly user mode applications. Therefore, it is no surprise that the fix comes in the form of a modified component of the Windows NT kernel.
      Prior to the release of the Microsoft fix, I had developed and distributed my own fix to the Pentium erratum for both Windows 95 and Windows NT. The fix, a single Win32-based executable, did what seemed impossible. When run, it modified the appropriate OS components, eradicating the bug under two very different operating systems without requiring the static modification of the kernel. This was met with comments ranging from "you must have a source license to Windows NT" to "what you've done is impossible!"
      Of course, it wasn't impossible. It just required an understanding of how to exploit the facilities available to you in Windows NT kernel mode. I was surprised that hardly any comments were made regarding such a fix for Windows 95 even though that was more difficult to implement! This was probably because Windows 95 and VxDs are a known quantity for many developers. Reaching in and doing things by brute force surprises few people today. It's time to bring that mindset forward to Windows NT by thinking about device drivers and DDKs differently.

A Brief Historical Perspective
      Programmers developing Windows-based apps for any substantial amount of time have inevitably been exposed to VxDs. A VxD is a mechanism in which a piece of hardware is emulated, or virtualized, to benefit multiple applications that require "exclusive" access to hardware. As you can imagine, VxDs require far-reaching powers to provide such capabilities-namely unrestricted access.
      Systems developers discovered that the unrestricted access provided by the VxD architecture could be used to perform normally impossible tasks on behalf of user mode applications. Interfaces reserved to provide limited access from user mode are exploited to perform this magic. It is not hard to imagine the potential value that running unrestricted code has to a developer of, say, a Windows-based utility application. In fact, VxDs today are utilized to provide MS-DOS¨-like access under the confining Windows environment. Of course, the wild, wild west of MS-DOS doesn't protect applications from hardware. As such, many Windows and Windows 95-based computers are littered with support VxDs for applications of many types. Most of these VxDs, as you'd suspect, have little or nothing to do with actual hardware devices.
      Windows NT also provides such an interface via its driver architecture. Much like VxDs, a Windows NT driver has unrestricted access to a computer's hardware. This is required to permit a driver to communicate with and manipulate actual hardware resources. It's interesting to note that Windows NT drivers, once called, are beyond any front-line security set up by the operating system. This means that if an application successfully calls into a Windows NT driver, the driver can do what it pleases. If you're a security buff, this may give you the willies.
      A Windows NT driver is a 32-bit modular component that runs at a privileged or supervisor level (Ring 0 to those familiar with the Intel architecture) on the host computer's CPU. As such, a Windows NT driver is running as a trusted component of the kernel, with seemingly unrestricted power to do what it wants. Just as a VxD becomes part of the VMM when loaded, a Windows NT driver becomes part of the kernel, supplied with the same powers as the kernel. Conceptually, a Windows NT driver gets loaded and is under the control of the I/O Manager (see Figure 1), which provides a primitive environment in which the driver is loaded and managed.

Figure 1  Windows NT and the I/O Manager
Figure 1  Windows NT and the I/O Manager


      Unlike VxDs, which can provide an interface that is seemingly transparent by hooking CPU faults, a Windows NT driver's interface is well defined. In fact, calls into a Windows NT driver from user mode must be performed with a conscious effort. This interface is generic, and is modeled after the file I/O interface. To call into a Windows NT driver, you only need to open a file handle to the driver. This is accomplished by a call to CreateFile:


// Open the driver...
hDevice = CreateFile("\\\\.\\MSJDRVR",
                    GENERIC_READ | GENERIC_WRITE, 
                    FILE_SHARE_READ | FILE_SHARE_WRITE,
                    0,
                    OPEN_EXISTING,
                    FILE_FLAG_OVERLAPPED,
                    0);
      The driver must be installed in the system before you can load it. Typically, a driver is statically installed in the system's registry. The path for drivers is \HKEY_LOCAL_ MACHINE\System\CurrentControlSet\Services. Services? Yes, drivers and services are treated as one and the same from user mode. This permits you to control the driver via the Service API. (For more detailed information on the Service API, see Jeffrey Richter's article, "Design a Windows NT Service to Exploit Special Operating System Features," MSJ, October 1997.) Later on, you'll see how to utilize the Service API to dynamically load drivers at runtime.
      If you peruse the Windows NT DDK documentation (the best documentation is the sample code included in the DDK, which includes the source to a number of shipped Windows NT drivers), you'll discover that Windows NT drivers seem to come in a number of flavors. The simplest type is a monolithic driver. This type generally has no dependencies on other drivers loaded in the system. They supply an interface to user mode applications and talk directly to hardware without any intervention or support from anyone else. This is the type of driver I'll be implementing here.
      A layered driver is one that handles user mode requests, processing and passing each request from the highest-level driver to the lowest-level driver. This is controlled by the Windows NT I/O Manager. Although the layering scheme has some limitations, it is a scheme that many of the system-supplied Windows NT drivers take advantage of.
      A filter driver is like a layered driver in that it sits on top of another driver, but it receives requests destined for the lower-level driver first, providing the ability to intercept, alter, or filter these requests. This is done without the knowledge or volition of the lower-level driver. As you could imagine, this has some powerful and interesting uses. For example, the Encrypting File System (EFS) driver that comes with Windows NT 5.0 encrypts data being written to or read from the NTFS driver. This is accomplished by the EFS driver's ability to intercept I/O requests and alter the data before the NTFS driver sees it.
      Finally, you can vaguely distinguish drivers as physical or logical in nature. Generally, a physical driver is the driver talking directly to some hardware. A logical driver generally talks to other drivers, which eventually end up communicating with the bare metal. In theory, logical drivers should be portable to other Windows NT platforms. In fact, the implementation of the Windows NT DDK APIs goes to great lengths to provide a level of portability between Windows NT implementations, even for physical drivers. But let's be honest here; a driver is a very low-level component that is usually dealing with highly computer-specific resources. Even though a generic, platform-independent interface is commendable, it simply isn't realistic in all circumstances. I know that I'll be skewered by the Windows NT kernel-mode purists for saying this.

Driver Visibility in User Mode
      Let's start off by examining how device drivers make themselves visible to user mode processes. The Query.exe sample application (see Figure 2) is a simple Win32 console app that enumerates and displays kernel-mode drivers that have made themselves visible to user mode Win32 processes. Query.exe does this by utilizing the Win32 API QueryDosDevices, which returns a buffer filled with every visible device name in the object namespace when NULL is passed as the first parameter. You'll immediately notice some familiar-looking devices, such as your local drive letters (C:, D:, and so on) as well as some standard MS-DOS devices, such as PRN, AUX, and NUL.

Figure 3 Query.exe run by an administrator
Figure 3 Query.exe run by an administrator


      Query.exe also attempts to open each device to demonstrate whether the device is accessible based on the privileges of the account that is currently logged in. Figure 3 (above) shows the results of running Query.exe on my Windows NT 4.0-based machine under the administrator account. All visible devices are accessible to the user (with the exception of the DISPLAY1 device; more on this later). Figure 4 (below) shows the results of Query.exe when run from the guest account. As you can see, file system-related drivers are inaccessible. This is generally the case with users that do not have administrative privileges.

Figure 4 Query.exe run by a guest
Figure 4 Query.exe run by a guest


      Once you know how to look for drivers and open handles to them, the next step is figuring out what APIs a driver offers. Drivers can supply functionality behind the ReadFile and WriteFile API calls, and can also provide I/O control codes (IOCTLs), accessible through the DeviceIoControl API call, for functions that do not fit in the read/write model.
      Unfortunately, there is no way to enumerate the IOCTLs that a particular driver supports. Fortunately, source code to many of the more interesting drivers is included in the Windows NT DDK (see Figure 5), and many IOCTLs are also discussed in the DDK documentation. But generally, you'll need to understand the basic structure of where a particular driver fits in to determine where to look.
      For example, if you examine the output of Query.exe, you'll notice one or more devices named PhyiscalDrivex. This device name represents a zero-based enumeration of each hard disk in your system—PhysicalDrive0 is your first hard disk, PhysicalDrive1 is your second hard disk, and so on. If you think about this for a moment, you may be confused about how this is done and what drivers are hiding behind each of these innocuous names. For instance, my system has two IDE hard disks, a SCSI hard disk, and a SCSI Iomega Zip drive. As you can see from the output of Query.exe, the Windows NT namespace contains PhysicalDrive0 through PhysicalDrive3, as expected. But the actual access to these devices follows quite different paths. Figure 6 shows the conceptual layout.

Figure 6  Accessing a Physical Drive
Figure 6 Accessing a Physical Drive

      This seemingly flat, generic interface to disparate hardware devices is provided by the coordination of ATDISK.SYS (a monolithic driver that controls all MFM/RLL/ESDI/IDE hard disk drives in a system), the SCSI port driver and its associated interface card specific miniport drivers, and the HAL (the Windows NT hardware abstraction layer). When Windows NT boots, ATDISK.SYS and SCSIPORT.SYS enumerate all devices under their control, modifying a configuration structure that indicates how many disks each driver controls. Generic symbolic names (in the form of PhysicalDrivex) are then created, representing each of the disks. It is the responsibility of ATDISK.SYS and the SCSI miniport drivers to supply a minimum level of functionality (like read and write) so other drivers, like the file system drivers (FAT and NTFS), can successfully talk to these drives.
      This design and the fact that the drives are accessible (at least to accounts with administrative privileges) gives you the ability to exploit this functionality within your own applications—without writing any driver code.

Accessing Drivers from User Mode
      Now that you've seen how device drivers make themselves known to user mode applications, let's see how to actually put them to work. As shown earlier, device drivers in coordination with the HAL provide a consistent interface to disparate devices—namely ATA and SCSI disks.
      One requirement of disk drivers is to provide read and write access to both applications and upper-layer drivers (such as file systems). The read and write functions in the device driver provide direct sector access to the actual drive, without regard to partitioning. (File systems, which utilize the drive letters, are concerned about partitions.) To demonstrate device drivers, as well as do something that is generally considered off-limits to applications, I'll show you Testapp.exe to access sector zero, or what is otherwise referred to as the Master Boot Record (MBR).
Figure 7  The MBR
Figure 7 The MBR
      The MBR is created when an operating system is first installed. Most users are familiar with the FDISK utility that comes with MS-DOS. This utility permits you to carve up the hard disk drive to create individual partitions, or logical disk drives. The Windows NT equivalent would be the Disk Administrator. The partition information created by these applications is stored in the MBR. The MBR is utilized by the system BIOS when the machine is initially booted. In addition to containing the partition information of the disk drive, it also contains an extremely small bootstrap program, which is where the rubber meets the road when an operating system loads. The structure of a typical MBR is shown in Figure 7.
      The need for access to the MBR should be obvious if you're writing utility applications. For example, a virus-scanning program may want to check the MBR for boot record viruses, or a disk utility application may need to verify the integrity of the MBR and the disk partition information it contains. As you'll see, doing this in a manner that is compatible across all types of disk drives in Windows NT is made easy by this well-defined interface.
      First, the driver that represents the first physical drive needs to be opened. This is done by referencing its name by the generic symbolic link that's been created:


 hDriver = CreateFile("\\\\.\\physicaldrive0",
            GENERIC_READ | GENERIC_WRITE,
         FILE_SHARE_READ | FILE_SHARE_WRITE,
            0,
            OPEN_EXISTING,
            0,
            0);
After the file handle to the driver is obtained, you just need to call ReadFile, reading 512 bytes from the first offset, since the file handle's position defaults to offset zero:

 ReadFile(hDriver, &data, 512,
          &dwBytesRead, NULL);
The buffer is now filled with the contents of sector zero of the first disk—the MBR. The contents are displayed in Figure 8.
Figure 8 Output of Testapp.exe
Figure 8 Output of Testapp.exe

      If the thought of reading the MBR off of the disk under Windows NT gives you the creeps, be aware that access to PhysicalDrivex devices (as shown earlier in the Query.exe application), as well as other file-system type drivers, is restricted to users with administrator privileges. Therefore, you need not be concerned with uncovering any blatant security holes by exploiting this fairly basic interface. But what if you really want non-administrator users to access the MBR? What if you've written a virus-scanning utility or a disk integrity program and you need users to access these privileged devices? Well, you'll need to write a Windows NT device driver!

Anatomy of a Windows NT Driver
      Before delving into the raw implementation that solves the problem presented above, I'll introduce the basic structure of a Windows NT driver that simply loads, unloads, and implements a few IOCTLs for brevity.
      When you look at existing documentation regarding Windows NT kernel mode drivers, long-winded dissertations about driver objects, device objects, controller objects, adapter objects, spin locks, processor affinities, and the like usually leaves the uninitiated quite discouraged. I'll attempt to peel away the chaff here and give a practical explanation and example.
      Put simply, a kernel mode driver is a Portable Executable (PE) format program like any other Win32 application or DLL. This means that you can easily examine and manipulate driver binaries with familiar development tools. For example, you could look at a driver's import or export tables by using DumpBin (included with the Win32 SDK and all versions of Visual C++®). The notable differences between a driver and a PE-format Win32 application or DLL is its subsystem type, which is native (type 1) instead of GUI (type 3) or console (type 2). The entry point is also different, pointing to DriverEntry, which is the kernel-mode equivalent to DllMain in a user mode Win32 DLL.
      The DriverEntry function is called by the I/O Manager when your driver is started. The function is passed a pointer to a DRIVER_OBJECT as well as a pointer to the registry string for your driver. Your DriverEntry function initializes the DRIVER_OBJECT structure passed to you with values specific to your driver. Typically, you create a device object for the driver by calling the I/O Manager's IoCreateDevice function:



 // Point uszDriverString at the driver name
 RtlInitUnicodeString(&uszDriverString,   
                      L"\\Device\\MSJDrvr");
 
 // Create and initialize device object
 ntStatus = IoCreateDevice(DriverObject,
                           0,
                           &uszDriverString,
                           FILE_DEVICE_UNKNOWN,
                           0,
                           FALSE,
                           &pDeviceObject);
Most drivers allocate any data that is global to the device driver within a driver-defined device extension. Memory for the device extension is allocated in the call to IoCreateDevice (based on the size specified in the second parameter).
      Also, the device type (the fourth parameter) plays a key role in the accessibility of the driver from non-administrator accounts. As seen in the output from Query.exe earlier, some drivers cannot be accessed from regular user accounts. By default, certain device types are restricted. These are listed in Figure 9. Figure 10 lists all of the remaining device types that are unrestricted to users (including guest accounts). Keep in mind that Windows NT drivers still need to be installed initially in the system's registry by an administrator (the steps are shown later).
      Once the device object has been created, it's time to create a name visible to user mode Win32 applications. This is easily done with a call to IoCreateSymbolicLink:

 // Point uszDeviceString at the device name
 RtlInitUnicodeString(&uszDeviceString, 
                      L"\\DosDevices\\MSJDrvr");
 
 // Create symbolic link to the user-visible name
 ntStatus = IoCreateSymbolicLink(&uszDeviceString, 
                                 &uszDriverString);
You can create one or more symbolic names by doing this. In addition, you can create additional user mode aliases by adding keys to the following registry path:

 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\DOS Devices
If you look at the values in this key, you'll see familiar MS-DOS device names listed (like NUL, PRN, and AUX); their associated data values are strings that match their device names or symbolic links. For example, NUL points to \Device\Null. Adding your own aliases is as trivial as adding your own string values here.
      The final thing to do is assign the DRIVER_OBJECT's structure members with pointers to your function handlers. Most of the initialization required is for the last structure member in DRIVER_OBJECT, which is defined as follows:

 PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
This is an array of function pointers that are used by the I/O Manager to dispatch various messages to your driver. It is at the end of the structure to permit the addition of control messages in future versions of Windows NT and other Microsoft® operating systems supporting this driver architecture (such as WDM in Windows 98). The current list of major messages defined in Windows NT 4.0 is shown in Figure 11.

 // Load structure to point to IRP handlers...
 DriverObject->DriverUnload = MSJUnloadDriver;
 DriverObject->MajorFunction[IRP_MJ_CREATE] = 
     MSJDispatch;
 DriverObject->MajorFunction[IRP_MJ_CLOSE] = 
     MSJDispatch;
 DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = 
     MSJDispatchIoctl;
      The I/O manager uses these pointers to dispatch messages from user mode apps, as well as internal control messages to your application. For example, when a Win32-based app opens your driver with a call to CreateFile, the I/O manager will call the function pointed to by the DriverObject->MajorFunction[IRP_MJ_CREATE] structure member. Within that function you'd do whatever initialization would be required by your driver. Likewise, when the Win32-based app closes the handle to your driver, DriverObject->MajorFunction[IRP_MJ_CLOSE] is called. The function pointed to by DriverObject->DriverUnload is called when your driver is stopped (by shutting down the system, the user typing NET STOP DriverName from a command prompt, or stopping the driver in the Devices applet in the Control Panel).

IRPs
      Of particular interest to your running driver are the IRP_MJ_DEVICE_CONTROL, IRP_MJ_READ, and IRP_ MJ_WRITE messages. IRP_MJ_READ and IRP_MJ_ WRITE are the results of a Win32 application calling the ReadFile and WriteFile APIs on your driver. From a Win32 app, ReadFile and WriteFile are passed a buffer for input or output, the number of bytes to read or write, with an implicit offset where the operation should be performed. By the time your driver's read or write function has been called, the user mode parameters have been packaged up in what's called an I/O Request Packet (IRP). In addition to containing information about the operation requested by the caller, the IRP is used by the I/O Manager to track the I/O request through the system. This puts the burden of controlling IRPs completely on the shoulders of the I/O Manager and out of the hands of the individual device driver. This is key to maintaining order in driver layering and filter drivers (which are not discussed in this article, but are covered in detail in the Windows NT DDK).
      IRPs are like window messages in a user mode application. Just as most Windows-based applications are event driven, reacting to and processing seemingly arbitrary messages from an opaque message pump, Windows NT drivers take on a similar form in which messages (in the form of IRPs) are passed to your handler functions.
      For example, the prototype to your read or write function in your driver might look like this:


 NTSTATUS DispatchReadWrite(
     IN PDEVICE_OBJECT DeviceObject, IN OUT PIRP Irp)
As a side note, you'll notice in your travels through Windows NT kernel land that function definitions typically contain keywords like IN and OUT. They indicate whether a parameter is for input, output, or both. Currently, IN and OUT are not defined, so their inclusion is benign. But they certainly improve the readability of your source code.
      The first parameter is the device object you created for your driver within your DriverEntry call. The I/O Manager will always pass this pointer to your control functions, so you don't have to tote around a pointer to it globally within your driver. The second parameter is a pointer to an IRP structure. This structure completely contains the information required to bust apart and process the request.
      Following the flow from the perspective of reading or writing, a call to the following code from a Win32 user mode app eventually calls into the I/O Manager, which creates an IRP with an IRP stack:

 ReadFile(hDriver, &data, 512, &dwBytesRead, NULL);
The IRP stack contains the parameters destined for your driver. For monolithic drivers (like those presented here), there will only be stack entries for the current driver. For layered or filter drivers, there will be a stack "frame" for each driver in the chain of drivers.
      A pointer to the stack location for your driver can be obtained by calling the following in your driver:

 PIO_STACK_LOCATION irpSp;
 •
 •
 •
 irpSp = IoGetCurrentIrpStackLocation( Irp );
IoGetCurrentIrpStackLocation is a macro that obtains the current stack pointer in the IRP (which is managed by the I/O Manager). IO_STACK_LOCATION is a structure that contains a union of structures that each represent parameters passed from specific IOCTLs. The most frequent one you will encounter in your own Windows NT drivers would be for read, write, and device I/O control.
      For the read example above, the parameters would be passed like so:

 irpSp->Parameters.Read.ByteOffset
 irpSp->Parameters.Read.Length
irpSp->Parameters.Read.ByteOffset represents the file position passed from user mode (set by a call to SetFilePointer). irpSp->Parameters.Read.Length is the direct result of the length parameter in the ReadFile call. The user mode buffer itself is pointed to by:

 Irp->AssociatedIrp.SystemBuffer
This buffer is mapped and ready for use by your driver. There is no additional mapping or any other setup that needs to occur before you can use the buffer. The I/O Manager prepares the transfer to and from user mode on your behalf. Note, however, that this pointer represents both the input and output buffers passed from user mode. Although not necessarily important with read or write (which is either all input or all output), this does become critical with calls from DeviceIoControl, where this buffer is shared, sized to be the larger of the input or output buffer. Get everything you need out of the input buffer before writing anything to the output buffer, since you'll clobber anything that's in there!
      At this point, you have all the information you need to process your device-specific request. Once you've completed processing, it's time to instruct the I/O Manager to return control to the calling user mode application. First, you should set the IRP's status member to indicate success of failure of the operation:

 Irp->IoStatus.Status = STATUS_SUCCESS;
Next, you need to set the IRP's Information member to represent the number of bytes to copy back to the user mode application:

 // Set # of bytes to copy back to user mode...
 Irp->IoStatus.Information=irpStack-
 >Parameters.DeviceIoControl. OutputBufferLength;
Finally, you instruct the I/O Manager that you are finished with the IRP:

 IoCompleteRequest(Irp, IO_NO_INCREMENT);
 return ntStatus;
The I/O Manager uses the notification via IoCompleteRequest to either complete the request or continue processing the IRP with other layered drivers.

Fun With DeviceIoControl
      Now that you've seen the basic flow of control from user mode to your Windows NT driver, it's time to implement a simple example. MSJDrvr.c (see Figure 12) implements code in its MSJDispatchIoctl function to handle an IOCTL named IOCTL_MSJDRVR_GET_STRING. This IOCTL is defined using the CTL_CODE macro included in NTDDK.H:


 #define IOCTL_MSJDRVR_GET_STRING
 CTL_CODE(IOCTL_DISK_BASE, 0x0800, METHOD_BUFFERED, 
          FILE_READ_ACCESS | FILE_WRITE_ACCESS)
The CTL_CODE macro mangles together the passed-in elements. Each of these elements is critical to the I/O Manager and its treatment of IRPs and their contents. This differs from the Windows 95 treatment of IOCTLs, which does not depend on the IOCTL's value for processing. (See the sidebar, "The Windows 95 Connection" for more information.)
      CTL_CODE crams together the parameters into a DWORD value. The resulting format is shown in Figure 13. The first parameter is the device type that was specified when your driver's device object was created. The second parameter is a SHORT that is a custom control code for your driver. Values 0x0000 through 0x07ff are reserved by Microsoft, and are typically predefined for use by drivers included in Windows NT. Values 0x0800 and higher are for your own use.
Figure 13  IOCTL Format
Figure 13 IOCTL Format

      The third parameter is the data transfer type performed by this IOCTL. This parameter is the source for much confusion, since the DDK documentation is not terribly clear. Lacking a definitive description, see Knowledge Base article Q115758. A typical data transfer type is METHOD_ BUFFERED. The DDK documentation says to use METHOD_BUFFERED "if the driver transfers small amounts of data for the request." This is because the data transferred to and from user mode is actually copied into and out of kernel mode by the I/O Manager. Even though no restrictions are placed on the size of data transferred using METHOD_BUFFERED, actually copying large amounts of data could be unacceptably time-consuming. Note that the input and output buffers are shared when using METHOD_ BUFFERED, and the buffer is pointed to by Irp->AssociatedIrp.SystemBuffer (as shown earlier).

Loading a Device Driver
      At this point, you probably want to see the driver actually run. Windows NT and Windows 95 share similar functionality in their file I/O-based access to device drivers. Both permit on-demand loading by calling CreateFile. Under the hood, they are quite different. Using CreateFile on a VxD under Windows 95 permits on-demand dynamic loading of VxDs without any prerequisites. Using CreateFile on a Windows NT driver requires that the driver be loaded and started in the system before it can be accessed. This means a system administrator needs to install a device driver before it can be utilized, largely for security reasons. Keeping users without appropriate rights from running kernel-mode code willy-nilly is a good thing. Trust me.
      Statically loaded Windows NT drivers are located by default in the %SystemRoot%\System32\Drivers directory. The driver file should be installed in this location so the system can locate it. This location can be optionally altered by a registry key, which I'll discuss later.
      As mentioned earlier, device drivers and services live in the same area of the system registry: \HKEY_LOCAL_ MACHINE\System\CurrentControlSet\Services. In this path, you'd add a key that matches the file name (minus the .SYS extension) of your driver. A minimum of three DWORD values are required under this key to get the driver started. The three essential keys are: Type, Start, and ErrorControl. The Type value should be set to 1 to indicate a kernel driver, or 2 to indicate a file system driver. Other types are reserved for user mode services. The Start value can be one of the five values shown in Figure 14. After your driver has been installed, the Start value can be manipulated in the Devices Control Panel applet. ErrorControl indicates the failure severity if the driver cannot be started. These values (see Figure 15) have the most impact if the driver is started during the boot process or system startup.
      Other values can also be used with device drivers. Two common values are Description, which is a string value that allows you to give your driver a long description for display by the Service Control Manager (SCM), and ImagePath, a string value that allows you to specify a fully-qualified path name to your driver image. You'd use this if your driver isn't stored in the default location.
      As you can see, drivers and services are largely treated the same from an installation and configuration standpoint. For more detailed information on the installation and configuration of services, see the Platform SDK documentation.

Access to Restricted Resources
      You now have all the ingredients required to do privileged-mode operations within the Windows NT driver architecture. It's time to pull all this knowledge together and show you how to provide meaningful services to user mode applications.
      As a secure system, Windows NT places restrictions on almost all hardware resources from user mode. For example, all I/O ports in a system are restricted to access from kernel-mode only. This differs from Windows 95, which only restricts access to I/O ports on demand from a VxD for the purpose of I/O port virtualization.
      Most I/O ports in a system are utilized by peripherals. There are a number of system resources available via I/O ports that are off-limits to user mode processes under Windows NT. For example, the system's CMOS memory contains system configuration information such as the amount of physical RAM installed in the computer. Interrogating these ports to obtain this information is trivial under Windows 95 and MS-DOS; you can do it simply by utilizing the IN and OUT instructions, or by using their C runtime counterparts _inp and _outp. Under Windows NT, they are inaccessible to lowly user mode processes. Kernel-mode, though, has unrestricted access to these system resources.
      The amount of physical RAM present in a system is available in the form of base and extended memory. Accessing these values requires writing a byte to port 70h and reading the results from port 71h. The bytes that need to be written to port 70h can be the following:

15hGet the low byte of base memory
16hGet the high byte of base memory
17hGet the low byte of extended memory
18hGet the high byte of extended memory
The results are returned in kilobytes (in a SHORT value), and need to be obtained a byte at a time. For example, to get the amount of extended memory from the CMOS in MS-DOS or Windows 95, you'd do the following:

 _outp(0x70, 0x17);
 loByte = _inp(0x71);
 _outp(0x70, 0x18);
 hiByte = _inp(0x71);
 
 ExtendedMemoryInK = (hiByte << 8) | lowByte;
Using _inp and _outp in a Win32 application is completely valid. Running a Win32 application under Windows NT that uses this code will cause a fault because Windows NT restricts I/O permissions from user mode applications.
      The code shown above can be moved almost wholesale into a Windows NT driver, where direct access to I/O ports is permitted. In MSJDrvr.c (see Figure 12), the function MSJReadMemoryStatsFromCMOS implements this for the IOCTL_MSJDRVR_READ_CMOS control code. The base and extended memory values obtained from the CMOS are returned to the caller in a structure. The values are retrieved by the following code:

 // Get base memory bytes from CMOS
 WRITE_PORT_UCHAR((PUCHAR)0x70, 0x15);
 byLowByte = READ_PORT_UCHAR((PUCHAR)0x71);
 WRITE_PORT_UCHAR((PUCHAR)0x70, 0x16);
 byHiByte = READ_PORT_UCHAR((PUCHAR)0x71);

 pMemory->ulBaseMem = (byHiByte << 8) | byLowByte;

 // Get extended memory bytes from CMOS
 WRITE_PORT_UCHAR((PUCHAR)0x70, 0x17);
 byLowByte = READ_PORT_UCHAR((PUCHAR)0x71);
 WRITE_PORT_UCHAR((PUCHAR)0x70, 0x18);
 byHiByte = READ_PORT_UCHAR((PUCHAR)0x71);
 
 pMemory->ulExtMem = (byHiByte << 8) | byLowByte;
As you can see, it's similar to the previous code snippet. The calls to _inp and _outp have been replaced by WRITE_PORT_UCHAR and READ_PORT_UCHAR, which are supplied by the Windows NT kernel as abstracted wrappers around I/O functionality. In theory, these functions are portable to other platforms. In practice, I could have easily placed my own inline assembler or C wrappers around the Intel IN and OUT instructions, but the results would be the same. Even though this code may not be applicable on non-Intel PC platforms, the functionality to read and write to I/O ports is provided. If there's no need to circumvent the infrastructure given to you to do the job, then don't do it!
      Even though this code works fine, the Windows NT driver architecture has a premise of one driver per device. This is really no different than the infrastructure for VxDs. For example, VPICD in Windows 95 restricts an IRQ to a single service routine. Even though interrupt chaining could be supported, Microsoft has chosen the strategy that when one device driver claims a resource, no others can wrest control away from it. This brings some sense of order to things, but unduly restricts layering schemes that could be implemented if, for example, ISRs could be chained.
      Similar restrictions are placed on hardware interrupt routines under Windows NT. The I/O Manager restricts more than one driver from attaching to an IRQ via IoConnectInterrupt if the target device is not level-sensitive. If the system's host bus is level-sensitive (where more than one hardware device can be attached to an IRQ line), the I/O Manager permits chaining under the assumption that each device has its own individual driver. If you want to be able to see IRQ requests from a device under the control of some other driver, you're generally out of luck unless the controlling device driver builds some type of interface or chaining mechanism itself (such as the parallel port driver in Windows NT).
      Windows NT tries to enforce a similar restriction on I/O ports through the use of the function IoReportResourceUsage. In theory, if your driver utilizes an I/O port or a range of ports, you should claim control of this resource through IoReportResourceUsage. As demonstrated, though, no such restriction is enforced by the Windows NT kernel. It is easy to envision how you could wreak havoc on an unwitting device driver that thinks it left a target device in an implicit state. In short, be careful and make sure you know what you're doing.
      Even though my earlier example of I/O port reading to obtain CMOS data works fine, there is a more appropriate method for obtaining this information under Windows NT. The CMOS resource is owned by HAL, and its contents can be obtained easily with a call to HalGetBusData, passing a bus type of Cmos as its first parameter. This is shown in MSJReadMemoryStatsFromCMOS in MSJDrvr.c:

 HalGetBusData(Cmos, 0, 0, &byBuffer, sizeof(byBuffer));
When this function returns, the buffer contains the CMOS data that was obtained by HAL. Plucking out the aforementioned base and extended memory values is easily done by accessing the appropriate offsets into the buffer:

 // Base memory
 byLowByte = byBuffer[0x15];
 byHiByte = byBuffer[0x16];
 pMemory->ulBaseMem = (byHiByte << 8) | byLowByte;
 
 // Extended Memory
 byLowByte = byBuffer[0x17];
 byHiByte = byBuffer[0x18];
 pMemory->ulExtMem = (byHiByte << 8) | byLowByte

Adding Access to PhysicalDrive0
      Earlier, I discussed how to access the MBR via the ATDISK/SCSI port driver from user mode. But access to this functionality is restricted to accounts with administrative privileges. Now that you've seen how to create and call drivers from non-administrative accounts, it's time to show how you can leverage this to get access to normally restricted operations.
      As explained earlier, two or more drivers generally cannot talk to the same hardware resource. Even if they could, it would be an awful duplication of effort to rewrite functionality to talk directly to ATA and SCSI devices simply to read a sector off of a disk. Instead, I'll use functionality provided by the I/O Manager to create a new IRP within my kernel mode driver. The IRP will be dispatched to the target device driver, just as if it were created by a call from a user mode process.
      Recall that once your driver is called by the I/O Manager, you're beyond any front-line security set up to restrict users from doing things that they are not supposed to. This means the drivers shown earlier that are restricted (generally, those that control the file system) are fair game within your device driver.
      Creating and dispatching an IRP is simple. The code to do this is presented in the MSJReadDriveZeroMasterBootRecord function in MSJDrvr.c. First, you need to obtain a pointer to the target device driver's DEVICE_OBJECT. This is done by a call to IoGetDeviceObjectPointer, passing in a Unicode string representing the name of the driver:


 // Initialize unicode string
 RtlInitUnicodeString(&uszDeviceName, 
                      L"\\DosDevices\\PhysicalDrive0");
 
 // Get a pointer to PhysicalDrive0
 ntStatus = IoGetDeviceObjectPointer(&uszDeviceName,
                                    FILE_READ_ATTRIBUTES,      
                                    &fileObject,
                                    &pDriveDeviceObject);
It's interesting to note that this function works equally well with symbolic names in the user mode namespace as it does with the kernel mode-only names referenced in the call to IoCreateDevice. This greatly simplifies your life since you can reference the generic symbolic link rather than the specific SCSI or ATA device driver by name.
      Also note that the pointer to the returned device object comes with no restrictions. If you have carnal knowledge of a particular device object—for example, if you know the structure of its device extension—you can access it via this pointer. Referencing a device extension in this manner is not recommended unless you absolutely know what you're doing.
      Now that you have a valid pointer to the target device driver's DEVICE_OBJECT, an IRP needs to be built and dispatched to it. Building IRPs is performed by calling the I/O Manager's IoBuildSynchronousFsdRequest, IoBuildAsynchronousFsdRequest, or IoBuildDeviceIoControlRequest functions. The first two functions are restricted to calling the IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_ FLUSH_BUFFERS, or IRP_MJ_SHUTDOWN IOCTLs. The latter function can call any IOCTL supported by the driver. For simplicity, I'll use IoBuildSynchronousFsdRequest.

 KeInitializeEvent(&event, NotificationEvent, FALSE); 
 
 sectorNum.LowPart = sectorNum.HighPart = 0;
 
 pIrp = IoBuildSynchronousFsdRequest(IRP_MJ_READ,     
                                      pDriveDeviceObject,
                                      pBuffer,
                                      512,
                                      &sectorNum,
                                      &event,
                                      &ioStatus);
The parameters passed to IoBuildSynchronousFsdRequest are similar to user mode's ReadFile counterpart. A kernel mode event object is created with a call to KeInitializeEvent and associated with the IRP. This event handle is used to block while the IRP is pending.
      Now that the IRP is created, it can be dispatched to the I/O Manager via a call to IoCallDriver, destined for the target driver:

 ntStatus = IoCallDriver(pDriveDeviceObject, pIrp);
 
 if(ntStatus == STATUS_PENDING)
 {
     KeWaitForSingleObject(&event, Suspended,   
                           KernelMode, FALSE, NULL);
     ntStatus = ioStatus.Status;
 }
Upon successful completion, the buffer referenced when the IRP was created will be filled with the results of the call. These results can be passed back to the caller, just as if the caller made the call to the device driver directly. It's fairly easy to see how your driver can act as a proxy to do things that are restricted or invisible to user mode apps.
      I have not touched on asynchronous processing of IRPs here (via IoBuildAsynchronousFsdRequest). Asynchronous processing permits you to assign a completion routine that is called within your kernel mode driver when an IRP completes. This permits you to return control to the user mode process, giving you the flexibility of signaling the user mode event handle when the operation is finished. This is the foundation of asynchronous I/O, and it makes use of the OVERLAPPED structure in your user mode I/O apparent.

Dynamically Loading a Windows NT Driver
      As mentioned earlier, Windows NT drivers are installed and managed much in the same way that Windows NT services are. They can be started and stopped using the same utilities as services (like the net command). So, it should be no surprise that drivers can be installed, started, stopped, and removed via the APIs provided by the SCM. Exploiting these APIs to dynamically load and unload drivers on demand is a snap. This is demonstrated in Dynamic.c (shown in Figure 16). I'll cover the required APIs here.
      Before you can call CreateFile to obtain a handle to your driver, the driver first needs a registry entry in this key:


 \HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services
Although typically added by the installation procedure of an application, you can easily add the appropriate registry entry via the SCM APIs. You first need to open a handle to the SCM:

 // Open Service Control Manager on the local machine...
 hSCManager = OpenSCManager(NULL, NULL, 
                            GENERIC_READ|GENERIC_WRITE); 
This succeeds only if it's called from an account with administrator privileges, which provides the security necessary so normal users cannot add and execute privileged code without the proper authority.
      Once the SCM has been opened, you add the driver by making a call to CreateService, passing the driver name and the fully qualified path name of the driver's file:

 // Create the driver entry in the SC Manager.
 hService=CreateService(
    hSCManager,           // SCManager database
     DriverName,           // name of service
     DriverName,           // name to display
     SERVICE_ALL_ACCESS,   // desired access
     SERVICE_KERNEL_DRIVER,// service type
     SERVICE_DEMAND_START, // start type
     SERVICE_ERROR_NORMAL, // error control type
     ServiceExe,           // service's binary
     NULL,                 // no load ordering group
     NULL,                 // no tag identifier
     NULL,                 // no dependencies
     NULL,                 // LocalSystem account
     NULL                  // no password
     );
      Now all you need to do is start the driver through the StartService call, passing the handle obtained from the previous call to CreateService:

 // Start the driver!
 bReturn = StartService(hService, 0, NULL);
At this point, you can call CreateFile, passing in the name of your newly loaded driver. Easy, huh?
      Stopping and unloading your driver is just as simple. If you previously disposed of the service handle for your driver, a call to OpenService will give you the handle required to manipulate the driver via the SCM:

 // Open the Service Control Manager for our driver 
 // service...
 hService = OpenService(hSCManager, DriverName, 
                        SERVICE_ALL_ACCESS);
First, you need to stop the driver. This is done by calling ControlService, passing a SERVICE_CONTROL_STOP message:

 // Stop the driver.  Will return TRUE on success...
 bReturn = ControlService(hService, SERVICE_CONTROL_STOP, 
                          &serviceStatus);
To complete your cleanup, you need to delete the registry entry made for you by the earlier call to CreateService. This is done by calling DeleteService:

 // Delete the driver from the registry...
 bReturn = DeleteService(hService);
      As you can see, dynamically manipulating a driver is fairly straightforward. Capitalizing on your knowledge of how services and the SCM work gives you the flexibility to minimize clutter and ease the installation burden of your Windows NT drivers.

Hiding Your Driver
      Now that you know how to dynamically load a Windows NT driver, you can take it one step further. Let's look at some sleight of hand that can be performed to effectively hide your privileged mode code. I'll demonstrate how to place your driver's image within your Win32 executable.
      Hiding your driver has a number of benefits. First, I hate clutter. These days, with applications that litter your computer with boatloads of files in numerous directories, eradicating an application and its supporting cast is nearly impossible. If you have a driver that, say, pokes around the CMOS (like my sample) and there is no hope for anyone else ever reusing it, you might as well place it within your Win32 executable. Second, you're effectively hiding the driver from view, throwing off the "how did you do that" riffraff who easter-egg through your application looking for clues.
      Implementing this is head-slappingly simple. Simply place your driver in with the resource data of your application. Inclusion is straightforward: place your driver in the source directory of your application, and add the following line to your .RC file:


 MSJDATNT    BINRES  MOVEABLE PURE   "MSJDRVR.SYS"
Now, within the initialization of your application, load the resource and write it out to a temporary file.

 HRSRC   hRsrc;
 HGLOBAL hDriverResource;
 DWORD   dwDriverSize;    
 LPVOID  lpvDriver;
 HFILE   hfTempFile;
 •
 •
 •

 hRsrc = FindResource(hInst,MAKEINTRESOURCE(MSJDATNT),"BINRES");
 
 hDriverResource = LoadResource(hInst, hRsrc);
 dwDriverSize = SizeofResource(hInst, hRsrc);
 lpvDriver = LockResource(hDriverResource);
     
 hfTempFile = _lcreat("msj.tmp",0);
 _hwrite(hfTempFile, lpvDriver, dwDriverSize); 
 _lclose(hfTempFile);
      At this point, you'd dynamically load your driver. Once it is loaded, you can safely delete the temporary file. It may be dopey and brute-force, but it does work. Keep in mind, of course, that you are restricted to doing this with users who have the appropriate privileges to dynamically load drivers (namely, those with administrator rights).

Final Pitfalls
      My biggest Windows NT driver pet peeve is a seemingly innocuous oversight of the BUILD environment that is set up by the Windows NT DDK. Even when using the SETENV.BAT file to set up your environment for building release or free versions of your driver, the NTDEBUG environment variable is not set. This has the impact of including your internal function and variable names in your driver file. Although it's not as bad as accidentally giving away your source code, someone who wants to unravel the innards of your device driver can easily use these internal symbols. Besides, it also bloats your driver unnecessarily.
      To underscore how serious this oversight is, I have been hard pressed to find a shipped, non-Microsoft Windows NT driver that does not have its internal symbols present. Do yourself a favor and do the following before building the release version of your driver:


 set NTDEBUG = retail
Your competitors may hate it, but I ensure that you'll sleep better at night knowing that you're not inadvertently giving away some deep, dark driver secret.
      The last dilemma to mention is some restrictions placed on video devices. As noted before with the output from Query.exe, the DISPLAY symbolic link is off-limits to everyone, including users with administrator rights. The premise behind this is that the Windows NT kernel should be the only one that has control of the video display.
      The problem gets worse when you dip into kernel mode. Let's say you try to execute the following code in your driver:

 RtlInitUnicodeString(&unicodeDeviceName, 
                      L"\\Device\\Video0");
 status = IoGetDeviceObjectPointer(&unicodeDeviceName,    
                                   FILE_READ_DATA, 
                                   &FileObject, 
                                   &DeviceObject);
Windows NT will bug check (you'll get a blue screen). Yes, you read that right, Windows NT will crash if you simply try to touch the video driver's device object! If you're trying to get hardware information on the video card installed, you're out of luck! The only other option is to dip into the registry to get whatever information the video driver may have placed there upon initialization.
      This is really just the tip of the iceberg when it comes to video support problems in kernel mode. Since so many video cards insist on using onboard BIOS for their only means of communication, special hacks have to be employed to support driver creation under Windows NT. In particular, a video miniport driver is highly dependent on the VideoPortInt10 API call, which is a trap door to the video card's BIOS. (VideoPortInt10 takes a structure of Intel x86 registers as a parameter—so much for portability!)

Conclusion
      Although I've presented quite a bit of information, I've left out almost as much. Trying to discuss everything that encompasses Windows NT driver development would easily fill a year's worth of MSJs. In particular, almost all of the hardware support aspects of Windows NT drivers, such as controller and adapter objects, asynchronous scheduling, IRQ and DPC processing, and the like has been omitted.
      At least you now have a demystified understanding of Windows NT kernel mode and the capabilities provided to you via Windows NT drivers. Although they are not the secret to world dominance, properly utilizing Windows NT drivers can give your applications previously unheard-of power. In future articles, I'll draw upon the base of knowledge presented here to further clarify the architecture and features available to you in Windows NT.

From the March 1998 issue of Microsoft Systems Journal.