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

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

2016-08-21_203643

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

2016-08-21_211926

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

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。

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 中。应用层接收并打印内容。代码如下:

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,应用层接收数据打印。

// 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 是应用层的实现。

评论