驱动开发入门——NTModel
上一篇博文中主要说明了驱动开发中基本的数据类型,认识这些数据类型算是驱动开发中的入门吧,这次主要说明驱动开发中最基本的模型——NTModel。介绍这个模型首先要了解R3层是如何通过应用层API进入到内核,内核又是如何将信息返回给R3,另外会介绍R3是如何直接向R0层下命令。API调用的基本流程一般在某些平台上进行程序开发,都需要使用系统提供的统一接口,linux平台直接提供系统调用,而window
上一篇博文中主要说明了驱动开发中基本的数据类型,认识这些数据类型算是驱动开发中的入门吧,这次主要说明驱动开发中最基本的模型——NTModel。介绍这个模型首先要了解R3层是如何通过应用层API进入到内核,内核又是如何将信息返回给R3,另外会介绍R3是如何直接向R0层下命令。
API调用的基本流程
一般在某些平台上进行程序开发,都需要使用系统提供的统一接口,linux平台直接提供系统调用,而windows上提供API,这两个并不是同一个概念(之前我一直分不清楚),虽然它们都是系统提供的实现某种功能的接口,但是它们有着本质的区别,系统调用在调用时会陷入到内核态,而API则不是这样,例如对于CreateFile这个我们不能说它是一个系统调用,在这个函数中并没有立即陷入到内核态,而是先进行参数检查,然后通过其他的一系列操作之后调用系统调用而进入到内核,所以它并不是系统调用。
windows在应用层提供了3个重要的动态库,分别是kernel.dll uer32.dll gdi.dll (现在基本上将所有的API都封装到kernell.dll中)
当用户程序调用一个API函数时,在这个API内部会调用用封装到ntdll.dll中以Zw或者Nt开头的同名函数,这些函数主要负责从用户态切换到内核态,这些函数叫做Native API,Native API进入到内核的方式是产生一个中断(XP及以前的版本)和调用sysenter(XP以上的版本),Native API在进入到内核中时会带上一个服务号,系统根据这个服务号在SSDT表中查找到相关的服务函数,最后调用这些服务函数完成相关功能,这个过程可以用下面的图来说明:
下面以CreateFile为例说明具体的调用过程:
1. 应用层调用CreateFile函数
2. 这个函数实际上被封装到了kernel32.dll中,在这个函数中调用NtCreateFile,这就是调用ntdll.dll中的native api ,ntdll.dll中一般又两组函数——以Nt开头,以Zw开头的,这两组函数本身没有什么太大的区别。
3. native api中通过中断 int 2eh(windows 2000 及以下),或者通过sysenter指令(windows xp及以上)进入内核,这种方式称为软中断,在产生中断时会带上一个服务号,根据服务号在ssdt表中以服务号进行查找(类似与8086中的中断机制)
4. 根据SSDT表中记录的服务函数地址,调用相关的服务函数。
5. 然后进入到执行组件中,对于CreateFile的操作,这个时候会调用IO管理器,IO管理器负责发起IO操作请求,并管理这些请求,主要时生成一个IRP结构,系统中有不同的管理器,主要有这样几个——虚拟内存管理器,IO管理器,对象管理器,进程管理器,线程管理器,配置管理器。
6. 管理器生成一个IRP请求,并调用内核中的驱动,来相应这个操作,对于CreateFile来说会调用NtCreateFile函数。
7. 最后调用内核实现部分,也就是硬件抽象层。最后由硬件抽象层操作硬件,完成打开或者创建文件的操作。
NTModel详解
R3与R0互相通信
在驱动程序中,入口是DriverEntry,函数会带入一个DriverObject指针。这个对象中有许多回调函数,它会根据R3层下发的操作调用对应的回调函数,比如应用层调用CreatFile时在驱动层会调用DispatchCreate。这样我们只要写好DispatchCreate就可以处理由R3层下发的CreateFile命令。
上述的一些函数只适用于一般的操作,对于一些特殊的,比如R3层要R0层产生一个输出语句等等,这个特殊的操作是通过DeviceIoControl向R0下发一个控制命令,在R0层根据这个控制码来识别具体是哪种控制,需要R0做哪种操作,函数原型如下:
BOOL DeviceIoControl(
HANDLE hDevice, //驱动的设备对象句柄
DWORD dwIoControlCode, //控制码
LPVOID lpInBuffer, //发往R0层的数据
DWORD nInBufferSize, //数据大小
LPVOID lpOutBuffer, //提供一个缓冲区,接受R0返回的数据
DWORD nOutBufferSize, //缓冲区的大小
LPDWORD lpBytesReturned, //R0实际返回数据的大小
LPOVERLAPPED lpOverlapped //完成例程
);
IRP的简介
R3与R0的通信是通过IRP进行数据的交换,IRP的定义如下:
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _IRP {
CSHORT Type;
USHORT Size;
PMDL MdlAddress;
ULONG Flags;
union {
struct _IRP *MasterIrp;
PVOID SystemBuffer;
} AssociatedIrp;
...
IO_STATUS_BLOCK IoStatus;
CHAR StackCount;
CHAR CurrentLocation;
...
PVOID UserBuffer;
...
struct {
union {
struct _IO_STACK_LOCATION *CurrentStackLocation;
...
};
} IRP;
IRP主要分为两部分,一部分是头,另一部分是IRP栈,在上一篇分析驱动中的数据结构时,说过驱动设备时分层的,上层驱动设备完成后,需要发到下层驱动设备,所有驱动设备公用IRP的栈顶,但是每个驱动都各自有自己的IRP栈,它们的关系如下如所示:
_IO_STACK_LOCATION 的结构如下:
typedef struct _IO_STACK_LOCATION {
UCHAR MajorFunction;
UCHAR MinorFunction;
UCHAR Flags;
UCHAR Control;
union {
struct {
PIO_SECURITY_CONTEXT SecurityContext;
ULONG Options;
USHORT POINTER_ALIGNMENT FileAttributes;
USHORT ShareAccess;
ULONG POINTER_ALIGNMENT EaLength;
} Create;
...
struct {
ULONG Length;
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;
} Read;
struct {
ULONG Length;
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;
} Write;
...
struct {
ULONG OutputBufferLength;
ULONG POINTER_ALIGNMENT InputBufferLength;
ULONG POINTER_ALIGNMENT IoControlCode;
PVOID Type3InputBuffer;
} DeviceIoControl;
...
struct {
PVOID Argument1;
PVOID Argument2;
PVOID Argument3;
PVOID Argument4;
} Others;
} Parameters;
PDEVICE_OBJECT DeviceObject;
PFILE_OBJECT FileObject;
PIO_COMPLETION_ROUTINE CompletionRoutine;
PVOID Context;
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;
这个结构中有一个共用体,当处理不同的R3层请求时系统会填充对应的共用体。
源代码分析
//设备名
#define DEVICE_NAME L"\\device\\NtDevice"
#define LINK_NAME L"\\??\\NtDevice"
//DriverEntry
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegisterPath)
{
PDEVICE_OBJECT pDeviceObject = NULL;
UNICODE_STRING uDeviceName = { 0 };
UNICODE_STRING uLinkName = { 0 };
NTSTATUS status = 0;
UNREFERENCED_PARAMETER(pRegisterPath);
DbgPrint("Start Driver......\n");
pDriverObject->DriverUnload = UnloadDriver;
//创建设备对象
RtlInitUnicodeString(&uDeviceName, DEVICE_NAME);
status = IoCreateDevice(pDriverObject, 0, &uDeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &pDeviceObject);
if (!NT_SUCCESS(status))
{
DbgPrint("create device error!\n");
return status;
}
pDeviceObject->Flags |= DO_BUFFERED_IO;
//创建符号连接
RtlInitUnicodeString(&uLinkName, LINK_NAME);
status = IoCreateSymbolicLink(&uLinkName, &uDeviceName);
if (!NT_SUCCESS(status))
{
DbgPrint("create link name error!\n");
return status;
}
//注册分发函数
for (ULONG i = 0; i < IRP_MJ_MAXIMUM_FUNCTION + 1; i++)
{
pDriverObject->MajorFunction[i] = IoDispatchCommon;
}
pDriverObject->MajorFunction[IRP_MJ_CREATE] = IoDispatchCreate;
pDriverObject->MajorFunction[IRP_MJ_READ] = IoDispatchRead;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = IoDispatchWrite;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IoDispatchControl;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = IoDispatchClose;
pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = IoDispatchClean;
return STATUS_SUCCESS;
}
这个函数是驱动的入口函数,类似与main函数或者WinMain函数。在该函数中首先创建一个控制设备对象,并为它创建一个符号链接,因为R3层不能直接通过设备的名称来访问设备,必须通过其符号链接。需要注意,设备名称必须以“\\device”开头,而符号链接需要以“\\??”开头,否则创建设备和符号链接会失败。
然后为这个驱动程序注册分发函数,分发函数保存在DriverObject结构中MajorFunction中,这个时一个数组,元素个数为IRP_MJ_MAXIMUM_FUNCTION,系统为每个位置定义一个宏,我们根据这个宏,在数据中填入对应 的函数指针,系统会根据R3层的操作来调用具体的函数。另外DriverObject中的DriverUnload 保存的是卸载驱动时系统回调用的函数,在这个函数中主要完成资源的释放工作
//UnloadDriver
VOID UnloadDriver(PDRIVER_OBJECT pDriverObject)
{
UNICODE_STRING uLinkName = { 0 };
UNREFERENCED_PARAMETER(pDriverObject);
RtlInitUnicodeString(&uLinkName, LINK_NAME);
IoDeleteSymbolicLink(&uLinkName);
IoDeleteDevice(pDriverObject->DeviceObject);
DbgPrint("Unload Driver......\n");
}
在这个函数中主要释放了之前创建的符号链接和控制设备对象。
NTSTATUS IoDispatchCommon(PDEVICE_OBJECT DeviceObject, PIRP pIrp)
{
UNREFERENCED_PARAMETER(DeviceObject);
//向R3返回成功
pIrp->IoStatus.Status = STATUS_SUCCESS;
//向R3返回的数据长度为0,不向R3返回数据
pIrp->IoStatus.Information = 0;
//默认直接返回
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
这个函数是我们注册的默认处理函数,它每步的操作在注释中也写了,需要注意的是在pIrp->IoStatus.Status = STATUS_SUCCESS;语句是向R3返回执行的状态,而最后返回成功是给驱动程序看的。
//IoDispatchRead
NTSTATUS IoDispatchRead(PDEVICE_OBJECT DeviceObject, PIRP pIrp)
{
//处理R3层的读命令,将数据返回给R3层
WCHAR wHello[] = L"hello world";
ULONG uReadLength = 0;
WCHAR *pBuffer = pIrp->AssociatedIrp.SystemBuffer;
ULONG uBufferLen = 0;
ULONG uMin = 0;
PIO_STACK_LOCATION pCurrStack = IoGetCurrentIrpStackLocation(pIrp);
UNREFERENCED_PARAMETER(DeviceObject);
uBufferLen = pCurrStack->Parameters.Read.Length;
uReadLength = sizeof(wHello);
uMin = (uReadLength < uBufferLen) ? uReadLength : uBufferLen;
RtlCopyMemory(pBuffer, wHello, uMin);
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = uMin;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
这个函数主要用来处理应用层的ReadFile请求,这个函数主要是将一段字符串拷贝到通信用的缓冲区中,模拟读的操作。,需要注意的是这个地址要根据不同的设备类型来不同的对待,对于DO_BUFFERED_IO类型的设备,是保存在pIrp->AssociatedIrp.SystemBuffer中,对于DO_DIRECT_IO类型的设备,这个缓冲区的地址是MdlAddress。而对于ReadFile这个API来说,应用层在调用这个函数时会给一个缓冲区的大小,为了获取这个大小,首先得到当前的IRP栈,这个操作用函数IoGetCurrentIrpStackLocation可以得到,然后在当前栈的Parameters共用体中,调用Read部分的Length。当得到这个缓冲区大小后,取缓冲区大小和对应字符串的大小的最小值,为什么要这样做?我们不妨考虑如果用缓冲区的长度的话,当这个长度比字符串的长度长,那么在拷贝时就会将字符串后面的一些无用内存给拷贝进去了,一来效率不高,二来这样应用层得到了内核层中内存的部分数据,存在安全隐患,如果我们采用字符串的长度,可能会出现用户提供的缓冲区不够的情况,这样会造成越界。所以采用它们的最小值是最合理的。完成之后返回,这个时候要注意返回的长度这一项需要填上真实拷贝的大小,不然R3是得不到数据的,或者得到的数据不完整。
NTSTATUS IoDispatchWrite(PDEVICE_OBJECT DeviceObject, PIRP pIrp)
{
//接受R3的写命令
UNICODE_STRING uWriteString = { 0 };
WCHAR *pBuffer = NULL;
ULONG uWriteLength = 0;
PIO_STACK_LOCATION pStackIrp = IoGetCurrentIrpStackLocation(pIrp);
UNREFERENCED_PARAMETER(DeviceObject);
uWriteLength = pStackIrp->Parameters.Write.Length;
uWriteString.MaximumLength = uWriteLength;
uWriteString.Length = uWriteLength - 1 * sizeof(WCHAR);
uWriteString.Buffer = ExAllocatePoolWithTag(PagedPool, uWriteLength, 'TSET');
pBuffer = pIrp->AssociatedIrp.SystemBuffer;
if (NULL == uWriteString.Buffer)
{
DbgPrint("Allocate Memory Error!\n");
pIrp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_INSUFFICIENT_RESOURCES;
}
RtlCopyMemory(uWriteString.Buffer, pBuffer, uWriteLength);
DbgPrint("Write Date: %wZ\n", &uWriteString);
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
这个函数用来处理WriteFile的请求,首先通过IRP中传进来的缓冲区的地址得到这个数据,然后将数据打印出来,通过这种方式来模拟向R3文件中写入数据。
//定义的控制码
#define IOCTL_BASE 0x800
#define MYIOCTRL_CODE(i) CTL_CODE(FILE_DEVICE_UNKNOWN, (IOCTL_BASE + i), METHOD_BUFFERED, FILE_ANY_ACCESS)
#define CTL_PRINT MYIOCTRL_CODE(1)
#define CTL_HELLO MYIOCTRL_CODE(2)
#define CTL_BYE MYIOCTRL_CODE(3)
//IoDispatchControl函数
NTSTATUS IoDispatchControl(PDEVICE_OBJECT DeviceObject, PIRP pIrp)
{
UNREFERENCED_PARAMETER(DeviceObject);
ULONG uBufferLength = 0;
WCHAR *pBuffer = NULL;
ULONG uIOCtrlCode = 0;
PIO_STACK_LOCATION pStack = IoGetCurrentIrpStackLocation(pIrp);
uBufferLength = pStack->Parameters.DeviceIoControl.InputBufferLength;
uIOCtrlCode = pStack->Parameters.DeviceIoControl.IoControlCode;
pBuffer = pIrp->AssociatedIrp.SystemBuffer;
switch (uIOCtrlCode)
{
case CTL_BYE:
DbgPrint("Good Bye");
break;
case CTL_HELLO:
DbgPrint("Hello World\n");
break;
case CTL_PRINT:
DbgPrint("%S", pBuffer);
break;
default:
DbgPrint("unknow command\n");
}
pIrp->IoStatus.Information = 0;
pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
这个宏CTL_CODE是微软官方定义的,主要用来将控制码和对应类型的设备进行绑定,主要传入4个参数,第一个是设备的类型,第二个是具体的控制码,需要注意的是:为了与微软官方的控制码区分,自定义的控制码需要在0x800以上。第三个参数是对应控制码传递参数的方式,主要的几种方式与设备对象和R3层传递数据的方式类似,我们在这传入的是METHOD_BUFFERED,表示的是通过内存拷贝的方式将R3的数据传入到R0。最后一个是操作权限,我们给它所有的权限。
在函数中我们根据R3传入的控制码来进行不同的操作。
R3部分的代码
R3部分主要完成的是驱动程序的加载、卸载、以及向驱动程序发送命令。驱动的加载和卸载是通过注册并启动服务和关闭并删除服务的方式进行的,至于怎么操作一个服务,请看本人另外一篇关于服务操作的博客。需要注意的是在创建服务时需要填入服务程序所在的路径,这个时候需要填生成的.sys文件的路径,不要写之前定义的设备名或者符号链接名。
在这主要贴出控制部分的代码:
//打开设备,获取它的设备句柄
HANDLE hDevice = CreateFileA("\\\\.\\NtDevice", GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (NULL == hDevice)
{
printf("打开设备失败\n");
return;
}
//读
CHAR szBuf[255] = "";
ULONG uLength = 0;
ReadFile(hDevice, szBuf, 255, &uLength, NULL);
printf("Read Date:%s\n", szBuf);
//写
WCHAR wHello[] = L"Hello world";
WriteFile(hDevice, wHello, (wcslen(wHello) + 1) * sizeof(WCHAR), &uLength, NULL);
printf("写操作完成");
//向其发送控制命令
WCHAR wCtlString[] = L"C:\\test.txt";
DeviceIoControl(hDevice, CTL_PRINT, wCtlString, (wcslen(wCtlString) + 1) * sizeof(WCHAR), NULL, 0, NULL, NULL);
DeviceIoControl(hDevice, CTL_HELLO, NULL, 0, NULL, 0, NULL, NULL);
DeviceIoControl(hDevice, CTL_BYE, NULL, 0, NULL, 0, NULL, NULL);
printf("控制操作完成");
CloseHandle(hDevice);
要控制一个设备对象,必须先得到设备对象的句柄,获得这个句柄,我们是通过函数CreateFile来得到的,这个时候填入的文件名应该是之前注册的符号链接的名字,在R3中这个名字以“\\\\ .”开头并加上我们为它提供的符号链接名。在调用CreateFile时会触发之前定义的DispatchCreate函数。然后我们通过调用ReadFile和WriteFile分别触发读写操作,最后调用DeviceIoControl函数,发送控制命令,在R3层中也要定义一份与R0中一模一样的控制码。这样就基本实现了R3与R0通信。
有的时候在加载驱动的时候,系统会报错,返回码为2,表示系统找不到驱动对应的文件,这个时候可能是文件的路径的问题,这个时候可以在系统的注册表HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\services\下,找到我们的驱动(还有可能是在ControlSet002)对应的路径,然后将R3程序拷贝到这个路径下,基本就可以解决这个问题
更多推荐
所有评论(0)