NT驱动程序与用户层程序基于事件EVENT实现同步通信

大葱

发布日期: 2019-02-14 13:42:22 浏览量: 1618
评分:
star star star star star star star star star_border star_border
*转载请注明来自write-bug.com

背景

之前我们在《NT驱动程序与用户层程序通信》这篇文章中讲了,用户层程序使用 DeviceIoControl 将 IOCTL 控制码、输入缓冲区、输出缓冲区传入到内核;内核响应 IRP_MJ_DEVICE_CONTRL 消息,并从 IRP 中获取传入的 IOCTL 控制码、输入缓冲区、输出缓冲区,以此实现数据的交互。

但是,当内核层想主动传递数据到用户层,用户层又怎样才能知道呢?因为只有用户层知道内核层有数据输出的时候,它才会调用 DeviceIoControl 函数去获取数据。所以,本文要介绍的就是基于事件 EVENT 实现的同步框架,可以解决这个的问题。现在,我就把实现思路和原理整理成文档,分享给大家。

函数介绍

CreateEvent 函数

CreateEvent是一个Windows API函数。它用来创建或打开一个命名的或无名的事件对象。如果想为对象指定一个访问掩码,应当使用CreateEventEx函数。

函数声明

  1. HANDLECreateEvent(
  2. LPSECURITY_ATTRIBUTESlpEventAttributes, // 安全属性
  3. BOOLbManualReset, // 复位方式
  4. BOOLbInitialState, // 初始状态
  5. LPCTSTRlpName // 对象名称
  6. );

参数

  • lpEventAttributes[in]
    一个指向SECURITY_ATTRIBUTES结构的指针,确定返回的句柄是否可被子进程继承。如果lpEventAttributes是NULL,此句柄不能被继承。
  • bManualReset[in]
    指定将事件对象创建成手动复原还是自动复原。如果是TRUE,那么必须用ResetEvent函数来手工将事件的状态复原到无信号状态。如果设置为FALSE,当一个等待线程被释放以后,系统将会自动将事件状态复原为无信号状态。
  • bInitialState[in]
    指定事件对象的初始状态。如果为TRUE,初始状态为有信号状态;否则为无信号状态。
  • lpName[in]
    指定事件的对象的名称,是一个以0结束的字符串指针。名称的字符格式限定在MAX_PATH之内。名字是对大小写敏感的。如果lpName为NULL,将创建一个无名的事件对象。

返回值

  • 如果函数调用成功,函数返回事件对象的句柄。
  • 如果函数失败,函数返回值为NULL,如果需要获得详细的错误信息,需要调用GetLastError。

ObReferenceObjectByHandle 函数

提供对象句柄访问许可。
如果访问被允许,返回相应的对象体的指针。

函数声明

  1. NTSTATUS ObReferenceObjectByHandle(
  2. _In_ HANDLE Handle,
  3. _In_ ACCESS_MASK DesiredAccess,
  4. _In_opt_ POBJECT_TYPE ObjectType,
  5. _In_ KPROCESSOR_MODE AccessMode,
  6. _Out_ PVOID *Object,
  7. _In_opt_ POBJECT_HANDLE_INFORMATION HandleInformation
  8. );

参数

  • Handle [in]
    为一个对象指定一个打开的句柄。

  • DesiredAccess [in]
    指定访问对象的类型。其中,EVENT_MODIFY_STATE 表示允许使用 KeSetEvent 和 KeResetEvent 函数。

  • ObjectType [in, optional]
    表明指向对象是什么类型的。其中,*ExEventObjectType 表示对象指针类型为 PKEVENT。

  • AccessMode [in]
    访问模式分UserMode 和KernelMode。其中,KernelMode 表示内核模式。

  • Object [out]
    指向映射句柄对象的指针。

  • HandleInformation [out, optional]

    驱动程序设置为 NULL。

返回值

  • 成功,则返回 STATUS_SUCCESS;否则,返回其它 NTSTATUS 错误码。

KeSetEvent 函数

如果事件尚未发出信号,则 KeSetEvent 函数将事件对象设置为信号状态,并返回事件对象的先前状态。

函数声明

  1. LONG KeSetEvent(
  2. _Inout_ PRKEVENT Event,
  3. _In_ KPRIORITY Increment,
  4. _In_ BOOLEAN Wait
  5. );

