Registry Filter Drivers (Part 3)

In Part 2 we looked at passively filtering a call to open a registry key (we were simply logging the name of keys opened), and we promised that next we would look at actively filtering the same kind of call. The most simple form of active filtering is simply completing the operation with some kind of error code. For example, if we wanted to prevent users from ever opening a certain registry key, we could just return access denied. This is as simple as returning the status code in your filter callback.

In the following example, we still need to get the full name of the registry key being opened as we did in Part 2, because we are determining whether to deny access based on the object name. If a caller tries to open HKLM\Software\MySecretTestKey they will receive an access denied error. There are some other side effects of putting this code in place. First, if we fail to get the name of the object being opened via our call to CmCallbackGetKeyObjectID, we will now be returning that failure code to the caller, with the result that the key will not be able to be opened even though it is not the key we are trying to block access to. This is a design decision, and probably has to do with whether a random failure should result in granting access.

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

    NTSTATUS status = STATUS_SUCCESS;

    ...

    PUNICODE_STRING pKeyNameBeingOpened = pLocalCompleteName ? pLocalCompleteName : CallbackData->CompleteName;

    //	Prevent callers from opening our secret registry key
    UNICODE_STRING TestKeyName;
    RtlInitUnicodeString( &TestKeyName, L"\\REGISTRY\\MACHINE\\SOFTWARE\\MySecretTestKey" );
    if ( !RtlCompareUnicodeString( pKeyNameBeingOpened, &TestKeyName, TRUE ) )
    {
        KdPrint( ( "RegFlt: PreOpenKeyEx for %wZ being denied!\n", pKeyNameBeingOpened ) );
        status = STATUS_ACCESS_DENIED;
        goto LExit;
    }

    ...

    return status;
}

The second side effect of our filter as written is that it handles open-key operations differently than create-key operations, which are two completely separate calls in the registry. This could result in a weird case where somebody could create the key, but then fail to open it in the future. (Similar to what can happen if you accidentally create a key with a security descriptor that doesn’t allow yourself access to the object.) In practice, I have found that it is generally best to have the create and open key callbacks perform all the same logic, and if possible to have them call the same code. I will change our sample to do this, but will not show the code here. I am going to being attaching the code file with each article so you can look there if you want to see how it’s done.

Now we put the driver onto a test system and sure enough we can pull up regedit and see that it will not let us open that key. But a strange thing is now happening on my Windows 7 test system. It doesn’t appear to prevent me from creating that key through regedit. If I create it through code, or from the command-line reg.exe, then the create fails as expected. What we are seeing here is that regedit actually creates a key named something like “New Key #1”, and then does a RENAME! (The rename-key functionality was added in Windows XP, and although very few applications use it, regedit is one that does.)

So what we are going to have to do now is filter the rename key call. This seems like it should be very simple. The REG_RENAME_KEY_INFORMATION structure provides a NewName member. We should just be able to test this against our secret key name and return access denied. But wait! Apparently, the NewName member only has the final path component (the rename key operation only allows the final component to be changed, i.e., renaming a key but not moving it to a different parent). So it looks like we are going to have to do our own name lookup and parsing to do our comparison; after all, we don’t want to block somebody from making a key named MySecretTestKey just anywhere!

Note that this is the kind of code that I really would like to avoid having to write. Pointer arithmetic and messing around with buffers directly is error prone, and great care must be taken, but sometimes it cannot be avoided. I decided to write a function that would take a UNICODE_STRING containing an object name and initialize two new UNICODE_STRING structures using the same buffer as the original, but only referring to the parent part of the name and the relative part of the name. I won’t say that the following is perfect code, but it is good enough to pass my cursory testing, and gives you an idea of what has to be done.

NTSTATUS _RfSplitParentAndRelativeNames( __in PCUNICODE_STRING pFullObjectName, __out_opt PUNICODE_STRING pParentObjectName, __out_opt PUNICODE_STRING pRelativeObjectName )
{
    //	Defensive programming. These names may come from somebody elses code, so be careful!
    if ( !pFullObjectName->Buffer || 0 == pFullObjectName->Length )
    {
        return STATUS_INVALID_PARAMETER;
    }

    //	Search backward through full object name for the path separator, taking care not to underflow!
    const wchar_t* pCh = &pFullObjectName->Buffer[ (pFullObjectName->Length / sizeof(wchar_t)) - 1 ];
    while ( pCh && pCh >= pFullObjectName->Buffer && *pCh != OBJ_NAME_PATH_SEPARATOR )
    {
        --pCh;
    }

    //	Again, be defensive. The string provided may not have had a backslash.
    if ( pCh <= pFullObjectName->Buffer )
    {
        return STATUS_INVALID_PARAMETER;
    }

    //	Everything before the character we stopped on is the parent, everything after is the relative name
    USHORT cbOffset = (USHORT) PtrDiff( pCh, pFullObjectName->Buffer );
    if ( pParentObjectName )
    {
        pParentObjectName->Length = pParentObjectName->MaximumLength = cbOffset;
        pParentObjectName->Buffer = pFullObjectName->Buffer;
    }

    if ( pRelativeObjectName )
    {
        cbOffset += sizeof( wchar_t );
        pRelativeObjectName->Length = pRelativeObjectName->MaximumLength = pFullObjectName->Length - cbOffset;
        pRelativeObjectName->Buffer = (PWCH) Add2Ptr( pFullObjectName->Buffer, cbOffset );
    }

    return STATUS_SUCCESS;
}

