Windows内核NT驱动框架基础分析

2017-09-06  本文已影响240人  Mon7ey
驱动框架

NT驱动框架 : 安全中用的最多的就是NT驱动模型,
WDM框架 : 支持热插拔功能,大多用于网卡一类的硬件

下图是NT驱动框架的示意图:
NT驱动框架

NT驱动框架的组成

NT驱动框架主要是由:驱动入口函数,若干分发函数.驱动卸载函数组成

// --------------驱动入口----------------------
 NTSTATUS  DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegPath)   // 驱动入口函数


// ------------------------若干分发函数-----------------------
NTSTATUS DispatchCommon(PDEVICE_OBJECT pObject,PIRP pIrp);
NTSTATUS DispatchClose(PDEVICE_OBJECT pObject,PIRP pIrp);
NTSTATUS DispatchClean(PDEVICE_OBJECT pObject,PIRP pIrp);
NTSTATUS DispatchRead(PDEVICE_OBJECT pObject,PIRP pIrp);
NTSTATUS DispatchCreate(PDEVICE_OBJECT pObject,PIRP pIrp);
NTSTATUS DispatchRead(PDEVICE_OBJECT pObject,PIRP pIrp);
NTSTATUS DispatchWrite(PDEVICE_OBJECT pObject,PIRP pIrp);
NTSTATUS DispatchIoctrl(PDEVICE_OBJECT pObject,PIRP pIrp);


// -----------------------------驱动卸载----------------------------------
VOID DriverUnload(PDRIVER_OBJECT pDriverObject)

驱动必须有设备名和符号链接

// 设备名 必须以\\device开头,后面的可以随意取 L表示Uncoid的宽字符
#define DEVICE_NAME L"\\device\\ntmodeldrv"     // 设备对象
// 符号链接 必须以\\dosdevices开头,或者以\\??开头也可以
#define LINK_NAME L"\\dosdevices\\ntmodeldrv"   // 符号连接

设备对象用于接收R3的IRP.而R3只有通过符号链接,才能找到R0中的驱动.从而下发IRP请求

DriverEntry()

