Handling IRP_MJ_DEVICE_CONTROL

Handling IRP_MJ_DEVICE_CONTROL

Each user-mode call to DeviceIoControl causes the I/O Manager to create an IRP with the major function code IRP_MJ_DEVICE_CONTROL and to send that IRP to the driver dispatch routine at the top of the stack for the addressed device. The top stack location contains the parameters listed in Table 9-2. Filter drivers might interpret some private codes themselves but will if correctly coded, that is pass all others down the stack. A dispatch function that understands how to handle the IOCTL will reside somewhere in the driver stack most likely in the function driver, in fact.

Table 9-2. Stack Location Parameters for IRP_MJ_DEVICE_CONTROL

Parameters.DeviceIoControl Field

Description

OutputBufferLength

Length of the output buffer sixth argument to DeviceIoControl

InputBufferLength

Length of the input buffer fourth argument to DeviceIoControl

IoControlCode

Control code second argument to DeviceIo Control

Type3InputBuffer

User-mode virtual address of input buffer for METHOD_NEITHER

A skeletal dispatch function for control operations looks like this:

#pragma PAGEDCODE NTSTATUS DispatchControl(PDEVICE_OBJECT fdo, PIRP Irp) { 

PAGED_CODE(); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; NTSTATUS status = STATUS_SUCCESS; ULONG info = 0;

PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength; ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength; ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; switch (code) {

default: status = STATUS_INVALID_DEVICE_REQUEST; break; } return CompleteRequest(Irp, status, info); }

  1. You can be sure of being called at PASSIVE_LEVEL, so there s no particular reason for a simple dispatch function to be anywhere but paged memory.

  2. The next few statements extract the function code and buffer sizes from the parameters union in the I/O stack. You often need these values no matter which specific IOCTL you re processing, so I find it easier always to include these statements in the function.

  3. This is where you get to exercise your own creativity by inserting case labels for the various IOCTL operations you support.

  4. It s a good idea to return a meaningful status code if you re given an IOCTL operation you don t understand.

    IMPORTANT

    Always switch on the full 32 bits of the I/O control code to prevent a user-mode program from sneaking past the access checks or causing the I/O Manager to prepare the parameters using the wrong buffering method.

The way you handle each IOCTL depends on two factors. The first and most important of these is the actual purpose of the IOCTL in your scheme of things. (Duh.) The second factor, which is critically important to the mechanics of your code, is the method you selected for buffering user-mode data.

In Chapter 7, I discussed how you work with a user-mode program sending you a buffer load of data for output to your device or filling a buffer with input from your device. As I indicated there, when it comes to read and write requests, you have to make up your mind at AddDevice time whether you re going to use the so-called buffered method or the direct method (or neither of them) for accessing user-mode buffers in all read and write requests. Control requests also use one of these addressing methods, but they work a little differently. Rather than specify a global addressing method via device-object flags, you specify the addressing method for each IOCTL by means of the 2 low-order bits of the function code. Consequently, you can have some IOCTLs that use the buffered method, some that use a direct method, and some that use neither method. Moreover, the methods you pick for IOCTLs don t affect in any way how you address buffers for read and write IRPs.

You choose one or the other buffering method based on several factors. Most IOCTL operations transfer much less than a page worth of data in either direction and therefore use the METHOD_BUFFERED method. Operations that will transfer more than a page of data should use one of the direct methods. The names of the direct methods seem to oppose common sense: you use METHOD_IN_DIRECT if the application is sending data to the driver and METHOD_OUT_DIRECT if it s the other way around. If the application needn t transfer any data at all, METHOD_NEITHER would be your best choice.

METHOD_BUFFERED

With METHOD_BUFFERED, the I/O Manager creates a kernel-mode copy buffer big enough for the larger of the user-mode input and output buffers. When your dispatch routine gets control, the user-mode input data is sitting in the copy buffer. Before completing the IRP, you fill the copy buffer with the output data you want to send back to the application. When you complete the IRP, you set the IoStatus.Information field equal to the number of output bytes you put into the copy buffer. The I/O Manager then copies that many bytes of data back to user mode and sets the feedback variable equal to that same count. Figure 9-3 illustrates these copy operations.

figure 9-3 buffer management with method_buffered.

Figure 9-3. Buffer management with METHOD_BUFFERED.

Inside the driver, you access both buffers at the same address namely, the AssociatedIrp.SystemBuffer pointer in the IRP. Once again, this is a kernel-mode virtual address that points to a copy of the input data. It obviously behooves you to finish processing the input data before you overwrite this buffer with output data. (I hardly need to tell you it s the kind of mistake you ll make only once.)

