网上找到一篇 AntBean写的 《驱动入门科普:从WRK理解IRP IRP Stack之实践篇》特别好,所以摘抄下来。 我把原文截取的图片源码替换成文字源码。
昨天通过WRK的代码对IRP和IRP Stack的概念作出了一个比较深的理解。但显然这样还不够。对程序员而言,没有比光说不练更坏的恶习了。这次,我们将通过一个文件过滤驱动来实现文件保护。
恩,是的,我们想写一个文件过滤驱动。而且我们也知道所谓文件过滤驱动也就是将自己的设备加到要过滤的设备栈的最顶端。说白了不就是IoAttachDeviceToDeviceStack嘛。
我们准备动手了,可是问题来了,我到底该Attach到哪个DeviceStack上呢?
楚狂人在文件过滤驱动二一书中提到,就是要Attach到相应的Volume上。我晕了。为啥子就是要Attach到相应的Volume上了?谁说的?即使Attach到相应的Volume上了,获得到的IRP到底都有啥参数呢,我该怎么检查获得的IRP以决定是否拒绝该IRP呢?难道我就只能对着他的代码从头敲到尾么?
如果我知道CreateFile的整个操作流程,显然CreateFile最终都是分配了一个IRP发送给文件系统的,那么我不就知道了该Attach的Device Stack么?
一、 CreateFile的整个流程与文件过滤驱动的Attach对象问题
显然,我们知道CreateFile在内核中调用的是NtCreateFile。
注 :NtCreateFile -> IoCreateFile -> IopCreateFilem -> ObOpenObjectByName
直接阅读NtCreateFile,可以看到NtCreateFile的请求最终是通过ObOpenObjectByName实现的。
而最难找的应该是ObOpenObjectByName中到底是哪个函数调用实现的创建IRP并发送IRP。
在ObOpenObjectByName中,对象管理器会获得该Name对应的Object,之后它会调用该Object的Parse Routine,在Parse Routine中完成IRP的构造和发送。这一点你可以在<深入解析windows操作系统>一书的系统机制一章中的对象管理器获得比较深的了解。
也就是说,当系统获得C:\\a.txt这个名字的时候,它调用ObOpenObjectByName打开该名称对应的对象。而ObOpenObjectByName则是调用ObpLookupObjectName查找该名字对应的对象,在ObpLookupObjectName中,调用该对象的Parse Routine。
如下:
Status = (*ObjectHeader->Type->TypeInfo.ParseProcedure)(
RootDirectory,
ObjectType,
AccessState,
AccessCheckMode,
Attributes,
ObjectName,
&RemainingName,
ParseContext,
SecurityQos,
&Object );
正如<深入解析windows操作系统>一书中提到的,这里对对象的管理类似于C++。这些对象都有一个差不多的基类,该基类定义了统一的接口方法,有Open,Close,Delete,Query name和Parse等。
突然,我们跟踪的线索断掉了,我们得找到文件对象的Parse Routine到底是啥。这样我们才能知道具体的IRP创建和目的设备是啥。
在WRK中的一番搜索,得到了IopParseFile即文件对象的Parse Routine。
在IopParseFile中,通过IoGetRelatedDeviceObject获得该Parse Object对应的DeviceObject,然后调用IopParseDevice完成整个操作。
当你读到IopParseDevice时,你震惊了。。。你想要的直接分配IRP的代码不就在这个地方么?
//
// Allocate and fill in the I/O Request Packet (IRP) to use in interfacing
// to the driver. The allocation is done using an exception handler in
// case the caller does not have enough quota to allocate the packet.
//
irp = IopAllocateIrp( deviceObject->StackSize, FALSE );
if (!irp) {
//
// An IRP could not be allocated. Cleanup and return an appropriate
// error status code.
//
IopDecrementDeviceObjectRef( parseDeviceObject, FALSE, FALSE );
if (vpb) {
IopDereferenceVpbAndFree(vpb);
}
return STATUS_INSUFFICIENT_RESOURCES;
}
irp->Tail.Overlay.Thread = CurrentThread;
irp->RequestorMode = AccessMode;
irp->Flags = IRP_CREATE_OPERATION | IRP_SYNCHRONOUS_API | IRP_DEFER_IO_COMPLETION;
securityContext.SecurityQos = SecurityQos;
securityContext.AccessState = AccessState;
securityContext.DesiredAccess = desiredAccess;
securityContext.FullCreateOptions = op->CreateOptions;
//
// Get a pointer to the stack location for the first driver. This is where
// the original function codes and parameters are passed.
//
irpSp = IoGetNextIrpStackLocation( irp );
irpSp->Control = 0;
if (op->CreateFileType == CreateFileTypeNone) {
//
// This is a normal file open or create function.
//
irpSp->MajorFunction = IRP_MJ_CREATE;
irpSp->Parameters.Create.EaLength = op->EaLength;
irpSp->Flags = (UCHAR) op->Options;
if (!(Attributes & OBJ_CASE_INSENSITIVE)) {
irpSp->Flags |= SL_CASE_SENSITIVE;
}
} else if (op->CreateFileType == CreateFileTypeNamedPipe) {
//
// A named pipe is being created.
//
irpSp->MajorFunction = IRP_MJ_CREATE_NAMED_PIPE;
irpSp->Parameters.CreatePipe.Parameters = op->ExtraCreateParameters;
} else {
//
// A mailslot is being created.
//
irpSp->MajorFunction = IRP_MJ_CREATE_MAILSLOT;
irpSp->Parameters.CreateMailslot.Parameters = op->ExtraCreateParameters;
}
//
// Also fill in the NtCreateFile service's caller's parameters.
//
irp->Overlay.AllocationSize = op->AllocationSize;
irp->AssociatedIrp.SystemBuffer = op->EaBuffer;
irpSp->Parameters.Create.Options = (op->Disposition << 24) | (op->CreateOptions & 0x00ffffff);
irpSp->Parameters.Create.FileAttributes = op->FileAttributes;
irpSp->Parameters.Create.ShareAccess = op->ShareAccess;
irpSp->Parameters.Create.SecurityContext = &securityContext;
//
// Fill in local parameters so this routine can determine when the I/O is
// finished, and the normal I/O completion code will not get any errors.
//
irp->UserIosb = &ioStatus;
irp->MdlAddress = (PMDL) NULL;
irp->PendingReturned = FALSE;
irp->Cancel = FALSE;
irp->UserEvent = (PKEVENT) NULL;
irp->CancelRoutine = (PDRIVER_CANCEL) NULL;
irp->Tail.Overlay.AuxiliaryBuffer = (PVOID) NULL;
... ...
KeInitializeEvent( &fileObject->Event, NotificationEvent, FALSE );
op->FileObject = fileObject;
IopQueueThreadIrp( irp );
status = IoCallDriver( deviceObject, irp );
if (status == STATUS_PENDING) {
(VOID) KeWaitForSingleObject( &fileObject->Event,
Executive,
KernelMode,
FALSE,
(PLARGE_INTEGER) NULL );
status = ioStatus.Status;
}
看完了这些,你想要的都在这儿了,只是那个目的Device还是不大清楚。静态分析只能做到这个地步了,下面要干啥,常在看雪论坛混的你懂的。
上windbg,在该函数中下断点,看看断下来是目标Device到底是啥,是\\Driver\\FtDisk的对象\\Device\\HarddiskVolumeX呢?还是文件系统\\FileSystem\\xx的unname对象呢?
下面是我在系统登陆前在windbg中下的一个断点的情况。
到底那个Driver是啥,你调试下就知道了。
实际上是Attach到相应的文件系统创建的对象的设备栈上。
解决了Attach的目的设备栈的问题和处理参数问题,下面到了完成整个文件过滤驱动了。
一、 文件过滤驱动\\Device\\HarddiskVolumeX与 \\FileSystem\\xx的unname对象
我们已经知道了,我们要Attach到\\FileSystem\\xx的unname对象上,那么我们的IRP到了该\\FileSystem\\xx对象后,它最终还是要转化成对卷的IO。可是它怎么知道应该将自己处理过的IRP发送给哪个卷呢?换句话说,\\Device\\HarddiskVolumeX和\\FileSystem\\xx的unname对象是怎样联系起来的?
老习惯,看代码。首先我们知道硬盘上的一个分区被文件系统识别的过程叫Mount。实际上是给文件系统控制对象发送了一个IRP_MJ_FILE_SYSTEM_CONTROL,这个过程建立了一个HarddiskVolumeX跟FileSystem的unname对象的联系。
这部分代码wrk中我没有看见,就查看了ReactOS的FileSystem部分。这里以Ntfs的代码为例。
这个就是Mount的过程了。
这部分代码很有趣。
简单说明下:
接收到要Mount的操作时,该NTFS的驱动中立即创建了一个Device Object,同时给该DeviceObject的DeviceExtension进行了赋值,其中含有该DeviceObject实际的Volume对象。同时呢,更新了该Volume对象的Vpb (Volume Parameter Block),Vpb中既存储了该Device Object又存储了实际的Storage Device。
最有趣的应该是这句
NewDeviceObject-->StackSize = DeviceExt-->StorageDevice--->StackSize + 1;
也就是说,产生的文件设备对象NewDeviceObject,其实它的StackSize来源于下面的StorageDevice的StackSize。换句话说,在设备栈中,它们是通过Volume设备对象的Vpb连接起来的,而不是通过Attached关系连接的。
也就是说,所谓设备栈并不是必须通过Attach关系连接的。
一、 一个文件过滤驱动实现文件保护的代码
参见下一个附件。具体我就不解释了。注意了,在其中我没有Attach文件系统的控制设备,这样会导致我们的过滤驱动无法Attach动态产生的unname的对象。你稍作修改应该就知道了。
整个过滤驱动的主要过程事实上就两步:
1. 找到要Attach的所有Device后Attach上。 这里是通过该Volume对象名找到它对应的文件系统对象,然后获得该文件系统对象的Driver,枚举该Driver的所有对象,并一一Attach上。注意,每个unname的对象都有一个设备栈。从volume对象获得文件系统对象的原理我已经在上面Mount过程的代码中给出来了。不详述了。
2. 编写要过滤的IRP的函数。怎么获得某个操作对象的IRP和该IRP的IRP Stack中的具体参数,已经在前面的跟踪过程中给出来了。当然你不必每个都跟踪。
如果你对IoSkipCurrentIrpStackLocation和IoCopyCurrentIrpStackLocationToNext()两者的使用感到困惑,最好的方式,你懂得。
为啥说IoSkipCurrentIrpStackLocation不改变当前的CurStackLocation的位置呢?你看了它的实现代码联系下IoCallDriver就知道为啥了。
至此,整个过程结束,这里仅仅是想通过源码+调试+编码的方式说明自己的探索过程,我是看雪AntBean,欢迎讨论。