首页
社区
课程
招聘
[原创]IoSkipCurrentIrpStackLocation等IO栈处理API的一些探索
2020-7-17 18:36 7191

[原创]IoSkipCurrentIrpStackLocation等IO栈处理API的一些探索

2020-7-17 18:36
7191

(以下内容适用于windows10 x64 内核)

最近新学驱动,一上来肯定是经典的过滤驱动走起。却在IoSkipCurrentIrpStackLocation和IoCopyCurrentIrpStackLocationToNext上犯了难。
在一番找各种资料&看了wrk源码以后,把一些自己走叉的不太好找的误区记录下来。可能不一定准确,还请各位大佬赐教。

我的核心问题有两个:
1)IoCopyCurrentIrpStackLocationToNext覆盖了下一层IO栈,不是等于覆盖了下一层的运行上下文?
2)为什么要IoSkipCurrentIrpStackLocation,而不让栈帧的指针直接由IoCallDriver指向下一层?
实际上最后发现是一个问题

0. 有关IRP包的生成

一切的一切,要从I/O请求包被创建的那一刻开始讲起。
IRP是I/O请求包的核心(https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_irp),在这个struct被初始化之后,
会紧接着分配和最上层Device的StackSize相等个数的IO_STACK_LOCATION(https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_io_stack_location)[4]像这样


很多文章都说每一个StackLocation都代表了一个Device。这么说虽然没错,但其实歧义很大。因为这些StackLocation在调用的过程中并不是一一对应的。
这也是我感到困惑的核心原因——如果我调用了IoCopyCurrentIrpStackLocationToNext,岂不是把下一层的Stack覆盖了?那为什么程序还能正常运行呢?

1. 有关IoCallDriver的运行机制
在讨论IoSkipCurrentIrpStackLocation和IoCopyCurrentIrpStackLocationToNext这两个函数之前,我先简单介绍一下IoCallDriver这个函数干了什么,是怎么把参数往底层“传”的
通过参考wrk1.2,IoCallDriver的流程请见WRK代码4.1

总而言之,IoCallDriver做了三件事[1]

(1). 递减了IRPCurrentLocation
(2). 获得下个位置的StackLocation,并设置到IRPCurrentStackLocation
(3). 设置StackLocation的DeviceObject,并调用该DeviceObject的DriverObject中该StackLocation指定的MajorFunction。


IoCallDriver做的前两件事理论上来说可以看成一件——IRP头中的CurrentLocation是StackLocation的索引,而IRP尾中的CurrentStackLocation是StackLocation的地址。
所以实际上干了两件事:1)把IO栈帧往底层移了一位;2)设置了下个IO栈的DeviceObject为传入的设备,并且调用这个设备传入的MajorFunction分发函数

2. 关于IoSkipCurrentIrpStackLocation和IoCopyCurrentIrpStackLocationToNext

2.1 IoSkipCurrentIrpStackLocation
这一段的源码wrk里写得很清晰



#define IoSkipCurrentIrpStackLocation( Irp ) { \ 
    (Irp)->CurrentLocation++; \ 
    (Irp)->Tail.Overlay.CurrentStackLocation++; }



IoCallDriver把IO栈帧往底层移一位,而IoSkipCurrentIrpStackLocation企图预先把IO栈帧往高层移一位,这样当IoCallDriver去调用下一层的分发函数时,获得的还是当前的栈内容。
2.2 IoCopyCurrentIrpStackLocationToNext
IoCopyCurrentIrpStackLocationToNext本质上就是个RtlCopyMemory,请见wrk

#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; }


从IO_STACK_LOCATION可以看到,CopyCurrentIrpStackLocation覆盖了除了这两个字段以外的全部内容

  PIO_COMPLETION_ROUTINE CompletionRoutine;
  PVOID                  Context;

我们知道,由于SkipCurrentIrpStackLocation本质上是把当前的栈帧在下次调用时重新利用,所以在需要设置回调函数时,需要一个不同于当前栈的地址。
所以当分发函数需要回调,必须要使用CopyCurrentIrpStackLocation来让下一层栈帧的CompletionRoutine设置为本层的回调函数。
有的朋友会问,既然SkipCurrent本质上重新利用了当前的栈,那我正常情况下也是用CopyCurrentIrpStackLocation有没有影响呢?
其实显然是可以的,但需要再手动设置一下Context和CompletionRoutine。一般倾向使用SkipCurrent完全是处于性能问题[3]

3. IO_STACK_LOCATION在内存中初始化的问题
回过头来,这篇文章最主要想解决开篇的两个问题
1)IoCopyCurrentIrpStackLocationToNext覆盖了下一层IO栈,不是等于覆盖了下一层的运行上下文?
2)为什么要IoSkipCurrentIrpStackLocation,而不让栈帧的指针直接由IoCallDriver指向下一层?
为了搞清这个情况,我在一个键盘过滤驱动里写了一段代码进行调试

NTSTATUS c2pDispatchGeneral(PDEVICE_OBJECT DeviceObject, PIRP irp) {
    DbgPrint("Other Dispatch");
    PIO_STACK_LOCATION irpStack;
    irpStack = IoGetCurrentIrpStackLocation(irp);
    DbgBreakPoint();
    IoSkipCurrentIrpStackLocation(irp);
    IoCallDriver(((PC2P_DEV_EXT)DeviceObject->DeviceExtension)->LowerDeviceObject, irp);
    return STATUS_SUCCESS;
}


c2pDispatchGeneral是一段典型的分发函数,DbgBreakPoint()中断后,在windbg中观察rax的值和内存情况,就知道IO_STACK_LOCATION的情况了。
显然,这样的键盘过滤驱动的IO_STACK_LOCATION应该至少有两层
然而在windbg中我们得出了这样的结果:

0: kd> dt nt!_IO_STACK_LOCATION
   +0x000 MajorFunction    : UChar
   +0x001 MinorFunction    : UChar
   +0x002 Flags            : UChar
   +0x003 Control          : UChar
   +0x008 Parameters       : <anonymous-tag>
   +0x028 DeviceObject     : Ptr64 _DEVICE_OBJECT
   +0x030 FileObject       : Ptr64 _FILE_OBJECT
   +0x038 CompletionRoutine : Ptr64     long
   +0x040 Context          : Ptr64 Void
   
0: kd> dd rax
ffffa486`75b52548  0001000e 00000000 00000000 00000000
ffffa486`75b52558  00000004 00000000 000b0008 00000000
ffffa486`75b52568  00000000 00000000 780d6a50 ffffa486
ffffa486`75b52578  771a76b0 ffffa486 00000000 00000000
ffffa486`75b52588  00000000 00000000 00000000 00000000
ffffa486`75b52598  00000000 00000000 00000000 00000000
ffffa486`75b525a8  00000000 00000000 00000000 00000000
ffffa486`75b525b8  00000000 00000000 00000000 00000000
0: kd> dd rax-0x40
ffffa486`75b52508  00000000 00000000 00000000 00000000
ffffa486`75b52518  00000000 00000000 00000000 00000000
ffffa486`75b52528  00000000 00000000 00000000 00000000
ffffa486`75b52538  00000000 00000000 00000000 00000000
ffffa486`75b52548  0001000e 00000000 00000000 00000000
ffffa486`75b52558  00000004 00000000 000b0008 00000000
ffffa486`75b52568  00000000 00000000 780d6a50 ffffa486
ffffa486`75b52578  771a76b0 ffffa486 00000000 00000000
0: kd> dd rax+0x40
ffffa486`75b52588  00000000 00000000 00000000 00000000
ffffa486`75b52598  00000000 00000000 00000000 00000000
ffffa486`75b525a8  00000000 00000000 00000000 00000000
ffffa486`75b525b8  00000000 00000000 00000000 00000000
ffffa486`75b525c8  00000000 00000000 00000000 00000000
ffffa486`75b525d8  00000000 00000000 00000000 00000000
ffffa486`75b525e8  00000000 00000000 00000000 00000000
ffffa486`75b525f8  00000000 00000000 00000000 00000000


不管是这个分发函数的上一个IO_STACK_LOCATION,还是分发函数的下一个IO_STACK_LOCATION,竟然值全部为0!
也就是说,在IRP包初始化申请空间时,IO管理器只申请了足够长的内存,以及初始化了第一个IO_STACK_LOCATION。
这样一来,为下一层构造一个合适的IO栈是高层驱动必须要做的事情。因为IO管理器并没有为我们做这一切。
这也解释了一个问题——IO管理器申请的栈空间只是一般意义上(即不在申请中构造新的包/驱动层)最多使用栈空间。
第一次看到这里的,特别是每一个设备和IO栈都要“一一对应”的时候,特别容易理解成每一层分发函数都独属一个栈空间。实际上这里的“栈”使用非常自由,你不一定要用完IO管理器申请的所有栈空间(即全部使用Copy复制下去)。引用一个资料[2]

You'll notice that the array of IO_STACK_LOCATIONs contains an entry at the very bottom that won't be used in this scenario. In fact, if drivers underneath us play the same trick, there might be more than one location that won't be used. That's not a problem, though—it just means that something allocated more stack locations than it needed to. It's not a problem that the stack gets unwound a little bit quicker during completion processing, either. IoCompleteRequest doesn't use any absolute indices or pointers when it unwinds the stack. It just starts at whatever the current location is when it gains control and works its way upward calling completion routines. All the completion routines that got installed will get called, and the then-current stack locations will be the ones that their drivers were expecting to work with.


