Drunkmars's Blog

深入理解注册表监控

字数统计: 2.6k阅读时长: 11 min
2022/05/01

注册表是windows的重要数据库,存放了很多重要的信息以及一些应用的设置,对注册表进行监控并防止篡改是十分有必要的。在64位系统下微软提供了CmRegisterCallback这个回调函数来实时监控注册表的操作,那么既然这里微软提供了这么一个方便的接口,病毒木马自然也会利用,这里我们就来探究其实现的原理和如何利用这个回调函数进行对抗

CmRegisterCallback

要想更好的进行对抗,就是深入底层去看这个函数到底做了什么事情,无论是监控还是反监控,这样我们才能够更好的进行利用

首先我们去msdn里面看一下它的结构

1
2
3
4
5
NTSTATUS CmRegisterCallback(
[in] PEX_CALLBACK_FUNCTION Function,
[in, optional] PVOID Context,
[out] PLARGE_INTEGER Cookie
);

image-20220501100957527

第一个参数指向RegistryCallback,它这里其实是通过EX_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
)
{...}

image-20220501101203532

主要是看第三个参数,REG_NOTIFY_CLASS结构如下

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
typedef enum _REG_NOTIFY_CLASS {
RegNtDeleteKey,
RegNtPreDeleteKey = RegNtDeleteKey,
RegNtSetValueKey,
RegNtPreSetValueKey = RegNtSetValueKey,
RegNtDeleteValueKey,
RegNtPreDeleteValueKey = RegNtDeleteValueKey,
RegNtSetInformationKey,
RegNtPreSetInformationKey = RegNtSetInformationKey,
RegNtRenameKey,
RegNtPreRenameKey = RegNtRenameKey,
RegNtEnumerateKey,
RegNtPreEnumerateKey = RegNtEnumerateKey,
RegNtEnumerateValueKey,
RegNtPreEnumerateValueKey = RegNtEnumerateValueKey,
RegNtQueryKey,
RegNtPreQueryKey = RegNtQueryKey,
RegNtQueryValueKey,
RegNtPreQueryValueKey = RegNtQueryValueKey,
RegNtQueryMultipleValueKey,
RegNtPreQueryMultipleValueKey = RegNtQueryMultipleValueKey,
RegNtPreCreateKey,
RegNtPostCreateKey,
RegNtPreOpenKey,
RegNtPostOpenKey,
RegNtKeyHandleClose,
RegNtPreKeyHandleClose = RegNtKeyHandleClose,
//
// .Net only
//
RegNtPostDeleteKey,
RegNtPostSetValueKey,
RegNtPostDeleteValueKey,
RegNtPostSetInformationKey,
RegNtPostRenameKey,
RegNtPostEnumerateKey,
RegNtPostEnumerateValueKey,
RegNtPostQueryKey,
RegNtPostQueryValueKey,
RegNtPostQueryMultipleValueKey,
RegNtPostKeyHandleClose,
RegNtPreCreateKeyEx,
RegNtPostCreateKeyEx,
RegNtPreOpenKeyEx,
RegNtPostOpenKeyEx,
//

//
RegNtPreFlushKey,
RegNtPostFlushKey,
RegNtPreLoadKey,
RegNtPostLoadKey,
RegNtPreUnLoadKey,
RegNtPostUnLoadKey,
RegNtPreQueryKeySecurity,
RegNtPostQueryKeySecurity,
RegNtPreSetKeySecurity,
RegNtPostSetKeySecurity,
//
// per-object context cleanup
//
RegNtCallbackObjectContextCleanup,
//
// new in Vista SP2
//
RegNtPreRestoreKey,
RegNtPostRestoreKey,
RegNtPreSaveKey,
RegNtPostSaveKey,
RegNtPreReplaceKey,
RegNtPostReplaceKey,

MaxRegNtNotifyClass //should always be the last enum
} REG_NOTIFY_CLASS;

这里有几个比较常见的类型

  • RegNtPreCreateKey 创建注册表 对应的Argument2PREG_CREATE_KEY_INFORMATION
  • RegNtPreOpenKey 打开注册表 对应的Argument2PREG_CREATE_KEY_INFORMATION
  • RegNtPreDeleteKey 删除键 对应的Argument2PREG_DELETE_KEY_INFORMATION
  • RegNtPreDeleteValueKey 删除键值 对应的Argument2PREG_DELETE_VALUE_KEY_INFORMATION
  • RegNtPreSetValueKey 修改键值 对应的Argument2PREG_SET_VALUE_KEY_INFORMATION

这里RegNtPreCreateKey RegNtPreOpenKey Argument2都是使用到PREG_CREATE_KEY_INFORMATION这个结构体,我们看下结构