参数

  • Event[in,out]
    指向调用者为其提供存储的初始化事件对象的指针。
  • Increment[in]
    如果设置事件导致等待满足,则指定要应用的优先级增量。其中,IO_NO_INCREMENT 表示不增加优先级。
  • Wait
    指定是否通过调用 KeWaitXxx 函数之一来立即跟踪对KeSetEvent的调用。 如果为TRUE,则KeSetEvent调用之后必须调用KeWaitForMultipleObjects,KeWaitForMutexObject或KeWaitForSingleObject。 有关详细信息,请参阅以下备注部分。

返回值

  • 如果事件对象的先前状态发出信号,则返回非零值。

实现原理

我们通过事件 EVENT 实现用户层与内核层的同步操作,具体的实现原理如下:

  • 首先,我们在用户层程序中调用 CreateEvent 函数创建事件 EVENT 并获取事件 EVENT 的句柄。事件初始状态为无信号,而且自动复原。

  • 然后,用户层程序调用 DeviceIoControl 函数将上述创建的 EVENT 句柄传递到内核层驱动程序中,并调用 WaitForSingleObject 函数等待事件 EVENT 的响应。直到事件 EVENT 响应后,程序才会进行下一步操作。

  • 接着,内核层程序就通过 IRP_MJ_DEVICE_CONTROL 消息响应函数获取从用户层传入的事件 EVENT 的句柄。调用 ObReferenceObjectByHandle 内核函数获取内核事件 EVENT 对象。

  • 然后,内核驱动程序可以调用 PsCreateSystemThread 创建多线程,继续执行操作。要想通知用户层程序进行下一步操作,只需调用 KeSetEvent 内核函数,将事件 EVENT 对象的状态设置为信号状态。那么,用户层程序中的事件 EVENT 就是一个有信号状态,WaitForSingleObject 函数就不会阻塞,而是继续往下执行。这样,就可以成功从内核层通知到用户层进行操作了。

  • 最后,用户层调用 CloseHandle 关闭事件 EVENT 句柄;内核层调用 ObDereferenceObject 释放内核事件 EVENT 对象。

这个框架的核心原理就是,将用户层的事件句柄传入内核层的驱动程序中,并有内核驱动程序设置事件对象的信号状态,以此触发用户层的响应。

编码实现

用户层代码

  1. int _tmain(int argc, _TCHAR* argv[])
  2. {
  3. HANDLE hEvent = NULL;
  4. HANDLE hDevice = NULL;
  5. char szOutput[MAX_PATH] = { 0 };
  6. DWORD dwOutput = MAX_PATH;
  7. DWORD dwRet = 0;
  8. BOOL bRet = FALSE;
  9. // 创建事件, 设置自动复位,初始状态为无信号
  10. hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
  11. if (NULL == hEvent)
  12. {
  13. printf("CreateEvent Error[%d]\n", ::GetLastError());
  14. }
  15. // 打开设备
  16. hDevice = ::CreateFile(SYM_NAME, GENERIC_READ | GENERIC_WRITE,
  17. FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
  18. FILE_ATTRIBUTE_ARCHIVE, NULL);
  19. if (INVALID_HANDLE_VALUE == hDevice)
  20. {
  21. printf("CreateFile Error[%d]\n", ::GetLastError());
  22. }
  23. // 数据交互, 向内核层中传入事件句柄
  24. bRet = ::DeviceIoControl(hDevice, IOCTL_MY_TEST, &hEvent, sizeof(hEvent), szOutput, dwOutput, &dwRet, NULL);
  25. if (FALSE == bRet)
  26. {
  27. printf("DeviceIoControl Error[%d]\n", ::GetLastError());
  28. }
  29. // 一直等待事件的响应
  30. ::WaitForSingleObject(hEvent, INFINITE);
  31. // 数据交互, 从内核层中获取数据
  32. bRet = ::DeviceIoControl(hDevice, IOCTL_MY_OUTPUT, NULL, 0, szOutput, dwOutput, &dwRet, NULL);
  33. if (FALSE == bRet)
  34. {
  35. printf("DeviceIoControl Error[%d]\n", ::GetLastError());
  36. }
  37. // 显示
  38. printf("[From Kernel Output]%s\n", szOutput);
  39. // 关闭设备句柄
  40. ::CloseHandle(hEvent);
  41. ::CloseHandle(hDevice);
  42. system("pause");
  43. return 0;
  44. }

内核层代码

IRP_MJ_DEVICECONTROL 消息处理函数

  1. // IRP_MJ_DEVICE_CONTROL 消息处理函数
  2. NTSTATUS DriverControlHandle(PDEVICE_OBJECT pDevObj, PIRP pIrp)
  3. {
  4. NTSTATUS status = STATUS_SUCCESS;
  5. // 获取当前 IRP 栈空间数据
  6. PIO_STACK_LOCATION pIoStackLocation = IoGetCurrentIrpStackLocation(pIrp);
  7. // 获取输入/输出缓冲区
  8. PVOID pBuffer = pIrp->AssociatedIrp.SystemBuffer;
  9. // 获取输入缓冲区数据长度
  10. ULONG ulInputLength = pIoStackLocation->Parameters.DeviceIoControl.InputBufferLength;
  11. // 获取输出缓冲区数据长度
  12. ULONG ulOutputLength = pIoStackLocation->Parameters.DeviceIoControl.OutputBufferLength;
  13. // 实际输出数据长度
  14. ULONG ulInfo = 0;
  15. // 获取控制码
  16. ULONG ulControlCode = pIoStackLocation->Parameters.DeviceIoControl.IoControlCode;
  17. // 根据操作码分别进行操作
  18. switch (ulControlCode)
  19. {
  20. case IOCTL_MY_TEST:
  21. {
  22. // 获取传入的事件句柄
  23. HANDLE hUserEvent = *(HANDLE *)pBuffer;
  24. // 处理类型32位、64位下类型不匹配的情况
  25. if (4 == ulInputLength)
  26. {
  27. hUserEvent = (HANDLE)((SIZE_T)hUserEvent & 0x0FFFFFFFF);
  28. }
  29. // 根据事件句柄获取内核事件对象
  30. status = ObReferenceObjectByHandle(hUserEvent, EVENT_MODIFY_STATE, *ExEventObjectType, KernelMode, (PVOID)(&g_pKernelEvent), NULL);
  31. if (!NT_SUCCESS(status))
  32. {
  33. DbgPrint("ObReferenceObjectByHandle Error[0x%X]\n", status);
  34. g_pKernelEvent = NULL;
  35. break;
  36. }
  37. // 创建多线程, 执行操作, 执行完毕后, 发送事件通知用户层
  38. HANDLE hThread = NULL;
  39. PsCreateSystemThread(&hThread, 0, NULL, NtCurrentProcess(), NULL, ThreadProc, g_pKernelEvent);
  40. break;
  41. }
  42. case IOCTL_MY_OUTPUT:
  43. {
  44. RtlCopyMemory(pBuffer, g_szOutputBuffer, 50);
  45. ulInfo = 50;
  46. break;
  47. }
  48. default:
  49. break;
  50. }
  51. pIrp->IoStatus.Status = status;
  52. pIrp->IoStatus.Information = ulInfo;
  53. IoCompleteRequest(pIrp, IO_NO_INCREMENT);
  54. return status;
  55. }

多线程处理函数

  1. // 多线程处理函数
  2. VOID ThreadProc(PVOID StartContext)
  3. {
  4. DbgPrint("Enter ThreadProc\n");
  5. // 获取内核对象
  6. PKEVENT pKernelEvent = (PKEVENT)StartContext;
  7. // 设置输出缓冲区
  8. RtlCopyMemory(g_szOutputBuffer, "I am DemonGan From Kernel Event.", (1 + strlen("I am DemonGan From Kernel Event.")));
  9. // 发送事件, 将事件对象设置为信号状态
  10. if (NULL != pKernelEvent)
  11. {
  12. KeSetEvent(pKernelEvent, IO_NO_INCREMENT, FALSE);
  13. }
  14. // 释放事件对象
  15. ObDereferenceObject(pKernelEvent);
  16. pKernelEvent = NULL;
  17. DbgPrint("Leave ThreadProc\n");
  18. }

程序测试

在 Win7 32 位系统下,驱动程序正常执行:

在 Win10 64 位系统下,驱动程序正常执行:

总结

这个框架理解起来不是很复杂,关键是理解事件 EVENT 的同步处理,实现操作的先后顺序。

参考

参考自《Windows黑客编程技术详解》一书

上传的附件 cloud_download NT_Event_Test.7z ( 149.81kb, 27次下载 )

发送私信

这一切都不是我的,但总有一天,会是我的

82
文章数
68
评论数
最近文章
eject