64 kernel development lecture 15 IRP dispatch function and communication. Drive frame supplement

IRP dispatch function and communication mode

I. IRP

1.1 IRP introduction to theoretical knowledge

In the Windows kernel, there is a data structure called IRP(I/O Request Package), that is, input / output request package. It is an important data structure related to input and output. As long as you understand IRP, you will understand more than half of the driver development and kernel.

When the upper application communicates with the driver, the application will issue} I/O requests. The operating system will translate the request into the corresponding IRP data. Different IRP data will be passed to different dispatch functions according to types.

1.2 type of IRP

When the application layer calls WINAPI functions such as ReadFile WriteFile CreateFile CloseHandle, the corresponding IRP type IRP will be generated, that is, IRP_MJ_CREATE IRP_MJ_WRITE IRP_MJ_READ IRP_MJ_CLOSE and pass it to the dispatch function in the driver.

In addition, the I/O processing function in the kernel will also generate IRP, so it can be seen that IRP is not completely generated by the application layer. For example, the file operation at the beginning of the Zw Series in the kernel will produce IRP.

IRP type source
IRP_MJ_CREATE CreateFile/ZwCreateFile
IRP_MJ_READ ReadFile/ZwReadFile
IRP_MJ_WRITE WriteFile/ZwWriteFile
IRP_MJ_CLOSE CloseHandle/ZwClose
... ...
... ...

1.3 dispatch function

When we know the IRP type, we only need to set the dispatch function for the driver. In this way, when the application layer calls the corresponding Winapi to send the IO request packet, our dispatch function will get it.

The code is as follows:

extern "C" NTSTATUS DriverEntry (
            IN PDRIVER_OBJECT pDriverObject,
            IN PUNICODE_STRING pRegistryPath    ) 
{
    NTSTATUS status;
    KdPrint(("Enter DriverEntry\n"));

    //Set unload function
    pDriverObject->DriverUnload = HelloDDKUnload;

    //Set dispatch function
    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;

    //Create drive device object
    status = CreateDevice(pDriverObject);

    KdPrint(("Leave DriverEntry\n"));
    return status;
}

There is a driver object parameter in our DriverEntry, where the MajorFunction of this parameter is an array. The array stores the callback function pointer of the dispatch function of IRP type. So we set it according to the above. When winapi sends an IO request, the corresponding dispatch function will be called.

1.4 equipment object and symbol link

The device object is also a very important object in the driver. Our IRP is to be sent to the device. So you need to create a device object. However, if the application layer wants to send IO requests (call WINAPI), the kernel driver must provide a symbolic link to the application layer. After the kernel layer creates the device, it can also specify the communication mode. That is, how the application driver communicates. How data is transmitted. We'll talk about this later.

The code is as follows:

NTSTATUS CreateDevice (
        IN PDRIVER_OBJECT    pDriverObject) 
{
    NTSTATUS status;
    PDEVICE_OBJECT pDevObj;
    PDEVICE_EXTENSION pDevExt;

    //Create device name
    UNICODE_STRING devName;
    RtlInitUnicodeString(&devName,L"\\Device\\MyDDKDevice");

    //Create device
    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; //The setting of communication mode will be described later.
    pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
    pDevExt->pDevice = pDevObj;
    pDevExt->ustrDeviceName = devName;
    //Create symbolic links
    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 introduction to IRP stack

IPR stack is also an important IO packet structure. Because the data recorded in the IRP structure is insufficient to meet our needs. Therefore, an IRP stack is provided. For example, the I/O request issued by the application is a read request, and this request will be sent to the read dispatch function of the kernel. Then the stack is the read stack. Therefore, different types of stacks will be filled with different contents.

Officially, the driver creates a device object and concatenates these device objects together. If a device stack is formed, the IRP will be sent to the top layer of the device stack by the operating system. If the dispatch function of the top-level device object ends the IRP request, the IRP request will end and will not be sent down. Otherwise, the operating system will forward the IRP to the next layer of the device stack for processing. If the device still can't handle it, continue to send it. Therefore, IRP will be forwarded multiple times. In order to record the operation of IRP in each layer of equipment, IRP will have a stack array. The number of stack array elements of IRP should be greater than the number of devices traversed by IRP. Each stack structure element records the operation made by the corresponding device.

The stack array structure described above is as follows:

Array name structure: 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;

In this structure, we can see the record field of IRP type

  UCHAR  MajorFunction;
  UCHAR  MinorFunction;

The device object file object is also recorded

 PDEVICE_OBJECT  DeviceObject;
 PFILE_OBJECT  FileObject;

The more important one is the Parameters parameter. It records Read write DeviceIoControl create and other structures. When our IRP type is Read. The dispatch function can obtain the Read length offset and other information from the Read field.

The API used to call the stack information of this layer is as follows

PIO_STACK_LOCATION 
  IoGetCurrentIrpStackLocation(
    IN PIRP  Irp
    );

II. Communication mode between kernel and application layer cache mode (buffer mode)

2.1 cache mode

The caching method is that the application layer sends data to the kernel layer, and the kernel layer establishes a buffer to save. And we can operate this buffer. The advantage is security and stability. The disadvantage is slow efficiency.

The caching method is after we create the device object. Set the flag of the device object to DO+_BUFFERD_IO

Just.

pDevObj->Flags |= DO_BUFFERED_IO;

If set to buffer mode. Then we only need to get the associated IRP in the IRP structure Just systembuffer.

The IRP structure is as follows

typedef struct _IRP {
  .
  .
  PMDL  MdlAddress;  //Direct IO will use
  ULONG  Flags;
  union {
    struct _IRP  *MasterIrp;
    .
    .
    PVOID  SystemBuffer; //Buffer mode usage
  } AssociatedIrp;
  .
  .
  IO_STATUS_BLOCK  IoStatus; //state
  KPROCESSOR_MODE  RequestorMode;
  BOOLEAN PendingReturned;
  .
  .
  BOOLEAN  Cancel;
  KIRQL  CancelIrql;
  PDRIVER_CANCEL  CancelRoutine;
  PVOID UserBuffer;         //Other ways
  . . . . . . . 
} IRP, *PIRP;

2.2 IRP size acquisition such as read and write control

If the buffer mode is specified in our dispatch function. Then we can get the SystemBuffer from IRP and use it.

However, the dispatch function will allocate different dispatch function calls according to different types of IRP. There will be IRP_ MJ_ READ IRP_ MJ_ WRITE IPR_ MJ_ According to different dispatch functions, the size of user transfer buffer obtained by DeviceControl is also different.

Such as IRP_MJ_READ

We want to set the parameters in the IRP stack Read. Length to get the length

If IRP_MJ_WRITE, then the corresponding should be in write Length to get the length

If it is in Control, it is obtained in DeviceIoControl.

Among them, it is also special, and its domain is as follows:

struct {
            ULONG  OutputBufferLength;
            ULONG POINTER_ALIGNMENT  InputBufferLength;
            ULONG POINTER_ALIGNMENT  IoControlCode;
            PVOID  Type3InputBuffer;
        } DeviceIoControl;

It records the length of the output buffer passed by the application layer. Enter the length of the buffer. Control code.

And a buffer for user areas using other communication types. It will be said later.

2.3 examples of using cache dispatch function

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); //Get stack

    pBuffer = pIrp->AssociatedIrp.SystemBuffer; //Get buffer in cache mode

    uReadLength = pIrpStack->Parameters.Read.Length;//Get the length in different fields according to different types
    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;
}

