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

驱动框架

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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 如果你看完书中的所有例子,你很可能已经做完你的实验和在已经越狱的iPhone上的研究。因为和许多人一样,几乎所有的...
    fishmai0阅读 15,293评论 2 42
  • 如果驱动程序要和应用程序通信,那么要生成一个设备对象. 设备对象和分发函数构成了整个内核体系的基本框架. 设备对象...
    f675b1a02698阅读 1,187评论 0 0
  • 一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于...
    SeanCST阅读 7,669评论 0 27
  • feisky云计算、虚拟化与Linux技术笔记posts - 1014, comments - 298, trac...
    不排版阅读 3,754评论 0 5
  • 前言 本文翻译自iOS Assembly Tutorial: Understanding ARM 翻译的不对的地...
    桃红宿雨阅读 17,890评论 7 198