Windows內核開發-8-監聽進程、線程和模塊


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可以注冊內核響應模塊加載,但是和前面一樣也需要注意結構體的信息。

總結

內核有很多強大的機制,這里介紹了進程、線程和模塊的創建銷毀的響應。

所以代碼的合集:

https://github.com/skrandy/SysMon


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM