0%

注册表监控

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; // IN
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

image-20220630164456292

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

image-20220630164913636

image-20220630165633538

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

image-20220630171826957

可以看到,系统分配0x30大小的内存来保存相应的内容,根据以上的分析可以得出下面的结论

  • 最开始的8个字节保存的是一个LIST_ENTRY类型的双向链表
  • 偏移0x10保存的是COOKIE
  • 偏移0x1C保存的Context
  • 偏移0x18保存的是回调函数的地址

可以想到注册表监控的回调函数是用LIST_ENTRY双向链表来一个个连起来的。而真正将分配的这块内存加入链表的函数则是SetRegisterCallback函数

image-20220630173058237

在这个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)