OSRLogo
OSRLogoOSRLogoOSRLogo x Subscribe to The NT Insider
OSRLogo
x

Everything Windows Driver Development

x
x
x
GoToHomePage xLoginx
 
 

    Thu, 13 Dec 2018     118021 members

   Login
   Join


 
 
Contents
  Online Dump Analyzer
OSR Dev Blog
The NT Insider
Downloads
ListServer / Forum
  Express Links
  · The NT Insider Digital Edition - May-June 2016 Now Available!
  · Windows 8.1 Update: VS Express Now Supported
  · HCK Client install on Windows N versions
  · There's a WDFSTRING?
  · When CAN You Call WdfIoQueueP...ously

Guest Article - Introduction to Registry Filtering in Vista (Part I)

By Jerry Kelley

The Registry Filtering Model (RFM) provides the developer with a framework to develop registry filters quickly and safely. Before the RFM, filter developers had to resort to potentially unsafe hacks to hook the registry service calls. With the RFM, a skeleton filter can be assembled in short order by anyone with any familiarity of filtering. This is the first of two articles which will present a whirlwind tour of the RFM in Vista. In this article, I'll show you how to put together a simple filter from which you can build upon to add functionality to suit your needs.

What can it do for me?
Let's get started by going over what the RFM actually does for the developer. It provides a framework whereby a driver can be developed that provides callbacks for registry IO that it's interested in. These callbacks are registered with the RFM and are called whenever specific registry IO is processed. All of the RFM interfaces that the driver uses are provided by the Configuration Manager (CM). In addition to managing the callbacks, the RFM also provides for three types of contexts that a driver can use to associate private information with an object or operation. Those who know the file system Filter Manager should already be acquainted with these features. The Filter Manager does provide a broader API than the RFM but, to be fair, it's been out there longer and file system filters are among the hardest drivers to develop so a lot of effort has gone into the Filter Manager to ease their burden.

Not exactly the new kid in town
You may have noticed that the RFM has been around since XP. The API was very limited in XP and expanded in Server 2003. The biggest change in Server 2003 was that the RFM added support for pre and post operation callbacks meaning the driver gets a callback before and after the CM processes an operation. (In XP, only a single operation callback is available which just announces the IO). Vista added callbacks for key flush, load, and unload as well as key security query and set.

Altitude is everything
Registry filters are assigned altitudes by the type of application they perform. An altitude arranges the layering of filters so that no matter when they load, they'll load within a pre-determined level based on their function. The altitude categories are the same as those for the file system minifilters and vendors who already have altitudes for their minifilters will use them for their registry filters as well. Altitudes can be requested from Microsoft via the WHDC Minifilter Altitude Allocation Web site.

Callbacks, of course
Before we discuss registering with CM for callbacks, let's take a look at the format of the callback routines. All of the callbacks are of the PEX_CALLBACK_FUNCTION type definition in WDM.H. The three input values represent, respectively, the registration context, the operation type, and a pointer to the operation's information structure. The PEX_CALLBACK_FUNCTION is shown in Figure 1.

typedef NTSTATUS (*PEX_CALLBACK_FUNCTION)(

       IN PVOID CallbackContext,

       IN PVOID Argument1,

       IN PVOID Argument2

       );

Figure 1 - Callback Routine Template

The callback context is really a registration context created when a filter registers with CM (This will be discussed further when we talk about registration). When a filter registers at an altitude it can set a private context that the filter can use to identify a specific registration. Argument1 is not really a pointer but an enumeration value of the REG_NOTIFY_CLASS type. This value indicates the specific registry operation. Sample enumeration values include RegNtPreDeleteKey, RegNtPostDeleteKey, RegNtPreCreateKeyEx, and RegNtPostCreateKeyEx. Be sure to treat it as an enumeration and not a pointer since it'll never hold a valid pointer value.

Argument2 is a pointer to an operation-specific structure. There are distinct pre-op and post-op structures that your filter will have to deal with. The pre-op structure depends on the operation. As an example, consider pre-create (CreateKeyEx), Argument2 is a REG_CREATE_KEY_INFORMATION pointer (See Figure 2).

typedef struct _REG_CREATE_KEY_INFORMATION {

       PUNICODE_STRING CompleteName;

       PVOID RootObject;

       PVOID ObjectType;

       ULONG CreateOptions;

       PUNICODE_STRING Class;

       PVOID SecurityDescriptor;

       PVOID SecurityQualityOfService;

       ACCESS_MASK DesiredAccess;

       ACCESS_MASK GrantedAccess;

       PULONG Disposition;

       PVOID *ResultObject;

       PVOID CallContext;

       PVOID Reserved;

} REG_CREATE_KEY_INFORMATION, REG_OPEN_KEY_INFORMATION,

*PREG_CREATE_KEY_INFORMATION, *PREG_OPEN_KEY_INFORMATION;

Figure 2 - REG_CREATE_KEY_INFORMATION Structure


In the pre-query key callback Argument2 provides a pointer to a REG_QUERY_KEY_INFORMATION structure (See Figure 3).

typedef struct _REG_QUERY_KEY_INFORMATION {

  PVOID Object;

  KEY_INFORMATION_CLASS KeyInformationClass;

  PVOID KeyInformation;

  ULONG Length;

  PULONG ResultLength;

  PVOID CallContext;

  PVOID ObjectContext;

  PVOID Reserved;

} REG_QUERY_KEY_INFORMATION, *PREG_QUERY_KEY_INFORMATION;

Figure 3 - REG_QUERY_KEY_INFORMATION Structure

You'll notice that REG_OPEN_KEY_INFORMATION is the same as REG_CREATE_KEY_INFORMATION. The pre-op structure for create-key and open-key are overloaded but the pre-op structures for the rest of the operations are not. These structures are defined in WDM.H and documented in the WDK documentation.

The pre-op structures all contain several common members that help the filter identify the target of the operation. Some contain only these values because nothing else is needed. Take delete-key for example. In Figure 4, the REG_DELETE_KEY _INFORMATION structure has only three members (ignoring Reserved) and this is all that's required to define the key to be deleted.

typedef struct _REG_DELETE_KEY_INFORMATION {

       PVOID Object;

       PVOID CallContext;

       PVOID ObjectContext;

       PVOID Reserved;

} REG_DELETE_KEY_INFORMATION, *PREG_DELETE_KEY_INFORMATION;

Figure 4 - REG_DELETE_KEY_INFORMATION Structure

The first three parameters are common to all pre-op structures except for create and open. To be exact, CallContext is used in all structures including create and open but Object does not exist until the create (or open) completes and ObjectContext is not valid unless it has been set on the registry object. Other pre-op structures may have more members but they all will have at least these three.

All post-op routines are passed a pointer to a REG_POST_OPERATION_INFORMATION structure providing common completion information regardless of the operation type. Figure 5 contains the definition of the REG_POST_OPERATION_INFORMATION structure.

typedef struct _REG_POST_OPERATION_INFORMATION {

       PVOID Object;

       NTSTATUS Status;

       PVOID PreInformation;

       NTSTATUS ReturnStatus;

       PVOID CallContext;

       PVOID ObjectContext;

       PVOID Reserved;

} REG_POST_OPERATION_INFORMATION,*PREG_POST_OPERATION_INFORMATION;

Figure 5 - REG_POST_OPERATION_INFORMATION Structure

Notice the three primary fields discussed previously - Object, CallContext, and ObjectContext. These give us the registry object and context information we need to properly identify the operation target. The Status field is the actual status from CM for the operation while ReturnStatus is what will be returned to the caller. Because a filter may choose to return a status other than Status the ReturnStatus member is where the filter can set the status it wants returned to the caller. This is covered in the section on completion processing. The PreInformation member is a pointer to the pre-op structure and provides the post-op routine with the original values defining the operation.