Given this new function we can split up the name of the object passed to us in the rename key callback, and pass the parent of the original object name, along with the new name, and test to see if it matches our secret key. So our rename key callback is fairly simple, as follows.

NTSTATUS RfPreRenameKey( __in PVOID CallbackContext, __in PVOID Argument1, __in PREG_RENAME_KEY_INFORMATION CallbackData )
{
    UNREFERENCED_PARAMETER( CallbackContext );
    UNREFERENCED_PARAMETER( Argument1 );

    NTSTATUS status = STATUS_SUCCESS;

    //	Get the full name of the new key
    PCUNICODE_STRING pObjectName;
    status = CmCallbackGetKeyObjectID( &g_CmCookie, CallbackData->Object, NULL, &pObjectName );
    if ( !NT_SUCCESS( status ) )
    {
        goto LExit;
    }

    //	Find the last path separator character and get a "parent object" key name
    UNICODE_STRING ParentObjectName;
    status = _RfSplitParentAndRelativeNames( pObjectName, &ParentObjectName, NULL );
    if ( !NT_SUCCESS( status ) )
    {
        goto LExit;
    }

    //	Prevent callers from opening our secret registry key
    if ( _RfIsKeyMySecretKey( &ParentObjectName, CallbackData->NewName ) )
    {
        KdPrint( ( "RegFlt: Rename of key to %wZ being denied!\n", CallbackData->NewName ) );
        status = STATUS_ACCESS_DENIED;
        goto LExit;
    }

LExit:
    return status;
}

Now, a few additional comments. First, as I was working on the code for this, I found a small bug in my code to get the key object name from part 2. The code attached with this post will contain a fix, and I will post a comment to the previous article with details.

Another strange behavior I noted while testing this is that when a key is renamed multiple times, the rename key callback often gets the ORIGINAL key name when it calls CmCallbackGetKeyObjectID. For example, in regedit you create a new key “New Key #1” and rename it to “TestKey”. Then you rename it again to “TestKey2”. The rename key callback will still get something like “\REGISTRY\MACHINE\SOFTWARE\New Key #1” for the object, and the NewName would contain TestKey2. So it appears that you may not be able to entirely rely on CmCallbackGetKeyObjectID to return the current name of the object. It may be cached for the handle/object, or it may be cached for a longer lifetime, I couldn’t tell for sure from my limited testing.

RegFlt Source Part 3

5 thoughts on “Registry Filter Drivers (Part 3)

  1. Hi, I have two notes:

    Firstly,
    “Another strange behavior I noted while testing this is that when a key is renamed multiple times, the rename key callback often gets the ORIGINAL key name when it calls CmCallbackGetKeyObjectID.”

    From MSDN:
    If a registry filter driver calls CmCallbackReleaseKeyObjectID to get an ObjectName structure pointer, and, after this call, the name of the registry key changes, subsequent calls to CmCallbackGetKeyObjectID get a pointer to a stale copy of the ObjectName structure, which contains the old key name. The stale copy of this structure persists until all handles to the key are closed. So drivers that run in versions of Windows earlier than Windows 8 should call CmCallbackGetKeyObjectID instead of CmCallbackGetKeyObjectIDEx.

    Next question is a little off topic, but maybe you will help. I wrote registry filter and I used registry object context. But problem is that my driver leak resources because CmUnRegisterCallback is not calling my cleanup routine for handles which were not be closed. Have you encountered similar behavior. Here is sample code: http://www.osronline.com/showthread.cfm?link=253350

    1. @martin, thanks for the comment. I had missed that comment from MSDN because it appears to only be documented under CmCallbackGetKeyObjectIDEx, which I completely ignored while writing the post because I knew it was Windows 8 and above only, and I was targeting Windows 7. Thanks for the feedback!

      I will try to get a little time to look at your resource leak this evening and reply again with any findings.

    2. @martin, I did some testing and this seems to be working correctly in my environment. I modified the driver from this article to handle a post-open, and if the keyname matched what I wanted to put a context on, I allocated and set the context. For testing, I did “reg query keyname” which opened and closed the object and set and cleaned up the context. Then I used regedit and clicked on the key, which keeps an open handle to it. While that was open, I unloaded my driver and got the cleanup context callback I was expecting. Is it possible that something strange is happening because you are unregistering your callback in a port disconnect callback, or something else completely unrelated?

  2. Thanks for your response.
    My driver is primary the file system minifilter (Filter Manager API). When user-mode application connect to the minifilter then registry callback is registered and vice versa. Also I tried unregister registry callback in driver unload routine but behavior is same (testing system: vista x64 and windows7 32bit).
    I’m always filtering specific process (from creation to termination – by PID) and it’s strange, that there are always same number of unfreed contexts. For example, simple hello world console application from process creation to ready state initiates fifteen successfull PostOpenKeyEx notifications. Then for twelve of them it will close handle imidiately. After process termination there are still three opened handles to registry object (associated with my context) and CmUnRegisterCallback is not calling cleanup callbacks for them. From WinDbg and with driver verifier enabled I found that this three specific objects for this simple example application have a path:
    “\REGISTRY\MACHINE”
    “\REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\Session Manager”
    “\REGISTRY\MACHINE\SYSTEM\ControlSet001\Control\Nls\Sorting\Versions”

    Very strange behavior.

  3. I have read all the filter driver articles, Great source of info. Thanks a Lot Jeremy

    Thanks,
    Suresh

Leave a Reply to martin 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.