《Windows内核安全与驱动编程》-第十章-磁盘的过滤学习-day1

磁盘的过滤

10.1 磁盘过滤驱动的概念

10.1.1 设备过滤和类过滤

​ 在前面学习过键盘过滤,建立一个过滤设备并将其绑定在一个有名字的设备上,这叫做设备过滤。但是在Windows中有一些情况,例如设备刚插入的时候,如何在这些设备刚加入时就自动的进行绑定呢?这就要用到类过滤了。这种类过滤驱动的驱动程序,能够在某一类特定的设备建立时由PnP管理器调用指定的过滤驱动代码,并且允许用户在此时对这一类设备进行绑定。根据用户设备在整个设备栈上相对系统本来存在设备的不同位置,可以分为上层过滤和下层过滤。其中以上层过滤最为常见,因为这时过滤设备在设备栈上位于实际功能设备的上面,会首先获得 Windows系统发下来的IRP请求,便于过滤设备的实现者进行处理。

10.1.2 磁盘设备和磁盘卷设备过滤驱动

​ 在Windows的存储系统中,最底层的是磁盘,而在磁盘上层的是卷,卷虽然只是逻辑上的一个概念,但是Window仍然为其建立了设备。所以会有磁盘设备和磁盘卷设备两种类型。

​ 磁盘卷并不仅仅是只对应一个磁盘,有时会出现跨磁盘的现象。所以我们必须将这两种设备区分开来。

​ 从驱动的角度上来说,这两种设备收到的读写请求都是针对磁盘大小或者在卷大小范围之内的请求,都是以扇区大小对齐的,处理起来没有太大的区别。所以本章主要讲解磁盘卷设备的上层过滤驱动,因为对于用户来说,卷是最直接看到的现象。而对于开发人员来说,使用卷过滤会在一定程度上减少工作量。因为不需要处理磁盘设备中才会遇到的一些问题,但是同时也限制了一些功能的实现,原因同样是不能处理磁盘设备上的问题,正所谓有利有弊!

10.1.3 注册表和磁盘卷设备过滤驱动

​ 在实际的系统运行过程中,一个普通的驱动程序是如何告知Windows操作系统它是一个类过滤驱动,并且如何和相应的设备联系起来呢?这就需要注册表的帮忙了。

​ 我们应该熟悉一个驱动程序作为服务是如何在注册表中存在的,在 HKEY\LOCAL\MACHINE\SYSTEM\CurrentSet\Services 下服务键的名字也就是这个服务的名字了。同时在HKEY\LOCAL\MACHINE\SYSTEM\Control\Class 下也会有许多类的名字,这些类的名字都是一长串数字,根据上一章的内容,这一长串数字实际上是一个ClassGUID,随意选择一个键,下面都会有一个叫做Class的值,这就是一个累。在这些键中,可以找到一个Class值为“Volume”的键,这就是磁盘卷类。在本章中最关键的是其中一个叫做 UpperFilters 的值,这个值起了最关键的作用——说明这个类的上层过滤驱动都有哪些。到此为止,我们应该明白,只需要在这个 UpperFilter 值中填入相应的驱动名,这个驱动就会作为这一类设备的上层过滤驱动被Windows操作系统是被,实际上这样也就完成了上层过滤驱动的安装工作了。

10.2 具有还原功能的磁盘卷过滤驱动

10.2.1 简介

​ 这个过滤驱动为讲解而写,需要使用 Windows XP系统,还有许多其他的限制。但是作为学习,还是了解到大致的框架即可。后续需要的时候,再去查相关的文档实现。这里实现的功能为,将磁盘还原到某一时刻的功能,在该时间点之后的所有操作将被抹去。

10.2.2 基本思想

​ 为了实现还原,一种简单的思路如下:

  • 在开启还原之后,所有对还原卷的操作将被写到另一个地方,而不会真正的写在还原卷上。这里所说的另一个地方也被称为转存处。
  • 在开启还原之后,所有对还原卷的读操作将分为两种情况处理。一种是读了开启还原之前就存在的内容,这种情况就按照正常的读取方式从还原卷上读取;另一种情况是读了开启还原卷之后还写到还原卷上的内容,这种情况将会从转存处把之前写过的内容读取出来。
  • 上述读写必须建立在互斥的基础上。
  • 重启之后,转存的数据清零。
  • 上述转存同样必须在卷设定还原之后立即起作用,而不能出现写了一半才开始转存的情况;否则数据会在重启之后不同步。

10.3 驱动分析

10.3.1 DriverEntry 函数

DriverEntry 函数作为过滤函数的入口函数,主要负责初始化本驱动的各个分发函数。此外,它还指定了这个驱动的 AddDeviceUnload 函数。由于这个设备驱动被注册成了磁盘卷设备的上层过滤驱动,PnP管理器将会载一个新的磁盘卷建立之后,首先调用本过滤驱动的 AddDevice 函数,然后再调用磁盘卷设备驱动中的 AddDevice 函数。而 Unload 函数会在过滤驱动结束时被调用。

​ 在 DriverEntry 函数的最后,还注册了一个 boot 类型驱动的完成回调函数。本驱动是作为一个 boot 类型驱动存在的,这一点在可以注册表的 HKEY\LOCAL\MACHINE\SYSTEM\CurrentSet\Services 下驱动服务的 start 值中指定,0为 boot 类型。boot 类型的驱动是启动最早的驱动程序,在系统引导时加载完毕;而对于注册为boot类型驱动的完成回调函数的函数,将会在所有的boot类型驱动执行完毕之后被调用一次,需要注意的是,这时候仍然是系统启动过程比较早的时候。在这里需要注册这个回调函数,因为驱动中有些工作需要等待到这个时间才能做,具体是什么工作稍后会讲到。

{
    //用来做循环控制变量
    int i;
    //调式时候产生断点
    //KdBreakPonit();
    for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;i++)
    {
        DriverObject->MajorFunction[i] = DPDispatchAny;
    }
    //下面将我们特殊关注的分发函数重新赋值为我们自己的处理函数
    DriverObject->MajorFunction[IRP_MJ_POWER] = DPDispatchPower;
    DriverObject->MajorFunction[IRP_MJ_PNP] = DPDispatchPnp;
    DriverObject->MajorFunction[IPR_MJ_DEVICE_CONTROL] = DPDispatchDeviceControl;
    DriverObject->MajorFunction[IRP_MJ_READ] = DPDispatchReadWrite;
    DriverObject->MajorFunction[IRP_MJ_WRITE] = DPDispatchReadWrite;
    
    //将这个驱动的AddDevice函数初始化为DPAddDevice函数
    DriverObject->DriverExtension->AddDevice = DPAddDevice;
    //同理初始化 Unload 函数
    DriverObject->DriverExtension->Unload = DOUnload;
    //注册一个boot驱动结束回调,这个回调函数会在所有的boot类型驱动运行完毕后执行
    IoRegisterBootDriverReinitialization(
    	DriverObject,
    	DPReinitializationRoutine.
    	NULL
    );
    //作为一个过滤驱动,无论如何都要返回成功
    return STATUS_SUCCESS;
}

10.3.2 AddDevice 函数

​ 该函数是在任何磁盘卷设备建立的时候被调用。但是需要注意的是,该 DPAddDevice 函数被调用的时候,实际上磁盘卷已经建立起来了,只是还不能使用,也就是说这个设备的设备对象已经有了,但是不能相应大部分的IRP请求。

​ 在DPAddDevice中建立一个过滤设备,这个设备将被绑定在真正的磁盘卷设备上。并且由于这是一个上层过滤设备。这个过滤设备将会位于磁盘卷设备的栈顶方向上,也就是先于磁盘卷收到IRP请求。在建立并绑定了这个过滤设备之后,需要对这个过滤设备做一些初始化,而过滤层设备的所有基本信息都会以 DP_FILTER_DEV_EXTENSION 结构的类型存储在设备扩展中。下面介绍该结构

