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