那么本文开头提到的两个问题的答案,自然也呼之欲出了

1)IoCopyCurrentIrpStackLocationToNext覆盖了下一层IO栈,不是等于覆盖了下一层的运行上下文?
——下一层IO栈没有任何内容,需要上一层分发函数自己构造。IoCallDriver已经帮我们填好了Device,我们只用考虑有没有回调函数或者需要修改的字段,来选择Skip或者Copy就好。

2)为什么要IoSkipCurrentIrpStackLocation,而不让栈帧的指针直接由IoCallDriver指向下一层?
——因为下一层没有任何内容,完全需要自己来构造。


4. 参考内容
4.1 WRK IoCallDriver

NTSTATUS
FORCEINLINE
IopfCallDriver(
    IN PDEVICE_OBJECT DeviceObject,
    IN OUT PIRP Irp
    )
/*++
Routine Description:
    This routine is invoked to pass an I/O Request Packet (IRP) to another
    driver at its dispatch routine.
Arguments:
    DeviceObject - Pointer to device object to which the IRP should be passed.
    Irp - Pointer to IRP for request.
Return Value:
    Return status from driver's dispatch routine.
--*/
{
    PIO_STACK_LOCATION irpSp;
    PDRIVER_OBJECT driverObject;
    NTSTATUS status;
    //
    // Ensure that this is really an I/O Request Packet.
    //
    ASSERT( Irp->Type == IO_TYPE_IRP );
    //
    // Update the IRP stack to point to the next location.
    //
    Irp->CurrentLocation--;
    if (Irp->CurrentLocation <= 0) {
        KiBugCheck3( NO_MORE_IRP_STACK_LOCATIONS, (ULONG_PTR) Irp, 0, 0 );
    }
    irpSp = IoGetNextIrpStackLocation( Irp );
    Irp->Tail.Overlay.CurrentStackLocation = irpSp;
    //
    // Save a pointer to the device object for this request so that it can
    // be used later in completion.
    //
    irpSp->DeviceObject = DeviceObject;
    //
    // Invoke the driver at its dispatch routine entry point.
    //
    driverObject = DeviceObject->DriverObject;
    //
    // Prevent the driver from unloading.
    //
    status = driverObject->MajorFunction[irpSp->MajorFunction]( DeviceObject,
                                                              Irp );
    return status;
}






4.2 其他参考文献
[1] AntBean [原创]驱动入门:从WRK看IRP 理论篇 , https://bbs.pediy.com/thread-130876.htm
[2] https://www-user.tu-chemnitz.de/~heha/oney_wdm/ch05e.htm
[3] https://community.osr.com/discussion/220685/ioskipcurrentirpstacklocation-and-iocopycurrentirpstacklocationtonext
[4] Windows内核原理与实现 潘爱民

风扫春残雪@52pojie/NONAME剑人@pediy


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2020-7-17 18:36 被NONAME剑人编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (7)
雪    币: 6977
活跃值: (1775)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
TopC 2020-7-17 19:04
2
0
我记得深入解析windows操作系统中有讲这个
雪    币: 740
活跃值: (953)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
NONAME剑人 3 2020-7-17 19:13
3
0
TopC 我记得深入解析windows操作系统中有讲这个
IRP flow基本上靠点谱的书都有,我也参考了internals,但是这书只是讲了"driver must prepare the next I/O stack location that would be looked at by the next driver in line",但是对irp flow差一点点讲到内存规划上(原来认为既然IRP package是i/o manager申请的,而且是和各个层设备“一一对应”的关系,那显然应该是已经初始化好的)。没想到只是申请了栈的内存,在IRP头构造好了指针和索引,具体的内容还需要copy或者skip才行
雪    币: 259
活跃值: (283)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ZwCopyAll 2020-7-22 10:58
4
0
那个图片 io_stack_location3 是属于上层设备吗 下一层的io_stack_location里面 什么major_function都没有吗
雪    币: 740
活跃值: (953)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
NONAME剑人 3 2020-7-22 16:49
5
0
ZwCopyAll 那个图片 io_stack_location3 是属于上层设备吗 下一层的io_stack_location里面 什么major_function都没有吗
啥也没有……我看wrk里分配的代码,栈空间和irp头的分配是连续的,但是在调试时内存里过滤驱动接受到的stack上下一个structure都是0
雪    币: 2580
活跃值: (2238)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
. 2020-10-15 16:12
6
0
点赞点赞
雪    币: 246
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
丿刀川 2021-3-20 15:46
7
0
谢谢分享  
雪    币: 35
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
编程两年半 2024-3-14 16:20
8
0
感谢分享
游客
登录 | 注册 方可回帖
返回