There's one more callback to cover, the context cleanup callback. This callback must be provided if your filter sets contexts on registry objects. This callback is executed when a registry object's handle is closed or the filter unregisters. In the unregister case, the filter will get a callback for every outstanding registry context assignment. This callback is where the filter releases any registry context resources. It is a PEX_CALLBACK_FUNCTION just like the rest of the other callbacks.

Callback templates
The callbacks have been discussed as far as their function prototype and structures passed so now we'll look at a template for a pre-op callback and then a post-op callback. For these examples I'm using CreateKeyEx but the template applies to any operation. The pre-op template shown in Figure 6 breaks down to enclosing the buffer access in a try/except block, casting Argument2 to a REG_CREATE_KEY_INFORMATION pointer, processing it and returning.

NTSTATUS PreCreateKeyExCallback(

  IN PVOID CallbackContext,

  IN PVOID Argument1,

  IN PVOID Argument2

  )

{

       __try

       {

              PREG_CREATE_KEY_INFORMATION pPreInfo =

                     (PREG_CREATE_KEY_INFORMATION)pInfo;

 

              //

              // perform pre-op processing

              //

       }

       __except(EXCEPTION_EXECUTE_HANDLER)

       {

              NTSTATUS Status = GetExceptionCode();

              //

              // handle the exception as required

              //

       }

 

       return STATUS_SUCCESS;

}

Figure 6 - Pre-Op Template

The post-op template in Figure 7 is similar to the pre-op template but adds an extra step where a pointer to the pre-op structure is extracted to a local as a shortcut. The processing is left to the filter implementation.

NTSTATUS PostCreateKeyExCallback(

  IN PVOID CallbackContext,

  IN PVOID Argument1,

  IN PVOID Argument2

  )

{

       NTSTATUS Status = STATUS_SUCCESS;

 

       __try

       {

              PREG_POST_OPERATION_INFORMATION pPostInfo =

                     (PREG_POST_OPERATION_INFORMATION)pInfo;

 

              PREG_CREATE_KEY_INFORMATION pPreInfo =

                     (PREG_CREATE_KEY_INFORMATION)pPostInfo->PreInformation;

 

              //

              // perform post-op processing

              //

       }

       __except(EXCEPTION_EXECUTE_HANDLER)

       {

              Status = GetExceptionCode();

              //

              // handle the exception as required

              //

       }

 

       return STATUS_SUCCESS;

}

Figure 7 - Post-Op Template

The pre and post-op callbacks have to be careful when using the buffer pointer in Argument2 since this can be a user-space pointer. With that in mind, all access to the buffer must take place in a try/except block. The processing depends on the filter's application, so just remember to use the Argument2 pointer safely.

The power of the registry IO handler
Now that we've looked at the callback templates and the structures used, we can take that a step further and talk about what a registry filter can do - or to be more precise - what a registry IO handler can do. In this case, the term handler refers to the pre-op and post-op callback pair. A handler considers the pre and post routines as a unit which processes a single registry IO operation. With that model in mind, we can quickly describe the primary functions of a registry filter; monitoring, blocking, and modifying.

The monitoring filter is passive in that it does not interfere with any of the registry IO other than to "sniff" it and possibly record it for an application. This is a common use of registry filtering and is found in many applications. The blocking filter uses criteria to determine if registry IO should be allowed. In this type of filter, the pre-op routine does the vast majority of the work since the idea is to stop IO before it can be passed on to CM. The criteria could be user, process, operation type, and so on. The final type of filter considered is one that modifies operations. There can be modifications made to the target of the operation or to the data itself. A typical modification is to the target in order to redirect the operation somewhere else in the registry. Another example is modifying the data of a value to encrypt or decrypt it.

What the Ex?
CreateKeyEx (and OpenKey by reference) were used in previous examples and we are discussing the Vista RFM implementation but let's diverge just for a moment to see just how different the "Ex" versions are from their predecessors. The "Ex" versions appeared in Server 2003 and replaced the "non-Ex" versions in XP. These "Ex" versions live up to their name by adding a wealth of new parameters describing the create or open. We've seen the REG_CREATE_KEY_I NFORMATION structure already; now let's look back at the REG_PRE_CREATE_KEY_INFORMATION used by CreateKey in Figure 8.

