前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >内核事件 KEVENT 实现驱动与应用层通讯

内核事件 KEVENT 实现驱动与应用层通讯

作者头像
我与梦想有个约会
发布2023-10-21 14:41:30
5870
发布2023-10-21 14:41:30
举报
文章被收录于专栏:jiajia_deng

前段时间一直在学习内核监控进程创建的知识,虽然成功监视,但一直在内核输出到 DebugView 中,不能通知我们的应用程序来显示指定内容,无论如何也不方便,所以赶在周末参考了 Windows 内核安全与驱动开发 中第五章 “应用与内核通讯” 制作了以下程序。程序主要使用了内核事件 KEVENT 实现同步,更多请参考 Windows 内核安全与驱动开发,程序运行后的效果如下:

可以看到,程序可以成功运行在 Win10x64 环境下,下面我们分别仔细的讲解一下程序的细节。程序的主要工作流程如下图:

先来看一下 DriverEntry 入口函数。函数中先创建了进程创建后的回调监听函数,我们在这个函数里面实现对进程的监控。随后初始化了链表锁、链表头(用于存放已经创建的进程数据)及事件句柄。最后入口函数设置了各个通知函数:

代码语言:javascript
复制
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath)
{
    NTSTATUS status = STATUS_SUCCESS;

    KdPrint(("pRegistryPath = %wZ", pRegistryPath));

    // 创建进程监视回调
    status = PsSetCreateProcessNotifyRoutineEx(CreateProcessNotifyEx, FALSE);
    if (!NT_SUCCESS(status))
    {
        KdPrint(("Failed to call PsSetCreateProcessNotifyRoutineEx, error code = 0x%08X", status));
    }

    // 初始化事件、锁、链表头
    KeInitializeEvent(&g_Event, SynchronizationEvent, TRUE);
    KeInitializeSpinLock(&g_Lock);
    InitializeListHead(&g_ListHead);

    // 创建设备和符号链接
    CreateDevice(pDriverObject);

    // 指派各分发函数
    pDriverObject->MajorFunction[IRP_MJ_CREATE] = CreateCompleteRoutine;
    pDriverObject->MajorFunction[IRP_MJ_CLOSE] = CloseCompleteRoutine;
    pDriverObject->MajorFunction[IRP_MJ_READ] = ReadCompleteRoutine;
    pDriverObject->MajorFunction[IRP_MJ_WRITE] = WriteCompleteRoutine;
    pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DeviceControlCompleteRoutine;

    pDriverObject->DriverUnload = DriverUnLoad;

    return status;
}

接下来我们看一下创建进程会调用的回调函数中,我们设定若发现新进程创建,则将进程的信息作为链表一个节点插入到链表中,并设置全局的 g_Event 为有信号状态。这里一定要注意 KeSetEvent 函数的使用,如果第三个参数设置为 TRUE 的话,100% 会蓝屏,代码为 0x0000004A。

代码语言:javascript
复制
VOID CreateProcessNotifyEx(
    _Inout_  PEPROCESS              Process,
    _In_     HANDLE                 ProcessId,
    _In_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
)
{
    // 如果 CreateInfo 结构不为 NULL 则证明是创建进程
    if (NULL != CreateInfo)
    {
        // 创建一个链表节点
        PPROCESSNODE pNode = InitListNode();
        if (pNode != NULL)
        {
            // 给节点的 pProcessInfo 分配内存
            // 该 ProcessInfo 结构体与应用层使用的是同样的结构体
            // 应用层传入相同大小的内存提供内核写入相应数据
            pNode->pProcessInfo = ExAllocatePoolWithTag(NonPagedPool, sizeof(PROCESSINFO), MEM_TAG);

            // 给各节点赋值
            pNode->pProcessInfo->hParentId = CreateInfo->ParentProcessId;
            pNode->pProcessInfo->hProcessId = ProcessId;
            RtlFillMemory(pNode->pProcessInfo->wsProcessPath, MAX_STRING_LENGTH, 0x0);
            RtlFillMemory(pNode->pProcessInfo->wsProcessCommandLine, MAX_STRING_LENGTH, 0x0);
            wcsncpy(pNode->pProcessInfo->wsProcessPath, CreateInfo->ImageFileName->Buffer, CreateInfo->ImageFileName->Length / sizeof(WCHAR));
            wcsncpy(pNode->pProcessInfo->wsProcessCommandLine, CreateInfo->CommandLine->Buffer, CreateInfo->CommandLine->Length / sizeof(WCHAR));

            // 插入链表,设置事件
            ExInterlockedInsertTailList(&g_ListHead, (PLIST_ENTRY)pNode, &g_Lock);

            // 这里第三个参数一定要注意,如果为 TRUE 则表示 KeSetEvent 后面一定会有一个 KeWaitForSigleObject
            // 而如果 KeWaitForSigleObject 不在 KeSetEvent 调用之后,则设置为 FLASE,否则会导致 0x0000004A 蓝屏
            KeSetEvent(&g_Event, 0, FALSE);
        }
    }
}