Here s a simple example, drawn from the IOCTL sample program, of the code-specific handling for a METHOD_BUFFERED operation:

case IOCTL_GET_VERSION_BUFFERED: { if (cbout < sizeof(ULONG)) { status = STATUS_INVALID_BUFFER_SIZE; break; } PULONG pversion = (PULONG) Irp->AssociatedIrp.SystemBuffer; *pversion = 0x0004000A; info = sizeof(ULONG); break; }

We first verify that we ve been given an output buffer at least long enough to hold the doubleword we re going to store there. Then we use the SystemBuffer pointer to address the system copy buffer, in which we store the result of this simple operation. The info local variable ends up as the IoStatus.Information field when the surrounding dispatch routine completes this IRP. The I/O Manager copies that much data from the system copy buffer back to the user-mode buffer.

Always check the length of the buffers you re given with an IRP_MJ_DE VICE_CONTROL, at least when the IRP s RequestorMode isn t equal to Kernel Mode. With METHOD_BUFFERED and the two METHOD_XXX_DIRECT methods, the I/O Manager will verify that the address and the length of the input and output buffers are valid, but you are the only one who knows how long the buffers should be.

The DIRECT Buffering Methods

Both METHOD_IN_DIRECT and METHOD_OUT_DIRECT are handled the same way in the driver. They differ only in the access rights required for the user-mode buffer. METHOD_IN_DIRECT needs read access; METHOD_OUT_DIRECT needs read and write access. With both of these methods, the I/O Manager provides a kernel-mode copy buffer (at AssociatedIrp.SystemBuffer) for the input data and an MDL for the output data buffer. Refer to Chapter 7 for all the gory details about MDLs and to Figure 9-4 for an illustration of this method of managing the buffers.

figure 9-4 buffer management with method_xxx_direct.

Figure 9-4. Buffer management with METHOD_XXX_DIRECT.

Here s an example of a simple handler for a METHOD_XXX_DIRECT request:

case IOCTL_GET_VERSION_DIRECT: { if (cbout < sizeof(ULONG)) { status = STATUS_INVALID_BUFFER_SIZE; break; } PULONG pversion = (PULONG) MmGetSystemAddressForMdlSafe(Irp->MdlAddress); *pversion = 0x0004000B; info = sizeof(ULONG); break; }

The only substantive difference between this example and the previous one is the bold line. (I also altered the reported version number so that I could easily know I was invoking the correct IOCTL from the test program.) With either DIRECT-method request, we use the MDL pointed to by the MdlAddress field of the IRP to access the user-mode output buffer. You can do direct memory access (DMA) using this address. In this example, I just called MmGetSystem AddressForMdlSafe to get a kernel-mode alias address pointing to the physical memory described by the MDL.

TIP
To achieve binary portability, use the portable workaround for MmGetSystemAddressForMdlSafe that I described in Chapter 7.

METHOD_NEITHER

With METHOD_NEITHER, the I/O Manager doesn t try to translate the user-mode virtual addresses in any way. Consequently, you most often use METHOD_NEITHER when you don t need to transfer any data into or out of the driver. IOCTL_SERIAL_SET_DTR, for example, is a standard serial port IOCTL for setting the Data Terminal Ready (DTR) signal line. It s defined to use METHOD_NEITHER because it has no data.

It s possible to use METHOD_NEITHER when you do have data, however, provided you follow some rules about pointer validation. You get (in the Type3InputBuffer parameter in the stack location) the user-mode virtual address of the input buffer, and you get (in the UserBuffer field of the IRP) the user-mode virtual address of the output buffer. Neither address is of any use unless you know you re running in the same process context as the user-mode caller. If you do know you re in the right process context, you can just directly dereference the pointers:

case IOCTL_GET_VERSION_NEITHER: { if (cbout < sizeof(ULONG)) { status = STATUS_INVALID_BUFFER_SIZE; break; } PULONG pversion = (PULONG) Irp->UserBuffer; if (Irp->RequestorMode != KernelMode) { __try { ProbeForWrite(pversion, sizeof(ULONG), 1); *pversion = 0x0004000A; } __except(EXCEPTION_EXECUTE_HANDLER) { status = GetExceptionCode(); break; } } else *pversion = 0x0004000A; info = sizeof(ULONG); break; }