其中两个关键的参数就是CompleteName表示指向路径的指针,以及RootObject表示指向注册表项的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct _REG_CREATE_KEY_INFORMATION {
PUNICODE_STRING CompleteName; // IN
PVOID RootObject; // IN
PVOID ObjectType;
ULONG CreateOptions;
PUNICODE_STRING Class;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
ACCESS_MASK DesiredAccess;
ACCESS_MASK GrantedAccess;
// to be filled in by callbacks
// when bypassing native code
PULONG Disposition;
// on pass through, callback should fill
// in disposition
PVOID *ResultObject;
// on pass through, callback should return
// object to be used for the return handle
PVOID CallContext;
PVOID RootObjectContext;
PVOID Transaction;
PVOID Reserved;
} REG_CREATE_KEY_INFORMATION, REG_OPEN_KEY_INFORMATION,*PREG_CREATE_KEY_INFORMATION, *PREG_OPEN_KEY_INFORMATION;

然后就是RegNtPreDeleteKey 对应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

image-20220501102243154

再就是RegNtPreDeleteValueKey 对应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;

image-20220501102233472

RegNtPreSetValueKey 对应的是PREG_SET_VALUE_KEY_INFORMATION结构,同样是Object指向要修改的注册表的指针,而ValueName就是指向具体需要修改的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct _REG_CREATE_KEY_INFORMATION {
PUNICODE_STRING CompleteName; // IN
PVOID RootObject; // IN
PVOID ObjectType;
ULONG CreateOptions;
PUNICODE_STRING Class;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
ACCESS_MASK DesiredAccess;
ACCESS_MASK GrantedAccess;
// to be filled in by callbacks
// when bypassing native code
PULONG Disposition;
// on pass through, callback should fill
// in disposition
PVOID *ResultObject;
// on pass through, callback should return
// object to be used for the return handle
PVOID CallContext;
PVOID RootObjectContext;
PVOID Transaction;
PVOID Reserved;
} REG_CREATE_KEY_INFORMATION, REG_OPEN_KEY_INFORMATION,*PREG_CREATE_KEY_INFORMATION, *PREG_OPEN_KEY_INFORMATION;

我们这里了解了CmRegisterCallback的一些常用结构,这里我们去IDA里面看一下其底层实现

这个函数首先调用了CmpRegisterCallbackInternal

image-20220501103504282

我们跟进去看看,首先是通过ExAllocatePoolWithTag来申请0x30大小的空间,然后通过test esi,esiBlinkFlink都指向自己,然后进行判断后跳转到69436B这个地址

image-20220501103825124

这段函数主要是将双向链表存入申请空间的前8字节,然后将Context结构保存到0x18的位置,将回调函数保存到0x1C的位置,然后进行判断后跳转到6943B1这个地址

image-20220501104354270

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

image-20220501104945941

然后再就是后面的代码,这里就是一些释放内存的操作

image-20220501105207771

我们从上面的分析可以得出CmRegisterCallback其实就是申请了一块空间,保存了一个双向链表、CookieContext、回调函数的地址,那么如果要存放注册表,只可能是放在了双向链表里面,这里我们就有理由猜测注册表监控的回调函数就是通过一个双向链表连接起来的

监控

我们在上面已经分析了CmRegisterCallback函数的原理,那么我们首先注册一个回调函数

1
2
3
4
5
6
7
NTSTATUS status = CmRegisterCallback(RegisterCallback, NULL, &RegCookie);
if (!NT_SUCCESS(status))
{
ShowError("CmRegisterCallback", status);
RegCookie.QuadPart = 0;
return status;
}

然后我们编写RegisterCallback这个回调函数,首先传入3个参数

1
NTSTATUS RegisterMonCallback(_In_ PVOID CallbackContext,_In_opt_ PVOID Argument1,_In_opt_ PVOID Argument2)

我们在前面已经说过CmRegisterCallback的第一个参数是操作类型,我们首先获取一下

1
LONG lOperateType = (REG_NOTIFY_CLASS)Argument1;

通过ExAllocatePool申请一块内存,使用ustrRegPath定义注册表的路径,这里设置非分页内存即可

1
ustrRegPath.Buffer = ExAllocatePool(NonPagedPool, ustrRegPath.MaxLength);

然后我们通过switch...case循环来判断lOperateType来进行具体的操作,比如这里是RegNtPreCreateKey

首先需要获取注册表的路径,这里就需要用到ObQueryNameString这个API

1
2
3
4
5
6
7
NTSTATUS
ObQueryNameString(
IN PVOID Object,
OUT POBJECT_NAME_INFORMATION ObjectNameInfo,
IN ULONG Length,
OUT PULONG ReturnLength
);

