前面两篇学习笔记分别介绍了IRP和IO_STACK_LOCATION,整个设备栈来处理这个IRP,但是每个设备都应该有自己的参数信息,这个参数信息就是通过IO_STACK_LOCATION 来保管的,那么IRP是怎么保管IO_STACK_LOCATION的呢?本文我们来分析一下IRP和IO_STACK_LOCATION交互作用的整个流程。
这个函数用来分配一个IRP,我们看一下IRP的分配过程:
PIRP IopAllocateIrpPrivate(
IN CCHAR StackSize,
IN BOOLEAN ChargeQuota
)
{
//...
packetSize = IoSizeOfIrp(StackSize);
allocateSize = packetSize;
//...
irp = ExAllocatePoolWithTag(NonPagedPool, allocateSize, ' prI');
//...
IopInitializeIrp(irp, packetSize, StackSize);
//...
}
这个函数中,我们通过packetSize = IoSizeOfIrp(StackSize);来计算整个IRP的大小,这个声明如下:
#define IoSizeOfIrp( StackSize ) \
((USHORT) (sizeof( IRP ) + ((StackSize) * (sizeof( IO_STACK_LOCATION )))))
也就是说是IRP的大小 + 设备栈个数个IO_STACK_LOCATION.
IopInitializeIrp : 用来初始化IRP,这个是个宏定义如下:
#define IopInitializeIrp( Irp, PacketSize, StackSize ) { \
RtlZeroMemory( (Irp), (PacketSize) ); \
(Irp)->Type = (CSHORT) IO_TYPE_IRP; \
(Irp)->Size = (USHORT) ((PacketSize)); \
(Irp)->StackCount = (CCHAR) ((StackSize)); \
(Irp)->CurrentLocation = (CCHAR) ((StackSize) + 1); \
(Irp)->ApcEnvironment = KeGetCurrentApcEnvironment(); \
InitializeListHead (&(Irp)->ThreadListEntry); \
(Irp)->Tail.Overlay.CurrentStackLocation = \
((PIO_STACK_LOCATION) ((UCHAR *) (Irp) + \
sizeof( IRP ) + \
( (StackSize) * sizeof( IO_STACK_LOCATION )))); }
这个IRP和IO_STACK_LOCATION交互得有三个成员:
(Irp)->StackCount = (CCHAR) ((StackSize)) 设备栈的大小:
(Irp)->CurrentLocation = (CCHAR) ((StackSize) + 1) : 当前设备栈,放到了最顶端的下一个。
(Irp)->Tail.Overlay.CurrentStackLocation : 指向最顶层的IO_STACK_LOCATION, 从这里发现IO_STACK_LOCATION是从后往前来使用的。
在使用IO_STACK_LOCATION的时候,我们先要获取这个这个程序,看下Windows是怎么获取的:
NTSTATUS
FASTCALL
IopfCallDriver(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp
)
{
PIO_STACK_LOCATION irpSp;
PDRIVER_OBJECT driverObject;
NTSTATUS status;
ASSERT( Irp->Type == IO_TYPE_IRP );
Irp->CurrentLocation--;
if (Irp->CurrentLocation <= 0) {
KeBugCheckEx( NO_MORE_IRP_STACK_LOCATIONS, (ULONG_PTR) Irp, 0, 0, 0 );
}
irpSp = IoGetNextIrpStackLocation( Irp );
Irp->Tail.Overlay.CurrentStackLocation = irpSp;
irpSp->DeviceObject = DeviceObject;
driverObject = DeviceObject->DriverObject;
PERFINFO_DRIVER_MAJORFUNCTION_CALL(Irp, irpSp, driverObject);
status = driverObject->MajorFunction[irpSp->MajorFunction]( DeviceObject,
Irp );
PERFINFO_DRIVER_MAJORFUNCTION_RETURN(Irp, irpSp, driverObject);
return status;
}
#define IoGetNextIrpStackLocation( Irp ) (\
(Irp)->Tail.Overlay.CurrentStackLocation - 1 )
在调用设备的分发函数之前,先使用IoGetNextIrpStackLocation是的IRP中的(Irp)->Tail.Overlay.CurrentStackLocation指向正确位置,因为初始化的时候是指向最后一个的末尾的,因此这里要前移一个。
在分发函数中,我们可以使用IoGetCurrentIrpStackLocation获取当前的IO_STACK_LOCATION,这个宏定义如下:
#define IoGetCurrentIrpStackLocation( Irp ) ( (Irp)->Tail.Overlay.CurrentStackLocation )
直接返回了IRP中的成员。
当设备栈中的设备完成处理之后,需要向下传递IRP,因此需要调用IoCallDriver,在上面我们看到过,IoCallDriver会调用IoGetNextIrpStackLocation下移设备栈的指针,因此我们需要对设备栈做如下之一的操作:
#define IoCopyCurrentIrpStackLocationToNext( Irp ) { \
PIO_STACK_LOCATION irpSp; \
PIO_STACK_LOCATION nextIrpSp; \
irpSp = IoGetCurrentIrpStackLocation( (Irp) ); \
nextIrpSp = IoGetNextIrpStackLocation( (Irp) ); \
RtlCopyMemory( nextIrpSp, irpSp, FIELD_OFFSET(IO_STACK_LOCATION, CompletionRoutine)); \
nextIrpSp->Control = 0; }
#define IoSkipCurrentIrpStackLocation( Irp ) \
(Irp)->CurrentLocation++; \
(Irp)->Tail.Overlay.CurrentStackLocation++;
IoCopyCurrentIrpStackLocationToNext 拷贝IO_STACK_LOCATION 成员到下一层。
IoSkipCurrentIrpStackLocation 上移一层,是的下次使用的时候仍旧使用当前的IO_STACK_LOCATION。
不管是I/O 管理器构建的IRP还是自己手动构建的IRP,对IRP的操作都有以下4种情况:
1、直接完成IRP,返回。
2、放入IRP队列中,调用StartIO完成IRP串行化。
3、传递IRP到下一层,由下一层(或者再下一层完成)并且不需要获得IRP完成的情况
4、传递IRP到下一层,同时需要得到获得下一层处理完IRP的信息(键盘过滤器等很多FilterDriver就这么干)
其实可以把第1点和第2点归为一起,都是本层处理,把第4点归结到第3点上,都是下一层处理,但是这两者之间的IO设备栈的处理有点差异。
接下来说明一个驱动程序的创建一般过程:
DriverEntry和AddDevice两个函数由即插即用管理器调用,DriverEntry里面完成分发函数的注册,AddDevice用来创建一个设备对象并且挂载到下一层设备对象上。一般的WDM驱动程序都有一个总线设备驱动程序,负责枚举和分配设备资源。为了说明问题,假设存在设备A和设备B,设备B挂载到设备A上,现在你要创建设备C也要挂载到A上,那么使用IoAttatchDeviceToDeviceStack函数挂载到A时,返回的是B的设备对象。也就是从上往下看IRP的传递是C->B->A的流向。到这里就可以讨论IO设备栈了!!!
回到一开始说的当收到一个IRP时,本层驱动需要做什么处理,4种情况处理IRP不同,如下:
到这里也许很多驱动的资料都会这么讲,但为什么需要这么做??每一层设备对象都需要一个设备栈,用来保存上一层创建或者传递IRP的信息,使用IoGetCurrentIrpStackLocation()可以获得当前设备栈,也即获得里面的IRP信息。当前设备首先获得IRP处理的权利,可以完成,可以放入队列,也可以传递到下一层。而是用IoCallDriver完成IRP的传递,可以使用反汇编查看IoCallDriver()或者看DDK文档就会知道,IoCallDriver里面首先会将CurrentLocation减1(汇编指令为dec),然后再去和0进行判断比较跳转,当当前值比0大,那么会去将当前设备栈指针移动到前一个设备栈(结合stack结构的概念,先入后出)。
那么如果当前设备不关心IRP的完成,那么也就不需要当前设备栈,那么为了让底层驱动处理这个IRP,就必须应该把当前设备栈信息告诉底层设备,可以采取两种方式,使用IoSkipCurrentIrpStackLocation()把指针拨回去(和IoCallDriver()方向相反),也就是下一层的设备栈其实为本层的设备栈(速度快,另外一种是拷贝的方式,见下面)。一般不是很关心的IRP都这么做,例如PnP的,电源管理的(记住,电源管理的传递IRP使用的是 PoCallDriver())。但是作为过滤器驱动,都不会放过Read和Write的,Rootkit不会,HIPS也不会(现在的攻防问题,也许不再是这么简单的Hook了,但是思想都是一样,看谁最先获得IRP的主控权)。那么IoCopyCurrentIrpStackLocationToNext()的好处就在于,把本层的设备栈拷贝到下一层,那么拷贝完之后呢??如何第一时间得到下一层完成了IRP,也就是数据已经有效了??
可以使用同步或者异步方式,同步方式是等待IoCallDriver()返回,异步方式采用的是完成例程和事件通知的方式。一般都是采用完成例程+事件通知方式(具体实现可以参考一些DDK例子),若是在IRP总设置了完成例程,那么下一层驱动程序完成IRP后会直接去调用完成例程(回调函数概念),Hook就达到,你就可以在这个例程中为所欲为了(例如我直接从IRP中获得读缓冲区地址和长度,然后加密数据,想搞破坏的人可能会原封不动的把数据上传,但是他会备份一份,前面一直提到了IRP的创建,创建IRP的方式有以下几种: