IRP基本数据结构:
IRP是由I/O管理器发出的,I/O管理器是用户态与内核态之间的桥梁,当用户态进程发出I/O请求时,I/O管理器就捕获这些请求,将其转换为IRP请求,发送给驱动程序。I/O管理器负责所有I/O请求的调度和管理工作,根据请求内容的不同,选择相应的驱动程序对象,设备对象,并生成、发送、释放各种不同的IRP。整个I/O处理流程是在它的指挥下完成的。
一个IRP是从非分页内存中分配的可变大小的结构,它包括两部分:IRP首部和辅助请求参数数组,这两部分都是由I/O管理器建立的。
IRP首部中包含了指向IRP输入输出缓冲区指针、当前拥有IRP的驱动指针等。
紧接着首部的是一个IO_STACK_LOCATION结构的数组。它的大小由设备栈中的设备数确定。IO_STACK_LOCATION结构中保存了一个I/O请求的参数及代码、请求当前对应的设备指针、完成函数指针等。
IRP运行流程:
操作系统用设备对象(device object)表示物理设备,每一个物理设备都有一个或多个设备对象与之相关联, 设备对象提供了在设备上的所有操作。 也有一些设备对象并不表示物理设备。 一个唯软件驱动程序(software-only driver, 处理 I/O 请求, 但是不把这些请求传递给硬件)也必须创建表示它的操作的设备对象。设备常常由多个设备对象所表示,每一个设备对象对应一个驱动程序来管理设备的 I/O请 求 。 一个设备的所有设备对象被组织成一个设备栈(device stack )。而且,IO_STACK_LOCATION 数组中的每个元素和设备栈中的每个设备是一一对应的,一般情况下,只允许层次结构中的每个设备对象访问它自己对应的 IO_STACK_LOCATION。无论何时,一个请求操作都在一个设备上被完成,I/O 管理器把 IRP 请求传递给设备栈中顶部设备的驱动程序(IRP 是传递给设备对象的,通过设备对象的 DriverObject 成员找到驱动程序) 。驱动程序访问它对应的设备对象在 IRP 中IO_STACK_LOCATION 数组中的元素检查参数,以决定要进行什么操作(通过检查结构中的 MajorFunction 字段,确定执行什么操作及如何解释 Parameters 共用体字段的内容) 。驱动程序可以根据 IO_STACK_LOCATION 结构中的MajorFunction 字段进行处理。每一个驱动或者处理 IRP,或者把它传递给设备栈中下一个设备对象的驱动程序。
传递 IRP 请求到底层设备的驱动程序需要经过下面几个步骤:
1. 为下一个 IO_STACK_LOCATION 结构设置参数。可以有以下两种方式:调用 IoGetNextIrpStackLocation 函数获得下个结构的指针,再对参数进行赋值;
调用 IoCopyCurrentIrpStackLocationToNext 函数(如果第 2 步中驱动设置了 IoCompletion函 数 ), 或 者 调 用 IoSkipCurrentIrpStackLocation 函 数 (如果第2步中驱动没有设置IoCompletion 函数)把当前的参数传递给下一个。
2. 如果需要的话, 调用 IoSetCompletionRoutine 函数设置 IoCompletion 函数进行后续处理。
3. 调用 IoCallDriver 函数将 IRP 请求传递给下一层驱动。 这个函数会自动调整 IRP 栈指针,并且执行下一层驱动的派遣函数。当驱动程序把 IRP 请求传递给下一层驱动之后,它就不再拥有对该请求的访问权,强行访问会导致系统崩溃。如果驱动程序在传递完之后还想再访问该请求,就必须要设置IoCompletion 函数。IRP 请求可以再其他驱动程序或者其他线程中完成或取消。
当某一驱动程序调用 IoCompleteRequest 函数时, I/O 操作就完成了。 这个函数使得 IRP的堆栈指针向上移动一个位置,如图 2 所示:
图 2 所示的当 C 驱动程序调用完 IoCompleteRequest 函数后 I/O 栈的情况。左边的实线箭头表明栈指针现在指向驱动 B 的参数和回调函数;虚线箭头是之前的情况。右边的空心箭头指明了 IoCompletion 函数被调用的顺序。
如果驱动程序把 IRP 请求传递给设备栈中的下层设备之前设置了 IoCompletion 函数,当 I/O 栈指针再次指回到该驱动程序时,I/O 管理器就将调用该 IoCompletion 函数。
IoCompletion 函数的返回值有两种:
(1 ) STATUS_CONTINUE_COMPLETION :告诉 I/O 管理器继续执行上层驱动程序 的IoCompletion 函数。
(2) STATUS_MORE_PROCESSING_REQUIRED: 告诉 I/O 管理器停止执行上层驱动程序,并将栈指针停在当前位置。在当前驱动程序调用 IoCompleteRequest 函数后再继续执行上层驱动的 IoCompletion 函数。当所有驱动都完成了它们相应的子请求时, I/O 请求就结束了。 I/O 管理器从 Irp
->IoStatus.Status 中更新状态信息,从 Irp ->IoStatus.Information 中获得传送字节数。
大体流程是这样的:
注意这个标准模型中,并不是每种IRP都经过这些步骤,由于设备类型和IRP种类的不同某些步骤会改变或根本不存在。
一、IRP创建。
由于IRP开始于某个实体调用I/O管理器函数创建它,可以使用下面任何一种函数创建IRP:
IoBuildAsynchronousFsdRequest 创建异步IRP(不需要等待其完成)。该函数和下一个函数仅适用于创建某些类型的IRP。
IoBuildSynchronousFsdRequest 创建同步IRP(需要等待其完成)。
IoBuildDeviceIoControlRequest 创建一个同步IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求。
IoAllocateIrp 创建上面三个函数不支持的其它种类的IRP。
由此我们知道,第一种起点拦截的办法就清楚了,那就是HOOK这几个IRP的创建函数。由于函数有多个,并且此时irp虽然已经创建,但是还没有进程初始化,也就是说irp堆栈单元的内容还没有填充。因此起点拦截的办法是得不到有用信息的。这种办法无效。
二、发往派遣例程
创建完IRP后,你可以调用IoGetNextIrpStackLocation函数获得该IRP第一个堆栈单元的指针。然后初始化这个堆栈单元。在初始化过程的最后,你需要填充MajorFunction代码。堆栈单元初始化完成后,就可以调用IoCallDriver函数把IRP发送到设备驱动程序了。IoCallDriver是一个宏,它内部实现中调用了IofCallDriver. 因此,到这里便有了第二种拦截方法,即中途拦截。
kd> u IoCallDriver
nt!IoCallDriver:
82813039 8bff mov edi,edi
8281303b 55 push ebp
8281303c 8bec mov ebp,esp
8281303e 8b550c mov edx,dword ptr [ebp+0Ch]
82813041 8b4d08 mov ecx,dword ptr [ebp+8]
82813044 e80fe40200 call nt!IofCallDriver (82841458)
82813049 5d pop ebp
8281304a c20800 ret 8
三、派遣例程的作用
1)在派遣例程中完成irp。通常我们做的过滤驱动或者一些简单的驱动,都是这么完成的,直接在派遣例程中返回。不需要经过后面的步骤,派遣函数立即完成该IRP。
例如:NTSTATUS OnStubDispatch( IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest (Irp, IO_NO_INCREMENT );
return Irp->IoStatus.Status;
}
派遣例程把该IRP传递到处于同一堆栈的下层驱动程序 。
在这种情况下,通过调用IcCallDriver可以将irp传递到其他的驱动,或者传递到下一层驱动,这时irp变成其他驱动要处理的事情,如果其他驱动的派遣例程处理了irp,就类似1)的情况了,如果没处理,继续向下传,如果中间FDO没有处理,最后传到最低层的硬件驱动上去,也就是我们所谓的PDO. 这个时候,I/O管理器就调用一次StartIo例程,硬件抽象层会通过硬件中断ISR,一个ISR最可能做的事就是调度DPC例程(推迟过程调用)。最后完成这个IRP.,回到I/O管理器。
排队该IRP以便由这个驱动程序中的其它例程来处理 。
例如:NTSTATUS DispatchXxx(…)
{
…
IoMarkIrpPending(Irp);
IoStartPacket(device, Irp, NULL, NULL);
return STATUS_PENDING;
}
如果设备正忙,IoStartPacket就把请求放到队列中。如果设备空闲,IoStartPacket将把社
备置成忙并调用StartIo例程。 接下来类似于2)中描述的那样,完成这样一个过程。
我们写驱动的时候,对感兴趣的irp,我们都会写派遣例程来进行处理。如果我们把派遣例程给替换了,便有了第三种的irp拦截。
对于第三种的拦截,有两种办法:
一种是写一个过滤驱动放在要拦截的驱动的上层,这是一种安全的办法。例如:
如果我们想拦截系统的文件操作,就必须拦截I/O管理器发向文件系统驱动程序的IRP。而拦 截IRP最简单的方法莫过于创建一个上层过滤器设备对象并将之加入文件系统设备所在的设备堆栈中。具体方法如下:首先通过IoCreateDevice创 建自己的设备对象,然后调用IoGetDeviceObjectPointer来得到文件系统设备(Ntfs,Fastfat,Rdr或Mrxsmb, Cdfs)对象的指针,最后通过IoAttachDeviceToDeviceStack或者IoAttachDevice等函数,将自己的设备放到设备堆栈上成为一个过滤器。这是拦截IRP最常用也是最保险的方法。
还有一种就是直接替换要拦截驱动对象的派遣例程函数表。它的方法更简单且更为直接。
例如:如果我们想拦截系统的文件操作,它先通过ObReferenceObjectByName得到文件系统驱动对象的指针。然后将驱动对象中 MajorFunction数组中的打开,关闭,清除,设置文件信息,和写入调度例程入口地址改为我们驱动中相应钩子函数的入口地址来达到拦截IRP的目的。
总结:
1) 可用办法之一:hook IofCallDriver实现irp 拦截。
2) 可用办法之二:写一个过滤驱动,挂在你要hook其irp的那个驱动之上。
3) 可用办法之三:直接修改你要hook其irp的那个驱动的MajorFunction函数表。
四、拦截键盘IRP实例
第一种方法可以使用inline hook来搞定,下面我们来说说第二种和第三种方法:
键盘过滤驱动:
首先自己生成一个设备,然后附加到目标设备,并在MajorFunction[IRP_MJ_READ]里设置完成函数。
主要代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | //生成设备、附加设备 RtlInitUnicodeString(&ConDeviceName,L"\\Device\\KbdFilterDev"); status=IoCreateDevice(DriverObject,sizeof(KbdConDeviceExt),&ConDeviceName,FILE_DEVICE_FILE_SYSTEM,0,FALSE,&ConDevice); if (!NT_SUCCESS(status)) { KdPrint(("IoCreateDevice Fail\n")); return status; } ConDevice->Flags|=DO_BUFFERED_IO; condevext=(pKbdConDeviceExt)ConDevice->DeviceExtension; condevext->devname=ConDeviceName; condevext->condevice=ConDevice; RtlInitUnicodeString(&syslinkname,L"\\??\\KbdFilterDev"); status=IoCreateSymbolicLink(&syslinkname,&ConDeviceName); condevext->syslinkname=syslinkname; if (!NT_SUCCESS(status)) { KdPrint(("IoCreateSymbolicLink Fail\n")); IoDeleteDevice(ConDevice); return status; } g_DriverObject=DriverObject; status=AttachDevice(DriverObject,RegistryPath); //设置完成函数 DeviceExt=(pKbdFilterDeviceExt)DeviceObject->DeviceExtension; DeviceExt->PendingIrp=Irp; g_KeyCount++; IoCopyCurrentIrpStackLocationToNext(Irp); IoSetCompletionRoutine(Irp,IoCompletionRead,NULL,TRUE,TRUE,TRUE); status=IoCallDriver(DeviceExt->pLowObject,Irp); //完成函数处理 NTSTATUS IoCompletionRead ( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context ) { PIO_STACK_LOCATION IrpSp; PKEYBOARD_INPUT_DATA keyData; int i=0; IrpSp=IoGetCurrentIrpStackLocation(Irp); if (NT_SUCCESS(Irp->IoStatus.Status)) { keyData=(PKEYBOARD_INPUT_DATA)Irp->AssociatedIrp.SystemBuffer; print_keystroke(keyData); } if (Irp->PendingReturned) { //标志着某个驱动的分发例程(分发函数)因需要被其他的驱动程序进一步处理最终返回STATUS_PENDING状态。 IoMarkIrpPending(Irp); } g_KeyCount--; return Irp->IoStatus.Status; } |