此时若有新进程创建全局的 g_Event 会被设置为有信号状态,接下来就到我们处理应用层使用 DeviceIoControl 在驱动中的响应功能了。我们设置了一个无限循环,一直从链表中取数据,若取出的数据为 NULL,则等待 g_Event,当 g_Event 为有信号状态时,证明有新进程创建了,那么 KeWaitForSigleObject 立即返回执行下一次循环,这次循环就可以取到链表中的节点信息了,取到以后直接拷贝给应用层提供的 Buffer 中。应用层接收并打印内容。代码如下:

代码语言:javascript
复制
NTSTATUS DeviceControlCompleteRoutine(PDEVICE_OBJECT pDeviceObject, PIRP pIrp)
{
    NTSTATUS            status = STATUS_SUCCESS;
    PIO_STACK_LOCATION  pIrpsp = IoGetCurrentIrpStackLocation(pIrp);
    ULONG               uLength = 0;

    PVOID pBuffer           = pIrp->AssociatedIrp.SystemBuffer;
    ULONG ulInputlength     = pIrpsp->Parameters.DeviceIoControl.InputBufferLength;
    ULONG ulOutputlength    = pIrpsp->Parameters.DeviceIoControl.OutputBufferLength;

    do 
    {
        switch (pIrpsp->Parameters.DeviceIoControl.IoControlCode)
        {
        case CWK_DVC_SEND_STR:          // 接收到发送数据请求
            {
                ASSERT(pBuffer != NULL);
                ASSERT(ulInputlength > 0);
                ASSERT(ulOutputlength == 0);

                // KdPrint(("pBuffer = %s", pBuffer));
            }
            break;
        case CWK_DVC_RECV_STR:          // 接收到获取数据请求
            {
                ASSERT(pBuffer != NULL);
                ASSERT(ulInputlength == 0);

                // 如果给出的 Buffer 大小小于 PROCESSINFO 结构体大小,则判断非法
                if (ulOutputlength < sizeof(PROCESSINFO))
                {
                    status = STATUS_INVALID_BUFFER_SIZE;
                    break;
                }

                // 创建一个循环,不断从链表中拿是否有节点
                while (TRUE)
                {
                    PPROCESSNODE pNode = (PPROCESSNODE)ExInterlockedRemoveHeadList(&g_ListHead, &g_Lock);

                    // 如果拿到了节点,则传给应用层,直接想 pBuffer 里面赋值,应用层 DeviceIoControl 就能收到数据
                    if (NULL != pNode)
                    {
                        PPROCESSINFO pProcessInfo = (PPROCESSINFO)pBuffer;
                        if (NULL != pNode->pProcessInfo)
                        {
                            // 给应用层 Buffer 赋值
                            pProcessInfo->hParentId = pNode->pProcessInfo->hParentId;
                            pProcessInfo->hProcessId = pNode->pProcessInfo->hProcessId;
                            wcsncpy(pProcessInfo->wsProcessPath, pNode->pProcessInfo->wsProcessPath, MAX_STRING_LENGTH);
                            wcsncpy(pProcessInfo->wsProcessCommandLine, pNode->pProcessInfo->wsProcessCommandLine, MAX_STRING_LENGTH);
                            uLength = sizeof(PROCESSINFO);

                            // 释放内存
                            ExFreePoolWithTag(pNode->pProcessInfo, MEM_TAG);
                        }

                        // 释放内存
                        ExFreePoolWithTag(pNode, MEM_TAG);
                        break;
                    }
                    else
                    {
                        // 如果没有取到节点,则等待一个事件通知,该事件在 CreateProcessNotifyEx 函数中会被设置
                        // 当产生一个新的进程时会向链表插入一个节点,同时该事件被设置为有信号状态
                        // 随后 KeWaitForSingleObject 返回继续执行循环,继续执行时就可以取到新的节点数据了
                        KeWaitForSingleObject(&g_Event, Executive, KernelMode, 0, 0);
                    }
                }
            }
            break;
        default:
        {
            status = STATUS_INVALID_PARAMETER;
        }
        break;
        }
    } while (FALSE);


    pIrp->IoStatus.Status = status;
    pIrp->IoStatus.Information = uLength;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);

    return status;
}

