After a bit of a hiatus, I am back to finish up this series on Security Through Process Isolation. In my last entry I covered some of the details on how the registry filtering subsystem, or CM, manages contexts and how, as a registry filter driver, one can register a callback for registry accesses. In this post I’ll cover some of the details and issues when implementing a registry isolation filter driver.
As noted in the earlier post, one of the main drawbacks of the CM subsystem is that a registry filter driver cannot create stand-alone registry objects like those in the file system space. This requires that protected or virtualized keys and values must be created in a shadow registry location. With this in mind, a registry isolation filter would be implemented such that the registry content would be copy-on-write. Key values would be read from the base location until they are modified. Once a key in the base location is modified, a new key is created in the shadow location which would represent the base key. The shadow key would reflect all aspects of the base key, including the list of values, attributes and security contexts. In addition to the creation of the shadow key, a context would be allocated that would be stored in a b-tree, indexed on a hash of the fully qualified base key name. This context would be later used when the base key is accessed again so the filter driver would recognize that a shadow key had been created for it and further accesses should be handled from the shadow location.
You might ask ‘why do we need to store these contexts in a b-tree when we can associate contexts to keys through CM?’ The reason is that the contexts associated to a key, as described in my earlier post, are only valid while that key is open. Once the key is closed, the context is removed and you no longer have access to it. By putting these contexts in a b-tree you can locate them from previous opens, assign the context to the open key and while that key is open, access it via CM. But the next time the key is opened, you will need to locate the context through the b-tree.
There is an additional point to be made here. When making a copy of the base key in the shadow location, there is no need to track child keys of this key. The reason is that keys are managed by using the fully qualified name. While there can be relative opens within the name space, in the isolation filter you can always fully construct the name, locate the context, if it exists, and move on. There is no need to understand that a given key is a child of a migrated key.
The next item to cover is how values are handled within a migrated key. For this discussion, I have chosen a design which maintains the values within the base key, if it existed, as well as the shadow key for new values created after it was migrated. Since value enumeration within the registry subsystem is index based, that is given a set of values, V1 … Vn, they are assigned an index based on how they are maintained within the listing. A caller can enumerate the entire list of values by enumerating indices starting from zero (0) through to the end of the list, specifying a monotonically increasing index value for each call to enumerate the value. Or the caller can specify a specific index, say 10, and the 11th value in the ordered list will be returned. Using this feature of the registry subsystem, the isolation filter can maintain 2 sets of values, merging the corresponding indices accordingly. Thus if the base key has 10 values and there were 6 values created after the key was migrated, then the first ordering of index values would be zero (0) through nine (9) assigned to the base key and ten (10) through fifteen (15) assigned to the shadow key. Taking this approach, the isolation filter can process queries such as the total value count or the longest value name from within the stored context information. As well, when a value enumeration is handled, the filter will recognize when it would need to start processing from the shadow information.
The previous approach describes the design for handling a simple set of values within a given key. In order to enhance performance of this processing, the isolation filter can opt to store this value enumeration information in a buffer initialized at the time of the first value enumeration request. Therefore 2 buffers would be stored, 1 for the base key values list and one for the shadow values list. Using this buffered method, as values are returned from the base key, a quick lookup in the shadow value list information can be made to determine if a given value has been modified. If it has, the filter driver can quickly update the returned value information before returning to the caller.
A similar approach can be taken for key enumeration. On the first call to enumerate the child keys for a given key, the isolation filter would build up a list of child keys within the base key, if it exists, and a second list of child keys for the shadow key. Of course if no shadow key exists for a given key then there is no need for additional handling and the request can be sent to the underlying CM for normal handling. This also applied to value enumeration, if the parent key has not been migrated to the shadow store then there is no additional processing required and the request can be sent directly to the CM for normal handling. But back to the processing of key enumeration … Provided the isolation filter has implemented a design similar to the one described above, the management of the key enumeration can be processed in the same manner as value enumeration. The list of base child keys are returned to the caller, checking each against the cached shadow list for changes. Once the base list has been exhausted, the shadow list is enumerated returning entries accordingly.
The next topic to be covered in the registry isolation processing is the handling of deleted keys and values. The handling of deleted keys are relatively straight forward and similar to the approach taken in the file system design. This approach would mark a deleted key as being deleted within the context structure for that key. Therefore when a new key is created with the same name, a quick lookup within the b-tree would reveal that it is deleted and a new key can be created within the shadow location. The difficulty resides in handling deleted values. Since the isolation driver does not maintain a context structure for each value, at least not in the design approach we are investigating, a different method must be used to handle deleted values. One approach is to create an alternate key in the shadow name space that represents a value which has been deleted. Note that only values which existed in the base key need to be tracked. If a value never existed in the base key, only in the shadow key, when it gets deleted we can simply delete it from the shadow key, there is no need to track this deletion case. But for values which exist in the base key and is deleted, we must take note that it has been deleted without actually deleting the value in the base key. This way if a value is later created with the same name, we can allow this creation to occur.
So how does a token value in the shadow location help us in tracking deleted values? It allows us to recognize when we are building the corresponding lists when a given value has been deleted from the base key. As an example, take a value called Value1 which exists in some base key. When the isolation filter builds the 2 lists of values as described above upon the first enumeration request, it would include the value name Value1 in the base value listing. In the shadow listing there would be an entry, call it ~Anti-Value1, which we recognize as the token value for the name Value1 which indicates it has been deleted. Thus in the enumeration for the key, the entry in the base list for Value1 would be checked against the shadow list and it would be recognized as a deleted value, resulting in the value being skipped in the returned enumeration.
Some final notes on registry isolation processing. One of the main items I see crop up when implementing the shadow design within the registry is handling access rights to keys when they are opened. Since the isolation filter will call into the Zw API for opening keys in the shadow location, it must pass along the correct access being requested by the caller. We do this so we can have the underlying CM handle the application of access rights and not re-implement it within filter driver. In order to correctly handle the application of access rights there is one particular generic right which must get remapped. This is the desired access MAXIMUM_ALLOWED. When this access is requested by the caller, it cannot be passed directly into the Zw API since it is not a true access right, it is a generic mapping. Thus when this access right is seen in an open or create of a key, it can be mapped over to KEY_ALL_ACCESS directly. This mapped, non-generic access right can then be safely passed to the underlying Zw API for opening or creating secondary keys.