Windows kernel

Windows kernel #

This list includes basic checks for Windows kernel drivers and modules.

  • Run CodeQL on the application’s driver. Microsoft has published CodeQL support and security query packs for Windows drivers.
    • If you can build the driver from source, CodeQL is a high-value SAST approach.
    • If you cannot build the driver from source, you may still be able to run CodeQL with --build-mode=none on the CodeQL CLI during database creation, but coverage and accuracy will be significantly diminished.
  • Run Driver Verifier against the driver binary to test it for issues.
  • Run BinSkim to check mitigation opt-in and other issues in the driver binary.
    • DEP (NX) support should be enabled.
    • Forced integrity checking (IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY) should be enabled to prevent unsigned binaries being loaded by the driver.
    • Note that old drivers built pre-VS2015 will have the INIT section marked as RWX and discardable. This is generally harmless as the INIT section is unmapped after DriverEntry returns, but it is a good indication that the driver was built with a very old toolchain (this was fixed in VS2015).
  • Check uses of InitializeObjectAttributes, the primary macro used to set up object attributes and security descriptors. This is widely relevant.
    • Ensure that OBJ_KERNEL_HANDLE is passed if the created object should only be accessed within the kernel.
    • If a security descriptor is passed (last argument), check that it is appropriate.
    • If a security descriptor is not passed (last argument is NULL), this will create the object with the default security descriptor.
      • On Windows 8.1 and prior, many system object namespaces (e.g., symlinks) have no inheritable ACEs by default, so a NULL security descriptor means the object is accessible by everyone unless the local security policy “ System objects: Strengthen default permissions of internal system objects (for example, Symbolic Links)” is manually enabled on the system.
      • On Windows 10 and later, the above local security policy is enabled by default, adding inheritable ACEs for read-only access by normal users and read-write-modify access by administrators. Therefore, a NULL descriptor will cause the object to be read-accessible by regular users and fully accessible by admins.
  • Check that the OBJ_KERNEL_HANDLE flag is passed as part of the object attributes where a handle is created that should be accessible only within the kernel (e.g., within a driver or shared between drivers, not accessed from a usermode process). The following APIs are common examples that create handles where this issue is relevant:
    • Files: IoCreateFile, ZwCreateFile, ZwOpenFile
    • Registries: IoOpenDeviceInterfaceRegistryKey, IoOpenDeviceRegistryKey, ZwCreateKey, ZwOpenKey
    • Threads: PsCreateSystemThread
    • Events: IoCreateSynchronizationEvent, IoCreateNotificationEvent
    • Symlinks: ZwOpenSymbolicLinkObject
    • Directory objects: ZwCreateDirectoryObject
    • Section objects: ZwOpenSection
  • Look for any general issues around IoCreateDevice and IoCreateDeviceSecure.
    • The device name should be null; usually the driver should be unnamed and the symlink (if one is created) should be the item that is named.
    • The DeviceCharacteristics argument should include the FILE_DEVICE_SECURE_OPEN flag.
    • IoCreateDeviceSecure should be used instead of IoCreateDevice.
    • If IoCreateDeviceSecure is used, check that the DefaultSDDLString SDDL (security descriptor) string is appropriate.
    • If IoCreateDeviceSecure is used, check that DeviceClassGuid is generated or otherwise unique, not an existing or shared GUID.
  • Look for any general issues with IoCreateSymbolicLink.
    • The SymbolicLinkName argument tells you the name of the symlink. It will appear in the \GLOBAL?? object namespace in WinObj.
    • A named symlink to a device should not be created unless necessary (e.g., for interoperability with a usermode application).
    • Check that the DACL is appropriate.
  • Find the DriverEntry function, check which dispatch routines are set in the MajorFunction property of the driver object, and evaluate their security impact. Almost all dispatch routines create some external attack surface, so they are all important to evaluate, but here are some other common dispatch routines of interest:
    • IRP_MJ_READ and IRP_MJ_WRITE handle ReadFile and WriteFile calls, respectively, on the driver object.
    • IRP_MJ_DEVICE_CONTROL handles DeviceIoControl calls on the driver object.
    • WMI requests are dispatched to IRP_MJ_SYSTEM_CONTROL.
  • If there is an IRP_MJ_DEVICE_CONTROL dispatch routine, check each IOCTL’s functionality for security impact.
  • Check that the Access (RequiredAccess) field is set appropriately for each IOCTL (e.g., FILE_WRITE_DATA to restrict access to the IOCTL to callers that have write access to the driver).
  • Check for buffer overruns in IRP dispatch routines.
    • Accesses to Irp->AssociatedIrp.SystemBuffer must respect the lengths provided in Parameters.DeviceIoControl.InputBufferLength and OutputBufferLength.
  • Where output buffers are used, ensure that the SystemBuffer is zeroed.
  • Review calls to MmGetSystemAddressForMdlSafe to ensure they check for NULL.
  • Where a path to an object (e.g. file, registry key, section, mutex, semaphore, event, etc.) is passed from an untrusted context (e.g. usermode) to the kernel, check whether the caller’s permissions and privileges are checked.
  • If a function can be reached from both kernelmode and usermode callers, is ExGetPreviousMode used to check whether the call came from usermode or kernelmode?
    • More details are available in the PreviousMode documentation.
  • Where event, mutex, semaphore, and timer synchronization objects are created or opened, check that they are done so securely.
    • Check the DACL with WinObj when the object is named (this usually appears in BaseNamedObjects); if a null security descriptor is passed, it will inherit the ACEs of the object namespace, if any (see the InitializeObjectAttributes information above).
    • These objects should not be created in the context of a usermode thread (e.g., in an IOCTL dispatch) unless they are intended to be shared with that process.
    • If a named synchronization object is created, check that OBJ_PERMANENT is passed in the object attributes to prevent it from being freed by usermode. ZwMakeTemporaryObject can be called from kernelmode to free it later.
    • If a handle to a non-permanent synchronization object is passed to usermode (e.g., in an IOCTL response), then ObReferenceObjectByHandle must be used to increment the refcount on the object to prevent the usermode thread from deleting the object by closing the handle. The reference should be stored in the driver device’s extension.
  • Look for cases where KeWaitForSingleObject and KeWaitForMultipleObjects wait on synchronization objects (event, mutex, semaphore, timer) that are accessible to or shared with usermode.
    • Can these functions deadlock the kernel thread by acquiring locks permanently or in the wrong order? Calls that pass NULL to the Timeout argument are at high risk since they block forever if the synchronization object is never released.
    • Can a usermode process block important functionality from running by acquiring a sync object and never releasing it?
    • Are error codes checked and handled properly?
  • Where section objects (shared memory regions) are created or opened, check that they are done so securely.
    • Check the DACL with WinObj when the section is named (this usually appears in BaseNamedObjects); if null is passed, it will inherit the ACEs of the object namespace, if any (see the InitializeObjectAttributes information above).
    • Section objects created in usermode (e.g., by CreateFileMapping) must not be mapped by the kernel. The section must be created and mapped in the kernel.
    • Section objects should not be opened using a handle provided from usermode.
    • When a section must not be accessible outside of the kernel, it should not be mapped in a usermode thread context (e.g., in a dispatch routine for an IOCTL).
    • If a named section object is created, OBJ_PERMANENT should be passed in the object attributes to prevent it from being unmapped by usermode. ZwMakeTemporaryObject can be called from kernelmode to unmap it later.
  • Where mapped section objects are accessed, check that they are done so safely.
    • Data in sections must be validated and treated as untrusted (especially if it is shared with usermode).
    • Check for TOCTOU and other race conditions; data in a section may be changed at any time. Ideally, data should be copied to kernel memory first and then processed.
    • Accesses should be wrapped in try/catch statements to prevent DoS.
    • If a handle to a non-permanent section object is passed to usermode (e.g., in an IOCTL response), then ObReferenceObjectByHandle must be used to increment the refcount on the object to prevent the usermode thread from deleting the object by closing the handle. The reference should be stored in the driver device’s extension.
  • Check that kernel addresses are not leaked in data written to sections that are usermode-accessible.
    • Kernel object handles may be opaque wrappers around kernel addresses.
  • Check whether handles are passed between usermode and kernelmode.
    • Usermode-to-kernelmode handle passing is really dangerous; handles should be created on the kernel side and passed to usermode.
    • Usermode-to-kernelmode handle passing can result in handle confusion. Can you pass the wrong type of handle (e.g., a mutex handle when a file handle is expected)?
  • Look for calls to ZwSetSecurityObject. Such calls can often be a sign of a race condition.
    • Ideally, the InitializeObjectAttributes macro should be used to set a security descriptor as part of the object attributes during creation, rather than securing the object after creation.
  • Check memory accesses around regions acquired by MmProbeAndLockPages and MmProbeAndLockSelectedPages calls. These are typically used to map usermode memory into kernel space for DMA or PIO operations.
    • Ensure ProbeForRead is used to check that the memory region is readable before it is accessed.
    • Ensure that data mapped from usermode is properly validated.
    • Ensure accesses are wrapped in try/catch statements.
    • Look for TOCTOU issues.
  • Check that MmSecureVirtualMemory is used to help prevent TOCTOU issues on page protection when accessing usermode memory directly. Also check that usermode memory accesses are wrapped in try/catch statements to account for edge cases.
  • Look for calls to MmIsAddressValid that may indicate insufficiently robust memory access patterns.
    • This is an older function. Generally, we want ProbeForRead and __try/__except, plus MmSecureVirtualMemory where appropriate.
    • See MSDN’s Buffer Handling for more info.
  • Look for uses of POOL_FLAG_NON_PAGED_EXECUTE on memory allocations and evaluate their security impact, as RWX memory in the kernel is risky.
  • Look for uses of memory allocation APIs and ensure they do not take a size argument based on untrusted input without validation (same as passing arbitrary size to malloc). The following are common examples of allocation APIs:
    • ExAllocatePool, ExAllocatePoolWithTag, ExAllocatePoolWithQuota, ExAllocatePoolWithQuotaTag, ExAllocatePoolWithTagPriority, ExAllocatePool2, ExAllocatePool3
    • MmAllocateContiguousMemory, MmAllocateContiguousMemoryEx, MmAllocateContiguousMemorySpecifyCache, MmAllocateContiguousMemorySpecifyCacheNode, MmAllocateContiguousNodeMemory
    • MmAllocateNonCachedMemory
    • AllocateCommonBuffer
  • Check that an NX POOL_TYPE is used when allocating pool memory (e.g., NonPagedPoolNx or NonPagedPoolNxCacheAligned).
  • Check that the memory allocations and frees are performed with matching APIs, and that the appropriate free function is used when memory is allocated internally within an API.
    • For example, if memory is allocated with ExAllocatePoolWithTag, then it should be freed with ExFreePoolWithTag. Deallocation with the wrong API may cause kernel heap corruption or a bugcheck. Refer to the MSDN documentation for each memory allocation API to find the correct deallocation function.
    • Many LSA functions that allocate memory internally require LsaFreeMemory to be used for deallocation.
  • Check that memory is zeroed before use and that outdated allocation functions are not used.
    • ExAllocatePool, ExAllocatePoolWithTag, ExAllocatePoolWithQuota, ExAllocatePoolWithQuotaTag, and ExAllocatePoolWithTagPriority should be replaced with ExAllocatePool2 and ExAllocatePool3, as these new functions automatically zero memory during the allocation to prevent memory disclosure issues.
    • Memory from other allocation functions should be zeroed first.
  • Check that RtlSecureZeroMemory is used to zero memory, not RtlZeroMemory.
  • Look for IoGetRemainingStackSize and IoGetStackLimits calls.
    • These are usually code smells (e.g., messing with kernel stacks or dynamic allocation in the stack) that can lead to DoS or other bugs if done wrong.
  • Look for IoWithinStackLimits calls.
    • These can be indicators that the code is doing something unusual with stack buffers, with the potential for errors that lead to bad accesses.
  • Look for TOCTOU issues in filesystem and registry API usage.
    • Look for uses of ZwOpenDirectoryObject or ZwQueryDirectoryFile to enumerate directory contents, followed by ZwOpenFile or ZwCreateFile to open the file without checking the call’s success to ensure that the file still exists.
    • Look for uses of ZwEnumerateKey to enumerate registry key contents, followed by ZwOpenKey to open a subkey without checking the call’s success to ensure that the key still exists.
    • Look for uses of ZwReadFile without checking that file contents did not change in between calls.
  • Look for usage of spinlocks that might be abused for denial of service.
    • Ensure that acquired spinlocks are released on all code flow paths, including cases where an exception might occur.
    • Ensure that code safeguards against excessive computation or long delays while a spinlock is acquired. A kernel thread that hangs waiting for a spinlock will consume a lot of CPU time and may trigger a THREAD_STUCK_IN_DEVICE_DRIVER bugcheck.
    • Look for cases where spinlock contention can be intentionally caused by a malicious usermode process, such as by repeatedly triggering an IOCTL that acquires the spinlock, resulting in system threads locking up or preventing important operations from being processed.
  • Look for interesting notify routines such as the following. These are commonly used by AV/EDR.
    • PsSetCreateProcessNotifyRoutine(Ex/Ex2)
    • PsSetCreateThreadNotifyRoutine(Ex)
    • PsSetLoadImageNotifyRoutine(Ex)
  • Look for uses of RtlCopyString or RtlCopyUnicodeString without verifying that the string being copied into has a large enough MaximumLength.
    • This does not lead to a buffer overflow, since these functions respect the MaximumLength field in the target string, but it does silently truncate the string.
  • Look for instances in which the result of RtlAppendUnicodeToString or RtlAppendUnicodeStringToString is not checked.
    • These return STATUS_BUFFER_TOO_SMALL if the target string’s backing buffer is not large enough to store the resulting string. If the return value is not checked, the string may not contain the expected value.
  • Look for calls to SeAccessCheck.
    • These are always security-relevant as they mean the driver is doing its own checks to see if a user has access to something.
  • Look for calls to SeAssignSecurity and SeAssignSecurityEx.
    • These alter ACEs on security descriptors and are, therefore, always security-relevant.
  • Look for calls to RtlQueryRegistryValues.
    • Ensure that the RTL_QUERY_REGISTRY_NOEXPAND flag is passed when a REG_EXPAND_SZ or REG_MULTI_SZ value type might be read. This prevents unsafe expansion of environment variables from within the kernelmode context.
    • Ensure that the RTL_QUERY_REGISTRY_TYPECHECK flag is passed when using the RTL_QUERY_REGISTRY_DIRECT flag. This causes the API call to safely fail when the target value type does not match the expected type, thus avoiding a buffer overflow.
  • Check if the driver implements IRP_MJ_POWER for power management events.
    • Does it reset or recreate any objects or state on sleep and resume? Can this be abused?
    • Does it incorrectly expect that external system information will remain identical after resuming from a low-power state (e.g., sleep or hibernate)? Can this be abused?
  • Assess whether the code assumes that KeQuerySystemTime (or the Precise variant) is monotonic.
    • Can you bypass rate limits and other time-related checks if the system clock changes (e.g., at a DST boundary)?
  • Assess the attack surface of WMI functionality, if the driver registers as a WMI provider.
  • Assess the use of event tracing (ETW).
  • If the driver is for a PCIe device (including Thunderbolt, USB4, M.2, U.2, and U.3), verify that the driver opts into DMA remapping.
This content is licensed under a Creative Commons Attribution 4.0 International license.