// 卷的名字 例如 C: D: 等卷名字中字母部分
WCHAR			VolumeLetter;
//这个卷是否处于保护状态
BOOL			Protect;
//这个卷的总大小,以byte为单位
LARGE_INTEGER	TotalSizeInByte;
//这个卷上的文件系统的每簇大小,以Byte为单位
DWORD			ClusterSizeInByte;
//这个卷上每个扇区的大小,以Byte为单位
DWORD			SectorSizeInByte;
//这个卷对应的过滤设备的设备对象
PDEVICE_OBJECT	FltDevObj;
//这个卷对应的过滤设备的下层设备对象
PDEVICE_OBJECT	LowerDevObj;
//这个卷设备对应的物理设备的设备对象
PDEVICE_OBJECT	PhyDevObj;
//这个数据结构是否已经初始完毕
BOOL			InitializeCompleted;
//这个卷上的保护系统室友的位图句柄
PDP_BITMAP		BitMap;
//用来转储的文件句柄
HANDLE			TempFile;
//这个卷上的保护系统使用的请求队列
LIST_ENTRY		ReqList;
//这个卷上的保护系统使用的请求队列的锁
KSPIN_LOCK		R	EQlOCK;
//这个卷上的保护系统使用的请求队列的同步事件
KEVENT			ReqEvent;
//这个卷上的保护系统使用的请求队列的处理线程的线程句柄
PVOID			ThreadHandle;
//这个卷上的保护系统使用的请求队列的处理线程的结束标志
BOOLEAN			ThreadTermFlag;
//这个卷上的保护系统的关机分页电源的计数事件
KEVENT			PagingPathCountEvent;
//这个卷上的保护系统的关机分页电源请求的计数
LONG 			PagingPathCount;

​ 在上述的数据结构中可以看到三个设备对象: 过滤设备、物理设备和下层设备。其中过滤设备是本过滤驱动自己建立的;物理设备是通过AddDevice 函数的参数传递进来的设备,是真正的磁盘卷设备;而下层设备是将在过滤设备绑定到物理设备上之后,返回的绑定之前的物理设备上的最上层的设备。

​ 在该数据结构中还可以看到,针对每个过滤设备都会建立一个处理线程和相应的请求队列。这是因为在这个驱动中采用了将所有的请求依次排队,然后使用一个单独的线程依次处理的方式。这样做的好处是将所有的读写请求串行化,程序易于编写而且不会出现同步问题。

​ 在该函数中还会发现初始化了 PagingPathCountEventPagingPathCount 这两个与分页路径相关的变量。它们将会在 PnP IRP 请求的处理中被用到。现在只需要知道初始化的值是多少即可。