As shown in the preceding code in boldface, the only real glitch here is that you want to make sure that it s OK to write into any buffer you get from an untrusted source. Refer to Chapter 3 if you re rusty about structured exceptions. ProbeForWrite is a standard kernel-mode service routine for testing whether a given user-mode virtual address can be written. The second argument indicates the length of the data area you want to probe, and the third argument indicates the alignment you require for the data area. In this example, we want to be sure that we can access 4 bytes for writing, but we re willing to tolerate single-byte alignment for the data area itself. What ProbeForWrite (and its companion function ProbeForRead) actually tests is whether the given address range has the correct alignment and occupies the user-mode portion of the address space it doesn t actually try to write to (or read from) the memory in question.

Additionally, you must perform the access within a structured exception frame. If any portion of the buffer happens to belong to nonexistent pages at the time of the access, the memory manager will raise an exception instead of immediately bugchecking. Your exception handler will backstop the exception and prevent the system from crashing.

Designing a Safe and Secure IOCTL Interface

How you design the IOCTL interface to your driver can have a big impact on the security and robustness of systems that eventually run your code. You ve probably noticed that this chapter has a lot of the little Security icons we re using to flag potential security problems. That s because it s pretty easy for sloppy coding to open unforeseen security holes or to unintentionally compromise system integrity.

So, in addition to all the other things I m pointing out in this chapter, here are some more things to consider as you design an IOCTL interface for your driver.

  • Don t assume that the only caller of your IOCTL interface will be your own application, which you have (of course!) crafted to supply only valid parameters. Cyberterrorists use the same technique that a housefly uses to break in: they buzz around the screen door until they find a hole. If your driver has a hole, hackers will find it and publicize it so widely that everyone who cares will know.

  • Don t pass null-terminated strings as arguments to an IOCTL. Provide a count instead. That way, the driver won t risk walking off the end of a page looking for a null terminator that happens not to be there.

  • Don t put pointers inside the structures you use in IOCTL calls. Instead, package all the data for a particular call into a single buffer that contains internal offset pointers. Take the time in the driver to validate the offsets and lengths with respect to the overall length of the parameter structure.

  • Don t write the equivalent of IOCTL_POKE_KERNEL_MEMORY. That is, don t invent some sort of utility control operation whose job is to write to kernel memory. Really don t do this, even in the debug version of your driver, because debug versions have been known to make it out of the lab.

  • Be careful with hardware pass-through IOCTLs. Maybe your application has a need to directly communicate with dedicated hardware, and maybe some sort of pass-through operation is the best way to provide this functionality. Just be careful how much functionality you open up.

  • Avoid making one IOCTL dependent on state information left over from some preceding operation. The one invariant rule about persistent state information is that it isn t. Persistent, that is. Something always goes wrong to clobber the data you were sure would stay put, so try to design IOCTL operations to be as nearly self-contained as you can.

  • It s better not to use METHOD_NEITHER for control operations that involve data transfer because of two risks. First, you might forget to do all the buffer validation that s required to avoid security holes. Second, somewhere down the road, someone might forget that the IOCTL used METHOD_NEITHER and call you in the wrong thread context. If you and the programmers that follow in your footsteps are supremely well organized, these problems won t arise, but one component of successful engineering is to take account of human foibles.

  • Above all, don t assume that no one will try to compromise the system through your driver. There need be only one supremely evil person in the world for it to be dangerous to trust everyone, and there are many more than just one. It s not an exaggeration to suggest that actual lives may be on the line when the enemies of civilization systematically exploit weaknesses in the world s most prevalent operating system.

You can use the DEVCTL tool in the DDK to help you test your IOCTL interface for robustness, by the way. This tool will send random IOCTLs and well-formed IOCTLs with bad parameters to your driver in an attempt to provoke a failure. This sort of attack mimics what a script kiddie will do as soon as he or she lays hands on your driver, so you should run this test yourself anyway.

Since there are limits on how cunning a generic tool such as DEVCTL can be, I also recommend that you build your own test program to thoroughly explore all the boundary conditions in your IOCTL interface. Here are some ideas for things to test:

  • Invalid function codes. Don t go nutty here since DEVCTL will do this type of testing exhaustively.

  • Function codes with mistakes in one or more of the four fields (device type, access mask, function code, and buffering method).

  • Missing or extraneous input or output buffers.

  • Buffers that are too short but not altogether missing.

  • Simultaneous operations from different threads using the same handle and using different handles.



Programming the Microsoft Windows Driver Model
Programming the Microsoft Windows Driver Model
ISBN: 0735618038
EAN: 2147483647
Year: 2003
Pages: 119
Authors: Walter Oney

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net