In the previous entries to Security Through Process Isolation (Part1 Part2 Part3 Part4), I focused on the file system aspects of process isolation. In the next few entries I will work through the details associated with virtualizing the registry access from an isolated process. While registry virtualization is, overall, a less complicated design than file system virtualization, it is handled using a very different approach than file system virtualization. The primary reason for this design difference is the required interface does not allow one to create, on the fly, objects which represent non-existent registry keys and values. But before jumping into the details of these differences, let’s examine the history of how Windows has changed registry filtering over the years.
In early versions of Windows, that would be prior to Windows XP, the only way to filter registry access was to patch the System Service Descriptor Table, or SSDT. This table contains the pointers to the location of the kernel mode functions and is accessed when a user mode API maps over to a kernel mode API. Thus one can patch this table so the call invoked would be your custom routine which could in turn call the function that the SSDT entry originally contained. Some of the issues which needed addressing when performing this patching included:
- Ensuring that your code was the only code updating the SSDT. This required that the IRQL be raised to a high level so the thread performing the patching would not be interrupted or preempted
- Be sure within the new function to either fail the call or continue the call chain to the next function from the SSDT
- Be certain that the function you are patching is the function you want to patch. This is generally done by dumping the SSDT in WinDbg and determining where the function you want to patch is located in the SSDT
- Starting with the x64 platforms, PatchGuard would monitor the SSDT and if it found any changes it would quickly bugcheck the system
Needless to say, there were lots of misuse of this interface as well as lots of crashes resulting from improper patching of the SSDT. In the end, Microsoft released a registry filtering framework in Windows XP called the Configuration Manager, or CM. The first few iterations of the framework only allowed monitoring of calls, no modification of calls or failing calls. As well the framework was fairly unstable and would lead to random crashes when one used it. So it wasn’t until Windows Vista … something good came out of Vista? … that developers started using the CM. The robustness and stability were much better than previous releases and you could actually do something with it, like modify data returned in a call or fail an open request to a key.
The CM, post Windows Vista, also allows the filters to be registered at different altitudes, just like the Mini-Filter model in file systems. So implementing a registry isolation driver requires you to submit to Microsoft for an altitude assignment. This is a nice feature since you can ensure that if your registry filtering driver is performing isolation or data modification, you want to be low in the stack so filters above you see the view you want to expose. Whereas a registry filtering driver which only monitors and may occasionally deny access to keys would want to be higher in the stack.
Now let’s move forward with some of the features of the CM offered by Microsoft. The first issue which I will discuss is that there are two ‘types’ of objects in the registry. There are keys and there are values. While you could think of these as being the directory and file equivalents in the file system world, they are handled very differently in the registry world. The biggest difference being that a key can be ‘opened’ while a value cannot. When operating on a key you must first open the key and then perform some subsequent operation on that key. When operating on a value you first open the key which contains the value and then perform a subsequent operation on the value by specifying the handle to the key and the value name. Related to this is the fact that, in general, registry operations are very short lived. Therefore you don’t get the caching aspects seen in file systems within the registry, typically a key is opened, an operation is performed and the key is closed. I suspect this is an artifact of how user mode code is written when accessing the registry and not related to how the underlying CM is designed.
The next big difference, which I touched upon previously, is that you cannot create a virtual key object within a registry filter and return this object to the caller. Whereas in file systems, you can fully virtualize a file by returning a file object for a file or directory that does not truly exist, it is virtualized. In the registry paradigm you always end up creating a key in the underlying registry and using that key object for the virtual placeholder of the key you want to manipulate. For example, if you want to virtualize a key, let’s call it
What you would need to do is to create a new key in some private location, let’s call that key
After you have successfully created this key in the private location, you can utilize the returned key object for the ‘Key1’ key as a placeholder for the real access to the ‘VirtualKey’. This way, changes to the ‘VirtualKey’ key are actually tracked within the ‘Key1’ key in the private store. Keeping in mind that for registry processing it is isolation not virtualization will allow your design to be more robust in the end.
One important point on the above handling of key objects which differs from the Mini-Filter model in file systems. The issue is that requests cannot be ‘redirected’ to a different key object simply by replacing the object value in the request with a different object value. This restriction mandates that if a registry filter driver wants to redirect a request to a different key is must handle all of the processing internal to the request and not pass the request down the stack to the next driver … more on this below.
Before diving into the details of registry isolation, one more item needs to be discussed. In the file system model, in particular within the Mini-Filter model, the framework offers the ability to track contexts on a per file basis, per handle basis as well as other object-centric methods. In the CM model you can associate a context to a key object only. Therefore you have the ability to allocate a piece of memory, associate it to a given key object and retrieve this memory later in the processing of the key object. This processing is generally handled when the key object is first opened so the memory would be available during requests such as queries and enumerations. There is also a final request sent after the key object is closed to allow you to deallocate this piece of memory. A nice tool to keep in the bag while designing your registry isolation filter driver.
Now that we have covered some of the basic principles of registry filtering, I’ll go over some of the APIs used to register the driver as well as some of the routines used while implementing a registry filter driver. The first API which I’ll discuss is the API used to register with the underlying CM. This API has the following prototype:
Note the ‘Ex’ variant of this call, the older pre-Windows Vista version of the API you passed in the callback function and a context which CM would pass you on each registry request. The above API, only available on Windows Vista and later operating systems, allows you to also pass in the Altitude of the driver and a Driver Object. This way CM can better manage registry filtering drivers and they are setup in a well defined hierarchy. The Context which is passed into the Callback function can be used to differentiate registry filters if, for some reason, you had multiple filters in the call stack. This way in the Callback function you can tell whether it is Filter1 or Filter2.
The Callback function receives the registry access requests passed in from the CM. As in the Mini-Filter model, each call into a registry filter driver will return back into the CM and the CM will decide to pass it on to the next filter in the stack or to complete the call back to the caller. This action is controlled by the returned status code which the callback returns. In general, there are 3 options which can be performed:
- The Callback can return a STATUS_SUCCESS status code and the CM will continue processing the request, passing it down the stack to the next filter driver
- The Callback can return STATUS_CALLBACK_BYPASS which tells the CM to stop processing the request and complete the call back to the caller with a successful status
- The Callback can return a failure status code and the CM will stop processing the request and fail the call back to the caller
As discussed earlier, using the STATUS_CALLBACK_BYPASS return code allows a registry filter driver to handle an operation in which the driver wants to redirect the request to a different key. For example, if a real key exists and a request to delete the key were received, to implement registry isolation the DeleteKey request would need to be redirected at a pseudo key created as described before. Thus the DeleteKey request would be handled internal to the filter driver, it would delete the pseudo key and then return STATUS_CALLBACK_BYPASS to the CM so the real underlying key would not be deleted. I’ll cover this in more detail when I discuss the implementation of the registry filtering driver.
As discussed earlier, one can associate a memory blob to a key object by calling the API
This API will associate the memory blob specified in the NewContext parameter to the key object specified in the Object parameter. This context is then passed in with each registry request within the control structure for the request. Using this model, you can associate this memory to the key object for the lifetime of the key being opened.
Now reviewing some information … we’ve covered 2 different contexts which can now be handled within CM. The ‘global’ context which can be established when registering with the CM and the same context pointer would be passed in with every registry call. Next we are able to associate a context with an open key object using the method described above. And the final way for tracking a context provided through the CM is a context associated to a specific request. For each request sent to the registry filter there is a ‘pre’ and ‘post’ request. For example the filter driver is sent a PreDeleteKey and a PostDeleteKey. Note that if the registry filter driver returns the STATUS_CALLBACK_BYPASS or a failure status then the ‘Post’ routine for that request is not called, it is only called when the request is passed down to the next filter in the stack. Now back to that context … for each request the filter driver can set a context in the ‘Pre’ operation and it will be passed back into the ‘Post’ operation for that same request. It is the responsibility of the driver to ensure that if memory is allocated and set in the ‘Pre’ handler, it is freed in the ‘Post’ handler.
This gives us 3 ways to associate contexts within the CM framework, plenty to build a functionally robust registry isolation driver.