DriverEntry()函数主要做3件事:创建设备对象、创建符号链接、初始化和注册分发函数

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,
                 PUNICODE_STRING pRegPath)
{
    UNICODE_STRING uDeviceName = {0};       // 设备对象名
    UNICODE_STRING uLinkName = {0};         // 符号连接
    NTSTATUS ntStatus = 0;
    PDEVICE_OBJECT pDeviceObject = NULL;    // 设备对象指针
    ULONG i = 0;

    DbgPrint("Driver load begin\n");
    // 设备对象用于接收R3的IRP
    RtlInitUnicodeString(&uDeviceName,DEVICE_NAME);
    // 符号链接 只有通过符号链接,R3才能找到驱动
    RtlInitUnicodeString(&uLinkName,LINK_NAME);

    //使用IoCreateDevice()创建设备对象
    // @param DriverObject 驱动对象,创建设备对象需要根据驱动内核对象创建
    // @param DeviceExtensionSize 设备扩展大小:驱动在创建设备对象时允许给设备对象指定一个空间,
    //                            这个设备扩展空间可以用来存放一些和设备对象相关的一些数据.这
    //                            里我们用不到所以指定为0
    // @param DeviceName 设备对象名
    // @param DeviceType 设备类型
    // @param DeviceCharacteristics 设备特性,目前设为0
    // @param Exclusive 是否独有 表示设备对象创建之后是否是独占的,是否允许进程独占.
    //                  true表示这个设备对象在R3只能被一个进程打开,其他进程无法打开,这样做是    为了提高驱动的安全性
    //                  false表示可以由多个进程打开
    // @param DeviceObject 设备对象指针,传一个指针,是一个输出参数,这个指针就指向新创建的设备对象
    //                     可以通过pDeviceObject来访问他
    // return ntStatus 是否创建成功的状态码.只有0表示成功,其他值可参见说名文档
ntStatus = IoCreateDevice(pDriverObject,0,&uDeviceName,FILE_DEVICE_UNKNOWN,0,FALSE,&pDeviceObject);
    
    //  判断是否创建成功,未成功打印错误码
    if (!NT_SUCCESS(ntStatus))
    {
    DbgPrint("IoCreateDevice failed:%x",ntStatus);
    return ntStatus;
    }
    
    // 规定R3和R0之间read和write的通信方式:
    // do_buffered_io : IoManager会在内核空间分配一个buffer,然后把R3发送的数据拷贝到buffer中
    //                  R0直接从IM分配的buffer中读取数据.内核对数据处理完成后把数据放入buffer中
    //                  ,由IM负责把数据返回给R3,最安全的通讯方式,但是效率低

    // direct_io :  R3通过IoManager从物理内存中找到一块空间,把R3存放数据的虚拟内存通过MDL(Memory Description List)
    //              映射到物理内存中并锁定这块物理内存.R0通过MDL把这块物理内存映射为内核中的虚拟地址,等于说R0和R3
    //              共享一块物理内存.这种方式比do_buffered_io效率更高,这种方式主要用于数据量大时.比如显卡等

    // neither_io : R0 直接访问R3的内存地址,但需要满足一下几点要求:
    //                  1.要保证R3和R0要处在同一个进程上下文
    //                  2.读操作要调用probeForRead()函数对内存进行校验
    //                  3.写操作要调用probeForWrite()函数对内存进行校验
    //                  4.必须把校验操作放在try{}excepted结构化异常中,一旦发生异常,可以将其捕获,保证程序的稳定性
    //              其实就是为了校验这个内存是否是R3的内存地址和对齐方式等; 是最不安全的方式
    // 设置这个Flag主要是用来规定Read和Write的通讯方式,就是说当R3和R0通过Read和Write这两个API进行通讯时,就是按照
    // Flag设置的方式进行通讯的

    /*
        IoCreateDevice()函数在创建DeviceObject时会为obj打上DO_DEVICE_INITIALIZING标志
        目的是为了防止其他组件在驱动程序完成初始化设备之前向设备发送IO请求,
        表示当前这个设备还没有初始化完成,R3或其他的驱动程序不要发送IRP过来

        清除: 如果我们是在DriverEntry()中创建的设备对象,由IOManager负责清除该标志,
              其他对象创建的设备对象,由驱动程序(自己)负责清除
    */
    pDeviceObject->Flags |= DO_BUFFERED_IO;

    // 创建符号链接 创建了符号链接R3才能够"看到"驱动对象,如果没有创建符号链接,R3的程序是无法访问驱动的
    ntStatus = IoCreateSymbolicLink(&uLinkName,&uDeviceName);
    if (!NT_SUCCESS(ntStatus))
    {
        // 如果没有创建成功就需要删除已经创建的设备对象
        IoDeleteDevice(pDeviceObject);
        DbgPrint("IoCreateSymbolicLink failed:%x\n",ntStatus);
        return ntStatus;
    }


    // 初始化驱动中的所有分发函数
    for(i = 0; i < IRP_MJ_MAXIMUM_FUNCTION + 1; i++)
    {
    // IRP_MJ_MAXIMUM_FUNCTION 代表内核驱动对象中分发函数的总个数
    // 最大是0x1b(27) + 1 个.
    // 分发函数都存放在MajorFunction这个数组中
    /* 这个循环就是把驱动对象中的分发函数初始化成一个公用的分发函数 */
    pDriverObject->MajorFunction[i] = DispatchCommon;
    }

    // 单独实现需要的分发函数,比如DispatchCreate(),如果写主防,这个函数就可以用来拦截应用层文件的创建和打开
    pDriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;
    pDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;   // 可以用来拦截读取操作
    pDriverObject->MajorFunction[IRP_MJ_WRITE] = DispatchWrite; // 拦截写
    pDriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
    pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = DispatchClean;
    pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoctrl;   // 可以做设备控制

    pDriverObject->DriverUnload = DriverUnload;

    DbgPrint("Driver load ok!\n");

    return STATUS_SUCCESS;
}

分发函数,这里主要贴出DispatchRead DispatchWrite DispatchIoControl

DispatchRead()