第二个参数指向OBJECT_NAME_INFORMATION结构,保存的是返回的名称

1
2
3
typedef struct _OBJECT_NAME_INFORMATION {
UNICODE_STRING Name;
} OBJECT_NAME_INFORMATION, *POBJECT_NAME_INFORMATION;

那么这里我们调用ObQueryNameString获取函数地址,写成一个函数方便调用

1
2
3
PVOID lpObjectNameInfo = ExAllocatePool(NonPagedPool, ulSize);
NTSTATUS status = ObQueryNameString(pRegistryObject, (POBJECT_NAME_INFORMATION)lpObjectNameInfo, ulSize, &ulRetLen);
RtlCopyUnicodeString(pRegistryPath, (PUNICODE_STRING)lpObjectNameInfo);

获取一下注册表的路径

1
GetRegisterPath(&ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->RootObject);

这里我们定位到注册表之后,就需要进行判断是否是我们想要保护的注册表,如果是的话就将status改成STATUS_ACCESS_DENIED即可达到保护注册表的效果

通过wcsstr()函数判断路径是否为我们想要保护的注册表名称,编写为Compare函数

1
2
3
4
5
6
7
8
9
BOOLEAN Compare(UNICODE_STRING ustrRegPath)
{
if (NULL != wcsstr(ustrRegPath.Buffer, L"RegTest"))
{
return TRUE;
}

return FALSE;
}

如果名称相同则设置为STATUS_ACCESS_DENIED

1
2
3
4
if (Compare(ustrRegPath))
{
status = STATUS_ACCESS_DENIED;
}

完整代码如下,这里因为是RegNtPreOpenKey所以Argument2对应的类型就是PREG_CREATE_KEY_INFORMATION,这里只需要修改Argunment2的类型即可,剩下的几个判断再这里就不赘述了

1
2
3
4
5
6
7
8
9
10
11
12
case RegNtPreOpenKey:
{
GetRegisterPath(&ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->RootObject);
// 判断是否是被保护的注册表
if (Compare(ustrRegPath))
{
status = STATUS_ACCESS_DENIED;
}
// 显示
DbgPrint("[RegNtPreOpenKey][%wZ][%wZ]\n", &ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->CompleteName);
break;
}

image-20220501142500180

回调函数的完整代码如下

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
NTSTATUS RegisterCallback(_In_ PVOID CallbackContext,_In_opt_ PVOID Argument1,_In_opt_ PVOID Argument2)
{

NTSTATUS status = STATUS_SUCCESS;
UNICODE_STRING ustrRegPath;

LONG lOperateType = (REG_NOTIFY_CLASS)Argument1;

ustrRegPath.Length = 0;
ustrRegPath.MaximumLength = 1024 * sizeof(WCHAR);
ustrRegPath.Buffer = ExAllocatePool(NonPagedPool, ustrRegPath.MaximumLength);
if (NULL == ustrRegPath.Buffer)
{
printf("ExAllocatePool error : %d\n", GetLastError());
return status;
}
switch (lOperateType)
{
// 创建注册表之前
case RegNtPreCreateKey:
{
GetRegisterPath(&ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->RootObject);

if (Compare(ustrRegPath))
{
status = STATUS_ACCESS_DENIED;
}
DbgPrint("[RegNtPreCreateKey][%wZ][%wZ]\n", &ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->CompleteName);
break;
}

// 打开注册表之前
case RegNtPreOpenKey:
{
GetRegisterPath(&ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->RootObject);

if (Compare(ustrRegPath))
{
status = STATUS_ACCESS_DENIED;
}
DbgPrint("[RegNtPreOpenKey][%wZ][%wZ]\n", &ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->CompleteName);
break;
}
// 删除键之前
case RegNtPreDeleteKey:
{
GetRegisterPath(&ustrRegPath, ((PREG_DELETE_KEY_INFORMATION)Argument2)->Object);

if (Compare(ustrRegPath))
{
status = STATUS_ACCESS_DENIED;
}

DbgPrint("[RegNtPreDeleteKey][%wZ]\n", &ustrRegPath);
break;
}
// 删除键值之前
case RegNtPreDeleteValueKey:
{
GetRegisterPath(&ustrRegPath, ((PREG_DELETE_VALUE_KEY_INFORMATION)Argument2)->Object);

if (Compare(ustrRegPath))
{
status = STATUS_ACCESS_DENIED;
}
DbgPrint("[RegNtPreDeleteValueKey][%wZ][%wZ]\n", &ustrRegPath, ((PREG_DELETE_VALUE_KEY_INFORMATION)Argument2)->ValueName);
break;
}
// 修改键值之前
case RegNtPreSetValueKey:
{
GetRegisterPath(&ustrRegPath, ((PREG_SET_VALUE_KEY_INFORMATION)Argument2)->Object);

if (Compare(ustrRegPath))
{
status = STATUS_ACCESS_DENIED;
}
// 显示
DbgPrint("[RegNtPreSetValueKey][%wZ][%wZ]\n", &ustrRegPath, ((PREG_SET_VALUE_KEY_INFORMATION)Argument2)->ValueName);
break;
}
default:
break;
}

if (NULL != ustrRegPath.Buffer)
{
ExFreePool(ustrRegPath.Buffer);
ustrRegPath.Buffer = NULL;
}

PEPROCESS pEProcess = PsGetCurrentProcess();
if (NULL != pEProcess)
{
UCHAR* lpszProcessName = PsGetProcessImageFileName(pEProcess);
if (NULL != lpszProcessName)
{
DbgPrint("Current Process[%s]\n", lpszProcessName);
}
}

return status;
}

