CmRegisterCallback
64位系统下微软提供了CmRegisterCallback
这个回调函数来实时监控注册表的操作,那详细探究一下其实现的原理。
1 | NTSTATUS CmRegisterCallback( |
上面为其结构,详细参数如下:
参数 | 说明 |
---|---|
Function | 指向RegistryCallback例程的指针,这个参数就是用来监控注册表操作的回调函数 |
Context | 配置管理器将作为CallbackContext参数传递给RegistryCallback例程中由驱动程序定义的值。此处设为NULL就好 |
Cookie | 指向LARGE_INTEGER变量的指针,该变量接收标识回调例程的值。当注册回调例程的时候,次值将作为Cookie参数传递给CmUnRegisterCallback |
其中第一个参数指向的类型是PEX_CALLBACK_FUNCTION
,该类型是一个回调函数。
1 | EX_CALLBACK_FUNCTION ExCallbackFunction; |
参数如下
参数 | 说明 |
---|---|
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 | typedef struct _REG_DELETE_KEY_INFORMATION { |
然后PREG_DELETE_VALUE_KEY_INFORMATION
结构体定义如下,Object
还是指向要删除的注册表的指针,而ValueName
就是指向具体需要删除的值
1 | typedef struct _REG_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 | NTSTATUS |
参数 | 含义 |
---|---|
Object | 指向请求名称的对象的指针。此参数是必需的,不能为NULL。 |
ObjectNameInfo | 指向接收对象名称信息的调用方分配的缓冲区的OBJECT_NAME_INFORMATION指针 |
Length | 参数二的缓冲区大小 |
ReturnLength | 实际返回到缓冲区中的数据大小 |
想要实现对监控函数的删除,可以使用CmUnRegisterCallback
函数来实现,该函数在文档中的定义如下。它只有一个参数,就是前面设置监控函数时候指定的Cookie
的地址。
1 | NTSTATUS |
实施监控
注册表的监控回调函数被以链表的形式保存到了内存中,其中这个链表头是ListBegin。接下来只要找到这个链表头并且遍历这个链表,取出COOKIE就可以实现回调函数的删除。通过字节码匹配可以找到链表头。
1 |
|
(PS:前面虽然说是64位,但是我发现参考的文章其实IDA分析的都是32位的ntoskrnl.exe)