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=noneon 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.
- Ideally, do this in a VM with WinDbg attached from outside (e.g., debugging via a virtual COM port) so that you can capture info about where crashes occur.
- 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
INITsection marked as RWX and discardable. This is generally harmless as theINITsection is unmapped afterDriverEntryreturns, 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_HANDLEis 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
NULLsecurity 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
NULLdescriptor will cause the object to be read-accessible by regular users and fully accessible by admins.
- On Windows 8.1 and prior, many system object namespaces (e.g., symlinks) have no inheritable ACEs by default, so a
- Ensure that
- Check that the
OBJ_KERNEL_HANDLEflag 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
- Files:
- Look for any general issues around
IoCreateDeviceandIoCreateDeviceSecure.- 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
DeviceCharacteristicsargument should include theFILE_DEVICE_SECURE_OPENflag. IoCreateDeviceSecureshould be used instead ofIoCreateDevice.- If
IoCreateDeviceSecureis used, check that theDefaultSDDLStringSDDL (security descriptor) string is appropriate.- Alternatively, the device model or device setup class should have its SDDL set in the registry via the INF file or setup APIs.
- If
IoCreateDeviceSecureis used, check thatDeviceClassGuidis generated or otherwise unique, not an existing or shared GUID.
- Look for any general issues with
IoCreateSymbolicLink.- The
SymbolicLinkNameargument 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.
- The
- Find the
DriverEntryfunction, check which dispatch routines are set in theMajorFunctionproperty 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_READandIRP_MJ_WRITEhandleReadFileandWriteFilecalls, respectively, on the driver object.IRP_MJ_DEVICE_CONTROLhandlesDeviceIoControlcalls on the driver object.- WMI requests are dispatched to
IRP_MJ_SYSTEM_CONTROL.
- If there is an
IRP_MJ_DEVICE_CONTROLdispatch 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_DATAto restrict access to the IOCTL to callers that have write access to the driver).IoValidateDeviceIoControlAccesscan be used in the IOCTL handler to implement stricter checks.
- Check for buffer overruns in IRP dispatch routines.
- Accesses to
Irp->AssociatedIrp.SystemBuffermust respect the lengths provided inParameters.DeviceIoControl.InputBufferLengthandOutputBufferLength.
- Accesses to
- Where output buffers are used, ensure that the
SystemBufferis zeroed.- Not zeroing the
SystemBufferleads to kernel memory disclosure and undefined behavior. - See MSDN’s Failure to Initialize Output Buffers for more information.
- Not zeroing the
- Review calls to
MmGetSystemAddressForMdlSafeto ensure they check forNULL. - 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 not, look for potential confused deputy attacks.
- 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 theInitializeObjectAttributesinformation 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_PERMANENTis passed in the object attributes to prevent it from being freed by usermode.ZwMakeTemporaryObjectcan 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
ObReferenceObjectByHandlemust 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 the DACL with WinObj when the object is named (this usually appears in
- Look for cases where
KeWaitForSingleObjectandKeWaitForMultipleObjectswait 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
NULLto theTimeoutargument 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?
- Can these functions deadlock the kernel thread by acquiring locks permanently or in the wrong order? Calls that pass
- 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 theInitializeObjectAttributesinformation 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_PERMANENTshould be passed in the object attributes to prevent it from being unmapped by usermode.ZwMakeTemporaryObjectcan be called from kernelmode to unmap it later.
- Check the DACL with WinObj when the section is named (this usually appears in
- 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
ObReferenceObjectByHandlemust 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
InitializeObjectAttributesmacro should be used to set a security descriptor as part of the object attributes during creation, rather than securing the object after creation.
- Ideally, the
- Check memory accesses around regions acquired by
MmProbeAndLockPagesandMmProbeAndLockSelectedPagescalls. These are typically used to map usermode memory into kernel space for DMA or PIO operations.- Ensure
ProbeForReadis 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.
- Ensure
- Check that
MmSecureVirtualMemoryis 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
MmIsAddressValidthat may indicate insufficiently robust memory access patterns.- This is an older function. Generally, we want
ProbeForReadand__try/__except, plusMmSecureVirtualMemorywhere appropriate. - See MSDN’s Buffer Handling for more info.
- This is an older function. Generally, we want
- Look for uses of
POOL_FLAG_NON_PAGED_EXECUTEon 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,ExAllocatePool3MmAllocateContiguousMemory,MmAllocateContiguousMemoryEx,MmAllocateContiguousMemorySpecifyCache,MmAllocateContiguousMemorySpecifyCacheNode,MmAllocateContiguousNodeMemoryMmAllocateNonCachedMemoryAllocateCommonBuffer
- Check that an NX
POOL_TYPEis used when allocating pool memory (e.g.,NonPagedPoolNxorNonPagedPoolNxCacheAligned). - 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 withExFreePoolWithTag. 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
LsaFreeMemoryto be used for deallocation.
- For example, if memory is allocated with
- Check that memory is zeroed before use and that outdated allocation functions are not used.
ExAllocatePool,ExAllocatePoolWithTag,ExAllocatePoolWithQuota,ExAllocatePoolWithQuotaTag, andExAllocatePoolWithTagPriorityshould be replaced withExAllocatePool2andExAllocatePool3, 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
RtlSecureZeroMemoryis used to zero memory, notRtlZeroMemory. - Look for
IoGetRemainingStackSizeandIoGetStackLimitscalls.- 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
IoWithinStackLimitscalls.- 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
ZwOpenDirectoryObjectorZwQueryDirectoryFileto enumerate directory contents, followed byZwOpenFileorZwCreateFileto open the file without checking the call’s success to ensure that the file still exists. - Look for uses of
ZwEnumerateKeyto enumerate registry key contents, followed byZwOpenKeyto open a subkey without checking the call’s success to ensure that the key still exists. - Look for uses of
ZwReadFilewithout checking that file contents did not change in between calls.
- Look for uses of
- 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_DRIVERbugcheck. - 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
RtlCopyStringorRtlCopyUnicodeStringwithout verifying that the string being copied into has a large enoughMaximumLength.- This does not lead to a buffer overflow, since these functions respect the
MaximumLengthfield in the target string, but it does silently truncate the string.
- This does not lead to a buffer overflow, since these functions respect the
- Look for instances in which the result of
RtlAppendUnicodeToStringorRtlAppendUnicodeStringToStringis not checked.- These return
STATUS_BUFFER_TOO_SMALLif 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.
- These return
- 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
SeAssignSecurityandSeAssignSecurityEx.- These alter ACEs on security descriptors and are, therefore, always security-relevant.
- Look for calls to
RtlQueryRegistryValues.- Ensure that the
RTL_QUERY_REGISTRY_NOEXPANDflag is passed when aREG_EXPAND_SZorREG_MULTI_SZvalue type might be read. This prevents unsafe expansion of environment variables from within the kernelmode context. - Ensure that the
RTL_QUERY_REGISTRY_TYPECHECKflag is passed when using theRTL_QUERY_REGISTRY_DIRECTflag. This causes the API call to safely fail when the target value type does not match the expected type, thus avoiding a buffer overflow.
- Ensure that the
- Check if the driver implements
IRP_MJ_POWERfor 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 thePrecisevariant) 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.
- Drivers that act as WMI providers will call the
IoWMIRegistrationControlAPI; see MSDN’s Registering as a WMI Data Provider for more information. - Most of the attack surface will arise from code that handles the WMI minor IRPs, although KMDF drivers use alternative callback APIs.
- Ensure that sensitive information is not exposed in WMI data.
- Drivers that act as WMI providers will call the
- Assess the use of event tracing (ETW).
- Event provider registration can be identified by finding
EtwRegistercall sites. Event writes can be identified by findingEtwWrite,EtwWriteEx,EtwWriteString, andEtwWriteTransfercall sites. - Ensure that sensitive information is not exposed in ETW messages.
- Ensure that channels are configured with appropriate access controls and isolation. This is typically configured in the
isolationproperty of theChannelTypedefinition in the instrumentation manifest for the driver at build time. See MSDN’s Adding Event Tracing to Kernel-Mode Drivers for details.- Usermode applications may also use the
EvtSetChannelConfigPropertyAPI to configure theEvtChannelConfigIsolationproperty with a value from theEVT_CHANNEL_ISOLATION_TYPEenum.
- Usermode applications may also use the
- Read our blog post on ETW internals for information on finding provider GUIDs and event-consuming applications.
- Geoff Chappell’s ETW Security page contains further useful information on the securable objects used in ETW.
- Event provider registration can be identified by finding
- 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.