上面是驱动层的实现,我们来看一下应用层的示例代码。通过 CreateFile 打开设备,并调用 DeviceIoControl 函数向驱动发送一个接收数据的请求。此时如果驱动链表中没有数据,那么会停在 KeWaitForSingleObject 函数,同时应用层也阻塞在 DeviceIoControl 函数上。一旦 g_Event 被设置为有信号状态,则 KeWaitForSingleObject 返回,拷贝数据给应用层提供的 Buffer,应用层接收数据打印。

代码语言:javascript
复制
// TestCommunication.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include "NtStructDef.h"

#define SYMBOLIC_NAME _T("\\??\\Communication")

int _tmain(int argc, _TCHAR* argv[])
{
    HANDLE      hStdHandle;
    HANDLE      hDevice = NULL;
    ULONG       ulResult = 0;
    BOOL        bRet = FALSE;

    // 设置控制台窗口大小,方便查看
    hStdHandle = GetStdHandle(STD_OUTPUT_HANDLE);
    SMALL_RECT rc = { 0, 0, 120 - 1, 25 - 1 };  
    SetConsoleWindowInfo(hStdHandle, TRUE, &rc);

    // 打开驱动设备
    hDevice = CreateFile(SYMBOLIC_NAME, GENERIC_READ  GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, 0);
    if (hDevice == INVALID_HANDLE_VALUE)
    {
        printf("Failed to Open device.\r\n");
        return -1;
    }

    // Receive message from driver
    PROCESSINFO stProcessInfo;
    while (TRUE)
    {
        memset(&stProcessInfo, 0, sizeof(PROCESSINFO));
        bRet = DeviceIoControl(hDevice, CWK_DVC_RECV_STR, NULL, 0, &stProcessInfo, sizeof(stProcessInfo), &ulResult, 0);
        if (bRet)
        {
            // 打印数据,wsProcessCommandLine 也是一个参数,如果需要可以自己放开,格式化字符串中增加一个 %ws
            printf("PPID = %ld, PID = %ld, %ws\r\n",
                stProcessInfo.hParentId,
                stProcessInfo.hProcessId,
                stProcessInfo.wsProcessPath/*,
                stProcessInfo.wsProcessCommandLine*/);
        }
    }

    CloseHandle(hDevice);
    system("PAUSE");

    return 0;
}

这里请注意应用层和驱动层使用了相同的 PROCESSINFO 结构体,该结构体包含了创建进程的各种信息,我们使用一个公共头文件保存他,所有代码请参考 github:https://github.com/nmgwddj/Learn-Windows-Drivers,其中 Communication 是驱动层的实现,TestCommunication 是应用层的实现。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2016-08-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档