Fun facts: Windows kernel and Device Extension Size

Today, I would like to start sharing some of the most amusing examples of the Windows kernel behavior that I often stumble upon while reverse-engineering its various areas, exploiting a particular vulnerability or just randomly exploring its code. Some of them might have certain implications for security, some are completely impractical and are presented for the sole purpose of entertainment. This post certainly belongs to the second group. Enjoy!

Oh and by the way, the discovery and exploitation of CVE-2011-2018 (as described in my detailed white paper) has been awarded with a Pwnie Award! Woot, thanks for the recognition :) Congratulations to all the other winners and nominees, especially Fermin Serna (@fjserna) with his amazing information leak research and Adobe Flash vulnerability.

Pwnie Award 2012 for Best Privilege Escalation Bug

Device extensions

As Microsoft states in the “Device Extensions” MSDN article:

For most intermediate and lowest-level drivers, the device extension is the most important data structure associated with a device object. Its internal structure is driver-defined, and it is typically used to:

  • Maintain device state information.
  • Provide storage for any kernel-defined objects or other system resources, such as spin locks, used by the driver.
  • Hold any data the driver must have resident and in system space to carry out its I/O operations.

In essence, a device extension is a memory region that the NT kernel allocates from the non-paged pool and associates with a particular Device object. The extension’s size is arbitrary and fully controlled by every device driver through the DeviceExtensionSize of the IoCreateDevice routine, a declaration of which is shown below:

NTSTATUS IoCreateDevice(
 _In_      PDRIVER_OBJECT DriverObject,
 _In_      ULONG DeviceExtensionSize,
 _In_opt_  PUNICODE_STRING DeviceName,
 _In_      DEVICE_TYPE DeviceType,
 _In_      ULONG DeviceCharacteristics,
 _In_      BOOLEAN Exclusive,
 _Out_     PDEVICE_OBJECT *DeviceObject
);

Now, the public documentation doesn’t give any clue about how much bytes for the extension can actually be requested from the system, nor does it otherwise specify any restrictions in regards to the size. Let’s look into how nt!IoCreateDevice works under the hood.

The actual implementation of the routine in Windows XP / 2003 (which is still valid) can be found in the base\ntos\io\iomgr\iosubs.c file of the Windows Research Kernel package. The important snippet of code is as follows:

        RoundedSize = (sizeof( DEVICE_OBJECT ) + DeviceExtensionSize)
                      % sizeof (LONGLONG);
       if (RoundedSize) {
           RoundedSize = sizeof (LONGLONG) - RoundedSize;
       }

       RoundedSize += DeviceExtensionSize;

       status = ObCreateObject( KernelMode,
                                IoDeviceObjectType,
                                &objectAttributes,
                                KernelMode,
                                (PVOID) NULL,
                                (ULONG) sizeof( DEVICE_OBJECT ) + sizeof ( DEVOBJ_EXTENSION ) +
                                        RoundedSize,
                                0,
                                0,
                                (PVOID *) &deviceObject );

To complete the picture, the ObCreateObject declaration is shown below:

NTSTATUS
ObCreateObject (
    __in KPROCESSOR_MODE ProbeMode,
    __in POBJECT_TYPE ObjectType,
    __in POBJECT_ATTRIBUTES ObjectAttributes,
    __in KPROCESSOR_MODE OwnershipMode,
    __inout_opt PVOID ParseContext,
    __in ULONG ObjectBodySize,
    __in ULONG PagedPoolCharge,
    __in ULONG NonPagedPoolCharge,
    __out PVOID *Object
    )

The ObjectBodySize parameter specifies the desired size of the object memory area in bytes, and as such is used as a part of the actual allocation size – in addition to sizeof(OBJECT_HEADER) – eventually performed by ExAllocatePoolWithTag. Now, since the device extension is allocated as a part of the device object and no specific checks are performed to prevent an integer overflow from occurring, the sizeof(DEVICE_OBJECT) + sizeof(DEVOBJ_EXTENSION) + DeviceExtensionSize expression can easily overflow for a large enough extension size. This, in turn, would lead to a typical buffer overflow condition due to an undersized buffer, and later inevitably to a system crash. The following piece of code has been used to confirm the observed behavior:

#include <ntddk.h>

