CmRegisterCallback
64位系统下微软提供了CmRegisterCallback这个回调函数来实时监控注册表的操作,那详细探究一下其实现的原理。
1 2 3 4 5
| NTSTATUS CmRegisterCallback( [in] PEX_CALLBACK_FUNCTION Function, [in, optional] PVOID Context, [out] PLARGE_INTEGER Cookie );
|
上面为其结构,详细参数如下:
| 参数 |
说明 |
| Function |
指向RegistryCallback例程的指针,这个参数就是用来监控注册表操作的回调函数 |
| Context |
配置管理器将作为CallbackContext参数传递给RegistryCallback例程中由驱动程序定义的值。此处设为NULL就好 |
| Cookie |
指向LARGE_INTEGER变量的指针,该变量接收标识回调例程的值。当注册回调例程的时候,次值将作为Cookie参数传递给CmUnRegisterCallback |
其中第一个参数指向的类型是PEX_CALLBACK_FUNCTION,该类型是一个回调函数。
1 2 3 4 5 6 7 8
| EX_CALLBACK_FUNCTION ExCallbackFunction;
NTSTATUS ExCallbackFunction( [in] PVOID CallbackContext, [in, optional] PVOID Argument1, [in, optional] PVOID Argument2 ) {...}
|
参数如下
| 参数 |
说明 |
| CallbackContext |
在注册该RegistryCallback例程时,驱动程序作为Context参数传递给CmRegisterCallback的值 |
| Argument1 |
REG_NOTIFY_CLASS联合体类型的值,用于标识正在执行的注册表的操作类型,以及是否在执行注册表操作之前或之后调用RegistryCallback例程 |
| Argument2 |
指向特定于注册表操作信息的结构指针。结构的类型取决于Argument1中的REG_NOTIFY_CLASS类型值 |
REG_NOTIFY_CLASS结构中其中的几个比较常用的类型,它们的意义,以及对应的Argument2的结构体的内容如下
| REG_NOTIFY_CLASS的值 |
意义 |
Argument2结构体 |
| RegNtPreCreateKey |
创建注册表 |
PREG_CREATE_KEY_INFORMATION |
| RegNtPreOpenKey |
打开注册表 |
PREG_CREATE_KEY_INFORMATION |
| RegNtPreDeleteKey |
删除键 |
PREG_DELETE_KEY_INFORMATION |
| RegNtPreDeleteValueKey |
删除键值 |
PREG_DELETE_VALUE_KEY_INFORMATION |
| RegNtPreSetValueKey |
修改键值 |
PREG_SET_VALUE_KEY_INFORMATION |
关注一下这几个结构体,其中PREG_CREATE_KEY_INFORMATION就是有两个关键成员
| 成员 |
含义 |
| CompleteName |
指向包含新注册表项路径的UNICODE_字符串结构的指针。路径可以是绝对的,也可以是相对的。如果路径是绝对路径,则此结构包含以“\”字符开头的完全限定路径。对于绝对路径,RootObject成员指定\注册表项,它是注册表树的根目录。如果路径是相对的,则路径以“\”以外的字符开头,并且与RootObject成员指定的键相对 |
| RootObject |
指向注册表项对象的指针,该对象用作CompleteName成员指定的路径的根 |
然后关于PREG_DELETE_KEY_INFORMATION的结构体定义如下,Object参数指向要删除注册表的指针
1 2 3 4 5 6
| typedef struct _REG_DELETE_KEY_INFORMATION { PVOID Object; PVOID CallContext; PVOID ObjectContext; PVOID Reserved; } REG_DELETE_KEY_INFORMATION, *PREG_DELETE_KEY_INFORMATION
|
然后PREG_DELETE_VALUE_KEY_INFORMATION结构体定义如下,Object还是指向要删除的注册表的指针,而ValueName就是指向具体需要删除的值
1 2 3 4 5 6 7
| typedef struct _REG_DELETE_VALUE_KEY_INFORMATION { PVOID Object; PUNICODE_STRING ValueName; PVOID CallContext; PVOID ObjectContext; PVOID Reserved; } REG_DELETE_VALUE_KEY_INFORMATION, *PREG_DELETE_VALUE_KEY_INFORMATION;
|
最后是PREG_SET_VALUE_KEY_INFORMATION结构,同样是Object指向要修改的注册表的指针,而ValueName就是指向具体需要修改的值。
IDA逆向分析
这个函数首先将需要的参数入栈以后调用CmpRegisterCallbackInternal

通过test esi,esi将Blink和Flink都指向自己,然后进行判断后跳转到69436B这个地址


这段函数的主要作用就是将Cookie的值存入0x10偏移处,这里的设计很巧妙,因为一个cookie是占8位的,这里首先将[esi + 0x10]的值存入eax,然后将 ebx 指向的内存位置的内容移动到指向 eax 的内存位置,相当于赋值前4位,然后再进行同样的操作,这里取的是[esi + 0x14],也就是赋值后四位。

可以看到,系统分配0x30大小的内存来保存相应的内容,根据以上的分析可以得出下面的结论
- 最开始的8个字节保存的是一个
LIST_ENTRY类型的双向链表
- 偏移0x10保存的是COOKIE
- 偏移0x1C保存的Context
- 偏移0x18保存的是回调函数的地址
可以想到注册表监控的回调函数是用LIST_ENTRY双向链表来一个个连起来的。而真正将分配的这块内存加入链表的函数则是SetRegisterCallback函数

