NT驱动框架
内核层代码
入口函数DriverEntry
- 1.初始化驱动对象和符号链接;
- 2.创建设备对象;
- 3.对刚创建的设备对象指定一个通信方式;
- 4.创建符号链接
- 5.注册分发函数
- 6.注册卸载函数
/**
* Copyright (c) 2022, 源代码已同步到gitee(https://gitee.com/ciscco/system_secure)
* All rights reserved.
*
* @file ntdrv.c
* @version v0.1
* @author cisco(微信公众号:坚毅猿)
* @date 2022-02-13 13:52
*
* @brief
* @note
* @see //www.greatytc.com/p/cba34b3ad6de
*/
#include <ntddk.h>
/// @brief 设备对象名 "\\device(固定不能变)\\自己任意取",符号链接名 "\\dosdevices(固定定不变或者可以使用"??"代替"dosdevices")\\自己任意取";
/// @note "L"表示这里定义的是宽字节字符串,即"WCHAR *"或者"wchar_t *",但在驱动中统一使用的字符串类型是"UNICODE_STRING",所以需要调用函数"RtlInitUnicodeString(&uDeviceName, DEVICE_NAME)"来转换
/// "\\"代表"\"(是因为第一个'\'会被被识别成转义字符的标志,这是windows中表示路径的常规写法)
/// @see DriverEntry()
/// @warning 但最好设备对象名、符号链接名和驱动名取相同名字,避免混淆。因为不清楚什么时候使用对应的名字往往会导致驱动加载出问题;
#define DEVICE_NAME L"\\device\\ntmodeldrv"
#define LINK_NAME L"\\dosdevices\\ntmodeldrv"
#define IOCTRL_BASE 0x800 ///< 控制码的起始值,比如有5个控制码,第一个为0x800,第二个0x801,第三个0x802 ...
/// MYIOCTRL_CODE 用来定义控制码
/// FILE_DEVICE_UNKNOWN 在DriveEntry中定义的设备对象的类型
/// METHOD_BUFFERED 通信协议,这里使用的是buffer io
/// @warning 在DriveEntry中指定设备对象的r3和r0通信方式只是用来规定Read和Write,
/// 管不了DispatchIoctrl,DispatchIoctrl的通信协议是由这里的控制码METHOD_XXX来决定的
/// FILE_ANY_ACCESS 所以的权限,包含读和写
#define MYIOCTRL_CODE(i) \
CTL_CODE(FILE_DEVICE_UNKNOWN, IOCTRL_BASE+i, METHOD_BUFFERED,FILE_ANY_ACCESS)
/// 三个是r3和r0进行额外通信的控制码
/// @warning 要保证内核层和应用层定义一致,一般把这部分代码放在应用层和内核层共享的公共头文件中
/// @note 可以自定义更多的控制码,为了保证通信安全,也可以对控制码进行加密
///
#define CTL_HELLO MYIOCTRL_CODE(0)
#define CTL_PRINT MYIOCTRL_CODE(1)
#define CTL_BYE MYIOCTRL_CODE(2)
/**
* @brief DispatchCommon 为所有分发函数注册通用的分发函数,即对Irp请求不做任何处理直接返回,eg3:好比对一个变量初始化为0;[^13]
* @param[in] pObject 设备对象,在DriveEntry调用IoCreateDevice所创造设备对象,指向 DRIVER_OBJECT 结构;
* @param[in] pIrp IO REQUST PASKET缩写,Irp数据包封装应用层发下的数据和命令。[^13]
* @return ntStatus 就不是放回给应用层的,而是返回给IO管理器的;
* @warning 名字可以随便起,但接口定义,参数类型必须一样
* @warning 在内核层会调用各种IO函数会有一个返回状态,这个返回状态就是存放在这里,从而放回给应用层;而在内核层也有各种函数返回值,
* 比如DriverEntry中的"return STAUS_SUCCESS",就不是放回给应用层的,而是返回给IO管理器的;
* @see
* @author cisco(微信公众号:坚毅猿)
* @date 2022-01-22 22:09
*/
NTSTATUS DispatchCommon(PDEVICE_OBJECT pObject, PIRP pIrp)
{
UNREFERENCED_PARAMETER(pObject);
pIrp->IoStatus.Status = STATUS_SUCCESS; ///< 将Irp的头部的Status设置为成功
pIrp->IoStatus.Information = 0; ///< 传出的字节数设置为0
IoCompleteRequest(pIrp, IO_NO_INCREMENT); ///< 把Irp终止掉
return STATUS_SUCCESS;
}
/**
* @brief DispatchCreate 发现 DispatchCreate、DispatchCommon、DispatchClean和DispatchClose都是一样的,
* 因为NT驱动是非常简单的单层驱动,只需要接受自己应用层创建和打开,直接返回成功即可,
* 文件就允许被创建了,然后分配句柄表,在句柄表里面增加一项,但在过滤驱动中稍复杂,
* 必须在DispatchCreate中监控别的进程对文件的创建和打开;
* @param[in] pObject 设备对象,在DriveEntry调用IoCreateDevice所创造设备对象,指向 DRIVER_OBJECT 结构;
* @param[in] pIrp IO REQUST PASKET缩写,Irp数据包封装应用层发下的数据和命令。[^13]
* @return ntStatus 就不是放回给应用层的,而是返回给IO管理器的;
* @warning 名字可以随便起,但接口定义,参数类型必须一样
* @warning 在内核层会调用各种IO函数会有一个返回状态,这个返回状态就是存放在这里,从而放回给应用层;而在内核层也有各种函数返回值,
* 比如DriverEntry中的"return STAUS_SUCCESS",就不是放回给应用层的,而是返回给IO管理器的;
* @warning 提权漏洞:一般驱动只允许自己的进程打开,不允许别的进程打开[^11]
* @see
* @author cisco(微信公众号:坚毅猿)
* @date 2022-02-15 22:09
*/
NTSTATUS DispatchCreate(PDEVICE_OBJECT pObject, PIRP pIrp)
{
UNREFERENCED_PARAMETER(pObject);
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;
/// 为了避免提权漏洞,可以在运行到这里之前拿到打开驱动的进程的pid,通过pid拿到这个进程的全路径,然后验证签名,
/// 如果是自己的签名则允许打开,否则就做安全校验。
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
/**
* @brief DispatchRead 读 是相对于应用层来说,数据流向:r0->R3;
* @param[in] pObject 设备对象,在DriveEntry调用IoCreateDevice所创造设备对象,指向 DRIVER_OBJECT 结构;
* @param[in] pIrp IO REQUST PASKET缩写,Irp数据包封装应用层发下的数据和命令。[^13];
* @return ntStatus 就不是放回给应用层的,而是返回给IO管理器的;
* @warning 名字可以随便起,但接口定义,参数类型必须一样;
* @warning 在内核层会调用各种IO函数会有一个返回状态,这个返回状态就是存放在这里,从而放回给应用层;而在内核层也有各种函数返回值,
* 比如DriverEntry中的"return STAUS_SUCCESS",就不是放回给应用层的,而是返回给IO管理器的;
* @warning 内存溢出可能会造成提权漏洞
* @see
* @author cisco(微信公众号:坚毅猿)
* @date 2022-02-15 22:09
*/
NTSTATUS DispatchRead(PDEVICE_OBJECT pObject, PIRP pIrp)
{
UNREFERENCED_PARAMETER(pObject);
PVOID pReadBuffer = NULL; ///< buffer io缓存的首地址
ULONG uReadLength = 0; ///< 应用层传下来要读取的数据长度
PIO_STACK_LOCATION pStack = NULL; ///< 当前驱动对应的栈
ULONG uMin = 0; ///< 最小长度
ULONG uHelloStr = 0; ///< 内核层想要上传的数据长度uHelloStr
uHelloStr = (ULONG)(wcslen(L"hello world") + 1) * sizeof(WCHAR); ///< 计算出"hello world"的长度 +1是因为'/0',即内核层想要上传的数据长度
/// 第一步,拿到缓存的地址和长度[^13]
/// 从头部拿缓存地址,因为创建设备对象之后,为设备对象指定的通信方式是buffer io,所以对应SystenBuffer
pReadBuffer = pIrp->AssociatedIrp.SystemBuffer;
/// 从栈上拿缓存长度
pStack = IoGetCurrentIrpStackLocation(pIrp); ///< 拿到属于当前驱动对应的栈
uReadLength = pStack->Parameters.Read.Length; ///< 拿到应用层传下来要读取的数据长度,是存放在联合体Parameters中结构体Read的Length成员中
/// 第二步:读,写等操作[^8]
uMin = uReadLength > uHelloStr ? uHelloStr : uReadLength; ///< 传两者中的最小值
RtlCopyMemory(pReadBuffer, L"hello world", uMin); ///< 是将L"hello world"拷贝到pReadBuffer中去,"#define RtlCopyMemory(Destination,Source,Length) memcpy((Destination),(Source),(Length))"
/// 为什么选择copy 应用层传下来要读取的数据长度uReadLength和内核层想要上传的数据长度uHelloStr 两者之间的最小值
/// 情况1:应用层传下来要读取的数据长度uReadLength大于应用层上传的数据长度uHelloStr,这时候如果拷贝uReadLength的长度,
/// 对于L"hello world"来说,紧接着L"hello world"后面的内存的数据是不确定的,如果是数据,那就拷多了浪费时间和空间,
/// 如果这部分内存是无效的(无效就是没有对应的物理内存,只有虚拟内存,没有物理内存,就会缺页错误,也就是虚拟内存无效),
/// 那就可能会导致系统奔溃,所以这种情况下这时候应该拷贝uHelloStr的长度,也就是两者中最小值。
/// @todo 补地址转换这块知识
/// 情况2:如果uReadLength小于uHelloStr,这时候如果拷贝uHelloStr的长度,这时候pReadBuffer没有足够容量容纳L"hello world",
/// 会造成写溢出,此时如果蓝屏则是系统保护,不蓝屏则有可能已经造成提权漏洞,所以这种情况下这时候应该拷贝uHelloStr的长度,也就是两者中最小值。
/// 综上所述, 应该拷贝两者中的最小值。
/// 第三步,完成IRP
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = uMin; ///< Irp头部的IoStatus.Information设置成实际上拷贝的数据长度;[^13]
IoCompleteRequest(pIrp, IO_NO_INCREMENT); ///< @warning 必须调用这个函数,如果不调用这个,会导致Irp不会被终止,相当于被挂起;
return STATUS_SUCCESS;
}
/**
* @brief DispatchWrite 写 是相对于应用层来说,数据流向:r3->R0;
* @param[in] pObject 设备对象,在DriveEntry调用IoCreateDevice所创造设备对象,指向 DRIVER_OBJECT 结构;
* @param[in] pIrp IO REQUST PASKET缩写,Irp数据包封装应用层发下的数据和命令。[^13];
* @return ntStatus 就不是放回给应用层的,而是返回给IO管理器的;
* @warning 名字可以随便起,但接口定义,参数类型必须一样;
* @warning 在内核层会调用各种IO函数会有一个返回状态,这个返回状态就是存放在这里,从而放回给应用层;
* 而在内核层也有各种函数返回值,比如DriverEntry中的"return STAUS_SUCCESS",就不是放回给应用层的,而是返回给IO管理器的;
* @warning 内存溢出可能会造成提权漏洞
* @see
* @author cisco(微信公众号:坚毅猿)
* @date 2022-02-15 22:09
*/
NTSTATUS DispatchWrite(PDEVICE_OBJECT pObject, PIRP pIrp)
{
UNREFERENCED_PARAMETER(pObject);
PVOID pWriteBuff = NULL; ///< buffer io缓存的首地址
ULONG uWriteLength = 0; ///< 应用层传下来要写入的数据长度
PIO_STACK_LOCATION pStack = NULL; ///< 当前驱动对应的栈
PVOID pBuffer = NULL; ///< 指向分配的内存,假设要往一个内存中写数据
pWriteBuff = pIrp->AssociatedIrp.SystemBuffer; ///< 从Irp头部拿缓存地址
pStack = IoGetCurrentIrpStackLocation(pIrp); ///< 拿到属于当前驱动对应的栈
uWriteLength = pStack->Parameters.Write.Length; ///< 拿到应用层传下来要读取的数据长度,是存放在联合体Parameters中结构体Read的Length成员中
/**
* @brief ExAllocatePoolWithTag windows内核中用来分配内存的函数,例程分配指定类型的池内存,并返回指向已分配块的指针。;
* @param[in] PoolType 要分配的池内存的类型,内核层从堆[^15]上分配内存有两种类型(PagedPool(分页内存,比较多,对应的物理内存可以切换出去,)
* 和NonPagedPool(非分页内存比较少对应的物理内存是不会被切换出去的))[^9],而在应用层的malloc就没有这种讲究;
* @warning PagedPool在某些时候不能使用,比如在中断请求级别较高的时候,比如DISPTCH_LEVEL,只能使用NonPagedPool的内存,
* 因为PagedPool对应的物理内存可以切换出去,在切换出去的时候可能会引起睡眠,而在中断上下文中是不能够睡眠的,
* 而分发函数的中断级别都处在PASSIVE_LEVEL,因此使用PagedPool没有问题[^16];
* @param[in] NumberOfBytes 要分配的字节数;
* @param[in] Tag 用于分配内存的池标记。将池标记指定为非零字符文本,即一到四个字符(最多4个字符),由单引号分隔(例如,'TSET'),
* 标记中的每个 ASCII 字符都必须是0x20(空格)到0x7E(波浪号)范围内的值。一般写模块名或者作者名,
* 而且是倒着写('TEST' 写成 'TSET'),因为ASCII字符也是使用8bit的整数表示,而x86上整数的存储是低位优先(小端/低尾端)[^17],
* 所以当使用windbg调试查看内存的时候就显示成正的'TEST'了,方便观察,(是因为无论是地位优先或者高位优先,
* 整数的存储是按Byte为基本单位,只有在存储的数据本身是以8bit为一个单位来编码的,读取的时候以8bit的整数倍来显示才会有这个反着输进去,正着显示出来的效果),
* 比如0x12345678,
* 1.如果以4bit当作一个单位来编码则为0x1 2 3 4 5 6 7 8 ,按低位优先存储后为 0x78 56 34 12,
* 以4bit为一个单位来显示则是0x7 8 5 6 3 4 1 2 与原来的0x1 2 3 4 5 6 7 8并不是反的,
* 以8bit为一个单位来显示则是0x78 56 34 12 与原来的0x1 2 3 4 5 6 7 8并不是反的,
* 以16bit为一个单位来显示则是0x7856 3412 与原来的0x1 2 3 4 5 6 7 8并不是反的,
* 若以32bit为一个单位来呈现则是0x78563412与原来的0x1 2 3 4 5 6 7 8并不是反的,
* 2.如果以8bit当作一个单位来编码则为0x12 34 56 78,按低位优先存储后为 0x78 56 34 12,
* 以4bit为一个单位来呈现则是0x7 8 5 6 3 4 1 2与原来的0x12 34 56 78不是反的,
* 以8bit为一个单位来呈现则是0x78 56 34 12与原来的0x12 34 56 78是反的,
* 若以16bit为一个单位来呈现则是0x7856 3412与原来的0x12 34 56 78是反的,
* 若以32bit为一个单位来呈现则是0x78563412与原来的0x12 34 56 78是反的,
* 3.如果以16bit当作一个单位来编码则为0x1234 5678,按低位优先存储后为 0x78 56 34 12,
* 以4bit为一个单位来呈现则是0x7 8 5 6 3 4 1 2与原来的0x1234 5678不是反的,
* 以8bit为一个单位来呈现则是0x78 56 34 12与原来的0x1234 5678不是反的,
* 以16bit为一个单位来呈现则是0x7856 3412与原来的0x1234 5678不是反的,
* 以32bit为一个单位来呈现则是0x78563412与原来的0x1234 5678不是反的,
*
* @note Tag的作用:一旦发生内存泄漏,在windg观察内存泄漏的时候,可以看到系统的内存迅速被吞噬,可以知道被泄露内存的Tag,进而知道内存泄漏是由哪个模块,哪个作者引起的;
* @return ntStatus 成功返回0,否则为非0,如果例程成功,则它必须返回 STATUS_SUCCESS。
* 否则,它必须返回在 ntstatus中定义的错误状态值之一;
* @see https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-exallocatepoolwithtag
* @author cisco(微信公众号:坚毅猿)
* @date 2022-02-15 22:09
*/
pBuffer = ExAllocatePoolWithTag(PagedPool, uWriteLength, 'TSET');
if (pBuffer == NULL)
{
pIrp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES; ///< 分配失败,设置Irp头部的错误码为资源不够
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_INSUFFICIENT_RESOURCES; ///< ///< 分配失败,返回错误码资源不够
}
/// 把分配的内存初始化为0
memset(pBuffer, 0, uWriteLength);
RtlCopyMemory(pBuffer, pWriteBuff, uWriteLength); ///< 这里pBuffer和pWriteBuff长度一样,不存在DisptachRead的问题
/// 释放内存
/// @todo: 为什么要设置为NULL
ExFreePool(pBuffer);
pBuffer = NULL;
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = uWriteLength; ///< Irp头部的IoStatus.Information设置成实际上拷贝的数据长度;[^13]
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
/**
* @brief DispatchIoctrl 是用来响应应用层的DeviceIoControl,相当与READ和WRITE之外的扩展,可以自定义一些应用层和内核层之间的其他命令,
* 把这些命令发送到内核层中去,让驱动根据命令做其他操作。[^18]
* @note r3和r0进行通信,除了读和写之外还可以让驱动做一些事情,比如弹窗的拦截和阻止某些操作,强删文件或者强杀进程、
* 检测一次隐藏的木马,检测隐藏的端口;
* @note 内核层和应用层有一组通信编码,来让内核层知道应用层需要我做什么事情
* @param[in] pObject 设备对象,在DriveEntry调用IoCreateDevice所创造设备对象,指向 DRIVER_OBJECT 结构;
* @param[in] pIrp IO REQUST PASKET缩写,Irp数据包封装应用层发下的数据和命令。[^13]
* 该结构指定注册表中驱动程序的 Parameters 项的路径;
* @return ntStatus 成功返回0,否则为非0,如果例程成功,则它必须返回 STATUS_SUCCESS。
* 否则,它必须返回在 ntstatus中定义的错误状态值之一;
* @pre
* @see https://docs.microsoft.com/zh-cn/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol
* @author cisco(微信公众号:坚毅猿)
* @date 2022-01-22 22:09
*/
NTSTATUS DispatchIoctrl(PDEVICE_OBJECT pObject, PIRP pIrp)
{
UNREFERENCED_PARAMETER(pObject);
ULONG uIoctrlCode = 0; ///< 操作的控制代码
PVOID pInputBuff = NULL; ///< 指向输入缓冲区的指针,该缓冲区包含执行操作所需的数据
PVOID pOutputBuff = NULL; ///< 指向输出缓冲区的指针,该缓冲区将接收操作返回的数据
ULONG uInputLength = 0; ///< 输入缓冲区的大小(以字节为单位)。
ULONG uOutputLength = 0; ///< 输出缓冲区的大小(以字节为单位)。
PIO_STACK_LOCATION pStack = NULL; ///< 当前驱动对应的栈
/// pInputBuff和pOutputBuff共用一块内存, 从Irp头部拿缓存地址
pInputBuff = pOutputBuff = pIrp->AssociatedIrp.SystemBuffer;
/// 拿到属于当前驱动对应的栈
pStack = IoGetCurrentIrpStackLocation(pIrp);
///< 拿到应用层传下来要读取的数据长度,是存放在联合体Parameters中结构体DeviceIoControl的InputBufferLength成员中
uInputLength = pStack->Parameters.DeviceIoControl.InputBufferLength;
uOutputLength = pStack->Parameters.DeviceIoControl.OutputBufferLength;
///< 拿到应用层传下来的控制码,是存放在联合体Parameters中结构体DeviceIoControl的IoControlCode成员中
uIoctrlCode = pStack->Parameters.DeviceIoControl.IoControlCode;
/// 识别控制码
switch (uIoctrlCode)
{
case CTL_HELLO:
DbgPrint("Hello iocontrol\n");
break;
case CTL_PRINT:
DbgPrint("%ws\n", pInputBuff);
//*(DWORD *)pOutputBuff =2;
break;
case CTL_BYE:
DbgPrint("Goodbye iocontrol\n");
break;
default:
DbgPrint("Unknown iocontrol\n");
}
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;//sizeof(DWORD);
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
/// @note 对应文件句柄应用计数为0,handle句柄是不跨进程的[^19]
/// @warnig 写关闭需要单独处理,写关闭:以写的方式打开文件,然后写入文件,最后关闭,这时候其他进程有机可乘,关闭之前需要重新扫描一下。
NTSTATUS DispatchClean(PDEVICE_OBJECT pObject, PIRP pIrp)
{
UNREFERENCED_PARAMETER(pObject);
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
/// 对应fileobject引用计数为0,fileobject是跨进程的[^19]
NTSTATUS DispatchClose(PDEVICE_OBJECT pObject, PIRP pIrp)
{
UNREFERENCED_PARAMETER(pObject);
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
VOID DriverUnload(PDRIVER_OBJECT pDriverObject)
{
/// 卸载前把符号链接删掉
UNICODE_STRING uLinkName = { 0 };
RtlInitUnicodeString(&uLinkName, LINK_NAME);
IoDeleteSymbolicLink(&uLinkName);
/// 卸载前把设备对象删掉
/// @warning 设备对象可能有多个,遍历链表把所有的设备对象删掉
/// @warning DriverEntry创建的符号链接和设备对象如果不清理掉,会导致重新加载驱动会失败
/// (因为没删除,重新加载,但设备对象已经存在,必然会加载失败),除非重启系统
IoDeleteDevice(pDriverObject->DeviceObject);
DbgPrint("Driver unloaded\n");
}
/**
* @brief DriverEntry 是加载驱动程序后调用的第一个驱动程序提供的例程,它负责初始化驱动程序;
* @param[in] pDriverObject 由IO管理器创建,标识驱动,创造设备对象,指向 DRIVER_OBJECT 结构体;
* @param[in] pRegPath 驱动安装后在注册表中的路径,指向 UNICODE_STRING 结构的指针,
* 该结构指定注册表中驱动程序的 Parameters 项的路径;
* @return ntStatus 如果例程成功,则它必须返回 STATUS_SUCCESS。
* 由于#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
* 即成功返回0,否则为非0,**与应用层相反,应用层返回0则是失败**。
* 否则,它必须返回在 ntstatus 中定义的错误状态值之一;
* @warning 驱动入口函数名称是写死的不可修改。
* @warning 不要使用.cpp来写驱动,因为C++在编译器里面会改名(命名粉碎规则(name mangling))
驱动框架就不认识DriverEntry这个名字了,会导致编译出错;[^1]
* @see https://docs.microsoft.com/zh-cn/windows-hardware/drivers/wdf/driverentry-for-kmdf-drivers
* @author cisco(微信公众号:坚毅猿)
* @date 2022-02-13 22:09
*/
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,
PUNICODE_STRING pRegPath)
{
/// 函数内部并没有用到参数pRegPath,所以需要UNREFERENCED_PARAMETER(pRegPath),让编译器忽略掉未使用的变量;
UNREFERENCED_PARAMETER(pRegPath);
UNICODE_STRING uDeviceName = { 0 }; ///< 驱动设备对象的名字,UNICODE_STRING是windows内核中统一使用的字符串类型[^2]
UNICODE_STRING uLinkName = { 0 }; ///< 符号链接的名字
NTSTATUS ntStatus = 0; ///< 返回状态
PDEVICE_OBJECT pDeviceObject = NULL; ///< 设备对象
ULONG i = 0;
DbgPrint("Driver load begin\n");
///初始化,就是将"wchar_t *"转换 "UNICODE_STRING *",同时将DEVICE_NAME赋值给uDeviceName[^3]
///@todo 理清楚其实现逻辑
RtlInitUnicodeString(&uDeviceName, DEVICE_NAME);
RtlInitUnicodeString(&uLinkName, LINK_NAME);
/**
* @brief IoCreateDevice 创建设备对象;
* @param[in] pDriverObject 由IO管理器创建,标识驱动,创造设备对象,指向 DRIVER_OBJECT 结构体;
* @param[in] DeviceExtensionSize 设备扩展,创建设备对象的一个缓冲区空间,用来存放一些数据。不需要空间,则设置为0;
* @param[in] DeviceName 设备对象名
* @param[in] DeviceType 设备对象的类型,比如磁盘设备类型等,未知设备类型的定义为
* "#define FILE_DEVICE_UNKNOWN 0x00000022"
* @param[in] DeviceCharacteristics 文件的属性
* @param[in] Exclusive 排他性,最好设置为TURE表示只要有一个进程打开,其他进程就无法打开,从而提高驱动安全性;
* @param[in] &pDeviceObject 被创建的设备对象的指针的指针,是二级指针;
* @warning 如果不传二级指针,pDeviceObject是不会发生改变的,存在内存泄漏;[^4]
* @todo 为什么会导致内存泄漏
* @return ntStatus 如果例程成功,则它必须返回 STATUS_SUCCESS。
* 因为#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
* 即成功返回0,否则为非0,**与应用层相反,应用层返回0则是失败**。
* 否则,它必须返回在 ntstatus 中定义的错误状态值之一;
* @note 为什么要在驱动中创建一个设备对象?是因为只能用设备对象是用来接收应用层的IRP数据包。
* eg1:应用层调用CreateFile()进入内核层会封装成一个irp数据包,irp发给指定的由驱动创建的设备对象,
* 设备对象接收到irp之后才会传给驱动分发函数去处理;[^5]
* @note Q1:驱动对象和设备的对象的关系?互指。[^6]
* 因为同一个驱动对象可以创建多个设备对象,比如可以创建控制设备对象,可以创建过滤设备对象等,
* 这些设备对象都被串联起来存放在同一个链表中,而且每个设备对象都有一个指针指向创建它的驱动对象。
* "typedef struct _DEVICE_OBJECT *PDEVICE_OBJECT;"
* "typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _DEVICE_OBJECT {
* CSHORT Type;
* USHORT Size;
* LONG ReferenceCount;
* struct _DRIVER_OBJECT *DriverObject; ///< 指向创建它的内核驱动对象
* struct _DEVICE_OBJECT *NextDevice; ///< 指向下一个设备对象
* struct _DEVICE_OBJECT *AttachedDevice; ..."
* @see C:\Program Files (x86)\Windows Kits\10\Include\10.0.22000.0\km\wdm.h
* @author cisco(微信公众号:坚毅猿)
* @date 2022-01-22 22:09
*/
ntStatus = IoCreateDevice(pDriverObject,
0, &uDeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &pDeviceObject);
if (!NT_SUCCESS(ntStatus))
{
/// 错误状态值ntStatus如果以十进制打印出来还可能是个负数,不如十六进制来得直观[^7]
/// 通过查询ntstatus.h的宏可以很方便找到对应的错误类型。
/// 例如:#define STATUS_OBJECT_NAME_INVALID ((NTSTATUS)0xC0000033L);
DbgPrint("IoCreateDevice failed:%x", ntStatus);
return ntStatus;
}
/// 对刚创建的设备对象指定一个通信方式;
/// @note "|="是往Flges添加一些标志
/// @note Q2:何为通信?A2:应用层传数据到内核层,或者内核层发数据到应用层
/// Q3:何为通信协议?A3:把数据放在什么地方去
/// DO_BUFFERED_IO规定R3和R0之间的read和write的通信方式有三种[^8]
/// 1.buffered io 在内核层分配一块缓存,io管理器负责把应用层/内核层copy到buffer,
/// io管理器负责把buffer拷贝到io管理器负责把内核层/应用层
/// 优点:安全简单,因为不会操作应用层的内存,buffer是来自内核态的,
/// 应用层无法改内核层的数据,所以是安全的。
/// 缺点:效率低,因为一次通信有两次拷贝,一般传输数据量是不大的,buffer io是够用的,
/// 但如果是类似3d渲染,数据量大,direct io更适合;
/// @todo 解释有点牵强
/// 2.direct io io管理器通过MPL把应用层/内核层的虚拟地址映射成物理地址,然后lock,
/// 防止被这块内存切换出去(pageout),io管理器通过MPL把同一物理地址映射成内核层/应用层的物理地址。[^9]
/// 效率是最高的,一次通信只有一次拷贝,但稍复杂
/// 3.neither io 内核层直接访问应用层的数据,前提是应用层和内核层同处于一个进程上下文[^10]
/// (因为应用层内存地址是私有的,应用层进程切换之后内存就失效了)
/// @warnnig 要对内核层传入的内存地址要做检查(ProbeForRead/ProbeForWrite),否则会有提取漏洞[^11]
/// @note DO_DEVICE_INITIALIZING 是用来标识设备对象刚刚创建出来,正在初始化,还不能工作,
/// 告诉应用层我现在暂时无法处理ipr请求,不要给我发,发了我也处理不了,
/// Q4:由谁来去除这个标志?A4:在DriverEntry中创建的设备对象是由io管理器把这个标志去掉,
/// 但在其他地方创建的,比如过滤驱动,由驱动程序负责清除,即必须用程序员自己手动去除这个标志;
pDeviceObject->Flags |= DO_BUFFERED_IO;
/// 创建符号链接;
/// @note 设备对象在应用层的体现,r0和r3通信的过程中,r3需要符号链接才能看到设备对象将其打开,
/// 进而获得句柄,然后发送读写等各种请求;[^12]
/// eg2:磁盘中看到的盘符号(C:)、(D:)都是符号链接,对应内核的卷设备对象\Device\HarddiskVolume
///
ntStatus = IoCreateSymbolicLink(&uLinkName, &uDeviceName);
if (!NT_SUCCESS(ntStatus))
{
IoDeleteDevice(pDeviceObject);
DbgPrint("IoCreateSymbolicLink failed:%x\n", ntStatus);
return ntStatus;
}
/// "#define IRP_MJ_MAXIMUM_FUNCTION 0x1b" 分发函数存放在pDriverObject->MajorFunction,
/// 0x1b+1=28,所有的分发函数有28个
for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION + 1; i++)
{
pDriverObject->MajorFunction[i] = DispatchCommon;
}
/// 对感兴趣的Irp进行注册,还有两个重要的Irp(重命名和删除,Major),复制、粘贴、移动没有对应的Irp,因为这三个动作本质是读和写,
/// 文件过滤驱动中为检测这三个动作,只需要监控都和写的Irp就可以了;[^13]
/// @warnig 名字可以随便起,但接口定义,参数类型必须一样
pDriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate; ///< 创建
pDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead; ///< 读 是相对于应用层来说,数据流向:r0->R3
pDriverObject->MajorFunction[IRP_MJ_WRITE] = DispatchWrite; ///< 写 是相对于应用层来说,数据流向:r3->R0
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoctrl;
pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = DispatchClean;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
pDriverObject->DriverUnload = DriverUnload;
DbgPrint("Driver load ok!\n");
return STATUS_SUCCESS;
}
应用层代码
main
1.加载驱动LoadDriver
- 1.得到完整的驱动路径
- 2.打开服务控制管理器
- 3.创建驱动所对应的服务
- 4.开启此项服务,会触发驱动的DriverEntry的执行
2.测试驱动TestDriver
- 1.打开驱动或者句柄
- 2.读/写
- 3.发送控制码
- 4.把句柄关掉
#include <windows.h>
#include <winsvc.h>
#include <conio.h>
#include <stdio.h>
#include <winioctl.h>
#define DRIVER_NAME "ntmodeldrv" ///< 驱动名字
#define DRIVER_PATH ".\\ntmodeldrv.sys" ///< 驱动路径
/// 应用层的这部分控制码要和内核的控制码保持一致
#define IOCTRL_BASE 0x800
#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_PRINT MYIOCTRL_CODE(1)
#define CTL_BYE MYIOCTRL_CODE(2)
/**
* @brief LoadDriver 装载NT驱动程序;
* @param[in] lpszDriverName 驱动名字;
* @param[in] lpszDriverPath 驱动的路径;
* @return BOOL 成功返回0,否则为非0;
* @pre
* @see https://docs.microsoft.com/zh-cn/windows-hardware/drivers/wdf/driverentry-for-kmdf-drivers
* @author cisco(微信公众号:坚毅猿)
* @date 2022-02-15 22:09
*/
BOOL LoadDriver(char* lpszDriverName, char* lpszDriverPath)
{
//char szDriverImagePath[256] = "D:\\DriverTest\\ntmodelDrv.sys";
char szDriverImagePath[256] = { 0 }; ///< 驱动的完整路径
//得到完整的驱动路径
GetFullPathName(lpszDriverPath, 256, szDriverImagePath, NULL);
BOOL bRet = FALSE;
SC_HANDLE hServiceMgr = NULL;//SCM管理器的句柄
SC_HANDLE hServiceDDK = NULL;//NT驱动程序的服务句柄
//打开服务控制管理器
hServiceMgr = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (hServiceMgr == NULL)
{
//OpenSCManager失败
printf("OpenSCManager() Failed %d ! \n", GetLastError());
bRet = FALSE;
goto BeforeLeave;
}
else
{
////OpenSCManager成功
printf("OpenSCManager() ok ! \n");
}
//创建驱动所对应的服务
hServiceDDK = CreateService(hServiceMgr,
lpszDriverName, ///< 驱动程序的在注册表中的名字
lpszDriverName, ///< 注册表驱动程序的 DisplayName 值
SERVICE_ALL_ACCESS, ///< 加载驱动程序的访问权限
SERVICE_KERNEL_DRIVER,///< 表示加载的服务是驱动程序
SERVICE_DEMAND_START, ///< 注册表驱动程序的 Start 值,SERVICE_DEMAND_START表示需要的时候动态加载(3)[^20]
SERVICE_ERROR_IGNORE, ///< 注册表驱动程序的 ErrorControl 值 ,SERVICE_ERROR_IGNORE表示系统没加载成功驱动,则忽略这个错误
szDriverImagePath, ///< 注册表驱动程序的 ImagePath 值,测试的.exe和.sys文件放在同一个目录下,否则会报错,加载失败,errcode:2 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\ntmodelDrv\ImagePath
NULL, ///< GroupOrder HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GroupOrderList
NULL, ///< @warnig 如果驱动依赖了其他服务,要晚于所依赖的服务启动;
NULL,
NULL,
NULL);
DWORD dwRtn;
//判断服务是否失败
if (hServiceDDK == NULL)
{
dwRtn = GetLastError();
if (dwRtn != ERROR_IO_PENDING && dwRtn != ERROR_SERVICE_EXISTS)
{
//由于其他原因创建服务失败
printf("CrateService() Failed %d ! \n", dwRtn);
bRet = FALSE;
goto BeforeLeave;
}
else
{
//服务创建失败,是由于服务已经创立过
printf("CrateService() Failed Service is ERROR_IO_PENDING or ERROR_SERVICE_EXISTS! \n");
}
// 驱动程序已经加载,只需要打开
hServiceDDK = OpenService(hServiceMgr, lpszDriverName, SERVICE_ALL_ACCESS);
if (hServiceDDK == NULL)
{
//如果打开服务也失败,则意味错误
dwRtn = GetLastError();
printf("OpenService() Failed %d ! \n", dwRtn);
bRet = FALSE;
goto BeforeLeave;
}
else
{
printf("OpenService() ok ! \n");
}
}
else
{
printf("CrateService() ok ! \n");
}
///< 开启此项服务,会触发驱动的DriverEntry的执行
bRet = StartService(hServiceDDK, NULL, NULL);
if (!bRet)
{
DWORD dwRtn = GetLastError();
if (dwRtn != ERROR_IO_PENDING && dwRtn != ERROR_SERVICE_ALREADY_RUNNING)
{
printf("StartService() Failed %d ! \n", dwRtn);
bRet = FALSE;
goto BeforeLeave;
}
else
{
if (dwRtn == ERROR_IO_PENDING)
{
//设备被挂住
printf("StartService() Failed ERROR_IO_PENDING ! \n");
bRet = FALSE;
goto BeforeLeave;
}
else
{
//服务已经开启
printf("StartService() Failed ERROR_SERVICE_ALREADY_RUNNING ! \n");
bRet = TRUE;
goto BeforeLeave;
}
}
}
bRet = TRUE;
//离开前关闭句柄
BeforeLeave:
if (hServiceDDK)
{
CloseServiceHandle(hServiceDDK);
}
if (hServiceMgr)
{
CloseServiceHandle(hServiceMgr);
}
return bRet;
}
//卸载驱动程序
BOOL UnloadDriver(char* szSvrName)
{
BOOL bRet = FALSE;
SC_HANDLE hServiceMgr = NULL;//SCM管理器的句柄
SC_HANDLE hServiceDDK = NULL;//NT驱动程序的服务句柄
SERVICE_STATUS SvrSta;
//打开SCM管理器
hServiceMgr = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (hServiceMgr == NULL)
{
//带开SCM管理器失败
printf("OpenSCManager() Failed %d ! \n", GetLastError());
bRet = FALSE;
goto BeforeLeave;
}
else
{
//带开SCM管理器失败成功
printf("OpenSCManager() ok ! \n");
}
//打开驱动所对应的服务
hServiceDDK = OpenService(hServiceMgr, szSvrName, SERVICE_ALL_ACCESS);
if (hServiceDDK == NULL)
{
//打开驱动所对应的服务失败
printf("OpenService() Failed %d ! \n", GetLastError());
bRet = FALSE;
goto BeforeLeave;
}
else
{
printf("OpenService() ok ! \n");
}
//停止驱动程序,如果停止失败,只有重新启动才能,再动态加载。
if (!ControlService(hServiceDDK, SERVICE_CONTROL_STOP, &SvrSta))
{
printf("ControlService() Failed %d !\n", GetLastError());
}
else
{
//打开驱动所对应的失败
printf("ControlService() ok !\n");
}
//动态卸载驱动程序。
if (!DeleteService(hServiceDDK))
{
//卸载失败
printf("DeleteSrevice() Failed %d !\n", GetLastError());
}
else
{
//卸载成功
printf("DelServer:deleteSrevice() ok !\n");
}
bRet = TRUE;
BeforeLeave:
//离开前关闭打开的句柄
if (hServiceDDK)
{
CloseServiceHandle(hServiceDDK);
}
if (hServiceMgr)
{
CloseServiceHandle(hServiceMgr);
}
return bRet;
}
void TestDriver()
{
//打开驱动或者句柄
HANDLE hDevice = CreateFile("\\\\.\\NTmodeldrv", ///> 符号链接\\.\NTmodeldrv,use your own name
GENERIC_WRITE | GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
0,
NULL);
if (hDevice != INVALID_HANDLE_VALUE)
{
printf("Create Device ok ! \n");
}
else
{
printf("Create Device Failed %d ! \n", GetLastError());
return;
}
CHAR bufRead[1024] = { 0 };
WCHAR bufWrite[1024] = L"Hello, world";
DWORD dwRead = 0;
DWORD dwWrite = 0;
/**
* @brief ReadFile 调用ReadFile之后会进入内核层,封装成Irp数据包,发送到内核层创建的设备对象上,
* 交给ReadFile对应的分发函数把数据拿到,通过内核的io管理器把内核层buffer拷贝到bufRead上;
* @param[in] hFile 设备对象的句柄;
* @param[in] ipBuffer 存放从内核层读出来的数据的buffer;
* @param[in] nNumberOfBytesToRead 存放从内核读出来的数据buffer的长度;
* @param[in] lpNumberOfBytesToRead 实际从内存层读出来的数据长度;
* @param[in] lpOverlapped 用来控制异步通信
* @see https://docs.microsoft.com/zh-cn/windows-hardware/drivers/wdf/driverentry-for-kmdf-drivers
* @author cisco(微信公众号:坚毅猿)
* @date 2022-02-15 22:09
*/
ReadFile(hDevice, bufRead, 1024, &dwRead, NULL);
printf("Read done!:%ws\n", bufRead);
printf("Please press any key to write\n");
getch();
/**
* @brief WriteFile 调用ReadFile之后会进入内核层,封装成Irp数据包,发送到内核层创建的设备对象上,
* 交给ReadFile对应的分发函数把数据拿到,通过内核的io管理器把内核层buffer拷贝到bufRead上;
* @param[in] hFile 设备对象的句柄;
* @param[in] ipBuffer 存放从内核层读出来的数据的buffer;
* @param[in] nNumberOfBytesToRead 存放从内核读出来的数据buffer的长度;
* @param[in] lpNumberOfBytesToRead 实际从内存层读出来的数据长度;
* @param[in] lpOverlapped 用来控制异步通信
* @see https://docs.microsoft.com/zh-cn/windows-hardware/drivers/wdf/driverentry-for-kmdf-drivers
* @author cisco(微信公众号:坚毅猿)
* @date 2022-02-15 22:09
*/
WriteFile(hDevice, bufWrite, (wcslen(bufWrite) + 1) * sizeof(WCHAR), &dwWrite, NULL);
printf("Write done!\n");
printf("Please press any key to deviceiocontrol\n");
getch();
CHAR bufInput[1024] = "Hello, world";
CHAR bufOutput[1024] = { 0 };
DWORD dwRet = 0;
WCHAR bufFileInput[1024] = L"c:\\docs\\hi.txt";
printf("Please press any key to send PRINT\n");
getch();
DeviceIoControl(hDevice,
CTL_PRINT,
bufFileInput,
sizeof(bufFileInput),
bufOutput,
sizeof(bufOutput),
&dwRet,
NULL);
printf("Please press any key to send HELLO\n");
getch();
DeviceIoControl(hDevice,
CTL_HELLO,
NULL,
0,
NULL,
0,
&dwRet,
NULL);
printf("Please press any key to send BYE\n");
getch();
DeviceIoControl(hDevice,
CTL_BYE,
NULL,
0,
NULL,
0,
&dwRet,
NULL);
printf("DeviceIoControl done!\n");
//把句柄关掉
CloseHandle(hDevice);
}
int main(int argc, char* argv[])
{
//加载驱动
BOOL bRet = LoadDriver(DRIVER_NAME, DRIVER_PATH);
if (!bRet)
{
printf("LoadNTDriver error\n");
return 0;
}
//加载成功
printf("press any key to create device!\n");
getch();
TestDriver();
//这时候你可以通过注册表,或其他查看符号连接的软件验证。
printf("press any key to stop service!\n");
getch();
//卸载驱动
bRet = UnloadDriver(DRIVER_NAME);
if (!bRet)
{
printf("UnloadNTDriver error\n");
return 0;
}
return 0;
}
编译
.sys可以作为资源打包到.exe中去,在启动.exe的时候把.sys释放出来,把.sys加载起来,然后把.sys删掉。
怎么打包?使用pe工具打包[21]
.sys x86平台编译x86的,x64必须编译成x64的
.exe 既可以编译成x86的,也可以编译成x64,x86的应用程序是可以加载x64的驱动
测试
x64需要关闭数字签名,x86测试方便些。
参考资料
1.[[命名粉碎规则]]
2.字符串类型
3.RtlInitUnicodeString
RtlInitUnicodeString(
_Out_ PUNICODE_STRING DestinationString,
_In_opt_z_ __drv_aliasesMem PCWSTR SourceString
);
4.内存泄漏
5.Irp框架
6.设备对象
7.数的表示
8.[[通信方式]]
9.物理地址转换
10.进程上下文
11.提权漏洞
12.符号链接
13.[[Irp]]
14.内存溢出
15.堆
16.中断
17.整数存储
18.DeviceIoControl
/// @warning 这些数据都是封装在Irp中
BOOL DeviceIoControl(
[in] HANDLE hDevice, ///< 要在其上执行操作的设备的句柄。设备通常是卷、目录、文件或流。
[in] DWORD dwIoControlCode, ///< 操作的控制代码,可以自己定义
[in, optional] LPVOID lpInBuffer, ///< 指向输入缓冲区的指针,该缓冲区包含执行操作所需的数据
[in] DWORD nInBufferSize, ///< 输入缓冲区的大小(以字节为单位)。
[out, optional] LPVOID lpOutBuffer, ///< 指向输出缓冲区的指针,该缓冲区将接收操作返回的数据
[in] DWORD nOutBufferSize, ///< 输出缓冲区的大小(以字节为单位)。
[out, optional] LPDWORD lpBytesReturned, ///< 这次io实际传输的长度
[in, out, optional] LPOVERLAPPED lpOverlapped ///< 用作异步通信的
);
19.跨进程
20. 影响驱动启动的时机有两个因素
影响驱动启动的时机有两个因素,
一个是Startype
// Start Type
//
#define SERVICE_BOOT_START 0x00000000
#define SERVICE_SYSTEM_START 0x00000001
#define SERVICE_AUTO_START 0x00000002
#define SERVICE_DEMAND_START 0x00000003
#define SERVICE_DISABLED 0x00000004
StartType值有0,1,2,3,4,数值越小越早启动
- SERVICE_BOOT_START(0)是内核刚刚初始化之后,此时加载的都是与系统核心有关的驱动程序,比如磁盘驱动;
- SERVICE_SYSTEM_START(1) 稍晚一些;
- SERVICE_AUTO_START(2) 是在登陆界面出现的时候开始,如果登陆较快可能驱动还没加载就登陆进去了;
- SERVICE_DEMAND_START(3) 是需要的时候动态加载;
-
SERVICE_DISABLED(4) 是不加载,要假装之前必须把Start的值改小于4的值;
另一个是GroupOrder
当两个驱动的StartType相同,GroupOrder越靠前越先启动
@warnig 如果驱动依赖了其他服务,要晚于所依赖的服务启动;
期望:驱动越早启动越好,如果驱动比病毒木马启动晚的话,可能来不及杀掉病毒和木马就被反杀了。