IRP 派遣函数 与通信方式
一丶IRP
1.1 IRP介绍 理论知识
在Windows内核中,有一种数据结构叫做 IRP(I/O Request Package)
也就是输入输出请求包。它是与输入输出相关的重要数据结构
只要了解了IRP 那么驱动开发以及内核你就会了解一大半了。
当上层 应用程序
与驱动程序
进行通讯的时候 应用程序会发出 I/O
请求。操作系统会将请求转为相应的IRP 数据。不同的IRP
数据 会按照类型传递到不同的派遣函数中。
1.2 IRP的类型
当应用层调用 ReadFile WriteFile CreateFile CloseHandle
等WINAPI 函数 则会产生对应的IRP类型的的IRP 也就是 IRP_MJ_CREATE IRP_MJ_WRITE IRP_MJ_READ IRP_MJ_CLOSE
并且传送到驱动的中的派遣函数中。
另外 内核中的 I/O
处理函数也会产生IRP,所以可见IRP并不完全是由应用层产生的。比如内核中的 Zw系列开头的文件操作 一样会产生IRP。
IRP类型 | 来源 |
---|---|
IRP_MJ_CREATE | CreateFile/ZwCreateFile |
IRP_MJ_READ | ReadFile/ZwReadFile |
IRP_MJ_WRITE | WriteFile/ZwWriteFile |
IRP_MJ_CLOSE | CloseHandle/ZwClose |
... | ... |
... | ... |
1.3 派遣函数
当我们知道IRP类型之后只需要给驱动设置派遣函数即可。 这样当应用层调用对应的 Winapi发送IO请求数据包的时候我们的派遣函数则会获取到。
代码如下:
extern "C" NTSTATUS DriverEntry (
IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath )
{
NTSTATUS status;
KdPrint(("Enter DriverEntry\n"));
//设置卸载函数
pDriverObject->DriverUnload = HelloDDKUnload;
//设置派遣函数
pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_SET_INFORMATION] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_SHUTDOWN] = HelloDDKDispatchRoutin;
pDriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = HelloDDKDispatchRoutin;
//创建驱动设备对象
status = CreateDevice(pDriverObject);
KdPrint(("Leave DriverEntry\n"));
return status;
}
在我们的DriverEntry中 有一个驱动对象参数 其中此参数的 MajorFunction是一个数组。数组里面存放着记录着IRP类型的派遣函数的回调函数指针。所以我们根据如上设置之后。当winapi发送IO请求的时候对应的派遣函数则会调用。
1.4 设备对象 与符号链接
设备对象
也是驱动中的很重要的对象。 我们的IRP是要发送给设备的。所以需要创建设备对象。但是如果应用层想要发送IO请求(调用WINAPI) 那么内核驱动必须提供个符号链接给应用层使用。 内核层创建好设备之后还可以指定通讯方式。 也就是 应用-驱动 如何进行通信。数据如何传输。这个下面再说。
代码如下:
NTSTATUS CreateDevice (
IN PDRIVER_OBJECT pDriverObject)
{
NTSTATUS status;
PDEVICE_OBJECT pDevObj;
PDEVICE_EXTENSION pDevExt;
//创建设备名称
UNICODE_STRING devName;
RtlInitUnicodeString(&devName,L"\\Device\\MyDDKDevice");
//创建设备
status = IoCreateDevice( pDriverObject,
sizeof(DEVICE_EXTENSION),
&(UNICODE_STRING)devName,
FILE_DEVICE_UNKNOWN,
0, TRUE,
&pDevObj );
if (!NT_SUCCESS(status))
return status;
pDevObj->Flags |= DO_BUFFERED_IO; //通信方式设置后面说明。
pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
pDevExt->pDevice = pDevObj;
pDevExt->ustrDeviceName = devName;
//创建符号链接
UNICODE_STRING symLinkName;
RtlInitUnicodeString(&symLinkName,L"\\??\\HelloDDK");
pDevExt->ustrSymLinkName = symLinkName;
status = IoCreateSymbolicLink( &symLinkName,&devName );
if (!NT_SUCCESS(status))
{
IoDeleteDevice( pDevObj );
return status;
}
return STATUS_SUCCESS;
}
1.5 IRP堆栈介绍
IPR堆栈也是很重要的 IO数据包结构。因为IRP结构中记录的数据不足与满足我们的需求。所以提供了IRP堆栈。 比如 应用程序发出的I/O 请求是读的请求,并且此请求会发送到内核的读派遣函数中。 那么此时堆栈就是读的堆栈。 所以类型的不同堆栈被填充的内容也会是不同的。
官方说法是 驱动程序会创建一个设备对象,并且将这些设备对象串联到一起。形成了一个 设备栈
IRP会被操作系统发送到设备栈的顶层,如果顶层设备对象的派遣函数
结束了IRP请求,那么此次的IRP请求就会结束,不会往下发送了。否则操作系统就会将IRP再转发到设备栈的下一层设备进行处理。如果设备依旧不能处理,那么继续往下发。 因此IRP会被转发多次。为了记录IRP在每层设备中的操作,IRP会有一个堆栈数组。IRP的堆栈数组元素数应该大于IRP穿越过的设备数。每个 堆栈结构元素记录着对应设备所作的操作。
上面所述的堆栈数组结构如下:
数组名结构为: IO_STACK_LOCATION
typedef struct _IO_STACK_LOCATION {
UCHAR MajorFunction;
UCHAR MinorFunction;
UCHAR Flags;
UCHAR Control;
union {
//
// Parameters for IRP_MJ_CREATE
//
struct {
PIO_SECURITY_CONTEXT SecurityContext;
ULONG Options;
USHORT POINTER_ALIGNMENT FileAttributes;
USHORT ShareAccess;
ULONG POINTER_ALIGNMENT EaLength;
} Create;
//
// Parameters for IRP_MJ_READ
//
struct {
ULONG Length;
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;
} Read;
//
// Parameters for IRP_MJ_WRITE
//
struct {
ULONG Length;
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;
} Write;
//
// Parameters for IRP_MJ_QUERY_INFORMATION
//
struct {
ULONG Length;
FILE_INFORMATION_CLASS POINTER_ALIGNMENT FileInformationClass;
} QueryFile;
//
// Parameters for IRP_MJ_SET_INFORMATION
//
struct {
ULONG Length;
FILE_INFORMATION_CLASS POINTER_ALIGNMENT FileInformationClass;
PFILE_OBJECT FileObject;
union {
struct {
BOOLEAN ReplaceIfExists;
BOOLEAN AdvanceOnly;
};
ULONG ClusterCount;
HANDLE DeleteHandle;
};
} SetFile;
//
// Parameters for IRP_MJ_QUERY_VOLUME_INFORMATION
//
struct {
ULONG Length;
FS_INFORMATION_CLASS POINTER_ALIGNMENT FsInformationClass;
} QueryVolume;
//
// Parameters for IRP_MJ_DEVICE_CONTROL and IRP_MJ_INTERNAL_DEVICE_CONTROL
//
struct {
ULONG OutputBufferLength;
ULONG POINTER_ALIGNMENT InputBufferLength;
ULONG POINTER_ALIGNMENT IoControlCode;
PVOID Type3InputBuffer;
} DeviceIoControl;
..............................
} Parameters;
PDEVICE_OBJECT DeviceObject;
PFILE_OBJECT FileObject;
.
.
.
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;
在此结构中我们可以看到 IRP类型的记录域
UCHAR MajorFunction;
UCHAR MinorFunction;
也记录着 设备对象 文件对象
PDEVICE_OBJECT DeviceObject;
PFILE_OBJECT FileObject;
其中比较重要的就是 Parameters
参数。它里面记录着 Read Write DeviceIoControl Create
等结构。 当我们 IRP类型为Read的时候。派遣函数 则可以从 Read域中获取读取的长度 偏移等信息。
调用本层堆栈信息 使用的API如下
PIO_STACK_LOCATION
IoGetCurrentIrpStackLocation(
IN PIRP Irp
);
二丶内核与应用层的通信方式 缓存方式(缓冲区方式)
2.1 缓存方式
缓存方式 就是 应用层发送数据到内核层中,内核层建立一个缓冲区来进行保存。 而我们操作这个缓冲区即可。 这样的好处是安全 稳定。 缺点是效率慢。
缓存方式 在我们创建完设备对象之后。将设备对象的标志设置为 DO+_BUFFERD_IO
即可。
pDevObj->Flags |= DO_BUFFERED_IO;
如果设置为缓冲区模式。那么我们只需要在 IRP结构
中获取AssociatedIrp.SystemBuffer
即可。
IRP结构如下
typedef struct _IRP {
.
.
PMDL MdlAddress; //直接IO会使用
ULONG Flags;
union {
struct _IRP *MasterIrp;
.
.
PVOID SystemBuffer; //缓冲区模式使用
} AssociatedIrp;
.
.
IO_STATUS_BLOCK IoStatus; //状态
KPROCESSOR_MODE RequestorMode;
BOOLEAN PendingReturned;
.
.
BOOLEAN Cancel;
KIRQL CancelIrql;
PDRIVER_CANCEL CancelRoutine;
PVOID UserBuffer; //其它方式
。。。。。。。
} IRP, *PIRP;
2.2 读取 写入 控制等IRP的大小获取
在我们的派遣函数中如果指定了缓冲区模式。那么我们从IRP中获取 SystemBuffer
使用即可。
但是派遣函数 会根据 IRP不同的类型来分配不同的派遣函数调用。 其中就会有 IRP_MJ_READ IRP_MJ_WRITE IPR_MJ_DEVICECONTROL
根据派遣函数的不同我们获取的用户传递缓冲区方式的大小也是不同的。
比如IRP_MJ_READ
我们要在 IRP堆栈 中的 Parameters.Read.Length
来获取长度
如果是 IRP_MJ_WRITE 那么相应的要在 Write.length 来获取长度
如果是Control中 那么就是 DeviceIoControl中获取。
其中他还比较特殊它的域如下:
struct {
ULONG OutputBufferLength;
ULONG POINTER_ALIGNMENT InputBufferLength;
ULONG POINTER_ALIGNMENT IoControlCode;
PVOID Type3InputBuffer;
} DeviceIoControl;
记录着应用层传递的输出buffer的长度。 输入buffer的长度。 控制码。
以及 使用其它方式通讯类型的 用户区的缓冲区。 后面会说。
2.3 缓存方式派遣函数中的使用例子
NTSTATUS DisPathchRead_SystemBuffer(PDEVICE_OBJECT pDeviceobj, PIRP pIrp)
{
KdBreakPoint();
PVOID pBuffer = NULL;
ULONG uReadLength = 0;
ULONG ustrLen = 0;
PIO_STACK_LOCATION pIrpStack = NULL;
pIrpStack = IoGetCurrentIrpStackLocation(pIrp); //获取堆栈
pBuffer = pIrp->AssociatedIrp.SystemBuffer; //缓存方式获取缓冲区
uReadLength = pIrpStack->Parameters.Read.Length;//根据不同类型在不同域中获取长度
if (pBuffer != NULL && uReadLength > 0)
{
ustrLen = strlen("HelloWorld");
if (uReadLength < ustrLen)
goto END;
RtlCopyMemory(pBuffer, "HelloWorld", ustrLen);
}
END:
pIrp->IoStatus.Information = ustrLen;
pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
三丶MDL方式(直接IO方式)
3.1 直接IO方式
Mdl方式是将用户传递的Buffer进行映射。在内核中映射了一份。这样用户模式和内核模式的缓冲区都是指向了同一块物理内存,无论操作系统如何切换进程内核模式的地址都不会改变。
优点: 速度快 安全稳定。
使用MDL方式首先也是在创建设备之后设置设备通信方式为直接IO方式。
如下:
pDeviceObj->Flags |= DO_DIRECT_IO;
设置之后在IRP域中的 MdlAddress 则记录着映射的地址
3.2Mdl结构
MDL是一个结构,记录着这段虚拟内存(用户的buffer) 。
因为内存是不连续的所以MDL会像链表一样记录
typedef struct _MDL {
struct _MDL *Next; //下一个MDL
CSHORT Size; //记录本身MD
CSHORT MdlFlags;
struct _EPROCESS *Process; //记录当前进程的EP
PVOID MappedSystemVa;//记录内核中的地址
PVOID StartVa; //记录第一个页地址
ULONG ByteCount; //记录虚拟机内存大小
ULONG ByteOffset;//记录相对于页的偏移
} MDL, *PMDL;
MmGetMdlVirtualAddress 返回 MDL 描述的 i/o 缓冲区的虚拟内存地址。
MmGetMdlByteCount 返回 i/o 缓冲区的大小(以字节为单位)。
MmGetMdlByteOffset 返回 i/o 缓冲区开始处的物理页内的偏移量。
MmGetSystemAddressForMdlSafe例程将指定 MDL 描述的物理页面映射到系统地址空间中的虚拟地址
MDL很多,需要详细了解可以看一下微软文档。
这里只说明我们需要使用的。
其中虚拟内存首地址是我们计算出来的 VA = StartVa + ByteOffset
3.3 直接IO通信例子
NTSTATUS DisPathchRead_Mdl(PDEVICE_OBJECT pDeviceobj, PIRP pIrp)
{
KdBreakPoint();
PVOID pBuffer = NULL;
ULONG uReadLength = 0;
ULONG uOffset = 0;
ULONG ustrLen = 0;
PIO_STACK_LOCATION pIrpStack = NULL;
PVOID pKernelbase = NULL;
//获取堆栈,例子中没用。使用的时候需要根据IRP类型获取操作的长度
pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
uReadLength = MmGetMdlByteCount(pIrp->MdlAddress);//获取IO长度(数组的)
uOffset = MmGetMdlByteOffset(pIrp->MdlAddress); //页偏移
pBuffer = MmGetMdlVirtualAddress(pIrp->MdlAddress);//第一个页
//获取内核中映射的地址
pKernelbase = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
if (pKernelbase != NULL && uReadLength > 0)
{
ustrLen = strlen("HelloWorld");
if (uReadLength < ustrLen)
goto END;
RtlCopyMemory(pKernelbase, PsGetProcessImageFileName(pIrp->MdlAddress->Process), ustrLen);
}
END:
pIrp->IoStatus.Information = ustrLen;
pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
四丶其它方式读写
4.1 其它方式
其它方式读写则是直接将用户的缓冲区传递给内核。但是这样如果进程发生切换则会造成蓝屏。所以需要我们去判断是否可读。有点效率是最快的。 缺点 不安全。
设置 设备对象中的标志为 0即可
pDeviceObj->Flags = 0;
读写的数据都在 IRP结构中的UserBuffer中。
示例如下:
NTSTATUS DisPathchRead_UserBuffer(PDEVICE_OBJECT pDeviceobj, PIRP pIrp)
{
KdBreakPoint();
PVOID pBuffer = NULL;
ULONG uReadLength = 0;
ULONG uOffset = 0;
ULONG ustrLen = 0;
PIO_STACK_LOCATION pIrpStack = NULL;
PVOID pKernelbase = NULL;
pIrpStack = IoGetCurrentIrpStackLocation(pIrp); //获取堆栈
pBuffer = pIrp->UserBuffer;
uReadLength = pIrpStack->Parameters.Read.Length;//根据IRP类型不同获取不同的长度
if (pBuffer != NULL && uReadLength > 0)
{
ustrLen = strlen("HelloWorld");
if (uReadLength < ustrLen)
goto END;
__try
{
ProbeForRead(pBuffer, 1, 1);//是否可读写。ProbeForWrite
RtlCopyMemory(pKernelbase, PsGetProcessImageFileName(pIrp->MdlAddress->Process), ustrLen);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
.....
}
}
END:
pIrp->IoStatus.Information = ustrLen;
pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}