Kernel-Mode Mistakes

Kernel-Mode Mistakes

The same good citizenship practices apply to drivers and kernel mode as with user-mode software. Of course, any kernel-mode failure is catastrophic. Thus, security for drivers includes the even larger issue of driver reliability. A driver that isn't reliable isn't secure. This section outlines some of the simple mistakes made and how they can be countered, as well as some best practices. It's assumed you are familiar with kernel-mode software development.

But before I start in earnest, you must use both Driver Verifier and the checked versions of Ntoskrnl.exe and Hal.dll to test that your driver performs to a minimum quality standard. The Windows DDK documentation has extensive documentation on both of these. You should also consider using the kernel-mode version of Strsafe.h discussed in Chapter 5, Public Enemy #1: The Buffer Overrun for string handling. The kernel-mode version is called NTStrsafe.h and is described in the release notes for the Windows XP Service Pack 1 DDK at http://www.microsoft.com/ddk/relnoteXPsp1.asp. Now let's look at some specifics.

High-Level Security Issues

Almost all drivers that create device objects must set FILE_DEVICE_SECURE_OPEN as a characteristic when the device object is created. The only drivers that should not set this bit in their device objects are those that implement their own security checking, such as file systems. Setting this bit is prerequisite to the I/O Manager always enforcing security on your device object.

Device object protection, set by a discretionary access control list (DACL) in a security descriptor (SD), should be specified in the driver's INF file. This is the best place to protect device objects. An SD can be specified in an AddReg section in either [ClassInstall32] or [DDInstall.HW] section of the INF file. Note that if the INF is tampered with and the driver has been signed by Windows Hardware Quality Labs (WHQL), the installation will report the tampering.

Use IoCreateDeviceSecure new to the Microsoft Windows .NET Server 2003 and Windows XP SP1 DDKs to create named device objects and physical device objects (PDOs) that can be opened in raw mode (that is, without a function driver being loaded over the PDO). This function is usable in Windows 2000 and later; you must include Wdmsec.h in your source code and link with Wdmsec.dll.

Many IOCTLs have historically been defined with FILE_ANY_ACCESS. These can't easily be changed in legacy code, owing to backward compatibility issues. However, for new code, to tighten up security on these IOCTLs, drivers can use IoValidateDeviceIoControlAccess to determine whether the opener has read or write access. This function is usable in Windows 2000 and later and is defined in Wdmsec.h.

Windows Management Instrumentation (WMI) is used to control devices, and its security works differently, in that it is per-interface instead of per-device. For Windows XP and earlier operating system versions, the default security descriptor for WMI GUIDs allows full access to all users. For Windows .NET Server 2003 and later versions, the default security descriptor allows access only to administrators. WMI interface security can be specified by adding a [DDInstall.WMI] section (new to the Windows .NET Server 2003 and Windows XP SP1 DDKs) containing an AddReg section with an SDDL string.

Drivers should avoid implementing their own security checks internally. Hard-coding security rules into driver dispatch routine code can result in drivers defining system policy. This tends to be inflexible and can cause system administration problems.

Handles

There are two types of handles that drivers can use: process-specific handles created by user-mode applications and global system handles created by drivers. Drivers should always specify OBJ_KERNEL_HANDLE in the object attributes structure when calling functions that return handles. This ensures that the handle can be accessed in all process contexts, and cannot be closed by a user-mode application.

Drivers must be exceedingly careful when using handles given to them by user-mode applications. First, such handles are context-specific. Second, an attacker might close and reopen the handle to change what it refers to while the driver is using it. Third, an attacker might be passing in such a handle to trick a driver into performing operations that are illegal for the application because access checks are skipped for kernel-mode callers of Zw functions. If a driver must use a user-mode handle, it should call ObReferenceObjectByHandle to immediately swap the handle for an object pointer. Additionally, callers of ObReferenceObjectByHandle should always specify the object type they expect and specify user mode for the mode of access (assuming the user is expected to have the same access that the driver has to the file object).

Symbolic Links

Many driver writers incorrectly assume their device cannot be opened without a symbolic link. This is not true Windows NT uses a single unified namespace that is accessible by any application. As such, any openable device must be secured.

Quota

Drivers often allocate memory on behalf of applications. This memory should be allocated using the ExAllocatePoolWithQuotaTag function under a try/except block. This function will raise an exception if the application has already allocated too much of the system memory.

Serialization Primitives

Don't mix spin-lock types. If a spin lock is acquired with KeAcquireSpinLock, it must always be acquired using this primitive. You can't associate this spin lock elsewhere with an in-stack-queued spin lock, for example. Also, it can't be the external spin lock associated with an interrupt object or the spin lock used to guard an interlocked list via ExInterlockedInsertHeadList. Intermixing spin-lock types can lead to deadlocks.

NOTE
Build a locking hierarchy for all serialization primitives, and stick with it.

Of course, it's a basic rule that your driver can't wait for a nonsignaled dispatcher object at IRQL_DISPATCH_LEVEL or above. Trying to do so results in a bugcheck.

Buffer-Handling Issues

A widespread mistake is not performing correct validation of pointers provided to kernel mode from user mode and assuming that the memory location is fixed. As most driver writers know, the portion of the kernel-mode address space that maps the current user process can change dynamically. Not only that, but other threads and multiple CPUs can change the protection on memory pages without notifying your thread. It's also possible that an attacker will attempt to pass a kernel-mode address rather than a user-mode address to your driver, causing instability in the system as code blindly writes to kernel memory.

You can mitigate most of these issues by probing all user-mode addresses inside a try/except block prior to using functions such as MmProbeAndLockPages and ProbeForRead and then wrapping all user-mode access in try/except blocks. The following sample code shows how to achieve this:

