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