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.
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 :)