实现效果

我们加载驱动可以看到我们想要修改RegTest的二进制的值被拒绝

image-20220501142641363

删除也会被拦截

image-20220501142654260

重命名也同样被拦截

image-20220501142706899

卸载驱动之后能够成功重命名

image-20220501142719387

也能够修改二进制的内容

image-20220501142758141

也可以删除字符串

image-20220501142809044

反监控

我们在前面已经逆向分析了CmRegisterCallback的底层实现,就是通过申请一块内存空间存放双向链表,那么我们只要定位到这个双向链表删除回调函数即可得到反监控的效果

将链表加入我们内存的函数是SetRegisterCallback,这里我因为pdb文件的问题显示得有点问题,我们跟进去看看

image-20220501144151656

这里往下走可以看到offset CallbackListHead的操作,这里就是将链表头赋值给ebx

image-20220501144321163

然后将eax的值赋给[esi + 10],这里就是在进行初始化Cookie的操作

image-20220501144430141

然后最后再比较edi的值是否为ListBegin的地址,跳转到增加链表的代码

image-20220501144540775

那么这里我们明确下思路,我们想要定位到链表头,就首先需要通过在CmRegisterCallback里面定位SetRegisterCallback

image-20220501144914828

然后定位到链表头即可

image-20220501145028344

那么这里我们进行代码编写,首先定位到CmRegisterCallback函数

1
2
3
UNICODE_STRING uStrFuncName = RTL_CONSTANT_STRING(L"CmRegisterCallback");

pCmRegFunc = (PUCHAR)MmGetSystemRoutineAddress(&uStrFuncName);

然后通过硬编码定位到链表头

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
pCmRegFunc = (PUCHAR)MmGetSystemRoutineAddress(&uStrFuncName);

if (pCmRegFunc == NULL)
{
DbgPrint("MmGetSystemRoutineAddress error : %d\r\n",GetLastError());
return pListEntry;
}

while (*pCmRegFunc != 0xC2)
{
if (*pCmRegFunc == 0xE8)
{
pCmcRegFunc = (PUCHAR)((ULONG)pCmRegFunc + 5 + *(PULONG)(pCmRegFunc + 1));
break;
}
pCmRegFunc++;
}

if (pCmcRegFunc == NULL)
{
DbgPrint("GetCmcRegFunc error : %d\r\n",GetLastError());
return pListEntry;
}

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 : %d\r\n", GetLastError());
return pListEntry;
}

while (*pSetRegFunc != 0xC2)
{
if (*pSetRegFunc == 0xBB)
{
pListEntry = (PULONG) * (PULONG)(pSetRegFunc + 1);
break;
}
pSetRegFunc++;
}

定位到链表头之后,我们通过MmIsAddressValid对地址进行判断是否可用,通过0x10偏移定位到Cookie

1
2
3
4
5
pHead = GetRegisterList();
pListEntry = (PLIST_ENTRY)*pHead;

pLiRegCookie = (PLARGE_INTEGER)((ULONG)pListEntry + 0x10);
pFuncAddr = (PULONG)((ULONG)pListEntry + 0x1C);

然后调用CmUnRegisterCallback删除回调

1
status = CmUnRegisterCallback(*pLiRegCookie);

实现效果

这里如果连windbg有输出效果会更明显,但是我这台win7配双机调试一直不成功,这里就只能看一下输出的效果了

首先直接加载一个exe,可以看到修改注册表的值成功

image-20220501155155905

加载驱动发现创建注册表值失败

image-20220501155057647

然后再加载我们的绕过回调函数的驱动,又可以加载成功

image-20220501155246407

CATALOG
  1. 1. CmRegisterCallback
  2. 2. 监控
    1. 2.1. 实现效果
  3. 3. 反监控
    1. 3.1. 实现效果