11.6 读写操作的过滤
11.6.1 设置一个读处理函数
? 文件系统有多种操作,但是读写操作仍然是最重要的。比如杀毒软件对文件内容的读写进行监控,可以达到扫描病毒的目的。简单一点就是,文件在被读取时,杀毒软件扫描其中是否含有病毒特征码,就可以防止一些病毒从硬盘被加载到内存中执行;如果任意文件在被写入到磁盘时,也可以检测其中写入的内容是否含有病毒特征码,这样就可以防止病毒被写入硬盘,也就可以防止硬盘上的文件被感染。
? 在之前的代码中已经绑定了文件系统卷,而所有的文件都是保存在卷上的,所以处理截获到的主功能号为 IRP_MJ_READ/WRITE 的IRP,就可以实现扫描病毒特征码的功能。其中的关键就是可以从IRP中解析到要读取和写入到文件见的内容。
? 回忆在之前入口函数中分发函数的设置,其中处理读写的分别是 SfRead 和 SfWrite。这两个函数的处理过程非常详细,但是要注意的是——如果要获得操作的文件内容,读请求必须是完成后才能看到内容,而写请求可以直接得到。因为写请求是上层数据发送下来的,在请求完成之前就存在于缓冲区内;而读请求必须从硬盘上获得,因此读请求的过滤更加复杂一些。下面的例子都是讲述读请求的处理的,而写请求的处理不在追叙。
DriverObject->MajorFunction[IRP_MJ_READ] = SfRead;
11.6.2 设备对象的区分处理
? 对于SfRead中的处理,首先需要判断设备对象是不是一个绑定在文件系统卷设备上的过滤设备,如果是,那么就是一个读写文件的操作。但是同时,绑定在文件系统的控制设备上的过滤设备也有可能发生读请求,但是那就不是在读取文件了。
? 那么如何判断呢?记得绑定Volume的代码已经在设备扩展中设置了域StorageDev,如果不是,那么判断StorageDev是否问空,那么就知道这是否是一个文件系统的卷设备。由此可见,过滤设备上的设备扩展是非常有用的。实际上就是用来在绑定时保存任意信息,将来在过滤时能够得到这些信息的上下文。
PSFILTER_DEVICE_EXTENSION devExt = DeviceObject->DeviceExtension;
if(devExt->StorageDev != NULL)
{...
}
? 其他的情况不需要捕获,直接传递到下层。读请求的IRP情况非常复杂,最好的学习方法就是打印IRP的各个细节,自己查看文件操作的完成过程。
? 下面实现SfRead,这里判断处理稍有不同。如果是本程序自己的控制设备的读操作,则返回失败,因为本程序的控制设备并没有提供读接口。然后判断是不是文件读操作,如果不是直接放过。
NTSTATUS SfRead(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp
)
{PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(Irp);PFILE_OBJCET file_object = irpsp->FileObject;PSFILTER_DEVICE_EXTENSION devExt = DeviceObject->DeviceExtension;//对控制设备的读操作直接返回失败,不是读操作直接跳过if(IS_MY_CONTROL_DEVICE_OBJECT(DeviceObject)){Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;Irp->Iostatus.Information = 0;IoCompleteRequest(Irp,IO_NO_INCREMENT);return STATUS_INVALID_DEVICE_REQUEST;}//对其他设备的操作直接下发if(devExt->StorageDev != NULL){return SfPassThrough(DeviceObject,Irp);}//...}
11.6.3 解析读请求中的文件信息
? 接下来的工作是分析这个IRP。当前从这个IRP中可以获得一些文件信息,读取的偏移量和读取的长度。如
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
LARGE_INTEGER offset;
ULONG length;offset.QuadPart = irpsp->Parameters.Read.ByteOffset.QuadPart;
length = irpsp->Parameters.Read.Length;
? 如果是杀毒软件要扫描病毒,此时当然希望能得到所读到的数据内容。数据只能在完成之后获取,所以下面要设置完成函数。IRP的缓冲区放在那里取决于这个操作的IO方式。IO方式分为三种:缓冲方式、直接方式、和皆不是方式。对三种方式介绍如下:
- 缓冲方式。这种方式在读写请求中没有出现过。
- 直接方式。这届方式使用MDL来传递缓冲区的。因为请求一般都是用户进行发起的,原始的空间都在用户空间中。这些空间指针如果传递到内核代码里,虽然内核可以正常使用,但是却必须保证在当前进程范围空间内。为了避免麻烦,出现了直接方式,直接方式就是直接把用户空间的范围直接映射到内核空间。为此种种操作,MDL都已经做好了。
- 皆不是方式。该方式是最简单的,直接把用户空间的指针传递进来不做任何处理。这个指针就是Irp->UserBuffer。这个空间在SfRead中直接用当然是可以的,但是不可以被放到其他线程中去处理。一般可以设置一个事件,等待完成函数被调用后,在SfRead中处理。
? 在实际处理中,优先使用MDL,其次使用UserBuffer。那么获取读取的内容的主要方法示例如下:
KEVENT watiEvent;
void *buf;
ULONG length;
KeInitializeEvent(&waitEvent,NotiafactionEvent,FALSE);
IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(Irp,SfReadCompletion,&waitEvent,TRUE,TRUE,TRUE);
status = IoCallDriver(devExt->AttachedToDeviceObject,Irp);
if(STATUS_PEDING == status)
{status = KeWaitForSingleObject(&waitEvent,Executive,KernelMode,FALSE,NULL);
}
if(Irp->IoStatus.Status = STATUS_SUCCESS)
{//完成了,读取到的内容在缓冲区BUFF中//长度在Irp-IoStatus.Information中if(Irp->MdlAddress != NULL)buf = MmGetSystemAddressForMdl(Irp->MdlAddress);elsebuf = Irp->UserBuffer;length = Irp->IoStatus.Information
}
11.6.4 读请求的完成
? 所谓的完成读请求,是一个驱动程序报告它的上层驱动程序,一个IRP已经完成的过程。首先,任何驱动都能可以完成它收到的请求,对一个IRP而言,只要填好它需要的信息,然后就可以向上层报告这个IRP完成了。至于这些数据从哪里来,上层并不关心。即使实际上这个驱动请求根本就没有到达过下层,而是自己虚构完成的,也不会有什么问题。
? 一般的说,文件系统过滤驱动对IRP进行监控,允许或者禁止,或者对IRP进行修改,比如在写之前,先把缓冲区中要写的数据加密。但是还有一些情况需要自己完成这个请求。比如针对视频文件的读取,用户希望可以任意拖动进度条来观看整个视频。当驱动针对特殊的视频文件时,直接一次将文件读取到内存中,而在下次收到读取请求时候,就不必再访问下层驱动而直接从内存中取数据。这时候就需要驱动自己完成请求了。
? 这里又要谈到IRP的次功能号。其中 IRP_MJ_READ的次功能号有 IRP_MN_NORMAL、IRP_MN_MDL、IRP_MN_COMPLETE。还有其他集中情况,资料上有解释,不在赘述。
- IRP_MN_NORMAL的情况,既有可能在MDL中读取数据也有可能在UserBuff里读取数据。人如果完成这样的请求,就需要首先判断一下MDL里是否有数据。
- IRP_MN_MDL。这种情况比较罕见,数据即不在MDL也不在UserBuffer。该请求是请编程者分配一个MDL,然后把MDL指向数据所在的空间最后返回给上层。这种一般结合 IRP_MN_COMPLETE作为完成信号,比较少见。
书籍这里介绍了一些自己建立MDL再返回的,比较稀有。等遇到的时候再专门吧。
明日计划
开始学习Windows内核漏洞吧,从《0day 安全》 入门