NTSTATUS
DPAddDevice(
    IN	PDRIVER_OBJECT	DriverObject,
    IN	PDEVICE_OBJECT	PhysicalDeviceObject
)
{
	//NTSTATUS类型的函数返回值
	NTSTATUS					ntStatus = STATUS_SUCCESS;
    //用来指向过滤设备的设备扩展的指针
	PDP_FILTER_DEV_EXTENSION	DevExt = NULL;
	//过滤设备的下层设备的指针对象
	PDEVICE_OBJECT				LowerDevObj = NULL;
	//过滤设备的设备指针的指针对象
	PDEVICE_OBJECT				FltDevObj = NULL;
	//过滤设备的处理线程的线程句柄
	HANDLE						ThreadHandle = NULL;

	//建立一个过滤设备,这个设备是FILE_DEVICE_DISK类型的设备并且具有DP_FILTER_DEV_EXTENSION类型的设备扩展
	ntStatus = IoCreateDevice(
		DriverObject,
		sizeof(DP_FILTER_DEV_EXTENSION),
		NULL,
		FILE_DEVICE_DISK,
		FILE_DEVICE_SECURE_OPEN,
		FALSE,
		&FltDevObj);
	if (!NT_SUCCESS(ntStatus)) 
		goto ERROUT;
	//将DevExt指向过滤设备的设备扩展指针
	DevExt = FltDevObj->DeviceExtension;
	//清空过滤设备的设备扩展
	RtlZeroMemory(DevExt,sizeof(DP_FILTER_DEV_EXTENSION));

	//将刚刚建立的过滤设备附加到这个卷设备的物理设备上
	LowerDevObj = IoAttachDeviceToDeviceStack(
		FltDevObj, 
		PhysicalDeviceObject);
	if (NULL == LowerDevObj)
	{
		ntStatus = STATUS_NO_SUCH_DEVICE;
		goto ERROUT;
	}

	//初始化这个卷设备的分页路径计数的计数事件
	KeInitializeEvent(
		&DevExt->PagingPathCountEvent,
		NotificationEvent, 
		TRUE);

	//对过滤设备的设备属性进行初始化,过滤设备的设备属性应该和它的下层设备相同
	FltDevObj->Flags = LowerDevObj->Flags;
	//给过滤设备的设备属性加上电源可分页的属性
	FltDevObj->Flags |= DO_POWER_PAGABLE;
	//对过滤设备进行设备初始化
	FltDevObj->Flags &= ~DO_DEVICE_INITIALIZING;

	//将过滤设备对应的设备扩展中的相应变量进行初始化
	//卷设备的过滤设备对象
	DevExt->FltDevObj = FltDevObj;
	//卷设备的物理设备对象
	DevExt->PhyDevObj = PhysicalDeviceObject;
	//卷设备的下层设备对象
	DevExt->LowerDevObj = LowerDevObj;

	//初始化这个卷的请求处理队列
	InitializeListHead(&DevExt->ReqList);
	//初始化请求处理队列的锁
	KeInitializeSpinLock(&DevExt->ReqLock);
	//初始化请求处理队列的同步事件
	KeInitializeEvent(
		&DevExt->ReqEvent,
		SynchronizationEvent,
		FALSE
		);

	//初始化终止处理线程标志
	DevExt->ThreadTermFlag = FALSE;
	//建立用来处理这个卷的请求的处理线程,线程函数的参数则是设备扩展
	ntStatus = PsCreateSystemThread(
		&ThreadHandle,
		(ACCESS_MASK)0L,
		NULL,
		NULL,
		NULL,
		DPReadWriteThread,
		DevExt
		);
	if (!NT_SUCCESS(ntStatus))
		goto ERROUT;

	//获取处理线程的对象
	ntStatus = ObReferenceObjectByHandle(
		ThreadHandle,
		THREAD_ALL_ACCESS,
		NULL,
		KernelMode,
		&DevExt->ThreadHandle,	//获取到的线程对象
		NULL
		);
	if (!NT_SUCCESS(ntStatus))
	{
		DevExt->ThreadTermFlag = TRUE;
		KeSetEvent(
			&DevExt->ReqEvent,
			(KPRIORITY)0,
			FALSE
			);
		goto ERROUT;
	}

ERROUT:
	if (!NT_SUCCESS(ntStatus))
	{	
		//如果上面有不成功的地方,首先需要解除可能存在的附加
		if (NULL != LowerDevObj)
		{
			IoDetachDevice(LowerDevObj);
			DevExt->LowerDevObj = NULL;
		}
		//然后删除可能建立的过滤设备
		if (NULL != FltDevObj)
		{
			IoDeleteDevice(FltDevObj);
			DevExt->FltDevObj = NULL;
		}
	}
	//关闭线程句柄,我们今后不会用到它,所有对线程的引用都通过线程对象来进行了
	if (NULL != ThreadHandle)
		ZwClose(ThreadHandle);
	//返回状态值
    return ntStatus;
}

​ 与前面的过滤绑定差不多的操作,创建设备—>绑定设备->填充设备扩展->…但是复杂的是多了许多暂时还不确定用处的变量。但是带着对该函数的作用去理解还是可以的,毕竟这个 AddDevice 函数的作用,主要是绑定过滤设备。其他的可以暂时先放着不看。

明日计划

论文

继续学习驱动编程下一小节 PnP请求的处理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章