Dump Filter Hijack

While the bypass technique focuses on more advanced methods to use the crash dump I/O path completely outside its intended environment, the dump filter hijack technique focuses on new logging features in Windows 8 that inadvertently provide a crash dump filter driver an easy way to leverage the crash I/O path during a crash or hibernation to read/write to disk.  To fully grasp this technique, we will need to explore some of the internal workings of crashdmp.sys.

In the overview of crash dump filters, it was noted that the LoadFilterDrivers function creates an undocumented filter context structure for each installed crash dump filter. By observing what is copied into offsets of this structure inside LoadFilterDrivers and FilterCallback, it is possible to reconstruct a structure definition.  The starting point for determining the filter context structure definition is the invocation to the DriverEntry routine, which occurs in the FilterCallback function.  The relevant decompilation is shown below.

status = driverEntry(v7 + 0x28, v7);

This invocation immediately reveals that the two pointers that are passed to the filter’s DriverEntry() are actually pointers to fields in a parent structure (the filter context), with the first argument (pointer to type FILTER_EXTENSION) at offset 0 and the second argument (pointer to type FILTER_INITIALIZATION_DATA) at offset 0x28 (all offset values are specific to x86 platforms).

The only other references to fields in this filter context structure occur in LoadFilterDrivers.  It’s already been established that this function maintains a linked-list of filter context structures, and based on the code that assigns the pointer links, the LIST_ENTRY structure is at offset 0x78.  Two additional assignments in LoadFilterDrivers reveal the existence of two pointer fields at offset 0x84 and 0x88.  The pointer at offset 0x84 references a global context structure used throughout the crashdmp.sys driver, while the second pointer at offset 0x88 is a structure containing information about the filter driver’s image.  A sixth and final field in the filter context structure is not referenced and is therefore unknown.  The reconstructed definition of this structure is shown below.

typedef struct _FILTER_CONTEXT {
 FILTER_INITIALIZATION_DATA FilterInitialization;
 FILTER_EXTENSION FilterExtension;
 LIST_ENTRY Next;
 ULONG Unknown;
 PVOID GlobalContext;
 PVOID Image;
} FILTER_CONTEXT, *PFILTER_CONTEXT;

Indirectly passing a book-keeping context structure to a driver is a common Microsoft practice.  However, it is unclear why the GlobalContext field exists, as it is not referenced anywhere else in the crashdmp.sys binary, is not needed by filter drivers, and represents an unnecessary exposure of internal operating system state (the global context structure maintains internal state for the dump stack and is used in nearly all its functions).  As a result, any filter driver can manipulate the data stored in this context.  For example, a filter driver could unlink other filter drivers (disabling whole disk encryption of hibernation file and crash files).

Reconstructing the global context structure definition in its entirety is outside the scope of this website.  However, some of the fields inside this structure that control the dump stack logging feature represent an exposure of internal state that can be hijacked for arbitrary read/write through the crash I/O path. Before discussing these fields, it’s necessary to provide an overview of what exactly dump stack logging is.

Dump stack logging is an undocumented feature of the crash dump mechanism introduced in Windows 8.  The log file, located at <boot_drive>:DumpStack.log.tmp, is disabled by default.  When enabled, crashdmp.sys will write basic diagnostic information at each step in the dump process (such as when the dump started, what modules were called in the filter stack, and so on).  It can be enabled by setting the registry value name HKLMSYSTEMCurrentControlSetControlCrashControlEnableLogFile to a non-zero value.  The size of the log file and its verbosity is controlled via the registry value name HKLMSYSTEMCurrentControlSetControlCrashControlDumpLogLevel.  The table below summarizes the supported dump stack logging levels and their effects.

DumpLogLevel Value Log File Size Effect
-1 0x8000 Not Used
1 0x3000 Log dump performance statistics (#pages written, MB/sec) during bitmap dump
> 1 0x2000 Log dump performance statistics (#pages written, MB/sec) during bitmap dump

If enabled correctly in the registry, the log file is created and initialized in crashdmp.sys!CrashdmpInitialize after all other dump stack initialization is completed (including filter loading).  The function crashdmp.sys!InitDumpLogFile performs the following tasks to initialize the log file:

  1. Calls CheckForLogFile – attempts to open any existing log file and if it has the correct header signature, it will rename that existing file to C:DumpStack.log
  2. Stores and sets the new log file size based on DumpLogLevel
  3. Calls CreateLogFile – creates the file C:DumpStack.log.tmp as a hidden system file in exclusive mode with a system-level security descriptor, stores opened handle in global context
  4. Calls GetFileDiskRuns – locates the file layout structure that describes the physical layout of the log file by issuing FCTL_QUERY_RETRIEVAL_POINTERS to the file system driver, stores the resulting structure in global context
  5. Allocates a page-sized scratch buffer used to format the log string before writing it to the log file

The crashdmp.sys!InitDumpLogFile function reveals two important logging values stored in the global context structure:  a handle to the open log file and a log file layout structure.  The log file layout structure stored here is not explicitly documented, but it is possible to reconstruct it based on the documentation of FSCTL_QUERY_RETRIEVAL_POINTERS, where the structure is called a MappingPair1:

struct {
 LONGLONG SectorLengthInBytes;
 LONGLONG StartingLogicalOffsetInBytes;
} MappingPair;

The SectorLengthInBytes field indicates how many bytes are stored at the location StartingLogicalOffsetInBytes, which is the logical offset to the start of the specified sector relative to the start of the containing volume.  Note that FSCTL_QUERY_RETRIEVAL_POINTERS can only be used on files that are guaranteed to be on the boot volume – disk layout information for files that can span multiple volumes can only be retrieved using FSCTL_GET_RETRIEVAL_POINTERS, which uses a different structure to represent the disk runs (or mapping pairs).  The crashdmp.sys driver can safely use FSCTL_QUERY_RETRIEVAL_POINTERS, because the log file (and the dump file as well) are guaranteed to be on the boot volume, which has a one-to-one mapping of Virtual Cluster Numbers (VCN) to Logical Cluster Numbers (LCN).

Since there can be multiple runs for a file, the file layout structure stored by crashdmp.sys is an array of MappingPairs with a count value indicating how many entries are in the array:

typedef struct _LOG_DISK_RUNS {
 ULONG Count;
 PMAPPING_PAIR Array;
} LOG_DISK_RUNS, *PLOG_DISK_RUNS;

The logging functions in crashdmp.sys that directly rely on the log file layout structure are described below:

  • WriteLogDataToDisk – Iterates over disk runs stored in global context structure, building an MDL for each request and using internal I/O functions (which call into dump port driver) to write the data to disk using the crash I/O path
  • ReadLogDataFromDisk – Same as WriteLogDataToDisk, except it uses read functions to read the log data from disk

An important takeaway from these functions is that the log handle stored in the global context structure is never used and meant only as a means to hold exclusive access to the file.  In fact, the only thing that controls where data is arbitrarily written to or read from disk is the disk run structures stored in the global context structure.  Since crash dump filter drivers have access to the global context structure, it’s possible to modify the file layout structure, such that when the system hibernates or crashes, the internal logging functions access a different file.  Best of all, since the structure is not used during normal system operation, there are no synchronization or stability issues inherent to modifying them.

However, the effect of replacing the disk layout structure to point to some other file on disk is simply that the replaced file would act as the dump stack log file and get overwritten with log contents.  This doesn’t really accomplish anything.  Achieving arbitrary read/write through the crash logging mechanism requires calling the WriteLogDataToDisk and ReadLogDataFromDisk functions directly from the crash dump filter driver.  Since these functions are not exported, they can be located by scanning the text section of crashdmp.sys at runtime.

To call these functions, it is necessary to reconstruct their prototypes by reverse engineering crashdmp.sys.  These prototypes are shown below.

typedef
NTSTATUS
(__thiscall *
ReadLogDataFromDisk) (
 __in PVOID GlobalContext, //should go in ECX
 __inout PVOID Buffer,
 __in ULONG RunNumber,
 __in ULONG BytesToRead,
 __in LARGE_INTEGER DiskRunByteOffset
 );
typedef
NTSTATUS
(__stdcall *
WriteLogDataToDisk) (
 __in PVOID GlobalContext, //first param passed in <edi>
 __in PVOID Buffer,
 __in CHAR Update
 );

ReadLogDataFromDisk takes a pointer to the global context structure; a pointer to an output buffer; an index into the disk runs array representing the sector location of the target file to start reading; the number of bytes to read; and an offset into the data described to by the requested disk run.  The one location in crashdmp.sys that uses this function passes 0 for the run number and offset parameters, which results in the entire file being read.  WriteLogDataToDisk takes a pointer to the global context structure; a pointer to an output buffer which is always the scratch buffer allocated in the global context structure; and a BOOLEAN flag indicating whether or not the log position information should be updated in the global context structure.  Both functions have unusual prototypes (labeled __userpurge in IDA Pro) due to an x86 compiler optimization for custom calling conventions as part of the /LTCG option (Link Time Code Generation).  This optimization uses arbitrary register assignments for the first function parameter.  In the case of ReadLogDataFromDisk, the first parameter is passed in ecx, which is easily mimicked using the __thiscall calling convention.  However, for WriteLogDataToDisk, the parameter must be explicitly stored in edi using inline assembly prior to calling the function.

Before devising a strategy to use these functions outside of crashdmp.sys, it’s important to remember that these functions can only be used during a crash or hibernation event.  The reason is two-fold:

  1. These functions implicitly assume the restrictions of a crash dump environment – that is, single thread/uninterruptible CPU, synchronous I/O, HIGH_IRQL, and the normal I/O path is disabled (otherwise the disk registers and any I/O in progress will be trashed)2.
  2. Crashdmp.sys stores pointers to its internal functions that issue I/O requests to the dump port driver in the global context structure.  These function pointers not initialized until they are needed (at crash or hibernation time).  Thus, calling either of the logging functions outside of a crash/hibernate context will result in a null pointer dereference.

At the same time, it is necessary to use the normal I/O path to retrieve the disk runs of the file to replace the dump stack log file.  Because the normal I/O path is not active at crash/hibernation time, the runs must be located and staged before the crash occurs.

For these reasons, the log path must be hijacked in two phases:  a pre-crash staging where the target file’s layout is retrieved and a post-crash stage where the layout is implanted and the necessary functions called to achieve the desired read(s) and/or write(s).  The two-stage hijacking process is illustrated below (a crash is depicted for illustrative purposes but a hibernation event would work the same way).

2phase_hijack_flowchart

Figure 1: Two-phase hijacking of logging path

An example of using the two-phased approach to hijack the logging path functions to patch a driver on disk would be:

  1. Retrieve disk runs of the driver using FSCTL_QUERY_RETRIEVAL_POINTERS
  2. (Hibernation or crash occurs)
  3. Implant the file layout in the global context structure
  4. Call ReadLogDataFromDisk to retrieve the driver’s contents
  5. Modify the contents held in the resulting buffer in memory
  6. Call WriteLogDataToDisk to overwrite the driver with the modified contents
  7. Restore original log file layout so that FinalizedLogFile does not overwrite target file

The implementation of the two-stage approach is relatively simple.  The pre-crash staging work can be done any time after the filter driver is loaded by crashdmp.sys.  Post-crash staging can be done in any of the filter-provided callbacks (since all of the internal read/write pointers would have been populated in the global context structure), however, the best place is in the Dump_Finish callback, since at invocation time, crashdmp.sys has completed its logging and the possibility of corrupting its internal file position information is mitigated.

1  http://msdn.microsoft.com/en-us/library/windows/hardware/ff545507(v=vs.85).aspx

2  http://www.slideshare.net/CrowdStrike/io-you-own-regaining-control-of-your-disk-in-the-presence-of-bootkits

Advertisements
%d bloggers like this: