内核模式简单实现进程监控 (1)
在使用冰刃的时候我们可以发现它有一个“监视进线程创建”的功能,这个功能挺有用的,在用户模式下我们可以注册一个shell钩子来监视,或者通过挂钩一些进程创建的Win32 API来实现。
在内核模式下我们同样可以使用API HOOK来实现,但是还有一些简单的做法,比如我们今天要介绍的PsSetCreateProcessNotifyRoutine函数。
PsSetCreateProcessNotifyRoutine通过向系统注册一个回调例程便可以轻松获得关于进程创建、终止的信息,虽然这种方法很多大牛都不屑使用,但对于我们新手学习内核编程入门却还是很有帮助的。
与它类似的函数还有PsSetCreateThreadNotifyRoutine、PsSetLoadImageNotifyRoutine,它们的用法大同小异,因此我们仅以PsSetCreateProcessNotifyRoutine为例来学习。
第一部分:关于PsSetCreateProcessNotifyRoutine
在DDK的帮助文档里是这样介绍这个函数的:PsSetCreateProcessNotifyRoutine adds a driver-supplied callback routine to, or removes it from, a list of routines to be called whenever a process is created or deleted.
从上面可以看出,这个函数不仅可以向系统中注册一个回调例程,该例程在有进程被创建或结束的时候会被调用,而且这个函数还可以从系统中删除我们注册的回调例程(这个很重要,真的!)。
我们先来看一下这个函数的原型声明,如下所示:
NTSTATUS
PsSetCreateProcessNotifyRoutine(
IN PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
IN BOOLEAN Remove
);
可以看出它的参数很简单,第一个参数指定了要添加/删除的回调例程;第二个参数表明了是要添加还是删除这个例程。
跟它不太一样的是,PsSetCreateThreadNotifyRoutine和PsSetLoadImageNotifyRoutine函数都只有一个参数,它们的删除操作另有函数,比如PsRemoveCreateThreadNotifyRoutine。
下面我们来看一下进程回调例程的定义:
VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE) (
IN HANDLE ParentId,
IN HANDLE ProcessId,
IN BOOLEAN Create
);
它有三个参数,第一个参数指明了父进程的ID,第二个参数是进程本身的ID,最后一个参数指明了该进程是刚刚执行了创建还是结束操作。
有人可能会问,进程ID怎么会是HANDLE类型呢?别问我,DDK的帮助文档里就是这么写的。其实HANDLE与DWORD一样在本质上都是个32位的整数,因此这样定义是没什么问题的。
当我们注册了一个回调例程以后,如果发生了进程创建、结束的事件,系统就会调用该回调例程,这时侯我们就可以通过参数二和三分别得到进程的PID和创建/结束信息。我们可以在回调例程中简单地将这些信息打印出来,或者通知应用层的程序。这就涉及到了驱动程序与应用层程序的通信问题,下面我们进行介绍。
第二部分:关于驱动程序与应用层程序的通信
在多数情况下我们可以通过调用ReadFile/WriteFile或DeviceIoControl函数从驱动程序中读写信息,关于它们的详细使用方法这里就不废话了,简单说一下就行。
在调用上述三个Win32 API时,分别会产生三个与之对应的IRP,即:IRP_MJ_READ、IRP_MJ_WRITE、IRP_MJ_DEVICE_CONTROL,我们只要在驱动程序中进行相应的响应处理即可。
在多数情况下我们都是使用DeviceIoControl函数,它需要定义一个IOCTL,它需要在驱动程序和应用层程序之间共享,因此较好的做法是使用一个专门的头文件来进行定义。在定义IOCTL的时候需要注意使用哪种I/O方式,常见的做法是使用缓冲区I/O,这时候需要将我们创建的设备对象的Flags设置为DO_BUFFERED_IO,如下所示:
deviceObject->Flags |= DO_BUFFERED_IO;
因为在我使用的这个EasySys生成的驱动框架中没有明确将设备对象的读写方式设置为缓冲区I/O或直接I/O,这样它就会使用默认的其他I/O方式,而起初我没有考虑到这一点,造成了蓝屏的后果~~~
另外,我们的驱动程序和应用层程序之间必须实现同步,这可以通过事件对象来解决,否则我们要么只能在ring3建立一个线程不停地尝试读取数据,要么让驱动先把信息保存起来,我们隔一段时间去读取一次,这样都不是好的做法。
事件对象既可以在应用层创建,也可以在驱动中创建,全在于我们的喜好。
第三部分:代码分析
下面我们来分析代码,首先我们需要定义一些变量,还将一些重要的变量添加为驱动程序的DEVICE_EXTENSION子域(在驱动中要尽量避免使用全局变量),如下所示:
typedef struct _DEVICE_EXTENSION
{
HANDLE hProcessHandle; // 事件对象句柄
PKEVENT ProcessEvent; // 用户和内核通信的事件对象指针
HANDLE hParentId; // 在回调函数中保存进程信息
HANDLE hProcessId;
BOOLEAN bCreate;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
我们现在要做的事情是,在DriverEntry例程中创建一个事件对象并将其句柄保存起来,然后设置回调例程。对了,不要忘记设置I/O方式。因为我们定义的IOCTL是使用的缓冲区I/O,因此这里也要设置成缓冲区I/O方式,相关代码如下所示:
deviceObject->Flags |= DO_BUFFERED_IO;
// 创建符号链接与分发IRP的代码是自动生成的,不用考虑
// 保存设备对象指针
// 这里我们不得不使用全局变量g_pDeviceObject,我们在回调例程中需要用它
// 来得到DEVICE_EXTENSION以获得相关信息
g_pDeviceObject = deviceObject;
// 创建事件对象与应用层通信
RtlInitUnicodeString(&ProcessEventString, EVENT_NAME);
deviceExtension->ProcessEvent = IoCreateNotificationEvent(&ProcessEventString, &deviceExtension->hProcessHandle);
KeClearEvent(deviceExtension->ProcessEvent); // 设置为非受信状态
// 设置回调例程
Status = PsSetCreateProcessNotifyRoutine(ProcessCallback, FALSE);
下面我们来看一下回调例程,在这里我们仅仅是将得到的PID等信息保存到了驱动设备的DEVICE_EXTENSON结构中,如下所示:
VOID
ProcessCallback(
IN HANDLE ParentId, // 父进程ID
IN HANDLE ProcessId, // 发生事件的进程ID
IN BOOLEAN Create // 进程是创建还是终止
)
{
// 获得DEVICE_EXTENSION结构
PDEVICE_EXTENSION deviceExtension = (PDEVICE_EXTENSION)g_pDeviceObject->DeviceExtension;
// 保存信息
deviceExtension->hParentId = ParentId;
deviceExtension->hProcessId = ProcessId;
deviceExtension->bCreate = Create;
// 触发事件,通知应用程序
KeSetEvent(deviceExtension->ProcessEvent, 0, FALSE);
KeClearEvent(deviceExtension->ProcessEvent);
}
从上述代码中可以看到,我们在保存完信息后就通过触发事件来通知应用层程序有新的进程创建/结束事件发生,这时候应用程序就会向驱动发送一个IRP来获取信息。下面我们来看一下处理IRP的过程:
pCallbackInfo = Irp->AssociatedIrp.SystemBuffer;
outBufLength = irpStack->Parameters.DeviceIoControl.OutputBufferLength;
ioControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;
switch (ioControlCode)
{
case IOCTL_PROC_MON:
{
KdPrint(("[ProcMon] IOCTL: 0x%X", ioControlCode));
if (outBufLength >= sizeof(PCALLBACK_INFO))
{
pCallbackInfo->hParentId = deviceExtension->hParentId;
pCallbackInfo->hProcessId = deviceExtension->hProcessId;
pCallbackInfo->bCreate = deviceExtension->bCreate;
Irp->IoStatus.Information = outBufLength;
}
break;
}
default:
{
Status = STATUS_INVALID_PARAMETER;
Irp->IoStatus.Information = 0;
break;
}
}
在得到IRP后,我们直接修改它的Irp->AssociatedIrp.SystemBuffer就可以使应用层程序得到相关改变。至于IRP的其他处理,比如Irp->IoStatus.Information、Status等也很重要,这些都是我们必须掌握的。
上面都是驱动程序的代码,下面我们来看一下应用层程序的代码:
#include "windows.h"
#include "winioctl.h"
#include "stdio.h"
#include "../inc/ioctls.h"
#define SYMBOL_LINK "\\\\.\\ProcMon"
int main()
{
CALLBACK_INFO cbkinfo, cbktemp = {0};
// 打开驱动设备对象
HANDLE hDriver = ::CreateFile(
SYMBOL_LINK,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hDriver == INVALID_HANDLE_VALUE)
{
printf("打开驱动设备对象失败!\n");
return -1;
}
// 打开内核事件对象
HANDLE hProcessEvent = ::OpenEventW(SYNCHRONIZE, FALSE, EVENT_NAME);
while (::WaitForSingleObject(hProcessEvent, INFINITE))
{
DWORD dwRet;
BOOL bRet;
bRet = ::DeviceIoControl(
hDriver,
IOCTL_PROC_MON,
NULL,
0,
&cbkinfo,
sizeof(cbkinfo),
&dwRet,
NULL);
if (bRet)
{
if (cbkinfo.hParentId != cbktemp.hParentId || \
cbkinfo.hProcessId != cbktemp.hProcessId || \
cbkinfo.bCreate != cbktemp.bCreate)
{
if (cbkinfo.bCreate)
{
printf("有进程被创建,PID = %d\n", cbkinfo.hProcessId);
}
else
{
printf("有进程被终止,PID = %d\n", cbkinfo.hProcessId);
}
cbktemp = cbkinfo;
}
}
else
{
printf("\n获取进程信息失败!\n");
break;
}
}
::CloseHandle(hDriver);
return 0;
}
上述代码的思路就是:通过符号链接打开驱动设备,然后打开在内核创建的事件对象(通过名字),然后循环调用WaitForSingleObject来获取事件,一有事件发生就通过调用函数DeviceIoControl发送一个IOCTL_PROC_MON来获取信息。在这里我们使用了两个变量,主要是为了过滤一些实际上没有真正发生改变的信息。
最后我们可以看一下在驱动程序和应用层程序之间共享的ioctls.h头文件:
#ifndef IOCTLS_H
#define IOCTLS_H
// 定义IOCTL
#define FILE_DEVICE_PROCMON 0x8000
#define PROCMON_IOCTL_BASE 0x800
#define CTL_CODE_PROCMON(i) \
CTL_CODE( FILE_DEVICE_PROCMON, \
PROCMON_IOCTL_BASE + i, \
METHOD_BUFFERED, \ // 缓冲区I/O
FILE_ANY_ACCESS)
#define IOCTL_PROC_MON CTL_CODE_PROCMON(0)
// 定义事件对象名称
#define EVENT_NAME L"\\BaseNamedObjects\\ProcEvent"
typedef struct _CallbackInfo
{
HANDLE hParentId;
HANDLE hProcessId;
BOOLEAN bCreate;
}CALLBACK_INFO, *PCALLBACK_INFO;
#endif
事情到这里好像就结束了,但是,先别着急,我们还有很重要的一件事情没做,我们忘了在驱动的卸载例程中删除我们注册的回调例程。
如果不这样做的话,当我们动态卸载驱动后,我们的回调例程已经不存在了,但操作系统不知道,仍然尝试在有进程创建/结束事件发生时通知它,这时候访问该地址就有很大的可能造成蓝屏,我当时就蓝了好几次。
避免方法很简单,在驱动的卸载例程中添加一句如下所示的代码:
// 卸载回调例程
PsSetCreateProcessNotifyRoutine(ProcessCallback, TRUE);
现在我们就可以查看测试效果了,因为我们没有在应用层程序中动态加载驱动,所以这里需要首先使用一些辅助工具进行加载,我用的是KmdManager。
当把驱动加载成功后我们就可以运行应用层程序了,我的测试效果如下所示: