Search This Blog

Monday, March 7, 2011

Registry Monitoring sans Context

So, you've been asked to monitor the Windows registry with your driver and as you are a good programmer you've decided to do it by using the proper API and not by hooking the SSDT like every other coder does.

Very cool, problem is, you have no clue on how to go about it.
Fear not programmer friend! Here are 4 steps to help you out.

Step 1: CmRegisterCallbackEx

This is an API you can used in order to have Windows notify you whenever something happens with the registry. You can register pre-operation or post-operation callbacks, depending on your need, although monitoring will use post-operation callbacks.

Step 2: Creating a dispatch function

NTSTATUS RegistryMonitoringCallbackDispatch(
  __in      PVOID CallbackContext,
  __in_opt  PVOID Argument1,
  __in_opt  PVOID Argument2
)
{
  USHORT Class = (USHORT)Argument1;
  if (NULL == g_pCallbackFcns[Class])
  {
    return STATUS_SUCCESS;
  }
  return (*(g_pCallbackFcns[Class]))(CallbackContext, Argument1, Argument2);
}


As we can see above, this is a very simple function. All it does is check Argument1, which gives the type of notification and use it as an index for calling the appropriate callback.

The callbacks are defined like this:


NTSTATUS RegNtDeleteKeyCallback(__in PVOID CallbackContext, __in_opt PVOID Argument1, __in_opt PVOID Argument2);


They basically all follow the same prototype (which is the same as the dispatch function) and we can therefore, store them in a simple table which I have defined as such:


PEX_CALLBACK_FUNCTION g_pCallbackFcns[MaxRegNtNotifyClass] = {0};


The initialization of the monitoring module contains code such as this:


// Set the callbacks
//
g_pCallbackFcns[RegNtDeleteKey] = RegNtDeleteKeyCallback;


Now that this is done, we can go to the next step.

Step 3: Answering the call

Now that we have registered our driver with the system, whenever a registry change happens, our code will be called upon.

In that code, we don't have to do much. For example, we can merely output the name of a value that's being deleted.

If we read the documentation from Microsoft (and we always do, of course), we notice that each structure contained as Argument2 contains a pointer to a user defined context. Usually in that context you will want to store stuff like the name of the key that's being opened and its handle. This way, when a value gets added, modified, deleted etc, you will be able to know its location (under which key it's at).

But, because we're lazy and that's the point of this blog, we don't have to go through the whole context handling stuff as there's an easier way to remember what the key name is.

Step 4: Obtaining the name of a registry object.

Well, here the function that you'll need:


NTSTATUS GetObjectName(PUNICODE_STRING ObjectName, USHORT ccbLength, PVOID Object)
{
  NTSTATUS status = STATUS_SUCCESS;
  ULONG len = sizeof(OBJECT_NAME_INFORMATION) + sizeof(WCHAR) * ccbLength;
  PUCHAR Buffer = memalloc(len);
  POBJECT_NAME_INFORMATION ObjectNameInfo = (POBJECT_NAME_INFORMATION) Buffer;
  if (Buffer == NULL)
      return STATUS_NO_MEMORY;
  ObjectName->MaximumLength = ccbLength * sizeof(WCHAR);
  __try {
      // Make sure that the object is not null
      //
      if (NULL != Object)
      {
          RtlZeroMemory(Buffer, len);
          status = ObQueryNameString(Object, ObjectNameInfo, len, &len);
          if (NT_SUCCESS(status))
          {
              RtlCopyUnicodeString(ObjectName, &ObjectNameInfo->Name);
          }
      }
  }
  __except (EXCEPTION_EXECUTE_HANDLER)
  {
      // Object may have been invalid (This can happen when

      // dealing with registry notification)
      //
      status = GetExceptionCode();
  }

  memfree(Buffer);
  return status;
}


Not that hard now, was it?

Note: the 'memalloc' and 'memfree' are placeholders for proper allocation functions.

Then, for example, let's say that we are monitoring a value that's being deleted, here's a possible callback code for it:


NTSTATUS RegNtDeleteValueKeyCallback(__in PVOID CallbackContext, __in_opt PVOID Argument1, __in_opt PVOID Argument2)
{
    NTSTATUS status = 0;
    PREG_DELETE_VALUE_KEY_INFORMATION infoDeleteKey = (PREG_DELETE_VALUE_KEY_INFORMATION) Argument2;
    UNICODE_STRING ObjectName = {0};
    WCHAR* Buffer = (WCHAR*) memalloc(sizeof(WCHAR) * BUFFER_SIZE);
    HANDLE Pid = PsGetCurrentProcessId();
    RtlUnicodeStringInit(&ObjectName, Buffer);
    GetObjectName(&ObjectName, BUFFER_SIZE, infoDeleteKey->Object);
    SendRegistryDeleteValueNotification(ObjectName.Buffer, infoDeleteKey->ValueName->Buffer, Pid);
    DBGPRINT("Deleting value: %p [%ws\\%ws]\r\n", infoDeleteKey->Object, ObjectName.Buffer, infoDeleteKey->ValueName->Buffer);

    memfree(Buffer);
    return STATUS_SUCCESS;
}


In the code above, we get the name of the value from the structure we get in Argument2 but the name of the key is unknown. Fortunately, we have that nifty little function written above that will help us out.

So there you have it, registry monitoring made painless (kinda).

No comments:

Post a Comment