回调函数的注册和卸载
在VISTA以下的系统中,想要进行进程的保护,只能通过HOOK的方式来实现。
在VISTA及以上的系统中,微软提供了一个 进程回调函数
来实现对 线程
和 进程
进行管理。
1 2 3 4
| NTSTATUS ObRegisterCallbacks( _In_ POB_CALLBACK_REGISTRATION CallBackRegistration, _Out_ PVOID *RegistrationHandle );
|
1 2 3
| VOID ObUnRegisterCallbacks( _In_ PVOID RegistrationHandle );
|
在注册时,需要先初始化一个用来描述回调注册信息的结构体
1 2 3 4 5 6 7
| typedef struct _OB_CALLBACK_REGISTRATION { USHORT Version; USHORT OperationRegistrationCount; UNICODE_STRING Altitude; PVOID RegistrationContext; OB_OPERATION_REGISTRATION *OperationRegistration; } OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;
|
在这个结构体中引入了 altitude
的概念,这个参数本身是一个字符串形式表述的数字,
微软将其划分为不同的区段,来表示不同的功能,这里使用的是 WDK
例子中使用的 L"321124"
,
其参数成员 OB_OPERATION_REGISTRATION
用来定义具体的行为信息,结构体定义如下
1 2 3 4 5 6
| typedef struct _OB_OPERATION_REGISTRATION { POBJECT_TYPE *ObjectType; OB_OPERATION Operations; POB_PRE_OPERATION_CALLBACK PreOperation; POB_POST_OPERATION_CALLBACK PostOperation; } OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;
|
其中 ObjectType
参数表明我们想要处理的 对象类型
,可取值内容为
1 2
| PsProcessType PsThreadType
|
其中 Operations
参数表明我们想要处理的 操作类型
,可取值内容为
1 2
| OB_OPERATION_HANDLE_CREATE OB_OPERATION_HANDLE_DUPLICATE
|
其中 PreOperation
表示 动作之前
进行处理,而 PostOperation
表示 动作之后
进行处理
1 2 3 4
| OB_PREOP_CALLBACK_STATUS ObjectPreCallback( _In_ PVOID RegistrationContext, _In_ POB_PRE_OPERATION_INFORMATION OperationInformation );
|
1 2 3 4
| VOID ObjectPostCallback( _In_ PVOID RegistrationContext, _In_ POB_POST_OPERATION_INFORMATION OperationInformation );
|
这里我们只处理 动作之前
的行为,而 动作之后
忽略,设置为 NULL
值,相关代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| OB_OPERATION_REGISTRATION OperationRegister = { 0 }; OB_CALLBACK_REGISTRATION CallbackRegister = { 0 };
OperationRegister.ObjectType = PsProcessType; OperationRegister.Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; OperationRegister.PreOperation = ProcPreOperation;
CallbackRegister.Version = OB_FLT_REGISTRATION_VERSION; CallbackRegister.OperationRegistrationCount = 1; RtlInitUnicodeString(&CallbackRegister.Altitude, L"321124"); CallbackRegister.OperationRegistration = &OperationRegister;
|
操作的类型和行为的处理
在 动作之前
的处理函数中,我们可以通过取消句柄的退出权限,来阻止进程被退出
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
| OB_PREOP_CALLBACK_STATUS ProcPreOperation( IN PVOID Context, IN POB_PRE_OPERATION_INFORMATION PreOperInfo) { UNREFERENCED_PARAMETER(Context); (PreOperInfo->ObjectType != *PsProcessType); PsGetCurrentProcessId(); ((PEPROCESS)(PreOperInfo->Object) == PsGetCurrentProcess()); if (PreOperInfo->Operation == OB_OPERATION_HANDLE_CREATE) { if (PreOperInfo->Parameters->CreateHandleInformation.OriginalDesiredAccess & 0x0001) PreOperInfo->Parameters->CreateHandleInformation.DesiredAccess &= ~0x0001; if (PreOperInfo->Parameters->CreateHandleInformation.OriginalDesiredAccess & 0x0040) PreOperInfo->Parameters->CreateHandleInformation.DesiredAccess &= ~0x0040; } if (PreOperInfo->Operation == OB_OPERATION_HANDLE_DUPLICATE) { if (PreOperInfo->Parameters->DuplicateHandleInformation.OriginalDesiredAccess & 0x0001) PreOperInfo->Parameters->DuplicateHandleInformation.DesiredAccess &= ~0x0001; if (PreOperInfo->Parameters->DuplicateHandleInformation.OriginalDesiredAccess & 0x0040) PreOperInfo->Parameters->DuplicateHandleInformation.DesiredAccess &= ~0x0040; } return OB_PREOP_SUCCESS; }
|
关于句柄复制权限的问题
在以上的代码中,我们可以看到,只是做自保护防退出,为什么连 句柄复制
权限都要取消?
这是因为从WIN8开始,操作系统的 任务管理器
发生了很大的变化,尽管只是取消 退出权限
就能实现
自保护,但是之后会发生CPU占用率飙升到100%的现象。
经过用WINDBG和IDA对新的任务管理器进行分析后,发现在结束进程时,任务管理器会首先使用
DuplicateHandle
函数复制要结束进程的句柄,然后使用 QueueUserWorkItem
线程池函数,来运行
退出的代码,就是这个线程池函数导致CPU占用率飙升到了100%。所以在它之前,我们取消 句柄复制
的权限,任务管理器在复制句柄时失败,就不会再调用线程池函数。
关于获取进程名的问题
判断进程是不是我们想要保护的进程时,需要获取进程的全路径,一般使用 ZwQueryInformationProcess
函数来获取进程的全路径。这就产生一个问题,调用这个函数会触发 进程回调
行为,从而再次进入回调函数
中,造成了函数调用的重入问题(注:在Win10系统上,已变为从当前回调的下一个回调继续调用,所以就不会再出现
重入的问题。在注册表回调中操作注册表时,也是相同的处理方式,不会出现重入。
)
通过查看WRK1.2源码和IDA进行分析,发现这个函数会调用一次 ObReferenceObjectByHandle
函数,正是
这个引用句柄的函数导致了重入。接着再往下分析,找到是SeLocateProcessImageName
这个函数读取的进程
路径,把 ntoskrnl.exe
拖到PE工具里看一下导出表,正好导出了这个函数,我们直接声明这个函数的定义,
就可以使用了,使用这个函数不会触发重入问题。但需要注意的是,我们需要手动释放其返回的内存空间。
另外我们还可以先使用 PsReferenceProcessFilePointer
获取进程的文件对象,然后再根据文件对象使用
IoQueryFileDosDeviceName
查询对应的路径。因为涉及到磁盘读写操作,查询速度较慢,频繁查询会卡顿,
就需要把查询过的EPROCESS缓存起来,加快处理速度。
进程路径中盘符的转换
在以上操作中获取到的进程路径,有可能并不是我们常见的 C:\\
盘符的形式,
而是 \\Device\\HarddiskVolume1\\
这种卷的形式,所以还需要我们对其进行转换
在XP中可以使用 RtlVolumeDeviceToDosName
和在VISTA中使用 IoVolumeDeviceToDosName
来查询设备对象的盘符路径,这里使用的是 查询符号链接
的方式来进行转换
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
| WCHAR GetDosNameWideChar(PUNICODE_STRING Volume) { WCHAR Ch = L'A'; WCHAR DosBuffer[20] = { 0 }; UNICODE_STRING DosName = { 0 }; WCHAR VolumeBuffer[40] = { 0 }; UNICODE_STRING VolumeName = { 0 }; HANDLE LinkHandle = NULL; OBJECT_ATTRIBUTES ObjectAttributes = { 0 }; NTSTATUS Status = STATUS_SUCCESS; if (Volume == NULL) return L'\0'; if (Volume->Buffer == NULL) return L'\0'; DosName.Buffer = DosBuffer; DosName.Length = 0; DosName.MaximumLength = sizeof(DosBuffer); VolumeName.Buffer = VolumeBuffer; VolumeName.Length = 0; VolumeName.MaximumLength = sizeof(VolumeBuffer); RtlCopyMemory(DosName.Buffer, L"\\??\\C:", sizeof(L"\\??\\C:")); DosName.Length = sizeof(L"\\??\\C:") - sizeof(L'\0'); if (KeGetCurrentIrql() > PASSIVE_LEVEL) return L'\0'; for (Ch = L'A'; Ch <= L'Z'; ++Ch) { DosName.Buffer[4] = Ch; InitializeObjectAttributes(&ObjectAttributes, &DosName, OBJ_KERNEL_HANDLE, 0, NULL); Status = ZwOpenSymbolicLinkObject(&LinkHandle, GENERIC_READ, &ObjectAttributes); if (!NT_SUCCESS(Status)) continue; Status = ZwQuerySymbolicLinkObject(LinkHandle, &VolumeName, NULL); ZwClose(LinkHandle); if (!NT_SUCCESS(Status)) continue; if (RtlCompareUnicodeString(&VolumeName, Volume, TRUE) == 0) { return Ch; } } return L'\0'; }
|