在这个ListBegin就是链表头的地址,里面保存的就是第一个链表的地址。首先会将它保存的内容取出判断保存的是否就是ListBegin的首地址,如果是的话就说明到了链表的尾部,接下来就会跳转到loc_695306来把结构加进链表。如果不是的话,它会执行循环,不断的取链表中的下一个数据直到链表尾。
——————————————-我是一条分割线—————————————————————
根据上面的内容知道了,回调函数是可以获取要操作的注册表键的对象的,所以可以使用ObQueryNameString函数来获得要操作的键的名称,该函数在文档中的定义如下
1 2 3 4 5 6 7
| NTSTATUS ObQueryNameString( IN PVOID Object, OUT POBJECT_NAME_INFORMATION ObjectNameInfo, IN ULONG Length, OUT PULONG ReturnLength );
|
| 参数 |
含义 |
| Object |
指向请求名称的对象的指针。此参数是必需的,不能为NULL。 |
| ObjectNameInfo |
指向接收对象名称信息的调用方分配的缓冲区的OBJECT_NAME_INFORMATION指针 |
| Length |
参数二的缓冲区大小 |
| ReturnLength |
实际返回到缓冲区中的数据大小 |
想要实现对监控函数的删除,可以使用CmUnRegisterCallback函数来实现,该函数在文档中的定义如下。它只有一个参数,就是前面设置监控函数时候指定的Cookie的地址。
1 2 3
| NTSTATUS CmUnRegisterCallback( IN LARGE_INTEGER Cookie);
|
实施监控
注册表的监控回调函数被以链表的形式保存到了内存中,其中这个链表头是ListBegin。接下来只要找到这个链表头并且遍历这个链表,取出COOKIE就可以实现回调函数的删除。通过字节码匹配可以找到链表头。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| #include <ntifs.h> VOID DriverUnload(IN PDRIVER_OBJECT driverObject); PULONG GetRegisterList(); NTSTATUS DriverEntry(IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING registryPath) { NTSTATUS status = STATUS_SUCCESS; PULONG pHead = NULL; PLIST_ENTRY pListEntry = NULL; PLARGE_INTEGER pLiRegCookie = NULL; LARGE_INTEGER test; PULONG pFuncAddr = NULL; DbgPrint("驱动加载完成\r\n"); pHead = GetRegisterList(); if (pHead != NULL) { pListEntry = (PLIST_ENTRY)*pHead; while ((ULONG)pListEntry != (ULONG)pHead) { if (!MmIsAddressValid(pListEntry)) break; pLiRegCookie = (PLARGE_INTEGER)((ULONG)pListEntry + 0x10); pFuncAddr = (PULONG)((ULONG)pListEntry + 0x1C); if (MmIsAddressValid(pFuncAddr) && MmIsAddressValid(pLiRegCookie)) { status = CmUnRegisterCallback(*pLiRegCookie); if (NT_SUCCESS(status)) { DbgPrint("删除注册表回调成功,函数地址为0x%X\r\n", *pFuncAddr); } } pListEntry = pListEntry->Flink; } } exit: driverObject->DriverUnload = DriverUnload; return STATUS_SUCCESS; } PULONG GetRegisterList() { PULONG pListEntry = NULL; PUCHAR pCmRegFunc = NULL, pCmcRegFunc = NULL, pSetRegFunc = NULL; UNICODE_STRING uStrFuncName = RTL_CONSTANT_STRING(L"CmRegisterCallback"); pCmRegFunc = (PUCHAR)MmGetSystemRoutineAddress(&uStrFuncName); if (pCmRegFunc == NULL) { DbgPrint("MmGetSystemRoutineAddress Error\r\n"); goto exit; } while (*pCmRegFunc != 0xC2) { if (*pCmRegFunc == 0xE8) { pCmcRegFunc = (PUCHAR)((ULONG)pCmRegFunc + 5 + *(PULONG)(pCmRegFunc + 1)); break; } pCmRegFunc++; } if (pCmcRegFunc == NULL) { DbgPrint("GetCmcRegFunc Error\r\n"); goto exit; } while (*pCmcRegFunc != 0xC2) { if (*pCmcRegFunc == 0x8B && *(pCmcRegFunc +1) == 0xC6 && *(pCmcRegFunc + 2) == 0xE8) { pSetRegFunc = (PUCHAR)((ULONG)pCmcRegFunc + 2 + 5 + *(PULONG)(pCmcRegFunc + 3)); break; } pCmcRegFunc++; } if (pSetRegFunc == NULL) { DbgPrint("GetSetRegFunc Error\r\n"); goto exit; } while (*pSetRegFunc != 0xC2) { if (*pSetRegFunc == 0xBB) { pListEntry = (PULONG)*(PULONG)(pSetRegFunc + 1); break; } pSetRegFunc++; } exit: return pListEntry; } VOID DriverUnload(IN PDRIVER_OBJECT driverObject) { DbgPrint("驱动卸载完成\r\n"); }
|
参考链接:看雪、跳跳糖
(PS:前面虽然说是64位,但是我发现参考的文章其实IDA分析的都是32位的ntoskrnl.exe)