原文始发于跳跳糖(深入注册表监控
):
前言
注册表是windows的重要数据库,存放了很多重要的信息以及一些应用的设置,对注册表进行监控并防止篡改是十分有必要的。在64位系统下微软提供了CmRegisterCallback
这个回调函数来实时监控注册表的操作,那么既然这里微软提供了这么一个方便的接口,病毒木马自然也会利用,这里我们就来探究其实现的原理和如何利用这个回调函数进行对抗
CmRegisterCallback
要想更好的进行对抗,就是深入底层去看这个函数到底做了什么事情,无论是监控还是反监控,这样我们才能够更好的进行利用
首先我们去msdn里面看一下它的结构
NTSTATUS CmRegisterCallback(
[in] PEX_CALLBACK_FUNCTION Function,
[in, optional] PVOID Context,
[out] PLARGE_INTEGER Cookie
);
第一个参数指向RegistryCallback
,它这里其实是通过EX_CALLBACK_FUNCTION
这个回调函数实现,结构如下
EX_CALLBACK_FUNCTION ExCallbackFunction;
NTSTATUS ExCallbackFunction(
[in] PVOID CallbackContext,
[in, optional] PVOID Argument1,
[in, optional] PVOID Argument2
)
{...}
主要是看第三个参数,REG_NOTIFY_CLASS
结构如下
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 创建注册表 对应的
Argument2
为PREG_CREATE_KEY_INFORMATION
- RegNtPreOpenKey 打开注册表 对应的
Argument2
为PREG_CREATE_KEY_INFORMATION
- RegNtPreDeleteKey 删除键 对应的
Argument2
为PREG_DELETE_KEY_INFORMATION
- RegNtPreDeleteValueKey 删除键值 对应的
Argument2
为PREG_DELETE_VALUE_KEY_INFORMATION
- RegNtPreSetValueKey 修改键值 对应的
Argument2
为PREG_SET_VALUE_KEY_INFORMATION
这里RegNtPreCreateKey
和RegNtPreOpenKey
的Argument2
都是使用到PREG_CREATE_KEY_INFORMATION
这个结构体,我们看下结构
其中两个关键的参数就是CompleteName
表示指向路径的指针,以及RootObject
表示指向注册表项的指针
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
参数指向要删除注册表的指针
typedef struct _REG_DELETE_KEY_INFORMATION {
PVOID Object; // IN
PVOID CallContext;
PVOID ObjectContext;
PVOID Reserved;
} REG_DELETE_KEY_INFORMATION, *PREG_DELETE_KEY_INFORMATION
再就是RegNtPreDeleteValueKey
对应PREG_DELETE_VALUE_KEY_INFORMATION
结构体,我们通过名字可以判断这里我们要删除表项里面的值,所以这里Object
还是指向要删除的注册表的指针,而ValueName
就是指向具体需要删除的值
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;
RegNtPreSetValueKey
对应的是PREG_SET_VALUE_KEY_INFORMATION
结构,同样是Object
指向要修改的注册表的指针,而ValueName
就是指向具体需要修改的值
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
我们跟进去看看,首先是通过ExAllocatePoolWithTag
来申请0x30
大小的空间,然后通过test esi,esi
将Blink
和Flink
都指向自己,然后进行判断后跳转到69436B
这个地址
这段函数主要是将双向链表存入申请空间的前8字节,然后将Context
结构保存到0x18
的位置,将回调函数保存到0x1C
的位置,然后进行判断后跳转到6943B1
这个地址
这段函数的主要作用就是将Cookie
的值存入0x10
偏移处,这里的设计很巧妙,因为一个cookie是占8字节的,这里首先将[esi + 0x10]
的值存入eax,然后将ebx地址存入eax,相当于赋值前4位,然后再进行同样的操作,这里取的是[esi + 0x14]
,也就是赋值后四位
然后再就是后面的代码,这里就是一些释放内存的操作
我们从上面的分析可以得出CmRegisterCallback
其实就是申请了一块空间,保存了一个双向链表、Cookie
、Context
、回调函数的地址,那么如果要存放注册表,只可能是放在了双向链表里面,这里我们就有理由猜测注册表监控的回调函数就是通过一个双向链表连接起来的
监控
我们在上面已经分析了CmRegisterCallback
函数的原理,那么我们首先注册一个回调函数
NTSTATUS status = CmRegisterCallback(RegisterCallback, NULL, &RegCookie);
if (!NT_SUCCESS(status))
{
ShowError("CmRegisterCallback", status);
RegCookie.QuadPart = 0;
return status;
}
然后我们编写RegisterCallback
这个回调函数,首先传入3个参数
NTSTATUS RegisterMonCallback(_In_ PVOID CallbackContext,_In_opt_ PVOID Argument1,_In_opt_ PVOID Argument2)
我们在前面已经说过CmRegisterCallback
的第一个参数是操作类型,我们首先获取一下
LONG lOperateType = (REG_NOTIFY_CLASS)Argument1;
通过ExAllocatePool
申请一块内存,使用ustrRegPath
定义注册表的路径,这里设置非分页内存即可
ustrRegPath.Buffer = ExAllocatePool(NonPagedPool, ustrRegPath.MaxLength);
然后我们通过switch...case
循环来判断lOperateType
来进行具体的操作,比如这里是RegNtPreCreateKey
首先需要获取注册表的路径,这里就需要用到ObQueryNameString
这个API
NTSTATUS
ObQueryNameString(
IN PVOID Object,
OUT POBJECT_NAME_INFORMATION ObjectNameInfo,
IN ULONG Length,
OUT PULONG ReturnLength
);
第二个参数指向OBJECT_NAME_INFORMATION
结构,保存的是返回的名称
typedef struct _OBJECT_NAME_INFORMATION {
UNICODE_STRING Name;
} OBJECT_NAME_INFORMATION, *POBJECT_NAME_INFORMATION;
那么这里我们调用ObQueryNameString
获取函数地址,写成一个函数方便调用
PVOID lpObjectNameInfo = ExAllocatePool(NonPagedPool, ulSize);
NTSTATUS status = ObQueryNameString(pRegistryObject, (POBJECT_NAME_INFORMATION)lpObjectNameInfo, ulSize, &ulRetLen);
RtlCopyUnicodeString(pRegistryPath, (PUNICODE_STRING)lpObjectNameInfo);
获取一下注册表的路径
GetRegisterPath(&ustrRegPath, ((PREG_CREATE_KEY_INFORMATION)Argument2)->RootObject);
这里我们定位到注册表之后,就需要进行判断是否是我们想要保护的注册表,如果是的话就将status
改成STATUS_ACCESS_DENIED
即可达到保护注册表的效果
通过wcsstr()
函数判断路径是否为我们想要保护的注册表名称,编写为Compare
函数
BOOLEAN Compare(UNICODE_STRING ustrRegPath)
{
if (NULL != wcsstr(ustrRegPath.Buffer, L"RegTest"))
{
return TRUE;
}
return FALSE;
}
如果名称相同则设置为STATUS_ACCESS_DENIED
if (Compare(ustrRegPath))
{
status = STATUS_ACCESS_DENIED;
}
完整代码如下,这里因为是RegNtPreOpenKey
所以Argument2
对应的类型就是PREG_CREATE_KEY_INFORMATION
,这里只需要修改Argunment2
的类型即可,剩下的几个判断再这里就不赘述了
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;
}
回调函数的完整代码如下
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
的二进制的值被拒绝
删除也会被拦截
重命名也同样被拦截
卸载驱动之后能够成功重命名
也能够修改二进制的内容
也可以删除字符串
反监控
我们在前面已经逆向分析了CmRegisterCallback
的底层实现,就是通过申请一块内存空间存放双向链表,那么我们只要定位到这个双向链表删除回调函数即可得到反监控的效果
将链表加入我们内存的函数是SetRegisterCallback
,这里我因为pdb文件的问题显示得有点问题,我们跟进去看看
这里往下走可以看到offset CallbackListHead
的操作,这里就是将链表头赋值给ebx
然后将eax的值赋给[esi + 10]
,这里就是在进行初始化Cookie
的操作
然后最后再比较edi
的值是否为ListBegin
的地址,跳转到增加链表的代码
那么这里我们明确下思路,我们想要定位到链表头,就首先需要通过在CmRegisterCallback
里面定位SetRegisterCallback
然后定位到链表头即可
那么这里我们进行代码编写,首先定位到CmRegisterCallback
函数
UNICODE_STRING uStrFuncName = RTL_CONSTANT_STRING(L"CmRegisterCallback");
pCmRegFunc = (PUCHAR)MmGetSystemRoutineAddress(&uStrFuncName);
然后通过硬编码定位到链表头
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
pHead = GetRegisterList();
pListEntry = (PLIST_ENTRY)*pHead;
pLiRegCookie = (PLARGE_INTEGER)((ULONG)pListEntry + 0x10);
pFuncAddr = (PULONG)((ULONG)pListEntry + 0x1C);
然后调用CmUnRegisterCallback
删除回调
status = CmUnRegisterCallback(*pLiRegCookie);
实现效果
这里如果连windbg有输出效果会更明显,但是我这台win7没有配双机调试,这里就只能看一下输出的效果了
首先直接加载一个exe,可以看到修改注册表的值成功
加载驱动发现创建注册表值失败
然后再加载我们的绕过回调函数的驱动,又可以加载成功