0%

使用进程回调函实现进程过滤(1)

回调函数的注册和卸载

在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 };

// 初始化OB_OPERATION_REGISTRATION结构体
OperationRegister.ObjectType = PsProcessType;
OperationRegister.Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
OperationRegister.PreOperation = ProcPreOperation;

// 初始化OB_CALLBACK_REGISTRATION结构体
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) // 句柄创建
{
// 如果存在退出权限,则取消退出权限PROCESS_TERMINATE
if (PreOperInfo->Parameters->CreateHandleInformation.OriginalDesiredAccess & 0x0001)
PreOperInfo->Parameters->CreateHandleInformation.DesiredAccess &= ~0x0001;
// 如果存在句柄复制权限,则取消句柄复制权限PROCESS_DUP_HANDLE
if (PreOperInfo->Parameters->CreateHandleInformation.OriginalDesiredAccess & 0x0040)
PreOperInfo->Parameters->CreateHandleInformation.DesiredAccess &= ~0x0040;
}
if (PreOperInfo->Operation == OB_OPERATION_HANDLE_DUPLICATE) // 句柄复制
{
// 如果存在退出权限,则取消退出权限PROCESS_TERMINATE
if (PreOperInfo->Parameters->DuplicateHandleInformation.OriginalDesiredAccess & 0x0001)
PreOperInfo->Parameters->DuplicateHandleInformation.DesiredAccess &= ~0x0001;
// 如果存在句柄复制权限,则取消句柄复制权限PROCESS_DUP_HANDLE
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 }; // L"\\??\\C:"
UNICODE_STRING DosName = { 0 }; // 查询用盘符名称
WCHAR VolumeBuffer[40] = { 0 }; // L"\\Device\\HarddiskVolume1"
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';
// 初始化 UNICODE_STRING
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');
// 检查IRQL是否符合接下来的操作
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';
}