Ten useful kernel APIs


Developers building Windows kernel software drivers can call any API exported by NTOSKRNL. Microsoft documents a small subset of these APIs on MSDN but there are many others, some of which have little to no publicly available documentation, that can be useful especially for drivers that provide security-related functionality. This article lists the top ten APIs, documented and undocumented, which can be useful to kernel software driver developers.

All the APIs described in this article are available in NTOSKRNL.lib, so drivers can link to them directly as opposed to having to call them through a function pointer returned by MmGetSystemRoutineAddress().

RtlImageDirectoryEntryToData()

PVOID 
RtlImageDirectoryEntryToData( 
    PVOID BaseAddress, 
    BOOLEAN MappedAsImage,
    USHORT Directory,
    PULONG Size);

Given the base address of a kernel module and the index of an entry in the data directory, RtlImageDirectoryEntryToData() returns the virtual address and the size of the directory entry. The data directory index is one of the IMAGE_DIRECTORY_ENTRY_xxx constants in ntimage.h. This function is useful when parsing NT image headers to get to import descriptor table, import address table, export table, etc. If a driver simply needs to get hold of the PE image headers they can call RtlImageNtHeader[Ex]() instead. The Win32 version of this function ImageDirectoryEntryToData() is documented on MSDN.

RtlPcToFile[Name|Path|Header]()

NTSTATUS
RtlPcToFileName( 
    PVOID PcValue, 
    PUNICODE_STRING FileName);

NTSTATUS 
RtlPcToFilePath( 
    PVOID PcValue, 
    PUNICODE_STRING FilePath);

    PVOID 
RtlPcToFileHeader ( 
    PVOID PcValue, 
    PVOID *BaseOfImage);

RtlPcToFileName() and RtlPcToFilePath() enable a driver to get the name and path of a kernel module respectively, from an address that points to the PE image of the module in memory. RtlPcToFileName() and RtlPcToFilePath() walk-through the list of KLDR_DATA_TABLE_ENTRY structures anchored at PsLoadedModuleList and determine if the given address (Pc) falls within the extents in KLDR_DATA_TABLE_ENTRY.Base and KLDR_DATA_TABLE_ENTRY.Base + KLDR_DATA_TABLE_ENTRY.SizeOfImage and if so, return information about the module. RtlPcToFileName() returns the name of the driver form KLDR_DATA_TABLE_ENTRY.BaseDllName whereas RtlPcToFilePath() returns the full path to the driver from KLDR_DATA_TABLE_ENTRY.FullDllName. RtlPcToFileHeader(), on the other hand, looks up the Pc using PsInvertedFunctionTable and returns the ImageBase and writes it to the location pointed to by BaseOfImage.

RtlQueryModuleInformation()

typedef struct _RTL_MODULE_BASIC_INFO {
    PVOID ImageBase;
} RTL_MODULE_BASIC_INFO, *PRTL_MODULE_BASIC_INFO;

typedef struct _RTL_MODULE_EXTENDED_INFO {
    RTL_MODULE_BASIC_INFO BasicInfo;
    ULONG ImageSize;
    USHORT FileNameOffset;
    UCHAR FullPathName[256];
} RTL_MODULE_EXTENDED_INFO, * RTL_MODULE_EXTENDED_INFO;

NTSTATUS
RtlQueryModuleInformation (
    PULONG BufferSize, 
    ULONG UnitSize, 
    PVOID Buffer);

Drivers often need to obtain the list of modules in the kernel along with their names and base addresses. While drivers can obtain this list by walking through the linked list of KLDR_DATA_TABLE_ENTRY structures anchored at PsLoadedModuleList, RtlQueryModuleInformation() provides a safer way to achieve this using the native API ZwQuerySystemInformation(SystemModuleInformation). The Buffer parameter can be an array of RTL_MODULE_BASIC_INFO or RTL_MODULE_EXTENDED_INFO structure types. Depending on the type of structure passed in the Buffer parameter, the UnitSize parameter could be either sizeof(RTL_MODULE_BASIC_INFO) or sizeof(RTL_MODULE_EXTENDED_INFO). If Buffer is NULL, the function returns the required buffer length in BufferSize which the caller can use to allocate a buffer and call RtlQueryModuleInformation() again with this buffer.