NTSTATUS AddItem(PWSTR ItemName, ULONG Length, ITEM *pItem) { NTSTATUS status = STATUS_NO_MORE_MATCHES; try { ITEM *pNewItem = GetNextItem(); if (pNewItem) { // ProbeXXXX raises an exception on failure. // Align on LARGE_INTEGER boundary. ProbeForWrite(pItem, sizeof ITEM, TYPE_ALIGNMENT(LARGE_INTEGER)); RtlCopyMemory(pItem, pNewItem, sizeof ITEM); status = STATUS_SUCCESS; } } except (EXCEPTION_EXECUTE_HANDLER) { status = GetExceptionCode(); } return status; }

On the subject of buffers, here's something you should know: zero-length reads and writes are legal and result in an I/O request packet (IRP) being sent to your driver with the length field (ioStack->Parameters.Read.Length) set at zero. Drivers must check for this before using other fields and assuming they are nonzero.

On a zero-length read, the following are true depending on the I/O type:

  • For direct I/O

    Irp->MdlAddress will be NULL.

  • For buffered I/O

    Irp->AssociatedIrp.SystemBuffer will be zero.

  • For neither I/O

    Irp->UserBuffer is will point to a buffer, but its length will be zero.

Do not rely on ProbeForRead and ProbeForWrite to fail zero-length operations they explicitly allow zero-length buffers!

When completing a request, the Windows I/O Manager explicitly trusts the byte count provided in Irp->IoStatus.Information if Irp->IoStatus.Status is set to any success value. The value returned in Irp->IoStatus.Information is used by the I/O Manager as the count of bytes to copy back to the user data buffer if the request uses buffered I/O. This byte count is not validated. Never set Irp->IoStatus.Status with the value passed in from the user in, for example, IoStack->Parameters.Read.Length. Doing so can result in an information disclosure problem. For example, a driver provides four bytes of valid data, but the user specified an 8K buffer, so the allocated system buffer is 8K and the I/O Manager copies four bytes of valid data and 8K-4 bytes of random data buffer contents from the system buffer. The system buffer is not initialized when it's allocated, so the 8K-4 bytes being returned is random, old, contents of the system's nonpaged pool.

Also note that the I/O Manager also transfers bytes back to user mode if Irp->IoStatus.Status is a warning value (that is, 0x80000000-0xBFFFFFFF). The I/O Manager does not transfer any bytes in the case of an error status (0xC0000000-0xFFFFFFFF). The appropriate status code with which to fail an IRP might depend upon this distinction. For instance, STATUS_BUFFER_OVERFLOW is a warning (data transferred), and STATUS_BUFFER_TOO_SMALL is an error (no bytes transferred).

Direct I/O creates a Memory Descriptor List (MDL) that can be used to directly map a user's data buffer into kernel virtual address space. This means that the buffer is mapped into kernel virtual address space and into user space simultaneously. Because the user application continues to have access while the driver does, it's important to never assume consistency of this data between accesses. That is, don't take multiple bites of data from user data buffer and assume data is consistent. Remember, the user could be changing the buffer contents while you're trying to process it. Similarly, don't use the user data buffer for temporary storage of intermediate results and assume this data won't be changed by the user.

One of the most common problems with IOCTLs and FSCTLs is a lack of buffer validity checks (buffer presence assumed, sufficient length assumed, data supplied is implicitly trusted). There's a common belief that a specified user-mode application is the only one talking to the driver this is potentially incorrect.

There's an issue with using METHOD_NEITHER on IOCTLs and FSCTLs; the user arguments for Inbuffer, InBufLen, OutBuffer, and OutBufLen are passed from the I/O Manager precisely as provided by the user and without any validation. This makes using this transfer type much more complicated than using the more generally applicable METHOD_BUFFERED, METHOD_IN_DIRECT, and METHOD_OUT_DIRECT. Don't forget: access must be done in the context of the requested process!

The same issue occurs when using fast I/O. Although only file systems can implement fast I/O for reads and writes, ordinary drivers can implement fast I/O for IOCTLs. This is the same as using METHOD_NEITHER.

In both of these cases, even though the buffer pointer is non-NULL and the buffer length is nonzero, the buffer pointer still might not be valid or, worse, might specify a location to which the user does not have appropriate access but the driver does.

IRP Cancellation

Probably the single greatest cause of driver problems is the cancel routine because of inherent race conditions between IRP cancellation and I/O initiation, I/O completion, and IRP_MJ_CLEANUP. The best advice is to implement IRP cancel only if necessary. Drivers that can guarantee that IRPs will complete in a short period of time typically, a couple of seconds generally do not need to implement cancellation. Not implementing cancellation is a great way to reduce errors.

Avoid attempting to cancel in-progress I/O requests, because this is also a common source of problems. Unless the I/O will take an indeterminate amount of time to complete, don't try to cancel in-progress requests. Obviously, some drivers must implement in-progress cancellation for example, the serial port driver because a pending read IRP might sit there forever. Even in this case, perhaps use a timer to see if it will complete on its own in a short time.

Never try to optimize IRP cancellation. It's a rare event, so why optimize it? If you must implement IRP cancellation, consider using the IoCsqXxxx functions. These are defined in CSQ.H.

If using system queuing, consider using IoSetStartIoAttributes with NonCancellable set to TRUE. (This function is available in Windows XP and later.) This ensures that the driver's startIo entry point is never called with a cancelled IRP. This approach is helpful because it avoids nasty races, and you should always use it when using system queuing and when the driver does not allow in-progress requests to be cancelled.



Writing Secure Code
Writing Secure Code, Second Edition
ISBN: 0735617228
EAN: 2147483647
Year: 2001
Pages: 286

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