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

2016-08-21_203643 可以看到,程序可以成功运行在 Win10x64 环境下,下面我们分别仔细的讲解一下程序的细节。程序的主要工作流程如下图: 2016-08-21_211926 先来看一下 DriverEntry 入口函数。函数中先创建了进程创建后的回调监听函数,我们在这个函数里面实现对进程的监控。随后初始化了链表锁、链表头(用于存放已经创建的进程数据)及事件句柄。最后入口函数设置了各个通知函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 中。应用层接收并打印内容。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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,应用层接收数据打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 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 是应用层的实现。