typedef struct _REG_PRE_CREATE_KEY_INFORMATION {

       PUNICODE_STRING CompleteName;

} REG_PRE_CREATE_KEY_INFORMATION, *PREG_PRE_CREATE_KEY_INFORMATION;

Figure 8 - REG_PRE_CREATE_KEY_INFORMATION Structure

It's quite a bit thinner than REG_CREATE_KEY_ INFORMATION and the same is true for the structure used by OpenKey when compared to OpenKeyEx. The other thing about these older routines is that they have their own post-op structures. Figure 9 shows the REG_POST_CREATE_KEY_ INFORMATION structure.

typedef struct _REG_POST_CREATE_KEY_INFORMATION {

       PUNICODE_STRING CompleteName;

       PVOID Object;

       NTSTATUS Status;

} REG_POST_CREATE_KEY_INFORMATION, *PREG_POST_CREATE_KEY_INFORMATION;

Figure 9 - _POST_CREATE_KEY_INFORMATION Structure

Compare REG_POST_CREATE_KEY_INFORMATION with REG_POST_OPERATION_INFORMATION and you'll again see that the "Ex" API's provide much more information to the filter.

Be sure to get registered
Once your callbacks have been defined you're almost ready to register with CM to start receiving notifications. The registration routine takes a PEX_CALLBACK_ FUNCTION that it calls for all registry IO sent to your filter. This callback is the central point from which you'll pass on the calls to your IO callbacks which we have discussed so far. Using a single dispatch callback is relatively easy to implement. To begin with, define an array of PEX_CALLBACK_ FUNCTION entries that'll contain pointers to each operation callback you have implemented. The array definition is shown in Figure 10.

PEX_CALLBACK_FUNCTION g_pCallbackFcns[MaxRegNtNotifyClass];

Figure 10 - Defining the Callback Array

Initialize the array such that each callback pointer is located based on its REG_NOTIFY_CLASS enumeration value. Figure 11 shows the initialization for a filter that only monitors creates, opens, and deletes. Your filter can use whatever combination of operation monitoring it needs for your application.

memset(g_pCallbackFcns, 0, sizeof(g_pCallbackFcns));

g_pCallbackFcns[RegNtPreCreateKeyEx]   = PreCreateKeyExCallback;

g_pCallbackFcns[RegNtPostCreateKeyEx]  = PostCreateKeyExCallback;

g_pCallbackFcns[RegNtPreOpenKeyEx]     = PreOpenKeyEx;

g_pCallbackFcns[RegNtPostOpenKeyEx]    = PostOpenKeyEx;

g_pCallbackFcns[RegNtPreDeleteKey]     = PreDeleteKeyCallback;

g_pCallbackFcns[RegNtPostDeleteKey]    = PostDeleteKeyCallback;

 

Figure 11 - Initializing the Callback Array

With the array defined, the dispatch callback simply has to cast Argument1 to a USHORT and use it as an index into the array to obtain the callback pointer. Be sure to check for a NULL entry in the array before attempting to use the callback. Figure 12 shows how the dispatch callback could be implemented.

NTSTATUS DispatchCallback(

  IN PVOID CallbackContext,

  IN PVOID Argument1,

  IN PVOID Argument2

  )

{

       USHORT Class = (USHORT)Argument1;

 

       if (NULL == g_pCallbackFcns[Class])

       {

              return STATUS_SUCCESS;

       }

 

       return (*(g_pCallbackFcns[Class]))(CallbackContext, Argument1, Argument2);

}

Figure 12 - The Dispatch Callback Function

If you don't handle a specific type of IO (the array entry is NULL) you must return STATUS_SUCCESS. This is required because the operation must continue and should not be interrupted just because your filter doesn't care about a particular type of IO.