III. MDL mode (direct IO mode)

3.1 direct IO mode

The Mdl method is to map the Buffer passed by the user. A copy is mapped in the kernel. In this way, the buffers of user mode and kernel mode point to the same physical memory. No matter how the operating system switches the process, the address of kernel mode will not change.

Advantages: high speed, safety and stability.

Using MDL mode, first of all, set the device communication mode to direct IO mode after creating the device.

As follows:

pDeviceObj->Flags |= DO_DIRECT_IO;

After setting, the MdlAddress in the IRP domain records the mapped address

3.2Mdl structure

MDL is a structure that records this virtual memory (user's buffer).

Because the memory is discontinuous, MDL records like a linked list

typedef struct _MDL {
  struct _MDL      *Next;     //Next MDL
  CSHORT           Size;      //Record itself MD
  CSHORT           MdlFlags;
  struct _EPROCESS *Process;  //Record the EP of the current process
  PVOID            MappedSystemVa;//Record the address in the kernel
  PVOID            StartVa;   //Record first page address
  ULONG            ByteCount; //Record virtual machine memory size
  ULONG            ByteOffset;//Record offset from page
} MDL, *PMDL;

MmGetMdlVirtualAddress Returns the virtual memory address of the i/o buffer described by the MDL.

MmGetMdlByteCount Returns the size of the i/o buffer in bytes.

MmGetMdlByteOffset Returns the offset within the physical page at the beginning of the i/o buffer.

MmGetSystemAddressForMdlSafe The routine maps the physical page described by the specified MDL to the virtual address in the system address space

There are many MDL S. If you need to know more about them, you can see the Microsoft documentation.

Here is only what we need to use.

The first address of virtual memory is VA = StartVa + ByteOffset calculated by us

3.3 example of direct IO communication

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;
    //Get the stack, which is useless in the example. When using, you need to obtain the length of the operation according to the IRP type
    pIrpStack = IoGetCurrentIrpStackLocation(pIrp); 

    uReadLength = MmGetMdlByteCount(pIrp->MdlAddress);//Get IO length (of array)
    uOffset = MmGetMdlByteOffset(pIrp->MdlAddress);   //Page offset
    pBuffer = MmGetMdlVirtualAddress(pIrp->MdlAddress);//First page
    //Get the address mapped in the kernel
    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;
}

IV. reading and writing in other ways

4.1 other methods

Other ways of reading and writing are to directly pass the user's buffer to the kernel. However, if the process switches in this way, it will cause a blue screen. So we need to judge whether it is readable. A little efficiency is the fastest. Disadvantages are unsafe.

Set the flag in the device object to 0

pDeviceObj->Flags = 0;

The read and write data are in the UserBuffer in the IRP structure.

Examples are as follows:

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); //Get stack
    pBuffer = pIrp->UserBuffer;
    uReadLength = pIrpStack->Parameters.Read.Length;//Different lengths are obtained according to different IRP types
    if (pBuffer != NULL && uReadLength > 0)
    {
        ustrLen = strlen("HelloWorld");
        if (uReadLength < ustrLen)
            goto END;
        __try
        {
            ProbeForRead(pBuffer, 1, 1);//Whether it is readable or writable. 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;
}

Added by PurpleMonkey on Mon, 24 Jan 2022 09:12:31 +0200