NTSTATUS DispatchRead(PDEVICE_OBJECT pObject ,PIRP pIrp){
    PVOID pReadBuffer = NULL;   // buffer的首地址
    ULONG uReadLength = 0;  // buffer的长度
    PIO_STACK_LOCATION pStack = NULL;
    ULONG uMin = 0;
    ULONG uHelloStrLength = 0;  // 字符串的长度

    // 获取buffer
    pReadBuffer = pIrp->AssociatedIrp.SystemBuffer;
    
    // 获取IRP中的栈指针
    pStack = IoGetCurrentIrpStackLocation(pIrp);    
    // 获取Buffer的长度
    uReadLength = pStack->Parameters.Read.Length;

    // 向Buffer中填充数据,在内核中拷贝数据用RtlCopyMemory()函数
    /* 
      在内核后去字符串长度要用wcslen(宽字符串长度) 
      + 1 表示'\0'字符
      *(乘) sizeof(WCHAR) : 在内核中一个字符占用2个字节,所以长度*(WCHAR)占用的字节数
    */
    uHelloStrLength = (wcslen(L"hello world") + 1) * sizeof(WCHAR);     // 获取str的长度
    uMin = uReadLength > uHelloStrLength ? uHelloStrLength : uReadLength;
    RtlCopyMemory(pReadBuffer, L"hello world",uMin);

    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = uMin;

    IoCompleteRequest(pIrp,IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

DispatchWrite()

  NTSTATUS DispatchWrite(PDEVICE_OBJECT pObject ,PIRP pIrp){
    PVOID pWriterBuffer = NULL;
    ULONG uWriteLength = 0;
    PIO_STACK_LOCATION pStack = NULL;
    PVOID pBuffer = NULL;

    pWriterBuffer = pIrp->AssociatedIrp.SystemBuffer;
    
    pStack = IoGetCurrentIrpStackLocation(pIrp);
    uWriteLength = pStack->Parameters.Write.Length;

    /*
        ExAllocatePoolWithTag() R0分配内存函数

        @param POOL_TYPE PoolType 内存池类型,常用的有NonPagedPool和PagedPool
                                  NonPagedPool(非分页内存池):这种类型内存池中的内存是长期不会被切换出去的,
                                  会被一直锁住,访问非分页内存不会发生缺页中断,非分页内存池中的内存可以
                                  在驱动中的任何场景下使用.非分页内存池中的内存是有限的,大约100~200M左右

                                  PagedPool(分页内存池):这种方式分配的内存有可能会发生缺页中断,有可能被
                                  切换出去.分页内存池中分配的内存只能在IRQL为PASSIVE这种运行环境中使用
                                  ,分发函数都是PASSIVE级别(无中断级别),IRQL(中断请求运行级别)
                                  
            
        @param __in SIZE_T NumberOfBytes : 分配内存的长度

        @param __in ULONG Tag : 标签,标签最多不能超过4个字节,用单引号包裹'tset'
                                可以用这个Tag来标志我们分配的这块内存,用来跟踪这块内存,
                                如果内存泄漏,Windbug中有专门的工具可以通过这些Tag来跟踪我们分配的内存,
                                从而得知分配内存的使用情况.

    */
    pBuffer = ExAllocatePoolWithTag(PagedPool,uWriteLength,'TSET');
    // 判断内存是否分配成功
    if (pBuffer == NULL)
    {
        // 结束IRP
        pIrp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES;      // 资源不足
        pIrp->IoStatus.Information = 0;
        IoCompleteRequest(pIrp,IO_NO_INCREMENT);
        return STATUS_INSUFFICIENT_RESOURCES;
    }

//  printf/scanf/fopen/fclose/fwrite/fread/malloc/free不能用
//  sprintf/strlen/strcpy/wcslen/wcscpy/memcpy/memset可用但尽量不要调用
    memset(pBuffer,0,uWriteLength);

    RtlCopyMemory(pBuffer,pWriterBuffer,uWriteLength);

    ExFreePool(pBuffer);
    pBuffer = NULL;

    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = uWriteLength;

    IoCompleteRequest(pIrp,IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

DispatchIoctrl()

// --------------------------------------------------------------控制码----------------------------------------------------------

/*
    通过微软WDK框架提供的CTL_CODE()函数定义控制码
*/

#define IOCTRL_BASE 0x800       // 控制码基址

/*
    CTL_CODE( DeviceType, Function, Method, Access )
    @param DeviceType : 设备类型,要和在DriverEntry中创建设备对象时传的设备类型一样

    @param Function : 这个功能号是一个32位的整数,是由基址 + 自己定义的功能号组成的
                      微软专门给开发者留了一个空间,这个空间专门用来定义控制码,这个
                      空间的起始地址就是0x800,如果自己定义的控制码是1,那么结果就是
                      0x800 + 1 = 0x801

    @param Method : 通讯方式.在DriverEntry中定义的通信方式意义相同.在DriverEntry中定
                    义的值只能作用于Read和Write操作,DeviceIoControl需要重新定义.通讯
                    方式是相同的.

                    METHOD_BUFFERED: do_buffer方式
                    使用这种方式进行通讯时,IOManager分配的内存位置,可以在pIrp->AssociatedIrp.SystemBuffer 中获取

                    METHOD_IN_DIRECT:  (IN 是input输入的意思)
                    输入的数据,在pIrp->AssociatedIrp.SystemBuffer 中获取
                    METHOD_OUT_DIRECT:
                    输出的数据,需要放在pIrp->MdlAddress中.通过MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
                    把IRP中MdlAddress这个物理地址映射为R0中的虚拟地址,然后把R0中的数据拷贝进去.MdlAddress指向的
                    是一个物理内存
                    IN_DIRECT和OUT_DIRECT的区别是:当以只读权限打开设备时,METHOD_IN_RIRECT方式的IoControl会成功
                    而METHOD_OUT_DIRECT方式将会失败.如果是读写权限打开设备,两种方式都会成功

                    METHOD_NEITHER:
                    In(输入的数据):在Stack->Parameters.DeviceIoControl.Type3InputBuff中获取
                    Out(输出的数据): pIrp->UserBuffer;
                    使用这种通讯方式是,一定要ProbeForRead和ProbeForWrite

    @param Access : 表示访问权限,可读可写
*/
#define MYIOCTRL_CODE(i) \
    CTL_CODE(FILE_DEVICE_UNKNOWN, IOCTRL_BASE + i,     METHOD_BUFFERED,FILE_ANY_ACCESS)

// #define CTL_HELLO MYIOCTRL_CODE(0)
#define CTL_HELLO MYIOCTRL_CODE(0)
#define CTL_PRINT MYIOCTRL_CODE(1)
#define CTL_BYE MYIOCTRL_CODE(2)

//---------------------------------------------------------------函数体----------------------------------------------------------

NTSTATUS DispatchIoctrl(PDEVICE_OBJECT pObject, PIRP pIrp){

    ULONG uControlCode = 0;
    PVOID pInputBuff = NULL;
    PVOID pOutputBuffer = NULL; 
    ULONG uInputLength = 0;
    ULONG uOutputLength = 0;
    PIO_STACK_LOCATION pStack = NULL;

    // 虽然在R3中,Input和Output是2个Buffer,但在R0中是同一个
    pInputBuff = pOutputBuffer = pIrp->AssociatedIrp.SystemBuffer;

    pStack = IoGetCurrentIrpStackLocation(pIrp);
    uInputLength = pStack->Parameters.DeviceIoControl.InputBufferLength;
    uOutputLength = pStack->Parameters.DeviceIoControl.OutputBufferLength;

    uControlCode = pStack->Parameters.DeviceIoControl.IoControlCode;

    switch (uControlCode)
    {
    case CTL_HELLO:
        DbgPrint("Hello IoControl\n");
        break;
    case CTL_PRINT:
        DbgPrint("%ws\n",pInputBuff);
        break;
    case CTL_BYE:
        DbgPrint("Goodbye IoControl\n");
        break;
    default:
        DbgPrint("Unknown IoControl\n");
    }

    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest(pIrp,IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}

最后贴出完整的代码:

#include <ntddk.h>      // #include <ntddk.h>是驱动必须包含的头文件

// 设备名 必须以\\device开头,后面的可以随意取 L表示Uncoid的宽字符
#define DEVICE_NAME L"\\device\\ntmodeldrv"     // 设备对象
// 符号链接 必须以\\dosdevices开头,或者以\\??开头也可以
#define LINK_NAME L"\\dosdevices\\ntmodeldrv"   // 符号连接

// ------------------------------创建自己的控制码----------------------------
/*
    通过微软WDK框架提供的CTL_CODE()函数定义控制码
*/

#define IOCTRL_BASE 0x800       // 控制码基址

/*
CTL_CODE( DeviceType, Function, Method, Access )
@param DeviceType : 设备类型,要和在DriverEntry中创建设备对象时传的设备类型一样

@param Function : 这个功能号是一个32位的整数,是由基址 + 自己定义的功能号组成的
                  微软专门给开发者留了一个空间,这个空间专门用来定义控制码,这个
                  空间的起始地址就是0x800,如果自己定义的控制码是1,那么结果就是
                  0x800 + 1 = 0x801

@param Method : 通讯方式.在DriverEntry中定义的通信方式意义相同.在DriverEntry中定
                义的值只能作用于Read和Write操作,DeviceIoControl需要重新定义.通讯
                方式是相同的.

                METHOD_BUFFERED: do_buffer方式
                使用这种方式进行通讯时,IOManager分配的内存位置,可以在pIrp->AssociatedIrp.SystemBuffer 中获取

                METHOD_IN_DIRECT:  (IN 是input输入的意思)
                输入的数据,在pIrp->AssociatedIrp.SystemBuffer 中获取
                METHOD_OUT_DIRECT:
                输出的数据,需要放在pIrp->MdlAddress中.通过MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
                把IRP中MdlAddress这个物理地址映射为R0中的虚拟地址,然后把R0中的数据拷贝进去.MdlAddress指向的
                是一个物理内存
                IN_DIRECT和OUT_DIRECT的区别是:当以只读权限打开设备时,METHOD_IN_RIRECT方式的IoControl会成功
                而METHOD_OUT_DIRECT方式将会失败.如果是读写权限打开设备,两种方式都会成功

                METHOD_NEITHER:
                In(输入的数据):在Stack->Parameters.DeviceIoControl.Type3InputBuff中获取
                Out(输出的数据): pIrp->UserBuffer;
                使用这种通讯方式是,一定要ProbeForRead和ProbeForWrite

@param Access : 表示访问权限,可读可写
*/
#define MYIOCTRL_CODE(i) \
    CTL_CODE(FILE_DEVICE_UNKNOWN, IOCTRL_BASE + i,     METHOD_BUFFERED,FILE_ANY_ACCESS)

// #define CTL_HELLO MYIOCTRL_CODE(0)
#define CTL_HELLO MYIOCTRL_CODE(0)
#define CTL_PRINT MYIOCTRL_CODE(1)
#define CTL_BYE MYIOCTRL_CODE(2)

/*
R3通过OpenFile或CreateFile发送IRP后,就会被DispatchCreate()函数接收. 
R3会把这个驱动当做一个特殊的文件来打开,所以驱动加载后,在系统中也可以认为是一个特殊的文件.
R3会把这个驱动当做文件打开,进行读写操作.所以驱动必须有一个DispatchCreate函数,用于R3打开我们的驱动
并不是说在在磁盘上真正创建一个文件,只是把驱动当做特殊文件处理,从而把驱动打开,在函数中只需要返回
STATUS_SUCCESS,那么R3就能打开这个驱动文件.并且系统就会为R3的进程分配一个文件句柄,供R3的客户端处理
@param pObject 设备对象
@param pIrp 应用程序下发的irp
*/
NTSTATUS DispatchCreate(PDEVICE_OBJECT pObject,PIRP pIrp){

pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;

//结束IRp
IoCompleteRequest(pIrp,IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}
// R3发送的IRP会被设备对象接收,然后由设备对象调用分发函数来处理IRP
// 这个函数内部没有做任何操作,这个函数存在的意义就相当于"int i = 0" "int *i = NULL"
// 和初始化变量一样,真正要用的函数要单独进行处理
NTSTATUS DispatchCommon(PDEVICE_OBJECT pObject,PIRP pIrp){

// 这个成功是返回给R3的,因为R3也在等这次IRP处理的结果
pIrp->IoStatus.Status = STATUS_SUCCESS;
// 表示Io的一些额外信息.
// 比如在读写操作时表示实际读写的字节数
// 在其他地方可能有更多的意思
pIrp->IoStatus.Information = 0;

// 结束掉这个IRP
IoCompleteRequest(pIrp,IO_NO_INCREMENT);

// 这个return返回值是供IOManager使用,向IO框架提供处理结果
return STATUS_SUCCESS;
}
    
/*
DispatchRead()主要用于处理R3发起的读请求
BOOL WINAPI ReadFile(
_In_        HANDLE       hFile,                 // 文件句柄
_Out_       LPVOID       lpBuffer,              // 读文件的缓存
_In_        DWORD        nNumberOfBytesToRead,  // 指定这个Buffer的长度(打算读多少个字节)
_Out_opt_   LPDWORD      lpNumberOfBytesRead,   // 实际读取的字节数
_Inout_opt_ LPOVERLAPPED lpOverlapped           // 做异步操作的
);
*/
NTSTATUS DispatchRead(PDEVICE_OBJECT pObject ,PIRP pIrp){

PVOID pReadBuffer = NULL;   // buffer的首地址
ULONG uReadLength = 0;  // buffer的长度
PIO_STACK_LOCATION pStack = NULL;
ULONG uMin = 0;
ULONG uHelloStrLength = 0;  // 字符串的长度

// 获取buffer
pReadBuffer = pIrp->AssociatedIrp.SystemBuffer;

// 获取IRP中的栈指针
pStack = IoGetCurrentIrpStackLocation(pIrp);    
// 获取Buffer的长度
uReadLength = pStack->Parameters.Read.Length;

// 向Buffer中填充数据,在内核中拷贝数据用RtlCopyMemory()函数
/* 
    在内核后去字符串长度要用wcslen(宽字符串长度) 
    + 1 表示'\0'字符
* sizeof(WCHAR) : 在内核中一个字符占用2个字节,所以长度*(WCHAR)占用的字节数
*/
uHelloStrLength = (wcslen(L"hello world") + 1) * sizeof(WCHAR);     // 获取str的长度
uMin = uReadLength > uHelloStrLength ? uHelloStrLength : uReadLength;
RtlCopyMemory(pReadBuffer, L"hello world",uMin);

pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = uMin;

IoCompleteRequest(pIrp,IO_NO_INCREMENT);

return STATUS_SUCCESS;
}


/*
BOOL WINAPI WriteFile(
_In_        HANDLE       hFile,             // 句柄
_In_        LPCVOID      lpBuffer,          // IoManager分配的Buffer
_In_        DWORD        nNumberOfBytesToWrite, // 要写的字节数
_Out_opt_   LPDWORD      lpNumberOfBytesWritten,    // 实际写入的字节数
_Inout_opt_ LPOVERLAPPED lpOverlapped
);
*/
NTSTATUS DispatchWrite(PDEVICE_OBJECT pObject ,PIRP pIrp){

PVOID pWriterBuffer = NULL;
ULONG uWriteLength = 0;
PIO_STACK_LOCATION pStack = NULL;
PVOID pBuffer = NULL;

pWriterBuffer = pIrp->AssociatedIrp.SystemBuffer;

pStack = IoGetCurrentIrpStackLocation(pIrp);
uWriteLength = pStack->Parameters.Write.Length;

/*
    ExAllocatePoolWithTag() R0分配内存函数

    @param POOL_TYPE PoolType 内存池类型,常用的有NonPagedPool和PagedPool
                              NonPagedPool(非分页内存池):这种类型内存池中的内存是长期不会被切换出去的,
                              会被一直锁住,访问非分页内存不会发生缺页中断,非分页内存池中的内存可以
                              在驱动中的任何场景下使用.非分页内存池中的内存是有限的,大约100~200M左右

                              PagedPool(分页内存池):这种方式分配的内存有可能会发生缺页中断,有可能被
                              切换出去.分页内存池中分配的内存只能在IRQL为PASSIVE这种运行环境中使用
                              ,分发函数都是PASSIVE级别(无中断级别),IRQL(中断请求运行级别)
                              
        
    @param __in SIZE_T NumberOfBytes : 分配内存的长度

    @param __in ULONG Tag : 标签,标签最多不能超过4个字节,用单引号包裹'tset'
                            可以用这个Tag来标志我们分配的这块内存,用来跟踪这块内存,
                            如果内存泄漏,Windbug中有专门的工具可以通过这些Tag来跟踪我们分配的内存,
                            从而得知分配内存的使用情况.

*/
pBuffer = ExAllocatePoolWithTag(PagedPool,uWriteLength,'TSET');
// 判断内存是否分配成功
if (pBuffer == NULL)
{
    // 结束IRP
    pIrp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES;      // 资源不足
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest(pIrp,IO_NO_INCREMENT);
    return STATUS_INSUFFICIENT_RESOURCES;
}

//  printf/scanf/fopen/fclose/fwrite/fread/malloc/free不能用
//  sprintf/strlen/strcpy/wcslen/wcscpy/memcpy/memset可用但尽量不要调用
    memset(pBuffer,0,uWriteLength);

RtlCopyMemory(pBuffer,pWriterBuffer,uWriteLength);

ExFreePool(pBuffer);
pBuffer = NULL;

pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = uWriteLength;

IoCompleteRequest(pIrp,IO_NO_INCREMENT);

return STATUS_SUCCESS;
}

/*
BOOL WINAPI DeviceIoControl(
_In_        HANDLE       hDevice,               // 文件句柄
_In_        DWORD        dwIoControlCode,       // 控制码,子功能号
_In_opt_    LPVOID       lpInBuffer,            // 向驱动发送数据是,数据存储在inputBuffer中
_In_        DWORD        nInBufferSize,         // inputBuffer的长度
_Out_opt_   LPVOID       lpOutBuffer,           // 驱动向调用者返回的数据存放在这个outputBuffer中
_In_        DWORD        nOutBufferSize,        // 返回数据Buffer的长度
_Out_opt_   LPDWORD      lpBytesReturned,       // 实际传输的字节数
_Inout_opt_ LPOVERLAPPED lpOverlapped
);
*/
NTSTATUS DispatchIoctrl(PDEVICE_OBJECT pObject, PIRP pIrp){

ULONG uControlCode = 0;
PVOID pInputBuff = NULL;
PVOID pOutputBuffer = NULL; 
ULONG uInputLength = 0;
ULONG uOutputLength = 0;
PIO_STACK_LOCATION pStack = NULL;

// 虽然在R3中,Input和Output是2个Buffer,但在R0中是同一个
pInputBuff = pOutputBuffer = pIrp->AssociatedIrp.SystemBuffer;

pStack = IoGetCurrentIrpStackLocation(pIrp);
uInputLength = pStack->Parameters.DeviceIoControl.InputBufferLength;
uOutputLength = pStack->Parameters.DeviceIoControl.OutputBufferLength;

uControlCode = pStack->Parameters.DeviceIoControl.IoControlCode;

switch (uControlCode)
{
case CTL_HELLO:
    DbgPrint("Hello IoControl\n");
    break;
case CTL_PRINT:
    DbgPrint("%ws\n",pInputBuff);
    break;
case CTL_BYE:
    DbgPrint("Goodbye IoControl\n");
    break;
default:
    DbgPrint("Unknown IoControl\n");
}

pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp,IO_NO_INCREMENT);

return STATUS_SUCCESS;
}

NTSTATUS DispatchClose(PDEVICE_OBJECT pObject ,PIRP pIrp){

pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;

//结束IRp
IoCompleteRequest(pIrp,IO_NO_INCREMENT);

return STATUS_SUCCESS;
}

NTSTATUS DispatchClean(PDEVICE_OBJECT pObject ,PIRP pIrp){

pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;

//结束IRp
IoCompleteRequest(pIrp,IO_NO_INCREMENT);

return STATUS_SUCCESS;
}

/*
    驱动卸载函数(无返回值)
    主要功能是用用来清理DriverEntry中创建的设备对象、符号链接等分配的资源
*/
VOID DriverUnload(PDRIVER_OBJECT pDriverObject){

// 创建符号链接
// 初始化符号链接
// 删除符号链接
UNICODE_STRING uLinkName = {0};
RtlInitUnicodeString(&uLinkName,LINK_NAME);
IoDeleteSymbolicLink(&uLinkName);

//删除设备对象
// 当在DriverEntry中完成DeviceObject后,设备对象会保存在DriverObject(驱动对象)的一个链里,
// DeviceObject可以在DriverObject这个链中获得
IoDeleteDevice(pDriverObject->DeviceObject);

DbgPrint("Driver unloaded\n");
}

/*
驱动入口
在DriverEntry主要做3件事
一:创建设备对象
二:创建符号链接
三:初始化、注册分发函数
@param pDriverObject 
@param pRegPath 驱动在注册表中的路径
*/
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,
                 PUNICODE_STRING pRegPath)
{
UNICODE_STRING uDeviceName = {0};       // 设备对象名
UNICODE_STRING uLinkName = {0};         // 符号连接
NTSTATUS ntStatus = 0;
PDEVICE_OBJECT pDeviceObject = NULL;    // 设备对象指针
ULONG i = 0;

DbgPrint("Driver load begin\n");
// 设备对象用于接收R3的IRP
RtlInitUnicodeString(&uDeviceName,DEVICE_NAME);
// 符号链接 只有通过符号链接,R3才能找到驱动
RtlInitUnicodeString(&uLinkName,LINK_NAME);

//使用IoCreateDevice()创建设备对象
// @param DriverObject 驱动对象,创建设备对象需要根据驱动内核对象创建
// @param DeviceExtensionSize 设备扩展大小:驱动在创建设备对象时允许给设备对象指定一个空间,
//                            这个设备扩展空间可以用来存放一些和设备对象相关的一些数据.这
//                            里我们用不到所以指定为0
// @param DeviceName 设备对象名
// @param DeviceType 设备类型
// @param DeviceCharacteristics 设备特性,目前设为0
// @param Exclusive 是否独有 表示设备对象创建之后是否是独占的,是否允许进程独占.
//                  true表示这个设备对象在R3只能被一个进程打开,其他进程无法打开,这样做是为了提高驱动的安全性
//                  false表示可以由多个进程打开
// @param DeviceObject 设备对象指针,传一个指针,是一个输出参数,这个指针就指向新创建的设备对象
//                     可以通过pDeviceObject来访问他
// return ntStatus 是否创建成功的状态码.只有0表示成功,其他值可参见说名文档
ntStatus = IoCreateDevice(pDriverObject,0,&uDeviceName,FILE_DEVICE_UNKNOWN,0,FALSE,&pDeviceObject);

//  判断是否创建成功,未成功打印错误码
if (!NT_SUCCESS(ntStatus))
{
    DbgPrint("IoCreateDevice failed:%x",ntStatus);
    return ntStatus;
}

// 规定R3和R0之间read和write的通信方式:
// do_buffered_io : IoManager会在内核空间分配一个buffer,然后把R3发送的数据拷贝到buffer中
//                  R0直接从IM分配的buffer中读取数据.内核对数据处理完成后把数据放入buffer中
//                  ,由IM负责把数据返回给R3,最安全的通讯方式,但是效率低

// direct_io :  R3通过IoManager从物理内存中找到一块空间,把R3存放数据的虚拟内存通过MDL(Memory Description List)
//              映射到物理内存中并锁定这块物理内存.R0通过MDL把这块物理内存映射为内核中的虚拟地址,等于说R0和R3
//              共享一块物理内存.这种方式比do_buffered_io效率更高,这种方式主要用于数据量大时.比如显卡等

// neither_io : R0 直接访问R3的内存地址,但需要满足一下几点要求:
//                  1.要保证R3和R0要处在同一个进程上下文
//                  2.读操作要调用probeForRead()函数对内存进行校验
//                  3.写操作要调用probeForWrite()函数对内存进行校验
//                  4.必须把校验操作放在try{}excepted结构化异常中,一旦发生异常,可以将其捕获,保证程序的稳定性
//              其实就是为了校验这个内存是否是R3的内存地址和对齐方式等; 是最不安全的方式
// 设置这个Flag主要是用来规定Read和Write的通讯方式,就是说当R3和R0通过Read和Write这两个API进行通讯时,就是按照
// Flag设置的方式进行通讯的

/*
    IoCreateDevice()函数在创建DeviceObject时会为obj打上DO_DEVICE_INITIALIZING标志
    目的是为了防止其他组件在驱动程序完成初始化设备之前向设备发送IO请求,
    表示当前这个设备还没有初始化完成,R3或其他的驱动程序不要发送IRP过来

    清除: 如果我们是在DriverEntry()中创建的设备对象,由IOManager负责清除该标志,
          其他对象创建的设备对象,由驱动程序(自己)负责清除
*/
pDeviceObject->Flags |= DO_BUFFERED_IO;

// 创建符号链接 创建了符号链接R3才能够"看到"驱动对象,如果没有创建符号链接,R3的程序是无法访问驱动的
ntStatus = IoCreateSymbolicLink(&uLinkName,&uDeviceName);
if (!NT_SUCCESS(ntStatus))
{
    // 如果没有创建成功就需要删除已经创建的设备对象
    IoDeleteDevice(pDeviceObject);
    DbgPrint("IoCreateSymbolicLink failed:%x\n",ntStatus);
    return ntStatus;
}


// 初始化驱动中的所有分发函数
for(i = 0; i < IRP_MJ_MAXIMUM_FUNCTION + 1; i++)
{
    // IRP_MJ_MAXIMUM_FUNCTION 代表内核驱动对象中分发函数的总个数
    // 最大是0x1b(27) + 1 个.
    // 分发函数都存放在MajorFunction这个数组中
    /* 这个循环就是把驱动对象中的分发函数初始化成一个公用的分发函数 */
    pDriverObject->MajorFunction[i] = DispatchCommon;
}

// 单独实现需要的分发函数,比如DispatchCreate(),如果写主防,这个函数就可以用来拦截应用层文件的创建和打开
pDriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;
pDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;   // 可以用来拦截读取操作
pDriverObject->MajorFunction[IRP_MJ_WRITE] = DispatchWrite; // 拦截写
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = DispatchClean;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoctrl;   // 可以做设备控制

pDriverObject->DriverUnload = DriverUnload;

DbgPrint("Driver load ok!\n");

return STATUS_SUCCESS;
}
上一篇下一篇

猜你喜欢

热点阅读