We're now ready to notify CM that we want to filter registry IO. You can register your filter whenever makes the most sense for your application. Many filters will register in DriverEntry while others will register later. When you register, it's for all registry IO since you provide a single callback to CM. Therefore, the callback array should have NULL entries for any IO class (operation) you don't care about. This is why the dispatch callback must check for NULL entries (besides being a prudent practice anyway).

In addition to a pointer to the dispatch callback, the registration API takes a string description of the altitude, a pointer to the filter's driver object, a pointer to a registration context for the filter, and a pointer to a variable to receive a special cookie identifying the registration. The string description of the altitude is simply a UNICODE_STRING of the altitude. The registration context is optional and opaque to CM. It is a means for the filter to associate information regarding a particular registration. This context is passed into each callback in the CallbackContext parameter. The cookie is a LARGE_INTEGER that is opaque to the filter and used by CM to identify a registration. It is required by certain CM routines and thus the filter must maintain this value. A typical usage of CmRegisterCallbackEx is shown in Figure 13.

RtlInitUnicodeString(&altitudeString, L"360055");

 

NTSTATUS Status = CmRegisterCallbackEx(

       DispatchCallback,

       (PCUNICODE_STRING)&altitudeString,

       pDriver,

       &g_RegistrationContext1,

       &g_RegistrationContext1.Cookie,

       Null

       );

Figure 13 - Calling CmRegisterCallbackEx

A filter can register at more than one altitude provided it has been assigned multiple altitudes. It's up to the filter designer to determine how to differentiate IO at each altitude. You can still use the same dispatch callback and callback array but specify a different registration context for each altitude registered. When any of the callbacks are called, the altitude is determined by the callback using the registration context. Of course, a filter could define a separate dispatch callback and set of callbacks for each altitude but, depending on the application, this may result in a lot of duplicate code and more code to be debugged and maintained.

When the filter no longer needs to filter registry IO it must unregister with CM. This would typically be done in the driver's unload routine but depending on the functionality of the filter it could occur anywhere. A filter unregisters by calling CmUnRegisterCallback. This routine takes only the cookie that the filter received when it registered with CM. Continuing our example from Figure 13; Figure 14 depicts unregistering the same filter.

NTSTATUS Status = CmUnRegisterCallback(g_RegistrationContext1.Cookie)

Figure 14 - Calling CmUnRegisterCallback

As covered so far, there are several types of contexts available to a filter. Contexts allow a driver to associate private information with a distinct object or operation that is typically managed by another subsystem such as a service in the operating system. This means, for example, a driver can allocate and initialize a private blob of information that it attaches to an object, like a registry object in this case, and (nearly) every time the driver gets the object from operating system the context comes with it. So why is this useful? Well, those of us who developed file system filters prior to the Filter Manager can attest to the magnitude of overhead necessary to, say, track all open file and stream objects. Once we had context support it was so much easier to just allocate our private contexts and attach them to file system objects. Anytime we processed IO we just had to retrieve our contexts and we had the information that we used to have to track ourselves in trees or lists.

There are three types of contexts available to a registry filter:

  • Registration
  • Registry object
  • Registry operation

When we looked at registration earlier you'll recall we could pass a pointer to our initialized registration context. A filter uses this context to store per-registration information. This context is passed as the first parameter to every callback. This gives all callbacks access to the filter's private registration context to use as determined by the filter.

A registry filter can set a context on a registry object with a call to CmSetCallbackObjectContext (See Figure 15 for the prototype). This is very useful because registry objects are the central focus of registry IO (surprise) and with a context on each object the filter can easily track per-object information and status. For example, a registry object context could store the root and path, create/open options and the process and thread that created/opened it. The context is typically set in the create/open post-op if the create/open was successful. However, CmSetCallbackObjectContext can be called anytime from post-create (or post-open) to prior to the handle close pre-op to set the context. Once the context has been successfully set on the object, the filter will receive a RegNtCallbackObjectContextCleanup notification when the object's handle has been closed or the filter has unregistered. Therefore, if you're going to use object contexts you must register a context cleanup callback that releases your resources for the context.

NTSTATUS CmSetCallbackObjectContext(

  IN OUT PVOID Object,

  IN PLARGE_INTEGER Cookie,

  IN PVOID NewContext,

  OUT OPTIONAL PVOID *OldContext

  );

Figure 15 - CmSetCallbackObjectContext Prototype

The filter can track the context but it is passed to every pre-op callback in the ObjectContext member of the REG_Xxx_KEY_INFORMATION structure. If a filter registers at more than one altitude, different contexts for the same registry object can be assigned for each registration by using the cookies to delineate the assignments. CmSetCallbackObjectContext also lets you replace a context on a registry object. If you call CmSetCallbackObjectContext and a context already exists, it will remove the old context, attach the new one, and return a pointer to the old one in OldContext. If OldContext comes back non-NULL the filter is responsible for cleaning it up.

The registry operation context covers the scope between the pre-op and the post-op callbacks. It is passed from the pre-op to the post-op of a single operation. It does not span across operations and is not propagated beyond the operation by CM. The filter uses this context to pass private information from a pre-op to a post-op routine. This means the filter will allocate a context in the pre-op and free it in the post-op. The context is set in the CallContext member of REG_Xxx_KEY_ INFORMATION in the pre-op and retrieved from the CallContext member of REG_POST_OPERATION_ INFORMATION in the post-op.

Modification of Registry IO Calls
We're almost done with this discussion of registry filters but we have an important subject left to cover: modifying registry IO calls. There are generally four distinct areas of modifications and they can be combined:

  • Input parameters
  • Output parameters
  • Return value
  • Completion processing

The filter's pre-op callback can modify input parameters in the REG_Xxx_KEY_INFORMATION structure and pass the new value(s) on. Likewise, the post-op callback can alter values in REG_POST_OPERATION_INFORMATION and return the modified information (Think of redirection as an example). In the case where the filter's post-op wants to change the return value to the caller, it sets the ReturnStatus member of REG_POST_OPERATION_INFORMATION with the status value that it wants the caller to receive and returns STATUS_CALLBACK_BYPASS. This halts further processing on the operation and immediately returns ReturnStatus to the caller. The caveat here is that the filter is responsible for cleanup of any CM objects if the status is changed from a success to a failure. If it is changed from failure to success the filter will need to provide proper parameter values in REG_POST_OPERATION_ INFORMATION. If the filter wants to fail the operation, it sets ReturnStatus to the error status to be returned to the caller and returns an error status other than STATUS_CALLBACK_BYPASS (STATUS_CALLBACK_ BYPASS is defined as an error value by the way).

A filter can handle the operation completely and bypass the intended operation path. To do this, the pre-op callback processes the operation however it wants and returns STATUS_CALLBACK_BYPASS. The post-op callback is not called in this case because CM will immediately return STATUS_SUCCESS to the caller without calling any other registry filters or CM routines beyond what is necessary to complete the operation. The filter must set the corresponding REG_Xxx_KEY_INFORMATION structure with proper parameters to be returned to the caller.

More to Come
This article has covered a lot of material in a relatively short amount of space. The WDK provides fairly good coverage of this information but there are some crucial elements left out - I know because I had to fill in a lot of these holes to get my first registry filter working. I have tried to provide enough coverage for a developer to become familiar with the concepts and components of a registry filter. The next article will cover transactions and how to build a non-durable resource manager since the RFM doesn?t provide one (Your filter will need one to support transactions and there is no known public documentation on how to build one).

 

Jerry Kelley is a file system filter driver developer working on software virtualization systems for a major security software corporation in the U.S. He has twenty-five years of development experience including embedded systems with the last nine years in filter development. He can be reached at jerryjkelley@msn.com.

 

 

 

User Comments
Rate this article and give us feedback. Do you find anything missing? Share your opinion with the community!
Post Your Comment

Post Your Comments.
Print this article.
Email this article.
bottom nav links