Windows内核基础知识-8-监听进程、线程和模块
Windows内核有一种强大的机制,可以在重大事件发送时得到通知,比如这里的进程、线程和模块加载通知。
本次采用链表+自动快速互斥体来实现内核的主要架构。
进程通知
只要在内核里面注册了进程通知那么创建进程就会反馈给内核里面。
//注册/销毁进程通知函数
NTSTATUS PsSetCreateProcessNotifyRoutineEx(
PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,//回调函数
BOOLEAN Remove//False表示注册,TRUE表示销毁
);
PCREATE_PROCESS_NOTIFY_ROUTINE_EX PcreateProcessNotifyRoutineEx;
void PcreateProcessNotifyRoutineEx(
PEPROCESS Process,//得到的进程EPROCESS结构体
HANDLE ProcessId,//得到的进程句柄
PPS_CREATE_NOTIFY_INFO CreateInfo//得到的进程信息,如果是销毁就是NULL,创建就是一个指针
)
{...}
注意:在用到上述回调函数的驱动必须在PE的PE映像头里设有IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY标志,可以通过vs中的linker添加命令行:/integritycheck
实现进程通知
创建一个驱动项目,名为SysMon,文件结构图如下:
AutoLock和FastMutex是用来封装一个快速互斥体方便和保护多线程访问同一内容。pch是预编译头SysMonCommon.h是给User和Kernel公用的结构体文件,SysMon是驱动主要逻辑的代码文件。
首先是pch.h和pch.cpp,这个就是一个预编译头用来加速编译速度,预编译头只编译一次,内部用二进制保存下来并用于后面的编译,这样可以显著的加快编译速度:(就可以把不会变的头文件直接加进去来提速,但是后面的每一个cpp文件都必须包含pch.h,而头文件不用,头文件可以直接用pch的内容)
//pch.h
#include<ntddk.h>
//pch.cpp
#include"pch.h"
然后是AutoLock和FastMutex,这个在前面Windows内核开发-6-内核机制 Kernel Mechanisms - Sna1lGo - 博客园 (cnblogs.com)有讲过,这里直接上代码了:
//FastMutex.h
#pragma once
class FastMutex {
public:
void Init();
void Lock();
void Unlock();
private:
FAST_MUTEX _mutex;
};
//FastMutex.cpp
#include"pch.h"
#include"FastMutex.h"
void FastMutex::Init()
{
ExInitializeFastMutex(&_mutex);
}
void FastMutex::Lock()
{
ExAcquireFastMutex(&_mutex);
}
void FastMutex::Unlock()
{
ExReleaseFastMutex(&_mutex);
}
//AutoLock.h
#pragma once
//封装成一个自动的互斥体
template<typename TLock>
struct AutoLock {
AutoLock(TLock& lock):_lock(lock){
_lock.Lock();
}
~AutoLock()
{
_lock.Unlock();
}
private:
TLock& _lock;
};
//AutoLock.cpp
#include"pch.h"
#include"AutoLock.h"
接着是公用的结构体文件: SysMonCommon.h:
这里我们采用一些正式开发比较常用的办法:
//添加枚举类来进行区别响应的事件,这个采用的是C++11的有范围枚举(scoped enum)特性
enum class ItemType : short{
None,
ProcessCreate,
ProcessExit
};
//公有的内容就可以设置为一个头结构体,后面的再继承它来扩充
struct ItemHeader{
ItemType Type;
USHORT Size;
LARGE_INTEGER Time;//系统的时间类
};
//添加具体的事件信息结构体,退出一个进程没啥好知道的,知道个退出的进程ID就行
struct ProcessExitInfo : ItemHeader{
ULONG ProcessId;
};
最后是SysMon.h:
//这个头文件主要用来实现驱动的主要逻辑代码,因为我们采用链表来存储所有的信息,所以链表也要加在这里面
//采用模板类来让所有的结构体都可以利用链表串联起来而防止编写很多重复的代码
template<typename T>
struct FullItem{
LIST_ENTRY entry;
ProcessExitInfo Data;
}
//再建立一个统领全局的全局变量结构体,来存储所有的信息
//包含了驱动程序的所有全局状态的数据结构体
struct Globals{
LIST_ENTRY ItemsHead;//链表的头指针
int ItemCount;//事件的个数
FastMutex Mutex;//快速互斥体
}
DriverEntry例程
DriverEntry主要处理的就是建立设备对象,绑定符号链接,然后符号链接可以给User用,Device给Kernel用,再绑定IRP派遣函数,然后注册响应通知。
//这里有一些函数可以先添加申明,代码逻辑后面再讲
DriverEntry(PDRIVER_OBJECT DriverObject,PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
auto status = STATUS_SUCCESS;
InitializeListHead(&g_Globals.ItemHead);//初始化链表
g_Globals.Mutex.Init(); //初始化互斥体
//建立设备对象和符号链接
PDEVICE_OBJECT DeviceObject = NULL;
UNICODE_STRING symLinkName = RTL_CONSTANT_STRING(L"\\??\\sysmon");
bool symLinkCreate = FALSE;
do {
UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\sysmon");
status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, TRUE, &DeviceObject);
if (!NT_SUCCESS(status))
{
KdPrint(("failed to create device Error:(0x%08X)",status));
break;
}
DeviceObject->Flags |= DO_DIRECT_IO;//直接IO
status = IoCreateSymbolicLink(&symLinkName, &devName);
if (!NT_SUCCESS(status))
{
KdPrint(("failed to create SymbolcLink Error:(0x%08X)\n",status));
break;
}
symLinkCreate = TRUE;
//注册进程提醒函数
status = PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, FALSE);
if (!NT_SUCCESS(status))
{
KdPrint(("failed to register process callback (0x%08X)\n",status));
break;
}
if (!NT_SUCCESS(status))
{
if (symLinkCreate)
IoDeleteSymbolicLink(&symLinkName);
if (DeviceObject)
IoDeleteDevice(DeviceObject);
}
DriverObject->DriverUnload = SysMonUnload;
DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverObject->MajorFunction[IRP_MJ_CLOSE] = SysMonCreateClose;
DriverObject->MajorFunction[IRP_MJ_READ] = SysMonRead;
return status;
}
处理进程退出通知
前面讲到注册进程通知函数里面有一个回调函数,这个函数就是用来得到进程响应的信息,不管是进程退出还是创建都可以
//前面在注册进程提醒函数的时候有用到这条代码,所以我们需要完善的就是这个回调函数就行:
// status = PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, FALSE);
//前面进程通知的时候有讲函数原型,所以这里直接贴代码:
//PushItem是一个后续会完善的一个函数,用来将内容添加到链表里
void OnProcessNotify(PEPROCESS Process,HANDLE ProcessId,PPS_CREATE_NOTIFY_INFO CreateInfo)
{
UNREFERENCED_PARAMETER(Process);
//如果进程被销毁CreateInfo这个参数为NULL
if (CreateInfo)
{
//进程创建事件获取内容
}
else
{
//进程退出
//保存退出的进程的ID和事件的公用头部,ProcessExitInfo是封装的专门针对退出进程保存的信息结构体,DRIVER_TAG是分配的内存的标签位。
auto info = (FullItem<ProcessExitInfo>*)ExAllocatePoolWithTag(PagedPool, sizeof(FullItem<ProcessExitInfo>), DRIVER_TAG);
if (info == nullptr)
{
KdPrint(("when process exiting,failed to allocation\n"));
return;
}
//分配成功就开始收集信息
auto& item = info->Data;
KeQuerySystemTimePrecise(&item.Time);//获取进程时间
item.Type = ItemType::ProcessExit;//设置捕获的进行信息类型为枚举类的退出进程
item.ProcessId = HandleToULong(ProcessId);//把句柄转换为ulong类型(其实是一个)
item.Size = sizeof(ProcessExitInfo);
PushItem(&info->Entry);//将该数据添加到链表尾部
}
}
处理进程创建通知
这个其实有了前面的经验就知道了,只需要在进程响应回调函数里面的if语句中再添加代码就好了:
void OnProcessNotify(PEPROCESS Process,HANDLE ProcessId,PPS_CREATE_NOTIFY_INFO CreateInfo)
{
UNREFERENCED_PARAMETER(Process);
//如果进程被销毁CreateInfo这个参数为NULL
if (CreateInfo)
{
//进程创建事件获取内容
USHORT allocSize = sizeof(FullItem<ProcessCreateInfo>);
USHORT commandLineSize = 0;
if (CreateInfo->CommandLine)//如果有命令行输入
{
commandLineSize = CreateInfo->CommandLine->Length;
allocSize += commandLineSize;//要分配的内存大小
}
//分配进程创建结构体大小
auto info = (FullItem<ProcessCreateInfo>*)ExAllocatePoolWithTag(PagedPool, allocSize, DRIVER_TAG);
if (info == nullptr)
{
KdPrint(("SysMon: When process is creating,failed to allocate memory"));
return;
}
auto& item = info->Data;
KeQuerySystemTimePrecise(&item.Time);
item.Type = ItemType::ProcessCreate;
item.Size = allocSize;
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);//以wchar为单位
item.CommandLineOffset = sizeof(item);//从多久开始偏移是命令字符串的首地址
}
else
{
item.CommandLineLength = 0;
item.CommandLineOffset = 0;
}
PushItem(&info->Entry);
}
else
{
//进程退出
//保存退出的进程的ID和事件的公用头部,ProcessExitInfo是封装的专门针对退出进程保存的信息结构体,DRIVER_TAG是分配的内存的标签位。
auto info = (FullItem<ProcessExitInfo>*)ExAllocatePoolWithTag(PagedPool, sizeof(FullItem<ProcessExitInfo>), DRIVER_TAG);
if (info == nullptr)
{
KdPrint(("when process exiting,failed to allocation\n"));
return;
}
//分配成功就开始收集信息
auto& item = info->Data;
KeQuerySystemTimePrecise(&item.Time);//获取进程时间
item.Type = ItemType::ProcessExit;//设置捕获的进行信息类型为枚举类的退出进程
item.ProcessId = HandleToULong(ProcessId);//把句柄转换为ulong类型(其实是一个)
item.Size = sizeof(ProcessExitInfo);
PushItem(&info->Entry);//将该数据添加到链表尾部
}
}
将数据提供给用户模式User
这里就需要设计到IRP派遣函数了。派遣函数前面有讲过,主要就是用作User和Kernel的交互,可以比作Windows的消息处理机制,User读取Kernel的Device中的内容需要Read,然后这个Read通过派遣函数分发到了Kernel里面,Kernel里面。IRP比较复杂,可以暂时理解为一个桥梁,将User下的API和Kernel下的函数一一对应,比如说CreateFile通过IRP对应到了Kernel的TestCreate函数。
NTSTATUS SysMonRead(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
UNREFERENCED_PARAMETER(pDevObj);
auto stack = IoGetCurrentIrpStackLocation(pIrp);
auto len = stack->Parameters.Read.Length;//获取User的读取缓冲区大小
auto status = STATUS_SUCCESS;
auto count = 0;
NT_ASSERT(pIrp->MdlAddress);//MdlAddress表示使用了直接I/O
auto buffer = (UCHAR*)MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);//获取直接I/O对应的内存空间缓冲区
if (!buffer)
{
status = STATUS_INSUFFICIENT_RESOURCES;
}
else
{
//访问链表头,获取数据返回给User,获得内容后就直接删除
AutoLock<FastMutex> lock(g_Globals.Mutex);
while (TRUE)
{
if (IsListEmpty(&g_Globals.ItemHead))//如果链表为空就退出循环,当然检测ItemCount也是可以的
{
break;//退出循环
}
auto entry = RemoveHeadList(&g_Globals.ItemHead);
auto info = CONTAINING_RECORD(entry,FullItem<ItemHeader>, Entry);//返回首地址
auto size = info->Data.Size;
if (len < size)
{
//剩下的BUFFER不够了
//又放回去
InsertHeadList(&g_Globals.ItemHead, entry);
break;
}
g_Globals.ItemCount--;
::memcpy(buffer, &info->Data, size);
len -= size;
buffer += size;
count += size;
//释放内存
ExFreePool(info);
}
}
//完成此次
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = count;
IoCompleteRequest(pIrp, 0);
return status;
}
//Create和Close没啥用,因为它们只要能够让这个完整执行就行了,而一个IRP完整执行通常都会有一下的三条语句
NTSTATUS SysMonCreateClose(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
UNREFERENCED_PARAMETER(pDevObj);
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, 0);
return 0;
}
然后还有比较重要的User代码:
(主要的代码逻辑就是:接受内核传递的信息,然后输出出来)
#include<iostream>
#include<Windows.h>
#include"../SysMon/SysMonCommon.h"
using namespace std;
int Error(const char* Msg)
{
cout << Msg << endl;
return 0;
}
void DisplayTime(const LARGE_INTEGER& time)
{
SYSTEMTIME st;
::FileTimeToSystemTime((FILETIME*)&time, &st);
printf("%02d:%02d:%02d.%03d: ", st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
}
void DisplayInfo(BYTE* buffer, DWORD size)
{
auto count = size;//读取的总数
while (count > 0)
{
//利用枚举变量来区分,分开输出
auto header = (ItemHeader*)buffer;
switch (header->Type)
{
case ItemType::ProcessCreate:
{
DisplayTime(header->Time);
auto info = (ProcessCreateInfo*)buffer;
std::wstring commandline((WCHAR*)(buffer + info->CommandLineOffset), info->CommandLineLength);
printf("Process %d created.Command line:%ws\n", info->ProcessId, commandline.c_str());
break;
}
case ItemType::ProcessExit:
{
DisplayTime(header->Time);
auto info = (ProcessExitInfo*)buffer;
printf("Process %d Exited\n", info->ProcessId);
break;
}
case ItemType::ThreadCreate:
{
DisplayTime(header->Time);
auto info = (ThreadCreateExitInfo*)buffer;
printf("Thread %d Create in process %d\n", info->ThreadId, info->ProcessID);
break;
}
case ItemType::ThreadExit:
{
DisplayTime(header->Time);
auto info = (ThreadCreateExitInfo*)buffer;
printf("Thread %d Exit from process %d\n", info->ThreadId, info->ProcessID);
break;
}
case ItemType::ImageLoad:
{
DisplayTime(header->Time);
auto info = (ImageLoadInfo*)buffer;
printf("Image loaded into process %d at address 0x%p (%ws)\n", info->ProcessId, info->LoadAddress, info->ImageFileName);
break;
}
default:
break;
}
buffer += header->Size;
count += header->Size;
}
}
int main()
{
//通过符号链接来读取文件
auto hFile = ::CreateFile(L"\\\\.\\sysmon", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
return Error("Failed to open File");
}
BYTE buffer[1 << 16];//左移16位,64KB的BUFFER
while (1)
{
DWORD bytes;
if (!::ReadFile(hFile, buffer, sizeof(buffer), &bytes, nullptr))
Error("Failed to read File");
if (bytes != 0)
DisplayInfo(buffer, bytes);
::Sleep(2000);
}
system("pause");
}
线程通知
和前面一样,线程通知也是有注册线程通知信息的API,可以仿造着进程通知的方式来写,但是有一点不一样:
//sysMon.cpp中添加到进程注册后面的代码
//注册线程提醒函数
status = PsSetCreateThreadNotifyRoutine(OnThreadNotiry);
if (!NT_SUCCESS(status))
{
KdPrint(("failed to register thread callback (0x%08X)\n", status));
break;
}
可以看到这里的API:PsSetCreateThreadNotifyRoutine有一点点不一样
NTSTATUS PsSetCreateThreadNotifyRoutine(
PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);
PCREATE_THREAD_NOTIFY_ROUTINE PcreateThreadNotifyRoutine;
void PcreateThreadNotifyRoutine(
HANDLE ProcessId,
HANDLE ThreadId,
BOOLEAN Create
)
{...}
这里的函数是通过回调函数的Create标志位来判断是创建还是销毁。
前面的可以套用进程通知,但是有一些结构体需要扩充,比如说,SysMonCommand.h里面的内容:
//事件的类型
enum class ItemType : short {
None,
ProcessCreate,
ProcessExit,
ThreadCreate,
ThreadExit,
};
//线程的信息结构体
struct ThreadCreateExitInfo : ItemHeader {
ULONG ThreadId;//线程ID
ULONG ProcessID;//线程对应的进程ID
};
还有User的Switch语句,也要依据类型来不同的输出:
case ItemType::ThreadCreate:
{
DisplayTime(header->Time);
auto info = (ThreadCreateExitInfo*)buffer;
printf("Thread %d Create in process %d\n", info->ThreadId, info->ProcessID);
break;
}
case ItemType::ThreadExit:
{
DisplayTime(header->Time);
auto info = (ThreadCreateExitInfo*)buffer;
printf("Thread %d Exit from process %d\n", info->ThreadId, info->ProcessID);
break;
}
就依葫芦画瓢基本上可以解决掉。
模块载入通知
模块也和进程、线程加载差不多:(但是改API没有卸载的响应,这个暂时不清楚,我也没有尝试,有兴趣的可以试一下)
NTSTATUS PsSetLoadImageNotifyRoutine(
PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);
PLOAD_IMAGE_NOTIFY_ROUTINE PloadImageNotifyRoutine;
void PloadImageNotifyRoutine(
PUNICODE_STRING FullImageName,
HANDLE ProcessId,
PIMAGE_INFO ImageInfo
)
{...}
通过这些API可以注册内核响应模块加载,但是和前面一样也需要注意结构体的信息。
总结
内核有很多强大的机制,这里介绍了进程、线程和模块的创建销毁的响应。
所以代码的合集: