Registry Filter Drivers (Part 2)

In Part 1 we created a basic registry filter driver. For a sample, the code that we created was fine, but for production code, there are some parts of it that could become a little unwieldy. In this article we will do a little cleanup on the code, and then start looking at simple filtering of the open key operation.

Referring back to Part 1, the RfRegistryCallback function, which actually receives all the callbacks from the CM, has a nice little switch statement with three cases. In practice, there may be a good ten to twenty cases that you need to handle in order to implement a functional product. This switch statement gets a little difficult to maintain, and then you break things out into separate functions for each operation callback type. A good way to do this is just to convert our switch statement into some code that uses an array of function pointers, and calls the right operation callback depending on the operation class provided. So after these changes the RfRegistryCallback looks as follows:

NTSTATUS RfRegistryCallback( __in PVOID CallbackContext, __in PVOID Argument1, __in PVOID Argument2 )
{
    REG_NOTIFY_CLASS Operation = (REG_NOTIFY_CLASS) (ULONG_PTR) Argument1;

    //	If we have no operation callback routine for this operation then just return to Cm
    if ( !g_RegistryCallbackTable[ Operation ] )
    {
        return STATUS_SUCCESS;
    }

    //	Call our operation callback routine
    return g_RegistryCallbackTable[ Operation ]( CallbackContext, Argument1, Argument2 );
}

We also have a global array of function pointers for operation callback routines, and we initialize that during DriverEntry (prior to registering our callback):

PEX_CALLBACK_FUNCTION g_RegistryCallbackTable[ MaxRegNtNotifyClass ] = { 0 };

extern "C"
NTSTATUS DriverEntry( __in PDRIVER_OBJECT pDriverObject, __in PUNICODE_STRING pRegistryPath )
{
    //	... Set up unload routine, etc.

    //	Set up our registry callback table
    g_RegistryCallbackTable[ RegNtPreCreateKeyEx ] = (PEX_CALLBACK_FUNCTION) RfPreCreateKeyEx;
    g_RegistryCallbackTable[ RegNtPreOpenKeyEx ] = (PEX_CALLBACK_FUNCTION) RfPreOpenKeyEx;
    g_RegistryCallbackTable[ RegNtKeyHandleClose ] = (PEX_CALLBACK_FUNCTION) RfKeyHandleClose;

    //	... Register callback with CM
}

And then we have a few operation callback routines that all look similar, mainly differing in the type of the CallbackData parameter:

NTSTATUS RfPreCreateKeyEx( __in PVOID CallbackContext, __in PVOID Argument1, __in PREG_CREATE_KEY_INFORMATION CallbackData );

Now that we have a callback routine that is a bit cleaner and easier to maintain, let’s dive into filtering the open key operation. One of the most common things to need to do while filtering an open or create key operation is to examine the name of the object being opened. Maybe you have a list of keys you want to filter (or a list NOT to filter), or some other similar requirement. If we look at the CallbackData structure, which is of type REG_OPEN_KEY_INFORMATION, there is a CompleteName field. This field is not very well named, because it may or may not actually be the “complete” name. If the name begins with a backslash then it has the complete name, otherwise it is a name relative to the RootObject field, and we have to figure it out for ourselves. (Note that there is an API call CmCallbackGetKeyObjectID that purports to get the name of a key object. This API will not work in pre-Open/Create calls, because it is designed to work on an object, and cannot build us the complete name from an object and relative name. We will use this API to get the string name of the RootObject.)

The following sample function shows how to construct the full object name if necessary, and use either the constructed name or the one provided by Cm to display what key is being opened.

NTSTATUS RfPreOpenKeyEx( __in PVOID CallbackContext, __in PVOID Argument1, __in PREG_OPEN_KEY_INFORMATION CallbackData )
{
    UNREFERENCED_PARAMETER( CallbackContext );
    UNREFERENCED_PARAMETER( Argument1 );

    NTSTATUS status;
    PUNICODE_STRING pLocalCompleteName = NULL;

    //	Get the complete name of the key being opened
    if ( CallbackData->CompleteName->Length > 0 && *CallbackData->CompleteName->Buffer != OBJ_NAME_PATH_SEPARATOR )
    {
        PCUNICODE_STRING pRootObjectName;
        status = CmCallbackGetKeyObjectID( &g_CmCookie, CallbackData->RootObject, NULL, &pRootObjectName );
        if ( NT_SUCCESS( status ) )
        {
            //	Build the new name
            USHORT cbBuffer = pRootObjectName->Length;
            cbBuffer += sizeof( wchar_t );
            cbBuffer += CallbackData->CompleteName->Length;
            ULONG cbUString = sizeof(UNICODE_STRING) + cbBuffer;

            pLocalCompleteName = (PUNICODE_STRING) ExAllocatePoolWithTag( PagedPool, cbUString, 'tlFR' );
            if ( pLocalCompleteName )
            {
                pLocalCompleteName->Length = 0;
                pLocalCompleteName->MaximumLength = cbBuffer;
                pLocalCompleteName->Buffer = (PWCH) ( (PCCH) pLocalCompleteName + sizeof( UNICODE_STRING ) );

                RtlCopyUnicodeString( pLocalCompleteName, pRootObjectName );
                RtlAppendUnicodeToString( pLocalCompleteName, L"\\" );
                RtlAppendUnicodeStringToString( pLocalCompleteName, CallbackData->CompleteName );
            }
        }
        else
        {
            //	Could not get the complete name since we have a relative name and were
            //	unable to get the name of the root object
            goto LExit;
        }
    }

    KdPrint( ( "RegFlt: PreOpenKeyEx for %wZ\n", pLocalCompleteName ? pLocalCompleteName : CallbackData->CompleteName ) );

LExit:
    if ( pLocalCompleteName )
    {
        ExFreePool( pLocalCompleteName );
    }

    return STATUS_SUCCESS;
}

We now have a filter that get called for every open-key on the system and prints out in the debugger what is being opened. These techniques could be useful if you were building a monitoring product of some kind, but the real fun work comes when you want to change the behavior. In the next part we will look at actively filtering a registry open operation.

2 thoughts on “Registry Filter Drivers (Part 2)

  1. There is a bug in the name gathering code in this article. In the case where an open key callback as a root object but NO relative name at all, this code will not generate the correct name. Following is a more correct conditional statement for that code segment. See the code attached to part 3 for the whole deal.

    if ( CallbackData->CompleteName->Length == 0 || *CallbackData->CompleteName->Buffer != OBJ_NAME_PATH_SEPARATOR )

Leave a Reply to Jeremy Hurren Cancel reply

Your email address will not be published. Required fields are marked *

Complete the following to verify your humanity: * Time limit is exhausted. Please reload CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.