PDEVICE_OBJECT pDeviceObject = NULL;
VOID     DriverUnload(PDRIVER_OBJECT);

NTSTATUS
DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RPath) {
 const WCHAR DriverName[] = L"\\Device\\ExtensionSizeTest";
   UNICODE_STRING u_DriverName;
   NTSTATUS status;

   RtlInitUnicodeString(&u_DriverName, DriverName);

   status = IoCreateDevice(
           DriverObject,
           4294967232, // (ULONG)-64
           &u_DriverName,
           FILE_DEVICE_UNKNOWN,
           FILE_DEVICE_SECURE_OPEN,
           FALSE,
           &pDeviceObject
           );

   DriverObject->DriverUnload = DriverUnload;
   return status;
}

VOID
DriverUnload(PDRIVER_OBJECT pDriverObject)
{
   IoDeleteDevice(pDeviceObject);
}

After loading the above device driver, an attempt to unload it immediately results in the following crash; the actual crash pattern may differ in your test environment:

SYSTEM_THREAD_EXCEPTION_NOT_HANDLED (7e)
This is a very common bugcheck.  Usually the exception address pinpoints
the driver/function that caused the problem.  Always note this address
as well as the link date of the driver/image that contains this address.
Arguments:
Arg1: c0000005, The exception code that was not handled
Arg2: 829c0505, The address that the exception occurred at
Arg3: 89d87ac0, Exception Record Address
Arg4: 89d87520, Context Record Address

Debugging Details:
------------------

DBGHELP: e:\symbols\ntkrpamp.exe\4CE78A09412000\ntkrpamp.exe - OK

EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%08lx referenced memory at 0x%08lx. The memory could not be %s.

FAULTING_IP:
nt!IoDeleteAllDependencyRelations+27
829c0505 8b5808          mov     ebx,dword ptr [eax+8]

EXCEPTION_RECORD:  89d87ac0 -- (.exr 0xffffffff89d87ac0)
ExceptionAddress: 829c0505 (nt!IoDeleteAllDependencyRelations+0x00000027)
  ExceptionCode: c0000005 (Access violation)
 ExceptionFlags: 00000000

NumberParameters: 2
  Parameter[0]: 00000000
  Parameter[1]: 00000008
Attempt to read from address 00000008

CONTEXT:  89d87520 -- (.cxr 0xffffffff89d87520)
eax=00000000 ebx=00000000 ecx=94243900 edx=00000000 esi=859ad6a8 edi=00000000
eip=829c0505 esp=89d87b88 ebp=89d87b9c iopl=0         nv up ei pl nz ac po cy
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010213
nt!IoDeleteAllDependencyRelations+0x27:
829c0505 8b5808          mov     ebx,dword ptr [eax+8] ds:0023:00000008=????????
Resetting default scope

[...]

STACK_TEXT:  
89d87b9c 82818790 859ad5d0 94243900 94243900 nt!IoDeleteAllDependencyRelations+0x27
89d87bb8 923080a1 859ad5d0 89d87c00 829cbd46 nt!IoDeleteDevice+0x23
WARNING: Stack unwind information not available. Following frames may be wrong.
89d87bc4 829cbd46 8675cc40 94243900 84fe04c0 DevExtSize+0x10a1
89d87c00 82881aab 94243900 00000000 84fe04c0 nt!IopLoadUnloadDriver+0x1e
89d87c50 82a0df5e 00000001 18ae3bb3 00000000 nt!ExpWorkerThread+0x10d
89d87c90 828b5219 8288199e 00000001 00000000 nt!PspSystemThreadStartup+0x9e
00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x19

One could argue that this just shows that passing invalid parameters to kernel APIs can result in a bugcheck. That’s certainly true for 32-bit platforms – you cannot realistically expect the kernel to allocate 4GB of memory when it has a 2GB virtual address space. However, the behavior also affects the 64-bit edition of Microsoft Windows, which should typically allow the allocation of buffers of this size (or at least cleanly refuse through an adequate error code). Although I can’t imagine a scenario in which it could lead to a real security issue – device extensions always have constant sizes that are by no means controlled by anyone on the outside, and they usually fit into one or two memory pages – I still find it exceedingly funny that passing a theoretically valid parameter brings the machine down due to a silly arithmetic error. That’s about it, stay tuned for more posts :)