基於Minifilter的檔案過濾驅動以及與應用層通訊(付程式碼)
前一段時間在做一個檔案過濾系統, 為了配合公司的產品使用,希望對指定目錄禁止訪問。一開始使用的是sfilter的框架,很多事情需要自己做,建立過濾驅動的控制裝置,建立符號連結,設定IRP例程,設定FAST I/O例程,用這個框架做了一半,與應用層通訊比較麻煩,就又去學習了Minifilter框架,這個框架就非常簡單了,就是自由度沒有sfilter那麼大,不過跟老大溝通之後,發現本次需求對邊界問題要求比較模糊,就決定使用Minifilter了。
使用Minifilter開發真的方便太多了,我用的是vs2015+WDK10,用VS2015生成專案之後,就會發現所有的例程編譯器都幫你繫結好了,你只需要在目的位置新增一些程式碼就可以搞定,關於與應用層的通訊更是簡單,不需要應用層發IRP了,直接建立一個埠通訊,應用層和驅動層保持埠名一致即可。兩天就全部搞定了。
這次的攔截操作我在 IRP_MJ_CREATE 請求之前做的。看下微軟對這個IRP請求的解釋。
I/O管理器會在一個新的檔案或目錄被建立,或者已存在的檔案、裝置、目錄、卷,被開啟的時候發生這個請求。
The I/O Manager sends the IRP_MJ_CREATE request when a new file or directory is being created, or when an existing file, device, directory, or volume is being opened.
也就是說,不管是新建還是開啟檔案或目錄,都會被我的過濾器捕捉到,可以說對操作的捕捉是很完善了。之前我在IRP_MJ_DIRECTORY_CONTROL這個IRP請求發生的時候做的攔截,後來發現使用cmd不能進入目錄,但是可以在禁止訪問目錄的父目錄開啟目標目錄中的檔案,比如禁止了FileDriver目錄後,直接在父目錄使用FileDriver/123.txt 就會開啟123.txt,但是現在不會出現這種情況了
這裡分享一下相關程式碼,我是應用層傳送路徑給驅動層,然後驅動層用連結串列儲存下來,每次I/O操作之前,都去遍歷連結串列裡的路徑,只要匹配成功就返回沒有許可權訪問,否則IRP繼續下發。
首先是驅動層的程式碼
完整程式碼這裡就不貼了,太長了,大部分都是編譯器自動生成的,沒什麼意義,這裡貼幾個主要模組的程式碼
先貼一下主函式程式碼,主要是過濾器和建立通訊埠的程式碼
NTSTATUS
DriverEntry (
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath
)
{
NTSTATUS status;
PSECURITY_DESCRIPTOR securityDescriptor;
OBJECT_ATTRIBUTES objectAttributes;
UNICODE_STRING uniString;
UNREFERENCED_PARAMETER(RegistryPath);
PT_DBG_PRINT(PTDBG_TRACE_ROUTINES,
("FsFilter!DriverEntry: Entered\n" ));
__try
{
status = FltRegisterFilter(DriverObject,
&FilterRegistration,
&gFilterHandle);
if (!NT_SUCCESS(status)) //;
{
__leave;
}
status = FltBuildDefaultSecurityDescriptor(&securityDescriptor, FLT_PORT_ALL_ACCESS);
if (!NT_SUCCESS(status)) {
__leave;
}
RtlInitUnicodeString(&uniString, L"\\CommunicationPort");
InitializeObjectAttributes(&objectAttributes, &uniString, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE, NULL, securityDescriptor);
status = FltCreateCommunicationPort(gFilterHandle, &g_ServerPort, &objectAttributes, NULL, ConnectNotifyCallback, DisconnectNotifyCallback, MessageNotifyCallback, 1);
FltFreeSecurityDescriptor(securityDescriptor);
if (!NT_SUCCESS(status)) {
__leave;
}
status = FltStartFiltering(gFilterHandle);//結果統一判斷.
}
__finally {
if (!NT_SUCCESS(status))
{
if (NULL != g_ServerPort) {
FltCloseCommunicationPort(g_ServerPort);
}
if (NULL != gFilterHandle) {
FltUnregisterFilter(gFilterHandle);
}
}
}
return status;
}
可以看到註冊過濾器之後,就建立了通訊埠,建立過程也很方便,只需要我們構造埠的符號連結(埠名),但是一定要和應用層一致
對幾個函式說明一下,這是我們之後要用到的,這裡就像繫結例程一樣
ConnectNotifyCallback:過濾管理器在使用者模式應用程式呼叫 FilterConnectCommunicationPort 的時候,呼叫這個驅動例程。它的引數不能為NULL,必須在IRQL=PASSIVE_LEVEL上被呼叫。
DisconnectNotifyCallback:當用戶模式的對於交流PORT的控制代碼的計數為0或者微小過濾驅動解除安裝的時候,這個例程個被呼叫。它的引數不能為NULL,必須在IRQL=PASSIVE_LEVEL上被呼叫。
MessageNotifyCallback:這個例程被在IRQL=PASSIVE_LEVEL上被呼叫,當用戶模式應用程式呼叫FilterSendMessage通過交流PORT傳送訊息給微小過濾驅動的時候呼叫。其引數不能為NULL,可以看到下面那一段程式碼就是這個函式,我們的主要工作也在這個函式中,就是對資料的接收和處理。
下面這段程式碼是收到了應用層發來的結構體而做的處理
我是把命令和資料夾路徑打包成了一個結構體傳送的,這個結構體在應用層要和核心層統一。否則會解析失敗的。思想很簡單,就是根據命令去操作,但是這樣的程式碼結構看起來比較清晰
NTSTATUS MessageNotifyCallback(
IN PVOID PortCookie,
IN PVOID InputBuffer OPTIONAL,
IN ULONG InputBufferLength,
OUT PVOID OutputBuffer OPTIONAL,
IN ULONG OutputBufferLength,//使用者可以接受的資料的最大長度.
OUT PULONG ReturnOutputBufferLength)
{
NTSTATUS status = 0;
wchar_t buffer[] = L"had received";
PAGED_CODE();
int level = KeGetCurrentIrql();
UNREFERENCED_PARAMETER(PortCookie);
Data *data = (Data*) InputBuffer;
IOMonitorCommand command;
command = data->command;
char* p = data->filename;
wchar_t cachePathTemp[256] = { 0 };
int i = 0;
while (*p) {
cachePathTemp[i] = *p;
i++, p++, p++;
}
cachePathTemp[i] = 0;
//申請記憶體構造字串
UNICODE_STRING cachePath = {0};
__try {
cachePath.Buffer = (PWSTR)ExAllocatePool(NonPagedPool, 2 * i);
cachePath.Length = 2 * i;
cachePath.MaximumLength = 2 * InputBufferLength;
RtlCopyMemory(cachePath.Buffer, cachePathTemp, 2 * i);
}
__except (EXCEPTION_EXECUTE_HANDLER){
ExFreePool(cachePath.Buffer);
cachePath.Buffer = NULL;
cachePath.Length = cachePath.MaximumLength = 0;
return status;
}
switch (command)
{
case DEFAULT_PATH:
AddPathList(&cachePath);
break;
case ADD_PATH:
AddPathList(&cachePath);
break;
case DELETE_PATH:
DeletePathList(&cachePath);
break;
case CLOSE_PATH:
isOpenFilter = 0;
break;
case OPEN_PATH:
isOpenFilter = 1;
break;
}
//釋放記憶體
ExFreePool(cachePath.Buffer);
cachePath.Buffer = NULL;
cachePath.Length = cachePath.MaximumLength = 0;
//列印使用者發來的資訊
KdPrint(("使用者發來的資訊是:%ls\n", InputBuffer));
//返回使用者一些資訊.
ReturnOutputBufferLength = sizeof(buffer);
RtlCopyMemory(OutputBuffer, buffer, *ReturnOutputBufferLength);
return status;
}
下面這兩個函式分別是目錄路徑連結串列的路徑增加和刪除函式,因為是C實現的,所以連結串列得自己寫,比較簡單,在核心操作字串,還是推薦用UNICODE_STRING結構體,相對安全一點,否則就會經常藍屏- -
void AddPathList(PUNICODE_STRING filename)
{
filenames * new_filename, *current, *precurrent;
new_filename = ExAllocatePool(NonPagedPool, sizeof(filenames));
new_filename->filename.Buffer = (PWSTR)ExAllocatePool(NonPagedPool, filename->MaximumLength);
new_filename->filename.MaximumLength = filename->MaximumLength;
__try {
RtlCopyUnicodeString(&new_filename->filename, filename);
new_filename->pNext = NULL;
if (NULL == fileNames) //頭是空的,路徑新增到頭
{
fileNames = new_filename;
return;
}
current = fileNames;
while (current != NULL)
{
if (RtlEqualUnicodeString(&new_filename->filename, ¤t->filename, TRUE))
//連結串列中含有這個路徑,返回
{
RtlFreeUnicodeString(&new_filename->filename);
ExFreePool(new_filename);
new_filename = NULL;
return;
}
precurrent = current;
current = current->pNext;
}
//連結串列中沒有這個路徑,新增
current = fileNames;
while (current->pNext != NULL)
{
current = current->pNext;
}
current->pNext = new_filename;
return;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
RtlFreeUnicodeString(&new_filename->filename);
ExFreePool(new_filename);
new_filename = NULL;
return;
}
}
void DeletePathList(PUNICODE_STRING filename)
{
filenames * new_filename, *current, *precurrent;
current = precurrent = fileNames;
while (current != NULL)
{
__try {
if (RtlEqualUnicodeString(filename, ¤t->filename, TRUE))
{
//判斷一下是否是頭,如果是頭,就讓頭指向第二個,刪掉第一個
if (current == fileNames)
{
fileNames = current->pNext;
RtlFreeUnicodeString(¤t->filename);
ExFreePool(current);
return;
}
//如果不是頭,刪掉當前的
precurrent->pNext = current->pNext;
current->pNext = NULL;
RtlFreeUnicodeString(¤t->filename);
ExFreePool(current);
return;
}
precurrent = current;
current = current->pNext;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return;
}
}
}
然後就是我們的重點了,接管I/O請求之後,遍歷連結串列,如果本次I/O操作要訪問的目錄是我們要禁止的,目錄,那麼直接返回沒有許可權。這裡就可以看到Minifilter獲取檔案全路徑多麼的方便,只需要呼叫兩個函式就可以搞定
可以看到我這裡有個標誌位,這個沒什麼特別的,就是加了個暫停過濾的功能
FLT_PREOP_CALLBACK_STATUS NPPreCreate(
__inout PFLT_CALLBACK_DATA Data,
__in PCFLT_RELATED_OBJECTS FltObjects,
__deref_out_opt PVOID *CompletionContext)
{
UNREFERENCED_PARAMETER(FltObjects);
UNREFERENCED_PARAMETER(CompletionContext);
PAGED_CODE();
{
UCHAR MajorFunction = 0;
PFLT_FILE_NAME_INFORMATION nameInfo;
MajorFunction = Data->Iopb->MajorFunction;
__try {
if(isOpenFilter)//首先看標誌位,是否開啟過濾
{
if (IRP_MJ_CREATE == MajorFunction &&
NT_SUCCESS(FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED | FLT_FILE_NAME_QUERY_DEFAULT, &nameInfo)))
{
if (NT_SUCCESS(FltParseFileNameInformation(nameInfo)))
{//遍歷路徑字串連結串列,包含在內,就阻止訪問
filenames *current = fileNames;
while (current != NULL)
{
if (RtlEqualUnicodeString(&nameInfo->Name, ¤t->filename, TRUE))
{
Data->IoStatus.Status = STATUS_ACCESS_DENIED;
Data->IoStatus.Information = 0;
FltReleaseFileNameInformation(nameInfo);
return STATUS_ACCESS_DENIED;
}
current = current->pNext;
}
}
FltReleaseFileNameInformation(nameInfo);
}
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return FLT_PREOP_SUCCESS_WITH_CALLBACK;
}
}
return FLT_PREOP_SUCCESS_WITH_CALLBACK;
}
這裡把標頭檔案裡的相關定義也放上來,方便大家閱讀程式碼
.h
struct Data { //應用層發來的 路徑+操作命令 結構體
int command;
wchar_t filename[200];
};
typedef struct Data Data;
typedef enum _IOMONITOR_COMMAND { //操作命令
DEFAULT_PATH,
ADD_PATH,
DELETE_PATH,
CLOSE_PATH,
OPEN_PATH,
} IOMonitorCommand;
struct filenames { //路徑連結串列結點
UNICODE_STRING filename;
struct filenames* pNext;
};
typedef struct filenames filenames;
//void ModifyPathList(PUNICODE_STRING filename); //之前把新增和刪除在一個函式實現的,這個路徑存在就預設為刪除命令,不存在就預設本次為新增命令(因為懶得解析結構體,只發一個路徑),後來感覺還是不合適,就分開了
void DeletePathList(PUNICODE_STRING filename);
void AddPathList(PUNICODE_STRING filename);
到這裡驅動層的主要程式碼塊就結束了,其他部分非常簡單,無非就是關閉埠解除安裝過濾器什麼的。
然後是應用層的程式碼
可以看到這個函式只需要輸入要傳送的資料的首地址以及資料塊大小,就可以和驅動程式實現通訊了
通過FilterConnectCommunicationPort來開啟通訊埠拿到控制代碼,通過FilterSendMessage向驅動層傳送資料
HRESULT SendToDriver(LPVOID lpInBuffer,DWORD dwInBufferSize)
{
//通訊埠
HANDLE port = INVALID_HANDLE_VALUE;
HRESULT hResult = S_OK;
wchar_t OutBuffer[MAX_PATH] = { 0 };
DWORD bytesReturned = 0;
//開啟埠通訊
hResult = FilterConnectCommunicationPort(L"\\CommunicationPort", 0, NULL, 0, NULL, &port);
if (IS_ERROR(hResult)) {
OutputDebugString(L"FilterConnectCommunicationPort fail!\n");
return hResult;
}
hResult = FilterSendMessage(port, lpInBuffer, dwInBufferSize, OutBuffer, sizeof(OutBuffer), &bytesReturned);
if (IS_ERROR(hResult)) {
CloseHandle(port);
return hResult;
}
OutputDebugString(L"從核心發來的資訊是:");
OutputDebugString(OutBuffer);
OutputDebugString(L"\n");
CloseHandle(port);
return hResult;
}
下面是四個介面新增路徑,刪除路徑,暫停過濾,恢復過濾
把介面這樣分開,後期可複用性很高,相當於這個程式就是控制我們的驅動的,叫他DriverManager將這個DriverManager封裝成dll,別的應用想要呼叫我們的驅動,只需要呼叫這幾個介面就OK了,傳入引數也十分簡單
void AddToDriver(wchar_t * filename)
{
Data data;
void *pData =NULL;
wchar_t* pNtPath = new wchar_t[256];
NTSTATUS status;
status = DeviceDosPathToNtPath(filename, pNtPath);
AddPathList(filename);
data.command = ADD_PATH;
lstrcpy(data.filename,pNtPath);
pData = &data.command;
if (!NT_SUCCESS(status)) //失敗就退出
{
return ;
}
HRESULT hResult = SendToDriver(pData,sizeof(data));
if (IS_ERROR(hResult)) {
OutputDebugString(L"FilterSendMessage fail!\n");
}
else
{
OutputDebugString(L"FilterSendMessage is ok!\n");
}
}
void DeleteFromDriver(wchar_t * filename)
{
Data data;
void *pData =NULL;
wchar_t* pNtPath = new wchar_t[256];
NTSTATUS status;
status = DeviceDosPathToNtPath(filename, pNtPath);
DeletePathList(filename);
data.command = DELETE_PATH;
lstrcpy(data.filename,pNtPath);
pData = &data.command;
if (!NT_SUCCESS(status)) //失敗就退出
{
return ;
}
HRESULT hResult = SendToDriver(pData,sizeof(data));
if (IS_ERROR(hResult)) {
OutputDebugString(L"FilterSendMessage fail!\n");
}
else
{
OutputDebugString(L"FilterSendMessage is ok!\n");
}
}
void PauseDriver()
{
Data data;
void *pData =NULL;
data.command = CLOSE_PATH;
pData = &data.command;
HRESULT hResult = SendToDriver(pData,sizeof(data));
if (IS_ERROR(hResult)) {
OutputDebugString(L"FilterSendMessage fail!\n");
}
else
{
OutputDebugString(L"FilterSendMessage is ok!\n");
}
}
void RenewDriver()
{
Data data;
void *pData =NULL;
data.command = OPEN_PATH;
pData = &data.command;
HRESULT hResult = SendToDriver(pData,sizeof(data));
if (IS_ERROR(hResult)) {
OutputDebugString(L"FilterSendMessage fail!\n");
}
else
{
OutputDebugString(L"FilterSendMessage is ok!\n");
}
}
附上應用層標頭檔案,有的函式我這裡沒貼,太長了,主要是在應用層寫了個連結串列,為了顯示方便一點,這裡就不貼了,比較簡單,和驅動層差不多
#pragma once
#define SIZE 200
#include <stdio.h>
//檔案目錄連結串列
struct filenames {
wchar_t filename[SIZE];
struct filenames* pNext;
};
typedef struct filenames filenames;
//傳送資料結構體
struct Data{
int command;
wchar_t filename[SIZE];
};
//命令定義
typedef enum _IOMONITOR_COMMAND {
DEFAULT_PATH,
ADD_PATH,
DELETE_PATH,
CLOSE_PATH,
OPEN_PATH,
} IOMonitorCommand;
//顯示路徑操作
//void ModifyPathList(wchar_t * filename);
void DeletePathList(wchar_t * filename);
void AddPathList(wchar_t * filename);
//讀取檔案內容
void ReadPath();
//與驅動通訊
HRESULT SendToDriver(LPVOID lpInBuffer,DWORD dwInBufferSize);
//控制驅動路徑
void AddToDriver(wchar_t * filename);
void DeleteFromDriver(wchar_t * filename);
void PauseDriver();
void RenewDriver();