內容要點展示:
內核中文件的使用
內核定時器的使用
IO_WORKITEM 的使用
文章概要
最近一個項目呢,是做一個基於 TDI 的防火牆,
而在該防火牆的實現過程中呢,有對文件的處理,
因爲這個防火牆中涉及到日誌文件,黑名單文件,白名單文件的處理,
所以整個的 TDI 防火牆中對於文件處理這一塊,
就涉及到文件的創建,打開,讀取,寫入等等文件操作。
而在內核中處理文件呢,自然有其特殊的一面,所以不太好操作。
同時還需要有定時器的處理以及 IRQL 的控制等細節。
文件操作概要
在 Windows 操作系統的內核環境中,對於文件的路徑和在應用層中的表示是不同的,
在應用層中,我們對於文件的路徑很簡單,比如:E:\Driver\CodeProject\Sample.txt
但是在內核環境下,則必須在前面的路徑中再加上一點點:\DosDevices\E:\Driver\CodeProject\Sample.txt 。
其實這一點呢,估計寫過一點點驅動程序的都很熟悉了,因爲創建符號鏈接名的時候都是這樣來實現的,
還有值得一提的是,在 Windows 2000 以及以後的 Windows 操作系統中,可以通過 ?? 來代替 DosDevices 了,
也就是說內核中文件的路徑又可以寫成:\??\E:\Driver\CodeProject\Sample.txt 。
然後呢,既然有了文件的路徑,那麼同樣可以通過 內核 API(不是 Win32 API) 來打開文件從而獲得句柄。
獲得句柄以後呢,就可以通過 內核 API 來完成讀寫等等操作了。
文件操作涉及的內核 API
首先,來看一下將要使用的針對於文件處理的 內核 API 。
對於上面的這些 API 呢,MSND 上明確記載:
must be running at IRQL = PASSIVE_LEVEL and with APCs enabled.
也就是說,它們必須運行在 IRQL 爲 PASSIVE_LEVEL 層,
衆所周知的是,PASSIVE_LEVEL 是所有中斷請求級別 (IRQL) 中最低的,
也就是說,工作在這個中斷請求級別下的任務相比於工作在其他 IRQL 層(比如 DISPATCH_LEVEL)的任務來說,
它們獲得 CPU 處理的時間會更少,至於爲什麼對於文件的處理的時候 IRQL 必須爲最低呢,
這裏其實稍動腦子想想就能夠明白,對於文件的處理向來都是很慢的,需要消耗很多的 CPU 時間,
如果你在內核中進行文件操作,而且這些操作不是工作在 PASSIVE_LEVEL 層的話,
那麼這些文件操作就有更多的機會獲取到 CPU 時間,
從而會消耗很多的 CPU 時間,從而造成系統性能的大幅度下降。
然後的話,上面的這些 API 如果你不是在 PASSIVE_LEVEL 下調用的話,
那麼 Windows 會很坦率的給你一個藍屏,從而很顯示的提示您:非法了。
再來看看這些 API 的原型,至於這些 API 的具體信息,MSDN 中有很完整的解釋,這裏就不給出了:
NTSTATUS
ZwCreateFile(
OUT PHANDLE FileHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PLARGE_INTEGER AllocationSize OPTIONAL,
IN ULONG FileAttributes,
IN ULONG ShareAccess,
IN ULONG CreateDisposition,
IN ULONG CreateOptions,
IN PVOID EaBuffer OPTIONAL,
IN ULONG EaLength
);
NTSTATUS
ZwOpenFile(
OUT PHANDLE FileHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN ULONG ShareAccess,
IN ULONG OpenOptions
);
NTSTATUS
ZwClose(
IN HANDLE Handle
);
NTSTATUS
ZwReadFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID Buffer,
IN ULONG Length,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN PULONG Key OPTIONAL
);
NTSTATUS
ZwWriteFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PVOID Buffer,
IN ULONG Length,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN PULONG Key OPTIONAL
);
NTSTATUS
ZwQueryInformationFile(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID FileInformation,
IN ULONG Length,
IN FILE_INFORMATION_CLASS FileInformationClass
);
定時器涉及的內核 API
在內核中使用定時器呢,有好幾種方式,
我這裏只介紹了我在這個 TDI 防火牆項目中所使用的定時器,
至於其他幾種定時器的使用呢,我給出幾篇來自博客園和看雪的博文的鏈接,
這幾篇博文對定時器的介紹都是非常貼切詳細的,有想了解定時器的這裏榮老衲推薦一下。
定時器:http://www.cnblogs.com/mydomain/archive/2010/11/14/1877125.html
高手進階windows內核定時器之一:http://bbs.pediy.com/showthread.php?t=60474
高手進階windows內核定時器之二:http://bbs.pediy.com/showthread.php?t=60579
VOID
KeInitializeTimer(
IN PKTIMER Timer
);
VOID
KeInitializeDpc(
IN PRKDPC Dpc,
IN PKDEFERRED_ROUTINE DeferredRoutine,
IN PVOID DeferredContext
);
BOOLEAN
KeSetTimer(
IN PKTIMER Timer,
IN LARGE_INTEGER DueTime,
IN PKDPC Dpc OPTIONAL
);
// DPC 回調函數
VOID
CustomDpc(
IN struct _KDPC *Dpc,
IN PVOID DeferredContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
);
對於上面這些 API 的使用呢,就是先調用 KeInitializeTimer 來初始化一個內核定時器,
然後調用 KeInitializeDpc 來初始化一個 DPC ,在該 DPC 中需要指定回調函數 CustomDpc ,
然後再調用 KeSetTimer 來將定時器和 DPC 關聯起來,同時也在這個 API 中指定定時器觸發的時間間隔。
需要注意的是,使用 KeSetTimer 設置好定時器後,定時器只會觸發一次,
如果要持續的觸發定時器,則必須在 CustomDpc 這個回調函數中重新調用 KeSetTimer 來重新設置定時器觸發。
還有需要注意的一點是在 CustomDpc 這個回調函數其是工作在 DISPATCH_LEVEL 這個 IRQL 層而非 PASSIVE_LEVEL 。
IO_WORKITEM 概要
對於這個 TDI 防火牆項目呢,我要實現的是如下這種方式的文件處理操作,
1. 每隔 10s 即需要重新讀取黑名單和白名單中的 IP 地址。
2. 每隔 1h 即需要重新創建日誌文件。
所以,我的思路就是通過兩個內核定時器來實現,
然後在內核定時器中來處理上面的這兩個針對文件的操作,
但是上面也提到了,在定時器所綁定的 DPC 的回調函數 CustomDpc 是工作在 DISPATCH_LEVEL 下,
而我們的 ZwCreateFile 之類的函數均必須工作在 PASSIVE_LEVEL 下,
如果是直接在 CustomDpc 下直接進行 ZwCreateFile 這些函數的話,
乖乖,直接給個很耀眼的提示 - 藍屏。
要解決上面的這個問題呢,就必須要用到 IO_WORKITEM,
因爲 IO_WORKITEM 這個東西呢,與其綁定的回調函數是工作在 PASSIVE_LEVEL 下的,
IO_WORKITEM 其實就是一個 IO 工作項,在操作系統中,由系統自行維護了一個 IO_WORKITEM 隊列,
而每一個 IO_WORKITEM 呢均有其對應的一個回調函數,衆所周知的是,
操作系統有一個系統線程池,所以系統會自動的使用這個系統線程池中的某一個空閒線程,
然後這個空閒線程從 IO_WORKITEM 隊列中取出一個 IO_WORKITEM ,
然後這個線程便調用這個 IO_WORKITEM 的回調函數進行處理,
這樣這個 IO_WORKITEM 也就得到了處理,關鍵是它的處理還是在 IRQL 爲 PASSIVE_LEVEL 時進行的。
而這剛好適應了我前面的需求。
IO_WORKITEM 涉及的內核 API
只列出我所使用的,關於其他的可以查看 MSDN
PIO_WORKITEM
IoAllocateWorkItem(
IN PDEVICE_OBJECT DeviceObject
);
VOID
IoQueueWorkItem(
IN PIO_WORKITEM IoWorkItem,
IN PIO_WORKITEM_ROUTINE WorkerRoutine,
IN WORK_QUEUE_TYPE QueueType,
IN PVOID Context
);
// IO_WORKITEM 回調函數
VOID
WorkItem (
IN PDEVICE_OBJECT DeviceObject,
IN PVOID Context
);
不完整代碼展示:
基本定義:
//定義讀取黑白文件的時間間隔爲 10 秒
#define READ_FILE_INTERVAL -10000 * 1000 * 10
//定義創建新的日誌文件間隔爲 1 小時
#define CREATE_LOGFILE_INTERVAL -10000 * 1000 * 60 * 60
HANDLE hBlackFile;
typedef struct _KTIMER_EXT
{
KDPC kDpc; //存儲 DPC 對象
KTIMER kTimer; //存儲計時器對象
LARGE_INTEGER dueInterval; //記錄計時器間隔時間
} KTIMER_EXT, * PKTIMER_EXT;
//兩個內核定時器
KTIMER_EXT queryFileInfoKTimerExt;
KTIMER_EXT createLogKTimerExt;
//保存設備句柄
PDEVICE_OBJECT pDeviceObj;
//從黑名單文件中讀取出所有的黑名單記錄
VOID ReadBlackFile();
//查詢文件信息的 Work Item 的回調處理函數
VOID WorkerItemQueryFileInfoRoutine(PDEVICE_OBJECT DeviceObject, PVOID Context);
//查詢文件信息定時器的 DPC 回調函數
VOID QueryFileInfoTimerDpcRoutine(PKDPC pDpc, PVOID pContext, PVOID SysArg1, PVOID SysArg2);
打開或者創建文件:
WCHAR fileName2[] = L"\\??\\C:\\NdisBlack.txt";
WCHAR fileName3[] = L"\\??\\C:\\NdisWhite.txt";
NTSTATUS status;
UNICODE_STRING unifilename2;
UNICODE_STRING unifilename3;
OBJECT_ATTRIBUTES oa;
IO_STATUS_BLOCK iostatus;
RtlInitUnicodeString(&unifilename2,fileName2);
RtlInitUnicodeString(&unifilename3,fileName3);
InitializeObjectAttributes(&oa,&unifilename1, OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE, NULL,NULL);
status = ZwCreateFile(&hBlackFile, FILE_READ_DATA|FILE_READ_ATTRIBUTES , &oa, &iostatus, NULL,
FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ|FILE_SHARE_WRITE, FILE_OPEN_IF, 0, 0, 0);
if(!NT_SUCCESS(status))
{
DbgPrint("ZwCreateFile(BlackFile) Failed %8x",status);
return status;
}
讀取文件:
ULONG length;
PCHAR pBuffer;
NTSTATUS status;
LARGE_INTEGER offset = {0};
IO_STATUS_BLOCK ioStatus;
pBuffer = (PCHAR)ExAllocatePool(NonPagedPool, ROWLENGTH);
while(1)
{
RtlZeroMemory(pBuffer, ROWLENGTH);
status = ZwReadFile(hBlackFile, NULL, NULL, NULL, &ioStatus, pBuffer, ROWLENGTH, &offset, NULL);
if(!NT_SUCCESS(status))
{
if(STATUS_END_OF_FILE == status)
{
DbgPrint("Read All File Contents ! \n");
break;
}
}
DbgPrint("Current ID: %d \n",lTotalBlackRecordCount);
DbgPrint("Current Data: %s \n",chArrayBlack[lTotalBlackRecordCount]);
//包括一個 ; 以及每一行的結束符 \t\r 所以總共是 3 個字符
length = i + 3;
offset.QuadPart += length;
}
寫入文件:
NTSTATUS ntstatus;
IO_STATUS_BLOCK iostatus;
LARGE_INTEGER ByteOffset = { 0 } ;
ntstatus = ZwWriteFile(hLogFile, NULL, NULL, NULL, &iostatus, pszDest,pszDestLen, &ByteOffset, NULL);
定時器設置(在其中綁定了 DPC 回調函數):
pDeviceObj = theDriverObject->DeviceObject;
queryFileInfoKTimerExt.dueInterval.QuadPart = READ_FILE_INTERVAL;
KeInitializeTimer(&queryFileInfoKTimerExt.kTimer);
KeInitializeDpc(&queryFileInfoKTimerExt.kDpc, QueryFileInfoTimerDpcRoutine, (PVOID)&queryFileInfoKTimerExt);
KeSetTimer(&queryFileInfoKTimerExt.kTimer, queryFileInfoKTimerExt.dueInterval, &queryFileInfoKTimerExt.kDpc);
DbgPrint("讀文件定時器設置成功 ! \n");
定時器之 DPC 回調函數(在其中創建了 IO_WORKITEM):
//在該定時器的回調函數中重新讀取黑白文件
//定時器處理函數運行在 DISPATCH_LEVEL
VOID QueryFileInfoTimerDpcRoutine(PKDPC pDpc, PVOID pContext, PVOID SysArg1, PVOID SysArg2)
{
PKTIMER_EXT pTmpKTimerExt;
PIO_WORKITEM pIoWorkItem;
//由於 ZwQueryInformationFile 必須運行在 PASSIVE_LEVEL
//而當前的這個函數作爲內核定時器的處理函數,其工作在 DISPATCH_LEVEL
//所以如果在該函數中直接調用 ZwQueryInformationFile 會由於 IRQL 的不符合而導致藍屏
//所以在這裏使用一個 IO_WORKITEM 來解決
//因爲 IO_WORKITEM 的回調函數是在 PASSIVE_LEVEL 中處理的
//所以可以在 IO_WORKITEM 的回調函數中來調用 ZwQueryInformationFile
pIoWorkItem = IoAllocateWorkItem(pDeviceObj);
if(pIoWorkItem)
{
IoQueueWorkItem(pIoWorkItem, WorkerItemQueryFileInfoRoutine, DelayedWorkQueue, NULL);
}
//#if DBG
// _asm int 3
//#endif
pTmpKTimerExt = (PKTIMER_EXT)pContext;
KeSetTimer(&pTmpKTimerExt->kTimer, pTmpKTimerExt->dueInterval, &pTmpKTimerExt->kDpc);
DbgPrint("讀文件定時器再次設置成功 ! \n");
}
IO_WORKITEM 回調函數:
//Work Item 回調函數,用來查詢文件屬性信息從而判斷是否需要重新讀入黑白名單
//WorkItem 處理函數運行在 PASSIVE_LEVEL
VOID WorkerItemQueryFileInfoRoutine(PDEVICE_OBJECT DeviceObject, PVOID Context )
{
NTSTATUS status;
LARGE_INTEGER localTime;
IO_STATUS_BLOCK ioStatus;
FILE_BASIC_INFORMATION flBscInfo;
//查詢黑名單文件屬性信息
status = ZwQueryInformationFile(hBlackFile, &ioStatus, &flBscInfo, sizeof(FILE_BASIC_INFORMATION), FileBasicInformation);
if(NT_SUCCESS(status))
{
ExSystemTimeToLocalTime(&flBscInfo.ChangeTime, &localTime);
RtlTimeToTimeFields(&localTime, &timeFields);
if(prevReadBlackTime.QuadPart < localTime.QuadPart)
{
DbgPrint("上一次讀取的時間小於黑名單文件的最新修改時間,所以需要重新讀取黑名單 \n");
//重新讀取黑名單
ReadBlackFile();
//重新設置上一次讀取黑名單文件的時間
prevReadBlackTime.QuadPart = localTime.QuadPart;
}
else
{
DbgPrint("不需要重新讀取黑名單 \n");
}
}
}
版權所有,歡迎轉載,但轉載請註明: 轉載自 Zachary.XiaoZhen - 夢想的天空