selph
selph
发布于 2021-12-09 / 840 阅读
0
0

Windows 内核编程 ch8--进程和线程通知

内核驱动程序有一种强大的机制:在某些重要事件发生的时候得到通知

8.1 进程通知

驱动可以实时监测进程的创建与销毁,还能向调用者返回一个错误,阻止进程的创建

用于注册进程通知的 API:PsSetCreateProcessNotifyRoutineEx

NTSTATUS PsSetCreateProcessNotifyRoutineEx(
  [in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,	// 驱动程序通知回调例程
  [in] BOOLEAN                           Remove		// 指明注册(FALSE)还是取消注册(TRUE)
);

Windows10 新增了:PsSetCreateProcessNotifyRoutineEx2,可支持子系统微进程的监控

NTSTATUS PsSetCreateProcessNotifyRoutineEx2(
  [in] PSCREATEPROCESSNOTIFYTYPE NotifyType,
  [in] PVOID                     NotifyInformation,
  [in] BOOLEAN                   Remove
);

系统限制最多注册 64 个回调,理论上注册函数可能失败

第一个参数是注册通知回调例程:

PCREATE_PROCESS_NOTIFY_ROUTINE_EX PcreateProcessNotifyRoutineEx;

void PcreateProcessNotifyRoutineEx(
  [_Inout_]           PEPROCESS Process,		// 进程结构体
  [in]                HANDLE ProcessId,			// 进程ID
  [in, out, optional] PPS_CREATE_NOTIFY_INFO CreateInfo // 进程创建信息,进程销毁时为NULL
)
{...}

对于创建进程的监控,回调例程由正在被创建的线程执行

对于销毁进程的监控,回调例程由退出该进程的线程执行

回调例程都在关键区内部进行执行(禁用普通 APC)

用到回调的驱动程序需要在 PE 头中指定 IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY 标志,可通过链接器命令行参数处指定:/integritycheck

能从进程获取到的信息 CreateInfo:

typedef struct _PS_CREATE_NOTIFY_INFO {
  SIZE_T              Size;
  union {
    ULONG Flags;
    struct {
      ULONG FileOpenNameAvailable : 1;	// 影响ImageFileName字段
      ULONG IsSubsystemProcess : 1;	// 是否时微进程
      ULONG Reserved : 30;
    };
  };
  HANDLE              ParentProcessId;	// 父进程ID
  CLIENT_ID           CreatingThreadId;	// 进程ID和线程ID
  struct _FILE_OBJECT *FileObject;
  PCUNICODE_STRING    ImageFileName;	// 进程映像名称,FileOpenNameAvailable被设置时有效
  PCUNICODE_STRING    CommandLine;	// 进程创建的命令行
  NTSTATUS            CreationStatus;	// 返回给调用者的状态,返回STATUS_ACCESS_DENIED可阻止进程创建
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

在访问每一个指针前务必检测值是否为 NULL

8.2 实现进程通知

从这里开始往下就是写代码的部分了,这里只介绍学到的新知识,完整代码发到 github 上了:

Github 地址:ReadingNotesCode/WindowsKernelProgrammingBook/ch8_SysMon at main · kn0sky/ReadingNotesCode (github.com)


实现功能简介

本章实现的代码功能是进程线程映像加载的通知,会通过注册通知回调来获取事件信息,这里将获取到的信息发送给用户层程序,因为事件是不停产生的,在给用户程序之前,应该先存起来,然后等用户来进行接收,因为连续的内存很珍稀,所以这里采用链表来将事件信息存起来,等用户来取的时候,就把事件都给发出去

实现中的收获

这里使用了 c++11 的 scoped enum 类型作为通知类型的判断:这种类型有着强作用域的效果,比普通 enum 类型安全

enum class ItemType :short {
	None,
	ProcessCreate,
	ProcessExit,
	ThreadCreate,
	ThreadExit,
	ImageLoad
};

然后对于每一种通知事件,先抽象出了每个事件都共有的部分,然后再通过继承去编写每个事件独特的部分:

// 记录事件公有信息
struct ItemHeader {
	ItemType Type;		// 事件类型
	USHORT Size;		// 事件大小(用户能看到的大小)
	LARGE_INTEGER Time;	// 事件时间
};

//------用户模式能看到的数据信息--------
// 退出进程事件信息
struct ProcessExitInfo :ItemHeader {
	ULONG ProcessId;
};

// 创建进程事件信息
struct ProcessCreateInfo :ItemHeader {
	ULONG ProcessId;			// 进程ID
	ULONG ParentProcessId;		// 父进程ID
	USHORT CommandLineLength;	// 命令行长度
	USHORT CommandLineOffset;	// 命令行所在偏移量
	USHORT ImageFileNameLength;	// 映像名长度
	USHORT ImageFileNameOffset;	// 映像名所在偏移量
};

如果使用 C 语言,可以模拟继承操作:

struct ProcessExitInfo :ItemHeader {
	ItemHeader Header;
	ULONG ProcessId;
};

因为这些数据是要存到链表上的,所以数据结构里需要有链表结构,但又因为用户模式也需要这个数据结构的声明,所以需要给这个结构再封装一下,作为内核使用的部分:使用泛型编程来减少代码冗余

template<typename T>
struct FullItem {
	LIST_ENTRY Entry;
	T Data;
};

链表中的成员结构定义好了,现在的问题就是把链表放在哪里了,这里定义了一个结构来作为链表头节点进行链表信息统计,在驱动里定义为全局变量使用即可:

// 保存驱动全部状态
struct Globals {
	LIST_ENTRY ItemsHead;	// 驱动链表头部
	int ItemCount;			// 项数
	FastMutex Mutex;		// 快速互斥体
};

因为会同时有大量的线程来创建链表,所以需要给插入链表的函数用互斥量进行封装一下,因为快速互斥量更快,所以就用快速互斥量了:这里的快速互斥量的用法来自第六章的 C++ RAII 包装

void PushItem(PLIST_ENTRY entry) {
	// 拿到互斥量,函数结束自动释放
	AutoLock<FastMutex> lock(g_Globals.Mutex);

	// 链表模拟队列
	// 如果数据量过大,就每增加一条就删掉一条旧数据
	if (g_Globals.ItemCount > 1024) {
		// 移除第一个成员
		auto head = RemoveHeadList(&g_Globals.ItemsHead);
		g_Globals.ItemCount--;
		// 获取该成员首地址,进行内存释放
		auto item = CONTAINING_RECORD(head, FullItem<ItemHeader>, Entry);
		ExFreePool(item);
	}
	// 插入数据到链表尾部
	InsertTailList(&g_Globals.ItemsHead, entry);
	g_Globals.ItemCount++;
}

注册通知回调和注销回调都需要通过 API 进行(本章三种注册注销回调的 API 文末总结),DriverEntry 里注册,DriverUnload 里注销

DriverEntry 里除了初始化 Globals 结构的链表和快速互斥体以外,就是常规的创建驱动设备、创建符号链接,然后就是注册回调函数,设置分发例程了,没啥好说的,具体去看完整代码吧。

DriverUnload 里除了注销通知回调函数,删除符号链接和设备对象以外,还需要处理一下链表:

EXTERN_C
VOID
DriverUnload(
	_In_ struct _DRIVER_OBJECT* pDriverObject
) {
	PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, TRUE);
	PsRemoveCreateThreadNotifyRoutine(OnThreadNotify);
	PsRemoveLoadImageNotifyRoutine(OnImageNotify);

	UNICODE_STRING symLinkName = RTL_CONSTANT_STRING(SYMBOL_LINK_NAME);
	IoDeleteSymbolicLink(&symLinkName);
	IoDeleteDevice(pDriverObject->DeviceObject);

	while (!IsListEmpty(&g_Globals.ItemsHead)) {
		auto entry = RemoveHeadList(&g_Globals.ItemsHead);
		ExFreePool(CONTAINING_RECORD(entry, FullItem<ItemHeader>, Entry));
	}
}

进程通知回调函数的实现

进程通知回调函数参数里的 CreateInfo 的有无可判断是进程创建(有值)还是进程退出(NULL),不管是哪个,操作基本都一样:

  1. 计算申请内存大小(取决于事件结构大小),申请分页内存
  2. 向对应的事件结构填充数据
  3. 插入到链表

这里比较有趣的是,因为结构里有动态大小的值(进程命令行),这里通过结构里记录命令行的长度和偏移,然后多申请命令行长度的内存,把命令行字符串复制到整个事件结构体后面,用偏移标识该参数举例结构首地址的偏移来提供访问:

EXTERN_C
VOID
OnProcessNotify(
	_Inout_ PEPROCESS Process,
	_In_ HANDLE ProcessId,
	_Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
	) {
	if (CreateInfo) {	// 进程创建时
		// 获取数据结构大小
		USHORT allocSize = sizeof(FullItem<ProcessCreateInfo>);
		// 获取命令行+数据结构大小
		USHORT CommandLineSize = 0;
		if (CreateInfo->CommandLine) {
			CommandLineSize = CreateInfo->CommandLine->Length +2 ;
			allocSize += CommandLineSize;
		}
		// 获取映像名信息加到申请内存大小里
		USHORT ImageFileNameSize = 0;
		if (CreateInfo->ImageFileName) {
			ImageFileNameSize = CreateInfo->ImageFileName->Length + 2;
			allocSize += ImageFileNameSize;
		}

		// 申请内存
		auto info = (FullItem<ProcessCreateInfo>*)ExAllocatePoolWithTag(PagedPool, allocSize, DRIVER_TAG);
		if (info == nullptr) {
			KdPrint((DRIVER_PREFIX "failed allocation!\r\n"));
			return;
		}

		// 填充固定数据
		auto& item = info->Data;
		KeQuerySystemTimePrecise(&item.Time);
		ExSystemTimeToLocalTime(&item.Time, &item.Time);
		item.Type = ItemType::ProcessCreate;
		item.Size = sizeof(FullItem<ProcessCreateInfo>) + CommandLineSize + ImageFileNameSize;
		item.ProcessId = HandleToULong(ProcessId);
		item.ParentProcessId = HandleToULong(CreateInfo->ParentProcessId);

		// 填充命令行相关信息
		if (CommandLineSize > 0) {
			// 如果有命令行,就复制到结构的最后,填充相关信息
			::memcpy((UCHAR*)&item + sizeof(item), CreateInfo->CommandLine->Buffer, CommandLineSize);
			item.CommandLineLength = CommandLineSize / sizeof(WCHAR);
			item.CommandLineOffset = sizeof(item);
		}
		else {
			// 如果没命令行,就赋值0
			item.CommandLineLength = 0;
			item.CommandLineOffset = 0;
		}

		// 填充映像名相关信息
		if (ImageFileNameSize > 0) {
			::memcpy((UCHAR*)&item + sizeof(item) + CommandLineSize, CreateInfo->ImageFileName->Buffer,ImageFileNameSize);
			item.ImageFileNameLength = ImageFileNameSize / sizeof(WCHAR);
			item.ImageFileNameOffset = sizeof(item) + CommandLineSize;
		}
		else {
			item.ImageFileNameLength = 0;
			item.ImageFileNameOffset = 0;
		}

		// 插入到链表中
		PushItem(&info->Entry);

	}
	else {	// 进程退出时
		// 申请空间
		auto info = (FullItem<ProcessExitInfo>*)ExAllocatePoolWithTag(PagedPool, sizeof(FullItem<ProcessExitInfo>),DRIVER_TAG);
		if (!info) {
			KdPrint((DRIVER_PREFIX "failed allocation!\r\n"));
			return;
		}
		// 数据填充
		auto& item = info->Data;
		KeQuerySystemTimePrecise(&item.Time);
		ExSystemTimeToLocalTime(&item.Time, &item.Time);
		item.Type = ItemType::ProcessExit;
		item.ProcessId = HandleToULong(ProcessId);
		item.Size = sizeof(ProcessExitInfo);

		// 插入到链表中
		PushItem(&info->Entry);
	}
}

8.3 将数据提供给用户模式

这里只需要用户过来取数据,所以只需要一个读的操作即可,注册读的 IRP 派遣函数

这里呢拿到缓冲区、获取缓冲区大小,根据缓冲区大小,尽可能多的把存储在链表中的事件取出来,都复制到缓冲区给用户

EXTERN_C
NTSTATUS
SysMonRead(
	_In_ struct _DEVICE_OBJECT* DeviceObject,
	_Inout_ struct _IRP* Irp
) {
	auto stack = IoGetCurrentIrpStackLocation(Irp);
	auto len = stack->Parameters.Read.Length;
	auto status = STATUS_SUCCESS;
	auto count = 0;

	NT_ASSERT(Irp->MdlAddress);	// 这里使用的是直接I/O

	// 拿到缓冲区
	auto buffer = (PUCHAR)MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
	if (!buffer) {
		status = STATUS_INSUFFICIENT_RESOURCES;
	}
	else {
		AutoLock<FastMutex> lock(g_Globals.Mutex);// 要是没拿到互斥量会咋样?
		while (true) {
			// 链表空了就跳出
			if (IsListEmpty(&g_Globals.ItemsHead))
				break;
			// 从链表中拿走一项
			auto entry = RemoveHeadList(&g_Globals.ItemsHead);
			auto info = CONTAINING_RECORD(entry, FullItem<ItemHeader>, Entry);	// 为啥是这个结构
			auto size = info->Data.Size;
			if (len < size) {
				// 满了,再插进去,不拿了,跳出
				InsertHeadList(&g_Globals.ItemsHead, entry);
				break;
			}
			g_Globals.ItemCount--;
			// 把拿走的那一项复制到缓冲区
			::memcpy(buffer, &info->Data, size);
			// 计算剩余空间
			len -= size;
			// 缓冲区往后移动
			buffer += size;
			// 计算拿走的数量
			count +=size;

			ExFreePool(info);
		}
	}
	Irp->IoStatus.Status = status;
	Irp->IoStatus.Information = count;
	IoCompleteRequest(Irp, 0);
	return status;
}

用户模式对获取到的缓冲区的处理则是:获取到缓冲区,和缓冲区填入数据的大小(驱动函数返回的那个 information),然后遍历缓冲区中的事件结构体,每处理完一个事件的信息,就从结构体中取出事件结构的大小,然后向后移动缓冲区指针,同时计算缓冲区剩余大小,当大小为 0 时,表示遍历结束

void DisplayTime(const LARGE_INTEGER& time) {
	SYSTEMTIME st;
	::FileTimeToSystemTime((PFILETIME)&time, &st);
	printf("%02d:%02d:%02d:%03d ",st.wHour,st.wMinute,st.wSecond,st.wMilliseconds);
}

void DisplayInfo(PBYTE buffer, DWORD size) {
	auto count = size;
	while (count >0) {
		auto header = (ItemHeader*)buffer;
		switch (header->Type) {
		case ItemType::ProcessExit: {
			DisplayTime(header->Time);
			auto info = (ProcessExitInfo*)buffer;
			printf("【Process】【Exit】Process %06d Exited\r\n",info->ProcessId);
			break;
		}
		case ItemType::ProcessCreate: {
			DisplayTime(header->Time);
			auto info = (ProcessCreateInfo*)buffer;
			printf("【Process】【Create】Process %06d Created! ImageFileName: %ws  CommandLine: %ws\n",info->ProcessId,(PWCHAR)(info->ImageFileNameOffset + buffer),(PWCHAR)(info->CommandLineOffset+buffer));
			break;
		}
		default:
			break;
		}

		buffer += header->Size;	// 向后移动指针
		count -= header->Size;	// 计算剩余大小
	}
}

8.4 线程通知

线程通知在刚刚进程通知的基础之上,主要是新增线程通知相关结构体、注册卸载线程通知的 API,以及线程通知回调函数即可:线程通知回调函数的处理和进程通知基本一致,就是填充结构体,不同的大概就是回调函数提供的参数不同了吧

EXTERN_C
VOID
OnThreadNotify(
	_In_ HANDLE ProcessId,
	_In_ HANDLE ThreadId,
	_In_ BOOLEAN Create
) {
	auto size = sizeof(FullItem<ThreadCreateExitInfo>);

	PEPROCESS pEprocess = NULL;
	PUCHAR ProcessImageFileName = NULL;
	auto status = PsLookupProcessByProcessId(ProcessId,&pEprocess);
	auto ProcessImageFileNameLength = 0;
	if (NT_SUCCESS(status)) {
		ProcessImageFileName = PsGetProcessImageFileName(pEprocess);
		if (ProcessImageFileName) {
			ProcessImageFileNameLength = strlen((const char*)ProcessImageFileName) +1;
			size += ProcessImageFileNameLength;
		}
	}


	auto info = (FullItem<ThreadCreateExitInfo>*)ExAllocatePoolWithTag(PagedPool, size, DRIVER_TAG);
	if (!info) {
		KdPrint((DRIVER_PREFIX "failed allocation!\r\n"));
		return;
	}
	auto& item = info->Data;
	KeQuerySystemTimePrecise(&item.Time);
	ExSystemTimeToLocalTime(&item.Time, &item.Time);
	item.Size = size;
	item.Type = Create ? ItemType::ThreadCreate : ItemType::ThreadExit;
	item.ProcessId = HandleToULong(ProcessId);
	item.ThreadId = HandleToULong(ThreadId);

	if (ProcessImageFileNameLength > 0) {
		::memcpy((UCHAR*)&item + sizeof(item),ProcessImageFileName,ProcessImageFileNameLength);
		item.ProcessImageFileNameLength = ProcessImageFileNameLength;
		item.ProcessImageFileNameOffset = sizeof(item);
	}
	else {
		item.ProcessImageFileNameLength = 0;
		item.ProcessImageFileNameOffset = 0;
	}

	PushItem(&info->Entry);
}

8.5 映像载入通知

映像载入有通知回调机制,卸载却没有

具体操作类似线程通知:

EXTERN_C
VOID
OnImageNotify(
	_In_opt_ PUNICODE_STRING FullImageName,
	_In_ HANDLE ProcessId,                // pid into which image is being mapped
	_In_ PIMAGE_INFO ImageInfo
) {
	auto size = sizeof(FullItem<ImageLoadInfo>);
	auto ImageNameSize = 0;
	if (FullImageName) {
		ImageNameSize = FullImageName->Length + 2;
		size += ImageNameSize;
	}

	auto info = (FullItem<ImageLoadInfo>*)ExAllocatePoolWithTag(PagedPool, size, DRIVER_TAG);
	if (!info) {
		KdPrint((DRIVER_PREFIX "failed allocation!\r\n"));
		return;
	}
	auto& item = info->Data;
	KeQuerySystemTimePrecise(&item.Time);
	ExSystemTimeToLocalTime(&item.Time, &item.Time);
	item.Size = size;
	item.Type = ItemType::ImageLoad;
	item.ProcessId = HandleToULong(ProcessId);
	item.ImageBase = (ULONG)(ImageInfo->ImageBase);


	if (ImageNameSize > 0) {
		::memcpy((UCHAR*)&item + sizeof(item), FullImageName->Buffer, ImageNameSize);
		item.ImageLoadPathLength = ImageNameSize;
		item.ImageLoadPathOffset = sizeof(item);
	}
	else {
		item.ImageLoadPathLength = 0;
		item.ImageLoadPathOffset = 0;
	}

	PushItem(&info->Entry);
}

运行截图演示

image.png

8.6 总结

完整代码地址:ReadingNotesCode/WindowsKernelProgrammingBook/ch8_SysMon at main · kn0sky/ReadingNotesCode

作业就不在本文内容里写了,本章介绍了进程、线程、映像载入三种回调机制,三种回调的注册和卸载通过 API 进行:


注册函数卸载函数
进程通知PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, FALSE);PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, TRUE);
线程通知PsSetCreateThreadNotifyRoutine(OnThreadNotify);PsRemoveCreateThreadNotifyRoutine(OnThreadNotify);
映像通知PsSetLoadImageNotifyRoutine(OnImageNotify);PsRemoveLoadImageNotifyRoutine(OnImageNotify);

注册了通知机制的回调函数,在事件发生时,就会从发生事件的线程上下文里进入对应的回调函数,进行处理,本章的处理仅仅是记录下来事件的内容,插入链表里供通信的时候发给用户模式

这里处理通知的收获以外,学到了 C++ RAII 的用法,对事件结构体的继承用法,可变长结构的使用,以及链表的使用。


评论