Uninformed: Informative Information for the Uninformed

Vol 6» 2007.Jan

Interception of PsInvertedFunctionTable

Another variation on the theme of intercepting PatchGuard within the SEH code path critical to the system integrity check routine involves taking advantage of an optimization that exists in the x64 exception dispatcher. Specifically, it is possible to utilize the fact that the exception dispatcher on x64 uses a cache to improve the performance of exception handling. By taking advantage of this cache, it may be possible to intercept control of execution when the PatchGuard DPC routine deliberately creates an access violation exception in order to trigger the system integrity check. This proposed technique uses the nt!PsInvertedFunctionTable global variable in the kernel, which represents a cache used to perform a fast translation of RIP values to an associated image base and exception directory pointer, without having to do a (slow) search through the linked list of loaded kernel modules.

This technique is fairly similar to the one described in technique 4.2. Instead of altering the actual exception directory entries corresponding to each PatchGuard DPC routine in the kernel's image in-memory, this technique alters the cached exception directory pointer stored within PsInvertedFunctionTable. PsInvertedFunctionTable is consulted by RtlLookupFunctionTableEntry, in order to translate a RIP value to an associated image (and unwind metadata block). The logic within RtlLookupFunctionTable is essentially to search through the cached entries resident in PsInvertedFunctionTable for an image that corresponds to a given RIP value. If a hit is found, then the exception directory pointer is loaded directly from the PsInvertedFunctionTable cache, instead of through the (slower) process of parsing the PE header of the given image. If no hit is found, then the loaded module linked list is searched. Assuming a hit is made in the loaded module list, then the PE header for the associated module is processed in order to locate the exception directory for the module. From there, the exception directory is searched to locate the unwind metadata block corresponding to the function containing the specified RIP value.

The structure backing PsInvertedFunctionTable (RTL_INVERTED_FUNCTION_TABLE) can be described as so in C:

    PVOID                         ImageBase;
    ULONG                         ImageSize;
    ULONG                         ExceptionDirectorySize;

    ULONG Count;
    ULONG MaxCount; // always 160 in Windows Server 2003
    ULONG Pad[ 0x2 ];

In Windows Server 2003, there is space reserved for up to 160 loaded modules in the array contained within PsInvertedFunctionTable. In Windows Vista, this number has been expanded to 512 module entries. The array of loaded modules is maintained by the system module loader such that when a module is loaded or unloaded, a corresponding entry within PsInvertedFunctionTable is created or deleted, respectively. It is not a fatal error for the module array within PsInvertedFunctionTable to be exhausted; in this case, performance for exception dispatching relating to additional modules will be slower, but the system will still function.

Because the RIP-to-exception-directory cache described by PsInvertedFunctionTable maintains a full 64-bit pointer to the exception directory of the associated module, it is possible to disassociate the cached exception directory pointer from its corresponding image. In other words, it is possible to modify the ExceptionDirectory member of a particular cached RTL_INVERTED_FUNCTION_TABLE_ENTRY to point to an arbitrary location instead of the exception directory of that module. There are no security or integrity checks that validate that the ExceptionDirectory member points to within the given image. This could be exploited by a third-party driver in order to take control of exception dispatching for any of the first 160 (or 512, in the case of Windows Vista) kernel modules. This loaded module list includes critical images such as the HAL (typically the first entry in the cache) and the kernel itself (typically the second entry in the cache). With respect to bypassing PatchGuard, this makes it possible for a third party driver to copy the exception directory data of the kernel to dynamically allocated memory and adjust it such that exception handlers for the PatchGuard DPC routines point to a stub function that invokes a virtual unwind as described in technique 4.2. After setting up its altered shadow copy of the exception directory for the kernel, all that a third party driver would need to do is swap the ExceptionDirectory pointer within the PsInvertedFunctionTable cache entry for the kernel with the pointer to the shadow copy. Following that, this approach is essentially the same as the proposed approach described in 4.2. It has the added advantage of being more difficult to detect from the perspective of validating the integrity of the exception dispatching path, as the exception directory associated with the kernel image in-memory is not actually altered; only a pointer to the exception directory in a cache is changed.

This approach does require a reliable mechanism to detect PsInvertedFunctionTable (which is not exported) at run-time, however. The author feels that this is not a particularly difficult task, as the first few members of PsInvertedFunctionTable (specifically, the maximum entry count and the entries for the HAL and kernel) will have predictable values that can be used in a classic egghunt style search of kernel global variable space. Additional heuristics, such as requiring several data references to the suspected PsInvertedFunctionTable location within kernel code could be applied as well, in the interest of improving accuracy.

This proposed approach may be countered by many of the proposed counters to techniques 4.1 and 4.2. Additionally, this technique could also be countered by validating exception directory pointers within PsInvertedFunctionTable, such as by ensuring that such exception directory pointers are within the confines of the purported associated image. Although this validation is not perfect since it might still be possible for one to reposition the exception directory pointer to a different location within the image that could be safely modified at runtime, such as overlapping a large global variable array or the like, it would certainly increase the difficulty of subverting the exception dispatcher's RIP translation cache. Additional validation techniques, such as requiring that the exception directory point to read-only memory, could be similarly adopted to reduce the chance that a third party driver could meaningfully subvert the cache (with results leading to something other than a system crash).

It should be noted that in the current implementation, PsInvertedFunctionTable presents a relatively inviting target for potentially malicious software to hijack parts of the kernel without being detected. Indeed, through careful planned subversion of PsInvertedFunctionTable, third party software could take control of exception dispatchers throughout the kernel in order to gain control of execution. Though this technique would be much more limited than outright kernel patching, it has the advantage of being completely undetected by current PatchGuard versions (which cannot validate global variables that may change without notice at runtime, for obvious reasons). It also has the advantage of being undetected by current rootkit detection systems, which are presently (to the author's knowledge) blissfully unaware of PsInvertedFunctionTable. Although it would require administrative permissions (or an exploit granting such permissions) for an attacker to modify PsInvertedFunctionTable in the first place, Microsoft has at late focused a great deal of effort on protecting the kernel even from users with administrator permissions. For example, one could conceive of a rootkit-style program that intercepts exception dispatchers for system services, and passes invalid user mode pointers to system services in order to surreptitiously execute kernel mode code without detection when the standard pointer probe throws an exception indicating that the given usermode pointer parameter is invalid. Given this sort of threat (from the rootkit perspective), the author feels that it would be in Microsoft's best interests to put into place additional validation of PsInvertedFunctionTable's cached exception directory pointers (assuming that Microsoft wishes to continue down the path of strengthening the kernel against malicious administratively-privileged code).