FsRtlIsNameIn[UnUpcased]Expression()

BOOLEAN
FsRtlIsNameInExpression (
    PUNICODE_STRING Expression,
    PUNICODE_STRING Name,
    BOOLEAN IgnoreCase,
    PWCH UpcaseTable );

BOOLEAN 
FsRtlIsNameInUnUpcasedExpression(
    PUNICODE_STRING Expression,
    PUNICODE_STRING Name,
    BOOLEAN IgnoreCase,
    PWCH UpcaseTable );

This kernel function RtlIsNameInExpression() exported as FsRtlIsNameInExpression() is one of the few exports from NTOSKRNL.exe that is able to compare a string with a pattern containing wildcard characters. This function can be used to match file paths, registry paths, object paths or just simply strings to a given pattern containing wildcards. Wildcard characters ? and *, and MSDOS short filename matching characters <, >, and " are also supported. For example, the Expression 1<TXT would match the name 1.2.3.4.5.6.txt. RtlIsNameInExpression() expects the Expression to be in uppercase if IgnoreCase is TRUE whereas FsRtlIsNameInUnUpcasedExpression() does not have this requirement. RtlIsNameInExpression() attempts to convert the string provided in the Name parameter to uppercase which can fail and raise an exception which the caller must handle. Both these functions are documented, and their prototypes and the definition of the characters DOS_DOT, DOS_QM, and DOS_STAR are available in ntifs.h.

FsRtlGetFileNameInformation()

NTSTATUS 
FsRtlGetFileNameInformation(
    PFILE_OBJECT FileObject,
    ULONG NameOptions,
    PUNICODE_STRING FileName,
    PVOID *FileNameInformation );

VOID
FsRtlReleaseFileNameInformation ( 
    PVOID FileNameInformation );

FsRtlGetFileNameInformation() retrieves the name of a file, given the file object. The NameOptions parameter can be one of the FLT_FILE_NAME_OPTIONS constants defined in fltkernel.h. The name of the file is returned in FileName and FileNameInformation receives a pointer to the structure FLT_FILE_NAME_INFORMATION, also defined in fltkernel.h. The FileNme.Buffer and FileNameInformation.Name point to the same string that represents the full path to the file. FsRtlGetFileNameInformation() only populates the Name and Volume fields of the FLT_FILE_NAME_INFORMATION structure. To populate the rest of the fields i.e. to split the file path into its components, drivers can use the FLTMGR.sys function FltParseFileNameInformation() which is documented on MSDN. FsRtlGetFileNameInformation() internally calls FltMgrFsRtlGetFileNameInformation() which is registered by FltMgr.sys with NTOSKRNL.exe. FsRtlGetFileNameInformation() allocates the FLT_FILE_NAME_INFORMATION structure which can be freed by calling FsRtlReleaseFileNameInformation(). There are multiple components inside NTOSKRNL.exe as well as kernel mode drivers such as CI.dll which use these functions.

RtlFindExportedRoutineByName()

PVOID
RtlFindExportedRoutineByName ( 
    PVOID DllBase, 
    PCHAR RoutineName);

The kernel provides the documented function MmGetSystemRoutineAddress() for drivers to retrieve a pointer to an export, given the name of the export. MmGetSystemRoutineAddress() does have a restriction in that it works on exports in NTOSKRNL.exe and HAL.dll. MmGetSystemRoutineAddress() internally calls the undocumented function RtlFindExportedRoutineByName() with the base address of NTOSKRNL.exe stored in nt!PsNtosImageBase and with the base address of HAL.dll in nt!PsHalImageBase. It turns out that RtlFindExportedRoutineByName() is also exported by NTOSKRNL.exe and therefore available for drivers to call directly to get the exports of any kernel module just as applications call GetProcAddress() on any DLL. A notable difference between MmGetSystemRoutineAddress() and RtlFindExportedRoutineByName() is that while the former takes in the RoutineName as a Unicode string, the latter expects a NULL-terminated ASCII string.

SeLocateProcessImageName()

NTSTATUS 
SeLocateProcessImageName ( 
    PEPROCESS Process, 
    PUNICODE_STRING *ImageFileName);

Given the pointer to the EPROCESS structure, this function returns the full path to the PE file for that process which is available at EPROCESS.SeAuditProcessCreationInfo.ImageFileName.Name. A UNICODE_STRING structure containing the full image path is allocated and returned in the ImageFileName parameter. The caller is responsible for freeing the structure using ExFreePool(). This path can be useful when reading the PE file for the purposes of computing image-hash or implementing policy based on meta-data in the PE file.

RtlDuplicateUnicodeString()

NTSTATUS 
RtlDuplicateUnicodeString ( 
    ULONG Flags, 
    PCUNICODE_STRING StringIn, 
    PUNICODE_STRING StringOut );

Drivers can always use ExAllocatePollWithTag() to allocate memory for a Unicode string and RtlCopyUnicodeString() to copy of an existing Unicode string to that memory to make a deep copy of source Unicode string. RtlDuplicateUnicodeString() provides a convenient way to perform both these steps in a single function with some customization options to control how the destination Unicode string is created. The caller can request the new string to be NULL-terminated by specifying RTL_DUPLICATE_UNICODE_STRING_NULL_TERMINATE in the Flags parameter. If the source string is empty, the caller can request an empty NULL-terminated string to be created by specifying RTL_DUPLICATE_UNICODE_STRING_ALLOCATE_NULL_STRING along with the RTL_DUPLICATE_UNICODE_STRING_NULL_TERMINATE flag. The Widechar buffer at StringOut.Buffer is allocated using ExAllocatePoolWithTag() from PagedPool and with the tag Strg. The caller can free this string using RtlFreeUnicodeString(). Although RtlDuplicateUnicodeString() and the aforementioned flags are defined in the WDK header ntifs.h, they are not documented on MSDN.

ObReferenceObjectByName()

NTSTATUS
ObReferenceObjectByName(
    PUNICODE_STRING ObjectName,
    ULONG Attributes,
    PACCESS_STATE AccessState,
    ACCESS_MASK DesiredAccess,
    POBJECT_TYPE ObjectType,
    KPROCESSOR_MODE AccessMode,
    PVOID ParseContext,
    PVOID *Object );

Given the path to an object in the object manager namespace, ObReferenceObjectByName() returns a pointer to the object. If the caller specifies the AccessState parameter as NULL, ObReferenceObjectByName() builds its own ACCESS_STATE. The OBJECT_TYPE parameter is required and must match the object being retrieved otherwise this function fails with STATUS_OBJECT_TYPE_MISMATCH. For example, if the ObjectName is \Driver\NULL the ObjectType parameter should be IoDriverObjectType. Most frequently required object type variables such as IoDriverObjectType are exported from NTOSKRNL.exe, so drivers can link to these exports directly to get access to the object types. Most of the other parameters to ObReferenceObjectByName() are described in the documented variants of the function such as ObReferenceObjectByHandle() and ObReferenceObjectByPointer(). ObReferenceObjectByName() takes a reference to the object returned in Object, the caller must drop this reference through a call to ObDereferenceObject().

IsEqualGUID()

BOOLEAN
IsEqualGUID ( 
    REFGUID rguid1, 
    REFGUID rguid2);

There are quite a few kernel APIs that allow drivers to register a callback that receives a GUIDs as one of the parameters. These GUIDs may identify system defined entities such as PNP event, power events, device interfaces, WFP layer identifiers, etc. Drivers can call IsEqualGUID() to compare the GUID presented in the callback to a constant GUID to determine if they are identical. Examples of callbacks where such comparisons may be required are device notification callbacks installed by IoRegisterPlugPlayNotification() and power setting change notification callbacks installed by PoRegisterPowerSettingCallback().